Animasi kerangka di sisi kartu video

Unity baru-baru ini memperkenalkan ECS. Dalam proses belajar, saya menjadi tertarik pada bagaimana animasi dan ECS dapat dijadikan teman. Dan dalam proses pencarian, saya menemukan teknik menarik yang digunakan orang-orang dari NORDVEUS dalam demo mereka untuk laporan Unite Austin 2017.
Unite Austin 2017 - Pertempuran Masif di Alam Semesta Spellsouls.


Laporan ini berisi banyak solusi menarik, tetapi hari ini kita akan membahas pelestarian animasi kerangka dalam tekstur dengan maksud untuk aplikasi lebih lanjut.


Mengapa kesulitan seperti itu, Anda bertanya?


Orang-orang dari NORDVEUS secara bersamaan melukis di layar sejumlah besar jenis objek animasi yang sama: kerangka, pendekar pedang. Dalam hal menggunakan pendekatan tradisional: SkinnedMeshRenderers dan Animation \ Animator , akan memerlukan peningkatan panggilan draw dan beban tambahan pada CPU untuk rendering animasi. Dan untuk mengatasi masalah ini, animasi dipindahkan ke sisi GPU, atau lebih tepatnya ke vertex shader.



Saya sangat tertarik dengan pendekatan dan memutuskan untuk mencari tahu lebih detail, dan karena saya tidak menemukan artikel tentang topik ini, saya masuk ke kode. Dalam proses mempelajari masalah ini, artikel ini lahir, dan visi saya untuk menyelesaikan masalah ini.


Jadi mari kita potong gajah menjadi potongan-potongan:



  • Mendapatkan kunci animasi dari klip
  • Menyimpan data ke tekstur
  • Persiapan jala (mesh)
  • Shader
  • Menyatukan semuanya


Mendapatkan kunci animasi dari klip animasi


Dari komponen-komponen SkinnedMeshRenderers, kami mendapatkan serangkaian tulang dan jaring. Komponen Animasi menyediakan daftar animasi yang tersedia. Jadi, untuk setiap klip, kita harus menyimpan bingkai matriks transformasi demi bingkai untuk semua tulang mesh. Dengan kata lain, kita menjaga pose karakter per unit waktu.


Kami memilih array dua dimensi di mana data akan disimpan. Satu dimensi yang memiliki jumlah bingkai dikalikan panjang klip dalam hitungan detik. Lain adalah jumlah total tulang di jala:


var boneMatrices = new Matrix4x4[Mathf.CeilToInt(frameRate * clip.length), renderer.bones.Length]; 

Dalam contoh berikut, kami mengubah frame untuk klip satu per satu dan menyimpan matriks:


 //       for (var frameIndex = 0; frameIndex < totalFramesInClip; ++frameIndex) { //  : 0 -  , 1 - . var normalizedTime = (float) frameIndex / totalFramesInClip; //     animationState.normalizedTime = normalizedTime; animation.Sample(); //     for (var boneIndex = 0; j < renderer.bones.Length; boneIndex++) { //         var matrix = renderer.bones[boneIndex].localToWorldMatrix * renderer.sharedMesh.bindposes[boneIndex]; //   boneMatrices[i, j] = matrix; } } 

Matriks adalah 4 oleh 4, tetapi baris terakhir selalu terlihat seperti (0, 0, 0, 1). Oleh karena itu, untuk tujuan sedikit optimasi, dapat dilewati. Yang pada gilirannya akan mengurangi biaya transfer data antara prosesor dan kartu video.


 a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 0 0 0 1 

Menyimpan data ke tekstur


Untuk menghitung ukuran tekstur, kami mengalikan jumlah total frame dalam semua klip animasi dengan jumlah tulang dan jumlah baris dalam matriks (kami sepakat bahwa kami menyimpan 3 baris pertama).


 var dataSize = numberOfBones * numberOfKeyFrames * MATRIX_ROWS_COUNT); //      var size = NextPowerOfTwo((int) Math.Sqrt(dataSize)); var texture = new Texture2D(size, size, TextureFormat.RGBAFloat, false) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Point, anisoLevel = 0 }; 

Kami menulis data ke dalam tekstur. Untuk setiap klip, kami menyimpan bingkai matriks transformasi demi bingkai. Format data adalah sebagai berikut. Klip direkam secara berurutan satu per satu dan terdiri dari satu set bingkai. Yang pada gilirannya terdiri dari satu set tulang. Setiap tulang mengandung 3 baris matriks.


 Clip0[Frame0[Bone0[row0,row1,row2]...BoneN[row0,row1,row2].]...FramM[bone0[row0,row1,row2]...ClipK[...] 

Berikut ini adalah kode simpan:


 var textureColor = new Color[texture.width * texture.height]; var clipOffset = 0; for (var clipIndex = 0; clipIndex < sampledBoneMatrices.Count; clipIndex++) { var framesCount = sampledBoneMatrices[clipIndex].GetLength(0); for (var keyframeIndex = 0; keyframeIndex < framesCount; keyframeIndex++) { var frameOffset = keyframeIndex * numberOfBones * 3; for (var boneIndex = 0; boneIndex < numberOfBones; boneIndex++) { var index = clipOffset + frameOffset + boneIndex * 3; var matrix = sampledBoneMatrices[clipIndex][keyframeIndex, boneIndex]; textureColor[index + 0] = matrix.GetRow(0); textureColor[index + 1] = matrix.GetRow(1); textureColor[index + 2] = matrix.GetRow(2); } } } texture.SetPixels(textureColor); texture.Apply(false, false); 

Persiapan jala (mesh)


Tambahkan satu set koordinat tekstur tambahan yang untuk setiap titik kami menyimpan indeks tulang terkait dan berat pengaruh tulang pada titik ini.
Unity menyediakan struktur data yang memungkinkan hingga 4 tulang untuk satu simpul. Di bawah ini adalah kode untuk menulis data ini ke uv. Kami menyimpan indeks tulang dalam UV1, bobot dalam UV2.


 var boneWeights = mesh.boneWeights; var boneIds = new List<Vector4>(mesh.vertexCount); var boneInfluences = new List<Vector4>(mesh.vertexCount); for (var i = 0; i < mesh.vertexCount; i++) { boneIds.Add(new Vector4(bw.boneIndex0, bw.boneIndex1, bw.boneIndex2, bw.boneIndex3); boneInfluences.Add(new Vector4(bw.weight0, bw.weight1, bw.weight2, bw.weight3)); } mesh.SetUVs(1, boneIds); mesh.SetUVs(2, boneInfluences); 

Shader


Tugas utama shader adalah menemukan matriks transformasi untuk tulang yang terkait dengan verteks dan melipatgandakan koordinat verteks dengan matriks ini. Untuk melakukan ini, kita memerlukan seperangkat koordinat tambahan dengan indeks dan bobot tulang. Kami juga membutuhkan indeks dari frame saat ini, itu akan berubah seiring waktu dan akan dikirim dari CPU.


 // frameOffset = clipOffset + frameIndex * clipLength * 3 -     CPU // boneIndex -      ,   UV1 int index = frameOffset + boneIndex * 3; 

Jadi kami mendapat indeks baris pertama dari matriks, indeks kedua dan ketiga akan menjadi +1, +2, masing-masing. Masih menerjemahkan indeks satu dimensi ke dalam koordinat tekstur yang dinormalisasi dan untuk ini kita perlu ukuran tekstur.


 inline float4 IndexToUV(int index, float2 size) { return float4(((float)((int)(index % size.x)) + 0.5) / size.x, ((float)((int)(index / size.x)) + 0.5) / size.y, 0, 0); } 

Setelah mengurangi baris, kami mengumpulkan matriks tanpa melupakan baris terakhir, yang selalu sama dengan (0, 0, 0, 1).


 float4 row0 = tex2Dlod(frameOffset, IndexToUV(index + 0, animationTextureSize)); float4 row1 = tex2Dlod(frameOffset, IndexToUV(index + 1, animationTextureSize)); float4 row2 = tex2Dlod(frameOffset, IndexToUV(index + 2, animationTextureSize)); float4 row3 = float4(0, 0, 0, 1); return float4x4(row0, row1, row2, row3); 

Pada saat yang sama, beberapa tulang dapat memengaruhi satu simpul sekaligus. Matriks yang dihasilkan akan menjadi jumlah dari semua matriks yang mempengaruhi verteks dikalikan dengan bobot pengaruhnya.


 float4x4 m0 = CreateMatrix(frameOffset, bones.x) * boneInfluences.x; float4x4 m1 = CreateMatrix(frameOffset, bones.y) * boneInfluences.y; float4x4 m2 = CreateMatrix(frameOffset, bones.z) * boneInfluences.z; float4x4 m3 = CreateMatrix(frameOffset, bones.w) * boneInfluences.w; return m0 + m1 + m2 + m3; 

Setelah menerima matriks, kami mengalikannya dengan koordinat titik. Oleh karena itu, semua simpul akan dipindahkan ke pose karakter, yang sesuai dengan bingkai saat ini. Mengubah bingkai, kita akan menghidupkan karakter.


Menyatukan semuanya


Untuk menampilkan objek, kita akan menggunakan Graphics.DrawMeshInstancedIndirect, di mana kita akan mentransfer mesh dan material yang disiapkan. Juga, dalam materi, kita harus meneruskan tekstur dengan animasi ke ukuran tekstur dan sebuah array dengan pointer ke bingkai untuk setiap objek pada waktu saat ini. Sebagai informasi tambahan, kami melewati posisi untuk setiap objek dan rotasi. Cara mengubah posisi dan rotasi di sisi shader dapat ditemukan di [artikel] .


Dalam metode Pembaruan, tambah waktu yang telah berlalu sejak awal animasi di Time.deltaTime.


Untuk menghitung indeks bingkai, kita harus menormalkan waktu dengan membaginya dengan panjang klip. Oleh karena itu, indeks bingkai dalam klip akan menjadi produk dari waktu yang dinormalisasi dengan jumlah bingkai. Dan indeks bingkai dalam tekstur akan menjadi jumlah dari pergeseran awal klip saat ini dan produk dari frame saat ini dengan jumlah data yang disimpan dalam bingkai ini.


 var offset = clipStart + frameIndex * bonesCount * 3.0f 

Itu mungkin semua telah melewati semua data ke shader, kami menyebutnya Graphics.DrawMeshInstancedIndirect dengan mesh dan material yang disiapkan.


Kesimpulan


Pengujian teknik ini pada mesin dengan kartu grafis 1050 menunjukkan peningkatan kinerja sekitar 2 kali.


gambar

Animasi 4000 objek dari jenis yang sama pada CPU


gambar

Animasi 8000 objek dari jenis yang sama pada GPU


Pada saat yang sama, pengujian adegan ini pada macbook pro 15 dengan kartu grafis terintegrasi menunjukkan hasil yang berlawanan. GPU tanpa malu-malu kehilangan (sekitar 2-3 kali), yang tidak mengejutkan.


Animasi pada kartu video adalah alat lain yang dapat digunakan dalam aplikasi Anda. Tapi seperti semua alat, itu harus digunakan dengan bijak dan tidak pada tempatnya.


Referensi




[Kode proyek GitHub]

Terima kasih atas perhatian anda


PS: Saya baru mengenal Unity dan tidak tahu semua seluk-beluknya, artikel itu mungkin mengandung ketidakakuratan. Saya berharap dapat memperbaikinya dengan bantuan Anda dan memahami topik dengan lebih baik.


Source: https://habr.com/ru/post/id452416/


All Articles