MultiMeshInstanceを使用して何千もの魚をアニメーション化する

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

Godotでは、カスタムの ShaderMultiMeshInstance でこれを実現できます。次の手法を使用すると、ローエンドのハードウェア上でも数千のアニメーションオブジェクトをレンダリングできます。

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

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

魚1匹から始めます。魚モデルを MeshInstance にロードし、新しい ShaderMaterial を追加します。

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

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

注釈

このチュートリアルの魚モデルは、QuaterniusDev によって作成され、クリエイティブコモンズライセンスと共有されます。CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/

通常、ボーンと Skeleton を使用してオブジェクトをアニメーション化します。ただし、ボーンはCPU上でアニメーション化されるため、フレームごとに数千もの操作を計算しなくてはならず、数千ものオブジェクトを持つことは不可能になります。頂点シェーダーで頂点アニメーションを使用すると、ボーンを使用せずに、数行のコードで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

最後の動きは捻りです。これは脊椎に沿ったパンロールです。ピボットと同様に、最初に回転行列を作成します。

//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 は魚の頭の 0 から尾びれの先の 1 までのfloatです。smoothstep を使用して 0 から 1 へ移行が起こるポイントを制御します。

//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では、MultiMeshInstanceノードを使用して、何千もの同じオブジェクトを簡単にレンダリングできます。

MultiMeshInstanceノードは、MeshInstanceノードを作成するのと同じ方法で作成および使用されます。このチュートリアルでは、魚の群れが含まれているため、MultiMeshInstanceノードに School という名前を付けます。

MultiMeshInstanceを作成したら、インスペクタで MultiMesh を追加し、そのMultiMeshに上からシェーダーを使用して :ref:`Mesh <class_Mesh>`を追加します。

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を設定する必要があります。

MultiMesheのインスタンスごとの変換を設定するには、2つの方法があります。 1つ目は完全にエディタで行われ、MultiMeshInstanceチュートリアル で説明されています。

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

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

このスクリプトを実行すると、MultiMeshInstanceの位置を囲むボックス内のランダムな位置に魚が配置されます。

注釈

パフォーマンスが問題になる場合は、GLES2またはより少ない魚でシーンを実行してみてください。

すべての魚が遊泳サイクルの同じ位置にあることに注意してください。彼らは非常にロボット的に見えます。次のステップでは、各魚に遊泳サイクルの異なる位置を与え、群れ全体がより有機的に見えるようにします。

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

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 MeshInstances per frame, it'll still likely be slow.

次のチュートリアルでは、Particles を使用してGPUを活用し、インスタンス化の利点を享受しながら各魚を個別に移動する方法について説明します。