Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

Animieren von Tausenden von Fischen mit MultiMeshInstance3D

Dieses Tutorial beschäftigt sich mit einer Methode, die in dem Spiel ABZU verwendet wird, um Tausende von Fischen mit Hilfe von Vertex-Animation und statischem Mesh-Instancing zu rendern und zu animieren.

In Godot kann dies mit einem eigenen Shader und einem MultiMeshInstance3D erreicht werden. Mit der folgenden Methode können Sie Tausende von animierten Objekten rendern, sogar auf Low-End-Hardware.

Wir beginnen mit der Animation eines einzelnen Fisches. Dann werden wir sehen, wie wir diese Animation auf Tausende von Fischen ausweiten können.

Animieren eines Fischs

Wir werden mit einem einzelnen Fisch beginnen. Laden Sie Ihr Fischmodell in eine MeshInstance3D und fügen Sie ein neues ShaderMaterial hinzu.

Hier ist der Fisch, den wir für die Beispielbilder verwenden werden. Sie können jedes beliebige Fischmodell verwenden, das Ihnen gefällt.

../../../_images/fish.png

Bemerkung

Das Fischmodell in diesem Tutorial stammt von QuaterniusDev und wird unter einer Creative Commons Lizenz zur Verfügung gestellt. CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/

Normalerweise würden Sie Knochen und ein Skeleton3D verwenden, um Objekte zu animieren. Knochen werden jedoch auf der CPU animiert, so dass Sie am Ende Tausende von Operationen pro Frame berechnen müssen und es unmöglich wird, Tausende von Objekten zu haben. Wenn Sie die Vertex-Animation in einem Vertex-Shader verwenden, vermeiden Sie die Verwendung von Knochen und können stattdessen die gesamte Animation in wenigen Codezeilen und vollständig auf der GPU berechnen.

Die Animation soll aus vier Hauptbewegungen bestehen:

  1. Eine Bewegung von Seite zu Seite

  2. Eine Pivot-Drehbewegung um das Zentrum des Fisches

  3. Eine schwenkende Wellenbewegung

  4. Eine schwenkende Drehbewegung

Der gesamte Code für die Animation befindet sich im Vertex-Shader, wobei Uniforms den Umfang der Bewegung steuern. Wir verwenden Uniforms, um die Stärke der Bewegung zu steuern, so dass Sie die Animation im Editor optimieren und die Ergebnisse in Echtzeit sehen können, ohne dass der Shader neu kompiliert werden muss.

Alle Bewegungen werden mit Kosinuswellen ausgeführt, die auf VERTEX im Model-Space angewendet werden. Wir wollen, dass die Eckpunkte im Model-Space liegen, so dass die Bewegung immer relativ zur Ausrichtung des Fisches ist. Zum Beispiel wird der Fisch bei einer Seitwärtsbewegung immer von links nach rechts hin- und herbewegt, anstatt entlang der x-Achse im World-Space.

Um die Geschwindigkeit der Animation zu steuern, definieren wir zunächst unsere eigene Zeitvariable mit TIME.

//time_scale is a uniform float
float time = TIME * time_scale;

Die erste Bewegung, die wir implementieren werden, ist die Bewegung von Seite zu Seite. Sie kann durch eine Verschiebung von VERTEX.x um cos von TIME erzeugt werden. Jedes Mal, wenn das Mesh gerendert wird, werden alle Vertices um den Betrag von cos(time) zur Seite bewegt.

//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;

Die resultierende Animation sollte in etwa so aussehen:

../../../_images/sidetoside.gif

Als nächstes fügen wir den Pivot-Punkt hinzu. Da der Fisch bei (0, 0) zentriert ist, müssen wir nur VERTEX mit einer Rotationsmatrix multiplizieren, damit er sich um den Mittelpunkt des Fisches rotiert.

Wir konstruieren eine Rotationsmatrix wie folgt:

//angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around
//pivot is a uniform float
float pivot_angle = cos(time) * 0.1 * pivot;
mat2 rotation_matrix = mat2(vec2(cos(pivot_angle), -sin(pivot_angle)), vec2(sin(pivot_angle), cos(pivot_angle)));

Und dann wenden wir es auf die Achsen x und z an, indem wir es mit VERTEX.xz multiplizieren.

VERTEX.xz = rotation_matrix * VERTEX.xz;

Wenn Sie nur den Pivot anwenden, sollten Sie etwa so etwas sehen:

../../../_images/pivot.gif

Die nächsten beiden Bewegungen müssen die Wirbelsäule des Fisches abwärts schwenken. Dafür brauchen wir eine neue Variable, body. body ist ein Float, der am Schwanz des Fisches den Wert 0 und am Kopf den Wert 1 hat.

float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2

Die nächste Bewegung ist eine Kosinuswelle, die sich entlang der Länge des Fisches bewegt. Damit sie sich entlang der Wirbelsäule des Fisches bewegt, verschieben wir den Eingangswert von cos um die Position entlang der Wirbelsäule, was die Variable body ist, die wir oben definiert haben.

//wave is a uniform float
VERTEX.x += cos(time + body) * wave;

Dies sieht sehr ähnlich aus wie die Bewegung von Seite zu Seite, die wir oben definiert haben, aber in diesem Fall hat jeder Vertex entlang der Wirbelsäule eine andere Position in der Welle, so dass es so aussieht, als würde sich eine Welle entlang des Fisches bewegen, indem wir body als Offset für cos verwenden.

../../../_images/wave.gif

Die letzte Bewegung ist die Verdrehung, die eine schwenkende Rollbewegung entlang der Wirbelsäule ist. Ähnlich wie bei der Pivot-Rotation konstruieren wir zunächst eine Rotationsmatrix.

//twist is a uniform float
float twist_angle = cos(time + body) * 0.3 * twist;
mat2 twist_matrix = mat2(vec2(cos(twist_angle), -sin(twist_angle)), vec2(sin(twist_angle), cos(twist_angle)));

Wir wenden die Rotation in der xy-Achse an, so dass der Fisch um seine Wirbelsäule zu rollen scheint. Damit dies funktioniert, muss die Wirbelsäule des Fisches auf der z-Achse zentriert sein.

VERTEX.xy = twist_matrix * VERTEX.xy;

Hier ist der Fisch mit angewendeter Verdrehung:

../../../_images/twist.gif

Wenn wir alle diese Bewegungen nacheinander anwenden, erhalten wir eine flüssige geleeartige Bewegung.

../../../_images/all_motions.gif

Normale Fische schwimmen hauptsächlich mit der hinteren Hälfte ihres Körpers. Dementsprechend müssen wir die Schwenkbewegungen auf die hintere Hälfte des Fisches beschränken. Zu diesem Zweck erstellen wir eine neue Variable, mask.

mask ist ein Float, der von 0 an der Vorderseite des Fisches bis 1 am Ende reicht und smoothstep benutzt, um den Punkt zu steuern, an dem der Übergang von 0 zu 1 stattfindet.

//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);

Unten sehen Sie ein Bild des Fisches, der mask als COLOR verwendet:

../../../_images/mask.png

Für die Welle multiplizieren wir die Bewegung mit mask, wodurch sie auf die hintere Hälfte beschränkt wird.

//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;

Um die Maske auf die Verdrehung anzuwenden, benutzen wir mix. mix erlaubt es uns, die Position des Vertex zwischen einem vollständig rotierten Vertex und einem nicht rotierten zu mischen. Wir müssen mix benutzen, anstatt mask mit dem rotierten VERTEX zu multiplizieren, weil wir die Bewegung nicht zum VERTEX hinzufügen, sondern den VERTEX durch die rotierte Version ersetzen. Wenn wir das mit mask multiplizieren würden, würden wir den Fisch schrumpfen.

//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);

Wenn man die vier Bewegungen zusammenfügt, erhält man die endgültige Animation.

../../../_images/all_motions_mask.gif

Spielen Sie mit den Uniforms, um den Schwimmzyklus des Fisches zu verändern. Sie werden feststellen, dass Sie mit diesen vier Bewegungen eine große Vielfalt an Schwimmstilen erzeugen können.

Erstellen eines Fischschwarms

Godot macht es einfach, Tausende von ein und demselben Objekt mit einem MultiMeshInstance3D-Node zu rendern.

Ein MultiMeshInstance3D-Node wird auf die gleiche Weise erstellt und verwendet wie ein MeshInstance3D-Node. Für dieses Tutorial werden wir den MultiMeshInstance3D Node School nennen, da er einen Fischschwarm (engl. school of fish) enthalten wird.

Sobald Sie eine MultiMeshInstance3D haben, fügen Sie ein MultiMesh hinzu, und zu diesem MultiMesh fügen Sie Ihr Mesh mit dem Shader von oben hinzu.

MultiMeshes zeichnen Ihr Mesh mit drei zusätzlichen Propertys pro Instanz: Transform (Rotation, Translation, Skalierung), Color und Custom. Custom wird verwendet, um 4 mehrfach verwendbare Variablen mit einem Color zu übergeben.

instance_count gibt an, wie viele Instanzen des Meshes Sie zeichnen wollen. Für den Moment lassen Sie instance_count auf 0 stehen, da Sie keinen der anderen Parameter ändern können, solange instance_count größer als 0 ist. Wir werden instance count später in GDScript einstellen.

Mit transform_format wird angegeben, ob die verwendeten Transformationen 3D oder 2D sind. Für dieses Tutorial wählen Sie 3D.

Sowohl für color_format als auch für custom_data_format können Sie zwischen None, Byte und Float wählen. None bedeutet, dass Sie diese Daten (entweder eine COLOR-Variable pro Instanz, oder INSTANCE_CUSTOM) nicht an den Shader weitergeben werden. Byte bedeutet, dass jede Zahl, aus der die Farbe besteht, die Sie übergeben, mit 8 Bits gespeichert wird, während Float bedeutet, dass jede Zahl in einem Float (32 Bits) gespeichert wird. Float ist langsamer, aber präziser, Byte benötigt weniger Speicherplatz und ist schneller, aber Sie könnten möglicherweise einige visuelle Artefakte sehen.

Setzen Sie nun instance_count auf die Anzahl der Fische, die Sie haben möchten.

Als nächstes müssen wir die Transformationen für jede Instanz festlegen.

Es gibt zwei Möglichkeiten, Transformationen pro Instanz für MultiMeshes zu setzen. Die erste erfolgt vollständig im Editor und wird im MultiMeshInstance3D-Tutorial beschrieben.

Die zweite Möglichkeit besteht darin, eine Schleife über alle Instanzen zu bilden und ihre Transformationen im Code festzulegen. Im Folgenden verwenden wir GDScript, um eine Schleife über alle Instanzen zu bilden und ihre Transformation auf eine zufällige Position zu setzen.

for i in range($School.multimesh.instance_count):
  var position = Transform3D()
  position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
  $School.multimesh.set_instance_transform(i, position)

Wenn dieses Skript ausgeführt wird, werden die Fische an zufälligen Positionen in einer Box um die Position der MultiMeshInstance3D platziert.

Bemerkung

Wenn die Performance für Sie ein Problem darstellt, versuchen Sie, die Szene mit weniger Fischen auszuführen.

Haben Sie bemerkt, dass alle Fische in ihrem Schwimmzyklus die gleiche Position einnehmen? Das lässt sie sehr roboterhaft aussehen. Der nächste Schritt besteht darin, jedem Fisch eine andere Position im Schwimmzyklus zu geben, damit der gesamte Schwarm organischer aussieht.

Animieren eines Fischschwarmes

Einer der Vorteile der Animation der Fische mit Hilfe von cos-Funktionen ist, dass sie mit einem einzigen Parameter, time, animiert werden. Um jedem Fisch eine eindeutige Position im Schwimmzyklus zu geben, müssen wir nur die time verschieben.

Wir tun dies, indem wir den pro Instanz benutzerdefinierten Wert INSTANCE_CUSTOM zu time addieren.

float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

Als nächstes müssen wir einen Wert in INSTANCE_CUSTOM übergeben. Wir tun dies, indem wir eine Zeile in die for-Schleife von oben einfügen. In der for-Schleife weisen wir jeder Instanz einen Satz von vier zufälligen Floats zu, die sie verwenden soll.

$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))

Jetzt haben die Fische alle einzigartige Positionen im Schwimmzyklus. Du kannst ihnen ein wenig mehr Individualität geben, indem du INSTANCE_CUSTOM benutzt, um sie schneller oder langsamer schwimmen zu lassen, indem du mit TIME multiplizierst.

//set speed from 50% - 150% of regular speed
float time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

Sie können sogar damit experimentieren, die Farbe pro Instanz auf dieselbe Weise zu ändern, wie Sie den benutzerdefinierten Wert pro Instanz geändert haben.

Ein Problem, auf das Sie an dieser Stelle stoßen werden, ist, dass die Fische zwar animiert sind, sich aber nicht bewegen. Sie können sie durch Aktualisieren der pro-Instanz-Transformation für jeden Fisch in jedem Frame bewegen. Obwohl dies schneller ist als die Bewegung von Tausenden von MeshInstance3Ds pro Frame, wird es wahrscheinlich immer noch langsam sein.

Im nächsten Tutorial werden wir uns damit beschäftigen, wie man GPUParticles3D verwendet, um die Vorteile der GPU zu nutzen und jeden Fisch einzeln zu bewegen, ohne auf die Vorteile der Instanziierung zu verzichten.