Skelettanimation an der Seite der Grafikkarte

Unity hat kürzlich ECS eingeführt. Während des Studiums interessierte ich mich dafür, wie Animation und ECS Freunde werden können. Und im Suchprozess bin ich auf eine interessante Technik gestoßen, die die Jungs von NORDVEUS in ihrer Demo für den Unite Austin 2017-Bericht verwendet haben.
Unite Austin 2017 - Massive Schlacht im Spellsouls-Universum.


Der Bericht enthält viele interessante Lösungen, aber heute werden wir die Erhaltung der Skelettanimation in der Textur im Hinblick auf ihre weitere Anwendung diskutieren.


Warum solche Schwierigkeiten, fragst du?


Die Jungs von NORDVEUS haben gleichzeitig eine große Anzahl derselben Art von animiertem Objekt auf den Bildschirm gemalt: Skelette, Schwertkämpfer. Bei Verwendung des herkömmlichen Ansatzes: SkinnedMeshRenderers und Animation \ Animator werden die Zeichnungsaufrufe erhöht und die CPU für das Rendern der Animation zusätzlich belastet. Um diese Probleme zu lösen, wurde die Animation auf die Seite der GPU bzw. auf den Vertex-Shader verschoben.



Ich war sehr interessiert an dem Ansatz und beschloss, ihn genauer herauszufinden. Da ich keine Artikel zu diesem Thema fand, ging ich in den Code ein. Während des Studiums des Problems wurde dieser Artikel geboren und meine Vision, dieses Problem zu lösen.


Also schneiden wir den Elefanten in Stücke:



  • Abrufen von Animationstasten aus Clips
  • Speichern von Daten in der Textur
  • Netzvorbereitung
  • Shader
  • Alles zusammenfügen


Abrufen von Animationstasten aus Animationsclips


Aus den Komponenten von SkinnedMeshRenderers erhalten wir eine Reihe von Bones und ein Mesh. Die Animationskomponente bietet eine Liste der verfügbaren Animationen. Daher müssen wir für jeden Clip die Transformationsmatrix Frame für Frame für alle Knochen des Netzes speichern. Mit anderen Worten, wir behalten die Pose des Charakters pro Zeiteinheit bei.


Wir wählen ein zweidimensionales Array aus, in dem die Daten gespeichert werden. Eine Dimension hat die Anzahl der Frames multipliziert mit der Länge des Clips in Sekunden. Ein weiterer Grund ist die Gesamtzahl der Knochen im Netz:


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

Im folgenden Beispiel ändern wir die Frames für den Clip nacheinander und speichern die Matrizen:


 //       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; } } 

Matrizen sind 4 mal 4, aber die letzte Zeile sieht immer so aus (0, 0, 0, 1). Daher kann es zum Zwecke einer leichten Optimierung übersprungen werden. Dies reduziert wiederum die Kosten für die Datenübertragung zwischen dem Prozessor und der Grafikkarte.


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

Speichern von Daten in der Textur


Um die Größe der Textur zu berechnen, multiplizieren wir die Gesamtzahl der Frames in allen Animationsclips mit der Anzahl der Bones und der Anzahl der Zeilen in der Matrix (wir waren uns einig, dass wir die ersten 3 Zeilen speichern).


 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 }; 

Wir schreiben die Daten in die Textur. Für jeden Clip speichern wir die Transformationsmatrix Frame für Frame. Das Datenformat ist wie folgt. Clips werden nacheinander aufgenommen und bestehen aus einer Reihe von Frames. Was wiederum aus einer Reihe von Knochen besteht. Jeder Knochen enthält 3 Matrixreihen.


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

Das Folgende ist der Speichercode:


 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); 

Netzvorbereitung


Fügen Sie einen zusätzlichen Satz von Texturkoordinaten hinzu, für die wir für jeden Scheitelpunkt die zugehörigen Knochenindizes und das Gewicht des Einflusses des Knochens auf diesen Scheitelpunkt speichern.
Unity bietet eine Datenstruktur, in der bis zu 4 Knochen für einen Scheitelpunkt möglich sind. Unten finden Sie den Code zum Schreiben dieser Daten in UV. Wir speichern Knochenindizes in UV1, Gewichte in 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


Die Hauptaufgabe des Shaders besteht darin, die Transformationsmatrix für den mit dem Scheitelpunkt verbundenen Knochen zu finden und die Koordinaten des Scheitelpunkts mit dieser Matrix zu multiplizieren. Dazu benötigen wir einen zusätzlichen Satz von Koordinaten mit Indizes und Knochengewichten. Wir brauchen auch den Index des aktuellen Frames, er ändert sich im Laufe der Zeit und wird von der CPU übertragen.


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

Wir haben also den Index der ersten Zeile der Matrix erhalten, der Index der zweiten und dritten Zeile ist +1 bzw. +2. Es bleibt, den eindimensionalen Index in die normalisierten Koordinaten der Textur zu übersetzen, und dafür benötigen wir die Größe der Textur.


 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); } 

Nachdem wir die Zeilen subtrahiert haben, sammeln wir die Matrix, ohne die letzte Zeile zu vergessen, die immer gleich (0, 0, 0, 1) ist.


 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); 

Gleichzeitig können mehrere Knochen gleichzeitig einen Scheitelpunkt beeinflussen. Die resultierende Matrix ist die Summe aller Matrizen, die den Scheitelpunkt beeinflussen, multipliziert mit dem Gewicht ihres Einflusses.


 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; 

Nachdem wir die Matrix erhalten haben, multiplizieren wir sie mit den Koordinaten des Scheitelpunkts. Daher werden alle Scheitelpunkte in die Pose des Charakters verschoben, die dem aktuellen Frame entspricht. Wenn Sie den Rahmen ändern, werden wir den Charakter animieren.


Alles zusammenfügen


Zum Anzeigen von Objekten verwenden wir Graphics.DrawMeshInstancedIndirect, in das wir das vorbereitete Netz und Material übertragen. Außerdem müssen wir im Material die Textur mit Animationen an die Texturgröße und ein Array mit Zeigern auf den Rahmen für jedes Objekt zur aktuellen Zeit übergeben. Als zusätzliche Information übergeben wir die Position für jedes Objekt und die Drehung. Informationen zum Ändern der Position und Drehung auf der Shader-Seite finden Sie in [Artikel] .


Erhöhen Sie bei der Aktualisierungsmethode die Zeit, die seit Beginn der Animation in Time.deltaTime vergangen ist.


Um den Frame-Index zu berechnen, müssen wir die Zeit normalisieren, indem wir sie durch die Länge des Clips dividieren. Daher ist der Frame-Index im Clip das Produkt der normalisierten Zeit durch die Anzahl der Frames. Der Frame-Index in der Textur ist die Summe der Verschiebung des Beginns des aktuellen Clips und des Produkts des aktuellen Frames um die in diesem Frame gespeicherte Datenmenge.


 var offset = clipStart + frameIndex * bonesCount * 3.0f 

Nachdem wahrscheinlich alle Daten an den Shader übergeben wurden, rufen wir Graphics.DrawMeshInstancedIndirect mit dem vorbereiteten Netz und Material auf.


Schlussfolgerungen


Das Testen dieser Technik auf einem Computer mit einer 1050-Grafikkarte ergab eine Leistungssteigerung von etwa dem Zweifachen.


Bild

Animation von 4000 Objekten des gleichen Typs auf der CPU


Bild

Animation von 8000 Objekten des gleichen Typs auf der GPU


Gleichzeitig zeigt das Testen dieser Szene auf einem MacBook Pro 15 mit integrierter Grafikkarte das gegenteilige Ergebnis. Die GPU verliert schamlos (ca. 2-3 mal), was nicht überraschend ist.


Die Animation auf der Grafikkarte ist ein weiteres Tool, das in Ihrer Anwendung verwendet werden kann. Aber wie alle Werkzeuge sollte es mit Bedacht und unangebracht eingesetzt werden.


Referenzen




[GitHub-Projektcode]

Vielen Dank für Ihre Aufmerksamkeit.


PS: Ich bin neu in Unity und kenne nicht alle Feinheiten. Der Artikel enthält möglicherweise Ungenauigkeiten. Ich hoffe, sie mit Ihrer Hilfe zu beheben und das Thema besser zu verstehen.


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


All Articles