Terikat GPU. Cara mentransfer semuanya ke kartu video dan sedikit lagi. Animasi

Sekali waktu, itu adalah peristiwa besar ketika unit multitekstur atau transformasi & pencahayaan perangkat keras (T&L) muncul di GPU. Pengaturan Pipeline Fixed Function adalah sihir perdukunan. Dan mereka yang tahu cara mengaktifkan dan menggunakan fitur canggih dari chip tertentu melalui peretasan API D3D9, menganggap diri mereka telah mempelajari Zen. Tetapi waktu berlalu, shader muncul. Pada awalnya, sangat terbatas baik dalam fungsi maupun panjangnya. Lebih jauh, semakin banyak fitur, lebih banyak instruksi, lebih banyak kecepatan. Compute (CUDA, OpenCL, DirectCompute) muncul, dan ruang lingkup kapasitas kartu video mulai berkembang dengan cepat.

Dalam seri artikel (semoga) ini, saya akan mencoba menjelaskan dan menunjukkan bagaimana "luar biasa" Anda dapat menerapkan kemampuan GPU modern saat mengembangkan game, selain efek grafis. Bagian pertama akan dikhususkan untuk sistem animasi. Segala sesuatu yang dijelaskan didasarkan pada pengalaman praktis, diimplementasikan dan bekerja dalam proyek game nyata.

Oooo, sekali lagi animasinya. Tentang ini seratus kali sudah ditulis dan dijelaskan. Apa yang rumit? Kami mengemas matriks tulang dalam buffer / tekstur, dan menggunakannya untuk menguliti vertex shader. Ini dijelaskan kembali dalam GPU Permata 3 (Bab 2. Animated Crowd Rendering) . Dan diimplementasikan dalam Unite Tech Presentation baru-baru ini. Apakah mungkin dengan cara lain?

Technodemka dari Unity


Banyak hype, tetapi apakah itu benar-benar keren? Ada sebuah artikel di hub yang menjelaskan secara rinci bagaimana animasi kerangka dibuat dan bekerja di techno-demo ini. Pekerjaan paralel semuanya bagus, kami tidak menganggapnya. Tetapi kita perlu mencari tahu apa dan bagaimana di sana, dalam hal rendering.

Dalam pertempuran skala besar, dua pasukan bertempur, masing-masing terdiri dari satu jenis unit. Tengkorak di sebelah kiri, ksatria di sebelah kanan. Keragaman begitu-begitu. Setiap unit terdiri dari 3 LOD (masing-masing ~ 300, ~ 1000, ~ 4000 simpul), dan hanya 2 tulang yang memengaruhi verteks. Sistem animasi hanya terdiri dari 7 animasi untuk setiap jenis unit (saya ingat sudah ada 2 dari mereka). Animasi tidak berbaur, tetapi beralih secara terpisah dari kode sederhana yang dieksekusi di job'ax, yang ditekankan dalam presentasi. Tidak ada mesin negara. Saat kami memiliki dua jenis mesh, Anda dapat menarik seluruh kerumunan dalam dua panggilan draw yang dilakukan secara instan. Animasi kerangka, seperti yang sudah saya tulis, didasarkan pada teknologi yang dijelaskan pada tahun 2009.
Inovatif? Hmm ... sebuah terobosan? Mm ... Cocok untuk gim modern? Yah, mungkin, rasio FPS dengan jumlah unit membanggakan.

Kerugian utama dari pendekatan ini (pra-matriks dalam tekstur):

  1. Tingkat frame tergantung. Ingin dua kali lebih banyak bingkai animasi - beri memori dua kali lebih banyak.
  2. Kurangnya animasi campuran. Anda dapat membuatnya, tentu saja, tetapi di skin shader akan muncul kekacauan yang kompleks dari logika campuran.
  3. Kurang mengikat ke mesin negara Unity Animator. Alat yang mudah digunakan untuk menyesuaikan perilaku karakter, yang dapat dihubungkan ke sistem skinning, tetapi dalam kasus kami, karena poin 2, semuanya menjadi sangat sulit (bayangkan bagaimana cara memadukan BlendTree yang bersarang).

GPAS


Sistem Animasi yang Didukung GPU. Nama itu baru saja muncul.
Sistem animasi baru memiliki beberapa persyaratan:

  1. Bekerja cepat (well, dapat dimengerti). Anda perlu menghidupkan puluhan ribu unit yang berbeda.
  2. Jadilah analog yang lengkap (atau hampir) dari sistem animasi Unity. Jika ada animasi yang terlihat seperti ini, maka dalam sistem baru itu harus terlihat persis sama. Kemampuan untuk beralih antara sistem CPU dan GPU bawaan. Ini sering diperlukan untuk debugging. Ketika animasi "buggy", dengan beralih ke animator klasik, Anda dapat mengerti: ini adalah gangguan dari sistem baru, atau mesin negara / animasi itu sendiri.
  3. Semua animasi dapat disesuaikan di Unity Animator. Alat yang siap digunakan, nyaman, dan paling penting. Kami akan membuat sepeda di tempat lain.

Mari kita pikirkan kembali persiapan dan pembuatan animasi. Kami tidak akan menggunakan matriks. Kartu video modern bekerja dengan baik dengan loop, secara native mendukung int selain float, jadi kami akan bekerja dengan keyframe seperti pada CPU.

Mari kita lihat contoh animasi di Penampil Animasi:



Dapat dilihat bahwa keyframe diatur secara terpisah untuk posisi, skala dan rotasi. Untuk beberapa tulang, Anda membutuhkan banyak dari mereka, untuk beberapa hanya beberapa, dan untuk tulang-tulang yang tidak dianimasikan secara terpisah, hanya kerangka kunci awal dan akhir yang ditetapkan.

Posisi - Vector3, angka empat - Vector4, skala - Vector3. Struktur keyframe akan memiliki satu kesamaan (untuk penyederhanaan), jadi kita perlu 4 float agar sesuai dengan salah satu tipe di atas. Kami juga membutuhkan InTangent dan OutTangent untuk interpolasi yang benar antara keyframe sesuai dengan kelengkungan. Oh ya, dan waktu yang dinormalkan tidak lupa:

struct KeyFrame { float4 v; float4 inTan, outTan; float time; }; 

Untuk mendapatkan semua kerangka kunci, gunakan AnimationUtility.GetEditorCurve ().
Juga, kita harus mengingat nama-nama tulang, karena akan perlu untuk memetakan kembali tulang animasi di tulang kerangka (dan mereka mungkin tidak bertepatan) pada tahap mempersiapkan data GPU.

Mengisi buffer linear dengan array keyframe, kita akan mengingat offset di dalamnya untuk menemukan yang berhubungan dengan animasi yang kita butuhkan.

Sekarang menarik. Animasi kerangka GPU.

Kami menyiapkan besar ("jumlah kerangka animasi" X "jumlah tulang dalam kerangka" X "koefisien empiris dari jumlah maksimum campuran animasi"). Di dalamnya kita akan menyimpan posisi, rotasi dan skala tulang pada saat animasi. Dan untuk semua tulang animasi yang direncanakan dalam bingkai ini, jalankan compute shader. Setiap utas menjiwai tulangnya.

Setiap kerangka kunci, terlepas dari ukurannya (Translate, Rotation, Scale), diinterpolasi dengan cara yang persis sama (cari dengan pencarian linear, maafkan saya Knuth):

 void InterpolateKeyFrame(inout float4 rv, int startIdx, int endIdx, float t) { for (int i = startIdx; i < endIdx; ++i) { KeyFrame k0 = keyFrames[i + 0]; KeyFrame k1 = keyFrames[i + 1]; float lerpFactor = (t - k0.time) / (k1.time - k0.time); if (lerpFactor < 0 || lerpFactor > 1) continue; rv = CurveInterpoate(k0, k1, lerpFactor); break; } } 

Kurva adalah kurva Bezier kubik, sehingga fungsi interpolasi adalah sebagai berikut:

 float4 CurveInterpoate(KeyFrame v0, KeyFrame v1, float t) { float dt = v1.time - v0.time; float4 m0 = v0.outTan * dt; float4 m1 = v1.inTan * dt; float t2 = t * t; float t3 = t2 * t; float a = 2 * t3 - 3 * t2 + 1; float b = t3 - 2 * t2 + t; float c = t3 - t2; float d = -2 * t3 + 3 * t2; float4 rv = a * v0.v + b * m0 + c * m1 + d * v1.v; return rv; } 

Postur lokal (TRS) dari tulang dihitung. Selanjutnya, dengan penghitung komputasi terpisah, kami memadukan semua animasi yang diperlukan untuk tulang ini. Untuk melakukan ini, kami memiliki buffer dengan indeks animasi dan bobot masing-masing animasi dalam campuran terakhir. Kami mendapatkan informasi ini dari mesin negara. Situasi BlendTree di dalam BlendTree diselesaikan sebagai berikut. Misalnya, ada pohon:



BlendTree Walk akan memiliki berat 0,35, Jalankan - 0,65. Dengan demikian, posisi akhir dari tulang harus ditentukan oleh 4 animasi: Walk1, Walk2, Run1 dan Run2. Bobot mereka akan memiliki nilai (0,35 * 0,92, 0,35 * 0,08, 0,65 * 0,92, 0,65 * 0,08) = (0,322, 0,028, 0,598, 0,052), masing-masing. Perlu dicatat bahwa jumlah bobot harus selalu sama dengan satu, atau bug sihir disediakan.

"Jantung" dari fungsi blending:

 float bw = animDef.blendWeight; BoneXForm boneToBlend = animatedBones[srcBoneIndex]; float4 q = boneToBlend.quat; float3 t = boneToBlend.translate; float3 s = boneToBlend.scale; if (dot(resultBone.quat, q) < 0) q = -q; resultBone.translate += t * bw; resultBone.quat += q * bw; resultBone.scale += s * bw; 

Sekarang Anda dapat menerjemahkannya ke dalam matriks transformasi. Berhenti Tentang hierarki tulang benar-benar dilupakan.
Berdasarkan data dari kerangka, kami membangun array indeks, di mana sel dengan indeks tulang berisi indeks induknya. Di root, tulis -1.

Contoh:



 float4x4 animMat = IdentityMatrix(); float4x4 mat = initialPoses[boneId]; while (boneId >= 0) { BoneXForm b = blendedBones[boneId]; float4x4 xform = MakeTransformMatrix(b.translate, b.quat, b.scale); animMat = mul(animMat, xform); boneId = bonesHierarchyIndices[boneId]; } mat = mul(mat, animMat); resultSkeletons[id] = mat; 

Di sini, pada prinsipnya, semua poin utama rendering dan campuran animasi.

GPSM


Mesin yang Didukung GPU (Anda menebaknya dengan benar). Sistem animasi yang dijelaskan di atas akan bekerja dengan sempurna dengan Unity Animation State Machine, tetapi semua upaya akan sia-sia. Dengan kemungkinan menghitung puluhan (jika bukan ratusan) ribuan animasi per bingkai, UnityAnimator tidak akan menarik ribuan mesin negara yang bekerja secara bersamaan. Hmm ...
Apa itu mesin negara di Unity? Ini adalah sistem tertutup keadaan dan transisi, yang dikendalikan oleh sifat numerik sederhana. Setiap mesin keadaan beroperasi secara independen satu sama lain, dan untuk set input data yang sama. Tunggu sebentar. Ini adalah tugas yang ideal untuk GPU dan menghitung shader!

Fase pembakaran

Pertama, kita perlu mengumpulkan dan menempatkan semua data mesin negara dalam struktur ramah GPU. Dan ini: status (status), transisi (transisi) dan parameter (parameter).
Semua data ini ditempatkan dalam buffer linear, dan ditangani oleh indeks.
Setiap utas komputasi mempertimbangkan mesin negaranya. AnimatorController menyediakan antarmuka untuk semua struktur mesin keadaan internal yang diperlukan.

Struktur utama mesin negara:

 struct State { float speed; int firstTransition; int numTransitions; int animDefId; }; struct Transition { float exitTime; float duration; int sourceStateId; int targetStateId; int firstCondition; int endCondition; uint properties; }; struct StateData { int id; float timeInState; float animationLoop; }; struct TransitionData { int id; float timeInTransition; }; struct CurrentState { StateData srcState, dstState; TransitionData transition; }; struct AnimationDef { uint animId; int nextAnimInTree; int parameterIdx; float lengthInSec; uint numBones; uint loop; }; struct ParameterDef { float2 line0ab, line1ab; int runtimeParamId; int nextParameterId; }; struct Condition { int checkMode; int runtimeParamIndex; float referenceValue; }; 

  • Negara berisi kecepatan di mana negara dimainkan, dan indeks kondisi untuk transisi ke orang lain sesuai dengan mesin negara.
  • Transisi berisi indeks status "dari" dan "ke". Waktu transisi, waktu keluar, dan tautan ke berbagai kondisi untuk memasuki kondisi ini.
  • CurrentState adalah blok data runtime dengan data pada keadaan saat ini dari mesin negara.
  • AnimationDef berisi deskripsi animasi dengan tautan ke orang lain yang terkait dengannya oleh BlendTree.
  • ParameterDef adalah deskripsi dari parameter yang mengontrol perilaku mesin negara. Line0ab dan Line1ab adalah koefisien dari persamaan garis untuk menentukan berat animasi dengan nilai parameter. Dari sini:


  • Kondisi - spesifikasi kondisi untuk membandingkan nilai runtime dari parameter dan nilai referensi.

Fase runtime

Siklus utama setiap mesin negara dapat ditampilkan menggunakan algoritma berikut:



Ada 4 jenis parameter di Unity animator: float, int, bool dan trigger (yang bool). Kami akan menyajikan semuanya sebagai pelampung. Saat mengatur kondisi, dimungkinkan untuk memilih satu dari enam jenis perbandingan. Jika == Setara. IfNot == NotEqual. Jadi kita hanya akan menggunakan 4. Indeks operator dilewatkan ke bidang checkMode dari struktur Condition.

 for (int i = t.firstCondition; i < t.endCondition; ++i) { Condition c = allConditions[i]; float paramValue = runtimeParameters[c.runtimeParamIndex]; switch (c.checkMode) { case 3: if (paramValue < c.referenceValue) return false; case 4: if (paramValue > c.referenceValue) return false; case 6: if (abs(paramValue - c.referenceValue) > 0.001f) return false; case 7: if (abs(paramValue - c.referenceValue) < 0.001f) return false; } } return true; 

Untuk memulai transisi, semua kondisi harus benar. Label case aneh hanya (int) AnimatorConditionMode. Logika interupsi adalah logika yang rumit untuk mengganggu dan mengembalikan transisi.

Setelah kami memperbarui keadaan mesin keadaan dan menggulir perangko waktu pada bingkai delta, saatnya untuk menyiapkan data tentang animasi apa yang harus dibaca dalam bingkai ini, dan bobot yang sesuai. Langkah ini dilewati jika model unit tidak ada dalam bingkai (Frustum culled). Mengapa kita harus mempertimbangkan animasi yang tidak terlihat? Kami pergi ke negara sumber pohon campuran, sesuai dengan negara tujuan pohon campuran, menambahkan semua animasi dari mereka, dan kami menghitung bobot dengan waktu transisi normal dari sumber ke tujuan (waktu yang dihabiskan dalam transisi). Dengan data yang disiapkan, GPAS ikut bermain, dan menghitung animasi untuk setiap entitas animasi dalam game.

Parameter kontrol unit berasal dari logika kontrol unit. Misalnya, Anda perlu mengaktifkan running, mengatur parameter CharSpeed, dan mesin state yang dikonfigurasikan dengan benar memadukan animasi transisi dari "berjalan" ke "berlari".

Secara alami, analogi lengkap dengan Unity Animator tidak berhasil. Prinsip kerja internal, jika tidak dijelaskan dalam dokumentasi, harus dibalik dan dibuat analog. Beberapa fungsionalitas belum selesai (mungkin tidak). Misalnya, BlendType di BlendTree hanya mendukung 1D. Untuk membuat jenis lain, pada prinsipnya, tidak sulit, hanya saja sekarang tidak perlu. Tidak ada acara animasi, karena itu perlu untuk melakukan pembacaan kembali dengan GPU, dan pembacaan yang "benar" akan ada beberapa frame di belakang, yang tidak selalu dapat diterima. Tetapi itu juga mungkin.

Render


Render unit dilakukan melalui instancing. Menurut SV_InstanceID, dalam vertex shader, kita mendapatkan matriks dari semua tulang yang memengaruhi vertex, dan mengubahnya. Sama sekali tidak ada yang aneh:

 float4 ApplySkin(float3 v, uint vertexID, uint instanceID) { BoneInfoPacked bip = boneInfos[vertexID]; BoneInfo bi = UnpackBoneInfo(bip); SkeletonInstance skelInst = skeletonInstances[instanceID]; int bonesOffset = skelInst.boneOffset; float4x4 animMat = 0; for (int i = 0; i < 4; ++i) { float bw = bi.boneWeights[i]; if (bw > 0) { uint boneId = bi.boneIDs[i]; float4x4 boneMat = boneMatrices[boneId + bonesOffset]; animMat += boneMat * bw; } } float4 rv = float4(v, 1); rv = mul(rv, animMat); return rv; } 

Ringkasan


Apakah peternakan ini bekerja cepat? Jelas lebih lambat daripada mencicipi tekstur dengan matriks, tapi tetap saja, saya bisa menunjukkan beberapa angka (GTX 970).

Berikut adalah 50.000 mesin negara:



Berikut adalah 280.000 tulang animasi:



Merancang dan men-debug semua ini adalah hal yang sangat menyakitkan. Sekelompok buffer dan offset. Banyak komponen dan interaksinya. Ada saat-saat ketika tangan jatuh ketika Anda memukul kepala Anda tentang suatu masalah selama beberapa hari, tetapi Anda tidak dapat menemukan apa masalahnya. Terutama "bagus" ketika semuanya berfungsi sebagaimana mestinya pada data uji, tetapi dalam situasi "pertarungan" nyata tidak ada kesalahan animasi apa pun. Perbedaan antara pengoperasian mesin status Persatuan dan miliknya juga tidak segera terlihat. Secara umum, jika Anda memutuskan untuk membuat analog untuk diri sendiri, maka saya tidak iri dengan Anda. Sebenarnya, seluruh pengembangan di bawah GPU adalah hal yang patut dikeluhkan.

NB Saya ingin melempar batu di taman pengembang Unite TechDemo. Mereka memiliki sejumlah besar model identik reruntuhan dan jembatan di atas panggung, dan mereka tidak mengoptimalkan rendering mereka dengan cara apa pun. Sebaliknya, mereka mencoba dengan mencentang "statis". Hanya sekarang, dalam indeks 16-bit Anda tidak dapat menjejalkan banyak geometri (tiga kali haha, 2017) dan tidak ada yang datang bersamaan, karena modelnya sangat poligonal. Saya menempatkan "Aktifkan Instancing" untuk semua shader, dan "Statis" tidak dicentang. Tidak ada dorongan nyata, tapi, sial, Anda melakukan demo teknologi, berjuang untuk setiap FPS. Anda tidak bisa omong kosong seperti itu.

Apakah
 *** Summary *** Draw calls: 2553 Dispatch calls: 0 API calls: 8378 Index/vertex bind calls: 2992 Constant bind calls: 648 Sampler bind calls: 395 Resource bind calls: 805 Shader set calls: 682 Blend set calls: 230 Depth/stencil set calls: 92 Rasterization set calls: 238 Resource update calls: 1017 Output set calls: 74 API:Draw/Dispatch call ratio: 3.28163 298 Textures - 1041.01 MB (1039.95 MB over 32x32), 42 RTs - 306.94 MB. Avg. tex dimension: 1811.77x1810.21 (2016.63x2038.98 over 32x32) 216 Buffers - 180.11 MB total 17.54 MB IBs 159.81 MB VBs. 1528.06 MB - Grand total GPU buffer + texture load. *** Draw Statistics *** Total calls: 2553, instanced: 2, indirect: 2 Instance counts: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: >=15: ******************************************************************************************************************************** (2) 


Telah menjadi
 *** Summary *** Draw calls: 1474 Dispatch calls: 0 API calls: 11106 Index/vertex bind calls: 3647 Constant bind calls: 1039 Sampler bind calls: 348 Resource bind calls: 718 Shader set calls: 686 Blend set calls: 230 Depth/stencil set calls: 110 Rasterization set calls: 258 Resource update calls: 1904 Output set calls: 74 API:Draw/Dispatch call ratio: 7.5346 298 Textures - 1041.01 MB (1039.95 MB over 32x32), 42 RTs - 306.94 MB. Avg. tex dimension: 1811.77x1810.21 (2016.63x2038.98 over 32x32) 427 Buffers - 93.30 MB total 9.81 MB IBs 80.51 MB VBs. 1441.25 MB - Grand total GPU buffer + texture load. *** Draw Statistics *** Total calls: 1474, instanced: 391, indirect: 2 Instance counts: 1: 2: ******************************************************************************************************************************** (104) 3: ************************************************* (40) 4: ********************** (18) 5: ****************************** (25) 6: ********************************************************************************************* (76) 7: *********************************** (29) 8: ************************************************** (41) 9: ********* (8) 10: ************** (12) 11: 12: ****** (5) 13: ******* (6) 14: ** (2) >=15: ****************************** (25) 


PPS Di sepanjang waktu, game terutama terikat dengan CPU, mis. CPU tidak mengikuti GPU. Terlalu banyak logika dan fisika. Mentransfer bagian dari logika game dari CPU ke GPU, kami membongkar yang pertama dan memuat yang kedua, mis. membuat situasi GPU terikat lebih mungkin. Karenanya judul artikel.

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


All Articles