Rendering und Animation von Hunderttausenden Blöcken mit RenderMeshPrimitives

  • PublicPublic
  • 24 Sep, 2026
Rendering und Animation von Hunderttausenden Blöcken mit RenderMeshPrimitives

Ein Bild lässt sich über Graphics.RenderMeshPrimitives in ein riesiges Array farbiger Blöcke zerlegen und anschließend in Wellen, Zusammenbau, Abstoßung und Kollaps auf spielbarer Bildrate animieren. Der Ansatz beginnt damit, dass die Ausgangstextur in eine Liste mit Positions- und Farbdaten jedes Blocks umgewandelt wird. Es wird eine horizontale Blockanzahl festgelegt gridWidth, und die vertikale Anzahl wird aus dem Seitenverhältnis des Bildes abgeleitet — so werden die ursprünglichen Pixel in gleichmäßigen Abständen ausgedünnt, während vollständig transparente und fast schwarze Pixel als Hintergrund übersprungen werden.

Jeder Block wird in einer Struktur PixelBlock mit den Feldern float3 position und float4 color, die exakt dem HLSL-Layout entspricht. In Awake Methode texture.GetPixels32 liest das Ausgangsbild, und eine Schleife for über die Gitterkoordinaten ordnet jede Gitterzelle einem Quellpixel zu und verschiebt die Positionen so, dass der Mittelpunkt des Gitters im Koordinatenursprung liegt. Die Farbbytes werden durch 255 geteilt und so in Werte von 0–1 umgewandelt. Ein erster naiver Durchgang erstellt jeweils ein GameObject pro Block mit einem gemeinsamen Mesh, Material und MaterialPropertyBlock, doch bei etwa 100 000 Blöcken bricht alles auf etwa 6 FPS ein, weil jedes Objekt Zeichenbefehle und Komponenten-Overhead hinzufügt.

Die Lösung ist Graphics.RenderMeshPrimitives — eine API, die eigens dafür geschaffen wurde, dasselbe Mesh viele Male zu zeichnen. Die Instanzdaten werden einmalig in einen GraphicsBuffer (GraphicsBuffer.Target.Structured, sizeof(float) * 7 pro Element), dann SetData(blocks) überträgt das gesamte Array in einem Aufruf von der CPU auf die GPU. Das stride-Argument muss mit der Größe der HLSL-Struktur übereinstimmen und muss bei Änderungen an den Feldern aktualisiert werden. Ein einziger Aufruf Graphics.RenderMeshPrimitives(renderParams, cubeMesh, 0, blocks.Length) zeichnet alles, während der Shader Position und Farbe anhand von SV_InstanceID. worldBounds muss groß genug sein, um alle Blöcke zu enthalten: zu klein blendet Blöcke aus, zu groß hebt das Culling auf; der Buffer sollte in OnDisable, um Speicherlecks zu vermeiden.

Bei rund 100 000 Blöcken wuchs die GameObject-Version auf 54 525 Draw Calls und etwa 6 FPS an, während die RenderMeshPrimitives-Version bei 42 Draw Calls für die gesamte Szene blieb und 700–865 FPS erreichte. Bei einer etwa zehnfachen Steigerung auf 1,04 Millionen Blöcke blieben die Draw Calls bei 42, und die Bildrate hielt sich bei etwa 435 FPS; die Zahlen stammen aus dem Statistics-Fenster in Unity. Da die Verschiebung auf der GPU-Seite aus dem Puffer gelesen wird und die CPU nur einen einzigen Draw-Befehl ausgibt, bleiben die Kosten selbst bei wachsender Blockanzahl niedrig.

Die Animation läuft je nachdem, ob der Zustand des vorherigen Frames benötigt wird, über zwei Wege. Bewegung, die allein durch die Zeit definiert ist, läuft vollständig im Vertex-Shader: Eine Welle addiert sin entlang Z, unter Verwendung von _WaveAmplitude, _WaveFrequency und _WaveSpeed, während der Montageeffekt die verstreute Position zur Ausgangsposition interpoliert, gesteuert durch einen _Progress-Wert. Die Streurichtungen stammen aus GenerateHashedRandomFloat mit einem Schlüssel nach Instanz-ID versehen, daher sind sie deterministisch und benötigen keinen gespeicherten Zustand, und die nach Höhe verschobenen Startschwellen lassen das Bild von unten aufsteigen.

Für Bewegungen, die Geschwindigkeit oder Position zwischen Frames übertragen, wird ein Compute-Shader verwendet. Der Magnet-Effekt speichert die aktuellen Positionen in einem Lese-/Schreib-Buffer und stößt Blöcke in der Nähe des Cursors ab, dann lerpt er die Positionen jeden Frame zu ihren Zielen, bevor das Update gezeichnet wird. Der Gravitations-Kollaps integriert die Geschwindigkeit jeden Frame in den Buffer BlockState, springt bei der Landung ab mit _Restitution und schläft ein, wenn die Geschwindigkeit unter _SleepThreshold; _DeltaTime wobei dieser auf 1/30 Sekunde begrenzt wird, um ein Explodieren der Integration zu vermeiden, und die Ruhepositionen werden vorab aus der Dichte pro Spalte berechnet.