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.

Animating thousands of fish with MultiMeshInstance3D

このチュートリアルでは、頂点アニメーションと静的メッシュのインスタンス化を使用して、何千もの魚をレンダリングおよびアニメーション化するために、ゲームで使用されるテクニック `ABZU <https://www.gdcvault.com/play/1024409/Creating-the-Art-of-ABZ>`_を探ります。

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.

まず、1匹の魚をアニメーション化することから始めます。次に、そのアニメーションを何千もの魚に拡張する方法を見ていきます。

1匹の魚のアニメーション

We will start with a single fish. Load your fish model into a MeshInstance3D and add a new ShaderMaterial.

これがサンプル画像に使用する魚です。好きな魚モデルを使用できます。

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

注釈

The fish model in this tutorial is made by QuaterniusDev and is shared with a creative commons license. CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/

Typically, you would use bones and a Skeleton3D to animate objects. However, bones are animated on the CPU and so you end having to calculate thousands of operations every frame and it becomes impossible to have thousands of objects. Using vertex animation in a vertex shader, you avoid using bones and can instead calculate the full animation in a few lines of code and completely on the GPU.

アニメーションは、4つのキーモーションで構成されます:

  1. 左右の動き

  2. 魚の中心の周を旋回する動き

  3. 揺ら揺らした波状の動き

  4. 揺ら揺らした捻る動き

アニメーションのすべてのコードは、モーションの量を制御するuniformを持つ頂点シェーダーにあります。uniformを使用してモーションの強さを制御するため、エディタでアニメーションを微調整し、シェーダーを再コンパイルせずにリアルタイムで結果を確認できます。

All the motions will be made using cosine waves applied to VERTEX in model space. We want the vertices to be in model space so that the motion is always relative to the orientation of the fish. For example, side-to-side will always move the fish back and forth in its left to right direction, instead of on the x axis in the world orientation.

アニメーションの速度を制御するために、TIME を使用して独自の時間変数を定義することから始めます。

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

実装する最初のモーションは、左右のモーションです。これは、VERTEX.xTIMEcos でオフセットすることで作成できます。メッシュがレンダリングされるたびに、すべての頂点が cos(time) の量だけ横に移動します。

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

結果のアニメーションは次のようになります:

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

次に、ピボットを追加します。魚の中心は (0, 0) なので、魚の中心の周りを回転させるために、VERTEX に回転行列を掛けるだけで済みます。

次のような回転行列を作成します:

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

そして、それを VERTEX.xz に乗算して x および z 軸に回転を適用します。

VERTEX.xz = rotation_matrix * VERTEX.xz;

ピボットのみを適用すると、次のように表示されます:

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

次の2つの動きは、魚の背骨をパンダウンする必要があります。そのためには、新しい変数 body が必要です。 body は、魚の尾が 0 、頭に 1 のfloatです。

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

次の動きは、魚の長さを下るコサイン波です。それを魚の背骨に沿って移動させるために、背骨に沿った位置で cos への入力をオフセットします。これは上で定義した変数 body です。

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

これは上で定義した左右の動きに非常に似ていますが、この例では、body を使用して cos をオフセットすることにより、脊椎に沿った各頂点が波の中で異なる位置を持ち、波は魚に沿って動いているように見えます。

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

The last motion is the twist, which is a panning roll along the spine. Similarly to the pivot, we first construct a rotation matrix.

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

We apply the rotation in the xy axes so that the fish appears to roll around its spine. For this to work, the fish's spine needs to be centered on the z axis.

VERTEX.xy = twist_matrix * VERTEX.xy;

捻りが適用された魚は次のとおりです:

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

これらすべての動きを次々に適用すると、滑らかなゼリーのような動きが得られます。

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

通常の魚は主に体の後ろ半分で泳ぎます。したがって、パンの動きを魚の後ろ半分に制限する必要があります。これを行うために、新しい変数 mask を作成します。

mask is a float that goes from 0 at the front of the fish to 1 at the end using smoothstep to control the point at which the transition from 0 to 1 happens.

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

以下は、maskCOLOR として使用した魚の画像です:

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

波については、動きに mask を掛けて、後ろ半分に制限します。

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

In order to apply the mask to the twist, we use mix. mix allows us to mix the vertex position between a fully rotated vertex and one that is not rotated. We need to use mix instead of multiplying mask by the rotated VERTEX because we are not adding the motion to the VERTEX we are replacing the VERTEX with the rotated version. If we multiplied that by mask, we would shrink the fish.

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

4つのモーションを組み合わせると、最終的なアニメーションが作成されます。

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

さあ、魚の泳ぎ方を変えるためにunifomで遊んでください。これらの4つのモーションを使用して、さまざまな遊泳スタイルを作成できることがわかります。

魚の群れを作る

Godot makes it easy to render thousands of the same object using a MultiMeshInstance3D node.

A MultiMeshInstance3D node is created and used the same way you would make a MeshInstance3D node. For this tutorial, we will name the MultiMeshInstance3D node School, because it will contain a school of fish.

Once you have a MultiMeshInstance3D add a MultiMesh, and to that MultiMesh add your Mesh with the shader from above.

MultiMeshは、インスタンスごとの3つの追加プロパティでメッシュを描画します: 幾何学変換(回転、移動、スケール)、色、およびカスタム。カスタムは、Color を使用して4つの多目的変数を渡すために使用されます。

instance_count は、描画するメッシュのインスタンスの数を指定します。とりあえず、instance_count0 より大きい間は他のパラメーターを変更できないため、instance_count0 のままにします。後でGDScriptで instance_count を設定します。

``Transform Format``は、使用される変換が3Dか2Dかを指定します。このチュートリアルでは、3Dを選択します。

For both color_format and custom_data_format you can choose between None, Byte, and Float. None means you won't be passing in that data (either a per-instance COLOR variable, or INSTANCE_CUSTOM) to the shader. Byte means each number making up the color you pass in will be stored with 8 bits while Float means each number will be stored in a floating-point number (32 bits). Float is slower but more precise, Byte will take less memory and be faster, but you may see some visual artifacts.

さて、Instance Count を持ちたい魚の数に設定してください。

次に、インスタンスごとのtransformを設定する必要があります。

There are two ways to set per-instance transforms for MultiMeshes. The first is entirely in editor and is described in the MultiMeshInstance3D tutorial.

2つ目は、すべてのインスタンスをループ処理し、コード内でそれらのtansformを設定することです。以下では、GDScriptを使用してすべてのインスタンスをループし、それらの変換をランダムな位置に設定します。

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)

Running this script will place the fish in random positions in a box around the position of the MultiMeshInstance3D.

注釈

If performance is an issue for you, try running the scene with fewer fish.

Notice how all the fish are all in the same position in their swim cycle? It makes them look very robotic. The next step is to give each fish a different position in the swim cycle so the entire school looks more organic.

魚の群れをアニメーション化する

cos 関数を使用して魚をアニメーション化する利点の1つは、1つのパラメーター time でアニメーション化されることです。各魚に遊泳サイクルのユニークな位置を与えるために、``time``をオフセットする必要があります。

インスタンスごとのカスタム値 INSTANCE_CUSTOMtime に加算することでそれを行います。

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

次に、値を INSTANCE_CUSTOM に渡す必要があります。そのためには、上から for ループに1行追加します。for ループでは、各インスタンスに使用する4つのランダムフロートのセットを割り当てます。

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

今、魚はすべて遊泳サイクルでユニークな位置を持っています。INSTANCE_CUSTOM を使用して、TIME を掛けることでより速くまたは遅く泳ぐことで、彼らにもう少し個性を与えることができます。

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

インスタンスごとのカスタム値を変更したのと同じ方法で、インスタンスごとの色の変更を試すこともできます。

One problem that you will run into at this point is that the fish are animated, but they are not moving. You can move them by updating the per-instance transform for each fish every frame. Although doing so will be faster than moving thousands of MeshInstance3Ds per frame, it'll still likely be slow.

In the next tutorial we will cover how to use GPUParticles3D to take advantage of the GPU and move each fish around individually while still receiving the benefits of instancing.