Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
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, this can be accomplished with a custom Shader and a MultiMeshInstance3D. Using the following technique you can render thousands of animated objects, even on 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.

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:
Eine Bewegung von Seite zu Seite
Eine Pivot-Drehbewegung um das Zentrum des Fisches
Eine schwenkende Wellenbewegung
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:

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:

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.

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:

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

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:

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.

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.