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.

用 MultiMeshInstance3D 動畫化數以千計條魚

本教學探索了遊戲 ABZU 中使用的一種技術, 該技術使用頂點動畫和靜態網格實例, 來渲染和製作成千上萬的魚動畫.

在 Godot 中,這可以通過自訂 ShaderMultiMeshInstance 實作。使用下面的技術,即使是在低端硬體上你也可以渲染成千上萬的動畫物件。

我們將從一條魚的動畫開始. 然後, 我們將看到如何將該動畫擴充到數千條魚.

動畫化一條魚

按一下下面的材質 Plane Mesh 功能表並建立一個新的 ShaderMaterial.

這是我們用於範例圖像的魚, 您可以使用任何您喜歡的魚模型.

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

備註

本教學中的魚模型由 QuaterniusDev<http://quaternius.com> 製作, 使用如下知識共用許可.CC0 1.0通用(CC0 1.0)公共領域貢獻https://creativecommons.org/publicdomain/zero/1.0/

通常情況下,您將使用骨骼和 Skeleton 為對象做動畫。然而,骨骼的動畫是在 CPU 上做的,所以你必須為每一影格計算成千上萬的操作,因此就不可能有成千上萬的物件。在頂點著色器中使用頂點動畫,你就可以避免使用骨骼,而是完全在 GPU 上,使用幾行程式碼來計算完整的動畫。

動畫由四個關鍵影格動作組成:

  1. 從一邊運動到另一邊

  2. 繞著魚的中心作旋轉運動

  3. 平移波動運動

  4. 平移扭轉運動

所有的動畫程式碼都在頂點著色器中,並由 uniform 控制運動量。我們使用 uniform 來控制運動的強度,這樣你就可以在編輯器中調整動畫,並即時看到結果,而不用重新編譯著色器。

所有的運動都將使用餘弦波應用於模型空間中的 VERTEX . 我們希望頂點在模型空間中, 使運動總是相對於魚的方向. 例如,side-to-side將始終使魚在其左至右的方向上來回移動, 而不是在世界方向的 x 軸上.

為了控制動畫的速度,我們將通過使用 TIME 定義自己的時間變數開始。

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

我們將實施的第一項議案是左右運動. 它可以通過 TIMEcos 抵消 VERTEX.x 來製作. 每次渲染網格時, 所有頂點都會移動到 "cos(時間)" 的數量.

//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,應用到 xz 軸上。

VERTEX.xz = rotation_matrix * VERTEX.xz;

在只應用軸心的情況下,您會看到這個:

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

接下來的兩個動作需要沿著魚的脊柱平移. 為此, 我們需要一個新的變數, body . body 是一個浮點數,在魚的尾部是 0 ,在頭部是 1 .

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 ,我們用 smoothstep 來控制在哪裡進行由 01 的過渡。

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

下面是把 COLOR 設定成 mask 後這條魚的樣子:

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

我們在做波浪運動的地方乘以 mask 就可以把動作限制在後半部分。

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

為了將遮罩應用於扭曲, 我們使用 mix . mix 允許在完全旋轉的頂點和未旋轉的頂點之間混合頂點位置. 需要使用 mix 而不是將 mask 乘以旋轉後的 VERTEX , 因為不是將運動加到 VERTEX 上, 而是用旋轉後的版本替換 VERTEX . 如果把它乘以 mask , 就會把魚縮小.

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

將四個動作組合在一起, 就得到了最終的動畫效果.

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

繼續發揮 uniform 的作用, 以改變魚的游泳週期. 你會發現, 你可以用這四個動作創造出各種各樣的游泳方式.

製作一群魚

在 Godot 中可以很輕鬆地在屬性面板中建立自定資源。

MultiMeshInstance 節點的建立和使用與 MeshInstance 節點相同。在本教學中,我們將把 MultiMeshInstance 節點命名為 ``School``(魚群),因為裡面會有一群魚。

有了 MultiMeshInstance 之後就再新增一個 MultiMesh,然後在 MultiMesh 上新增使用上述著色器的 Mesh

MultiMeshes 使用三個額外的實例屬性來繪製 Mesh:變換(旋轉、平移、縮放)、顏色和自訂。自訂用於使用 Color 傳入 4 個多用途變數。

instance_count 指定要繪製的網格的實例數量。現在,將 instance_count 保留為 0,因為當 instance_count 大於 0 時,您不能更改任何其他參數。我們稍後將在 GDScript 中設定 instance_count

transform_format 指定使用的變換是 3D 還是 2D。對於本教學,請選擇 3D。

對於 color_formatcustom_data_format,你可以在 NoneByteFloat 之間選擇。None 意味著你不會將這些資料(無論是每個實例的 COLOR 變數還是 INSTANCE_CUSTOM)傳遞給著色器。Byte 意味著組成你傳入的顏色的每一個數位將被儲存為 8 位元,而 Float 意味著每一個數位將被儲存為浮點數(32 位)。Float 速度較慢但更精確,Byte 佔用記憶體較少、速度較快,但你可能會看到一些視覺上的偽像。

現在,將 instance_count 設定為您想要的魚的數量。

接下來, 我們需要設定每個實例的變換.

有兩種方法可以為多個時間軸設定每個實例的變換。第一個完全在編輯器中,在 MultiMeshInstance tutorial 中進行了描述。

第二種方法是, 走訪所有實例, 並在程式碼中設定它們的變換. 下面, 我們使用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)

運作此腳本, 會在多重網格實例位置周圍的框中隨機放置魚.

備註

如果你關注性能問題, 試著使用GLES2運作場景或擺放更少的魚.

你應該已經注意到所有魚的游泳動作都是同步的了吧?這樣看上去非常機械。下一步我們要做的就是讓每一條魚都處於游泳週期的不同位置,這樣整個魚群看起來就會更自然。

動畫魚群

使用 cos 函式給魚做動畫的一個好處是,它們只需要一個 time 參數。為了讓每條魚在游泳週期中處於單獨的位置,我們只需要偏移 time

為此,我們將每個實例的自訂值 INSTANCE_CUSTOM 新增到 time 中。

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

接下來,我們需要向 INSTANCE_CUSTOM 傳遞一個值。通過在上面的 for 迴圈中新增一行來實作這一點。在 for 迴圈中,為每個實例分配一組四個隨機浮點數來使用。

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

甚至您還可以像更改每個實例的自訂值一樣, 嘗試更改每個實例的顏色.

這時你會遇到一個問題, 那就是魚是有動畫的, 但是它們沒有移動. 你可以通過每一影格更新每個魚的實例變換來移動它們. 雖然這樣做會比每影格移動數千個MeshInstances要快, 但還是可能會很慢.

下一個教學中,我們將介紹如何使用:ref:`粒子 <class_Particles>`來利用 GPU,分別移動每條魚,同時還能獲得產生實體的好處。