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を使用してモーションの強さを制御するため、エディタでアニメーションを微調整し、シェーダーを再コンパイルせずにリアルタイムで結果を確認できます。

すべてのモーションは、モデル空間のVERTEXに適用されたコサイン波を使用して作成されます。動きが常に魚の向きに関連するように、頂点をモデル空間に配置する必要があります。たとえば、左右の動きは、ワールド方向の x 軸ではなく、必ず魚を左右に前後に移動します。

アニメーションの速度を制御するために、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)));

魚が脊椎の周りを回転するように見えるように、xy 軸に回転を適用します。これが機能するには、魚の脊椎を z 軸の中心に置く必要があります。

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;

マスクを捻りに適用するために、mix を使用します。mix を使用すると、完全に回転した頂点と回転していない頂点の間で頂点の位置を混合できます。VERTEX に動き追加するのではなく、VERTEX を回転したバージョンに置き換えているため、mask に回転したVERTEXを乗算する代わりに mix を使用する必要があります。それに mask を掛けると、魚は小さくなります。

//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を選択します。

Color FormatCustom Data Format の両方について、NoneByte、および Float から選択できます。None は、そのデータ(インスタンスごとの COLOR 変数または INSTANCE_CUSTOM のいずれか)をシェーダーに渡さないことを意味します。Byte は、渡す色を構成する各数値が8ビットで格納されることを意味し、FLoat は、各数値が浮動小数点数(32ビット)で格納されることを意味します。Float は遅くなりますが、より正確です。Byte はより少ないメモリで高速になりますが、視覚的なアーティファクトが見られることがあります。

さて、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);

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

この時点で遭遇する問題の1つは、魚はアニメーション化されていますが、移動はしていないことです。フレームごとに各魚のインスタンスごとのtransformを更新することにより、それらを移動できます。これを行うと、フレームごとに数千のMeshInstanceを移動するよりも高速になりますが、それでも低速になる可能性があります。

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