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...
用 MultiMeshInstance3D 動畫化數千條魚
本教學將介紹遊戲 ABZU 所採用的技術,透過頂點動畫與靜態網格實例化,來繪製並動畫化成千上萬條魚。
在 Godot 中,可以透過自訂 Shader 及 MultiMeshInstance3D 來實現。應用下述技術,即使在較低階硬體上也能渲染數以千計的動畫物件。
我們將先從動畫一條魚開始,接著說明如何將動畫擴展到數千條魚。
動畫化一條魚
我們先從一條魚開始。將你的魚模型載入 MeshInstance3D,然後新增一個 ShaderMaterial。
以下是本教程範例使用的魚模型圖片,你也可以選用任何你喜歡的魚模型。
備註
本教學所用魚模型由 QuaterniusDev 製作,並以 CC0 1.0 Universal (CC0 1.0) 公共領域貢獻協議釋出:https://creativecommons.org/publicdomain/zero/1.0/
一般來說,你會用骨骼與 Skeleton3D 進行物件動畫。不過,骨骼動畫運算是在 CPU 上執行,每一影格都需計算大量操作,因此難以同時處理大量物件。若改用頂點著色器進行頂點動畫,則可完全不依賴骨骼,只需幾行程式碼就能在 GPU 上完成整個動畫運算。
動畫主要由四個關鍵動作組成:
左右擺動
以魚中心為軸的旋轉
平移波浪運動
平移扭轉運動
所有動畫程式碼都寫在頂點著色器中,透過 uniform 變數來控制動作幅度。這讓你可以在編輯器中調整動畫強度,並即時預覽結果,而不用重新編譯著色器。
所有動作會利用餘弦波來變動模型空間的 VERTEX。我們使用模型空間,是為了讓動作總是依據魚的自身方向。例如,左右擺動時會隨魚的左右方向移動,不會跟著世界座標的 x 軸動。
為了控制動畫速度,我們會先用 TIME 定義自己的時間變數。
//time_scale is a uniform float
float time = TIME * time_scale;
我們首先實作的是左右擺動。只要用 cos(TIME) 去偏移 VERTEX.x,每次網格渲染時,所有頂點就會根據 cos(time) 的數值左右擺動。
//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;
這個動畫效果大致如下:
接著,我們加入旋轉軸。由於魚的中心點在 (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)));
然後將旋轉作用於 x 與 z 軸,只要把旋轉矩陣乘上 VERTEX.xz 即可。
VERTEX.xz = rotation_matrix * VERTEX.xz;
僅套用旋轉軸後,你會看到如下效果:
接下來的兩個動作需要沿著魚的脊椎方向平移。為此,我們要用一個新變數 body。body 是一個浮點值,尾巴端為 0,魚頭端為 1。
float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2
下一個動畫是沿著魚身長度滑動的餘弦波。為了讓波浪沿魚脊椎推移,我們用 body 這個變數來偏移 cos 的輸入值。
//wave is a uniform float
VERTEX.x += cos(time + body) * wave;
這個動作看起來跟前面的左右擺動很像,但這裡每個頂點因為 body 偏移而處在波浪曲線的不同位置,所以效果像是一個波浪順著魚身流動。
最後一個動作是扭轉,也就是沿脊椎方向的滾動。我們同樣先建立一個旋轉矩陣。
//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;
這是加上扭轉後的魚:
若將上述所有動作依序套用,就會得到類似果凍般流暢的運動感。
一般魚類主要用身體後半部游泳。因此,我們需要將波浪和平移的動作限制在魚的後半部。為此,我們會新增一個變數 mask。
mask 是個浮點值,利用 smoothstep 讓它從魚頭(0)平滑過渡到魚尾(1),用來控制動作作用區域。
//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);
下圖為將 mask 直接用於 COLOR 時魚的樣子:
對於波浪動作,只要將其乘以 mask,即可將影響範圍限制在後半部。
//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;
要將遮罩應用在扭轉動作上,我們會用 mix。mix 可以在未旋轉和完全旋轉之間混合頂點位置。這裡不能直接用 mask 乘旋轉後的 VERTEX,否則會導致魚縮小,所以必須用 mix 進行混和。
//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);
將這四個動作結合,就能得到最終的魚動畫效果。
你可以多調整不同 uniform 參數,改變魚的游泳週期。只靠這四種動作組合,就能創造各種不同的游泳風格。
製作魚群
Godot 利用 MultiMeshInstance3D 節點,能輕鬆渲染數以千計的同類物件。
MultiMeshInstance3D 的建立與使用方式和 MeshInstance3D 節點類似。在本教學中,我們將 MultiMeshInstance3D 節點命名為 School,因為這裡會放一整群魚。
建立 MultiMeshInstance3D 後,請新增一個 MultiMesh ,並將上面用到的 Mesh (含著色器)加入 MultiMesh。
MultiMesh 除了一般網格外,還能為每個實例設定三種屬性:變換(旋轉、平移、縮放)、顏色與自訂資料。自訂屬性可用 Color 傳遞 4 個浮點數作各種用途。
instance_count 決定要實例化幾個網格。現在可以先設成 0,因為只要大於 0,其他參數就無法修改。我們稍後會用 GDScript 設定 instance_count。
transform_format 用來指定要用 3D 還是 2D 變換。本教學請選擇 3D。
color_format 和 custom_data_format 都可選擇 None、Byte 或 Float。None 代表不傳送這些資料(無論是 COLOR 或 INSTANCE_CUSTOM)給著色器。Byte 會用 8 位元儲存每個數值,Float 則用 32 位元浮點數。Float 精度高但較慢,Byte 記憶體用量少且速度快,但可能出現畫面誤差。
現在請將 instance_count 設為你想要的魚的數量。
接著要設定每個實例的變換資訊。
MultiMesh 的每個實例變換可以有兩種設定方式:一種完全在編輯器中完成,詳見 MultiMeshInstance3D 教學。
另一種做法是用程式逐一設定每個實例的變換。以下例子用 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)
執行此腳本會將所有魚隨機分布在 MultiMeshInstance3D 節點附近的方形區域中。
備註
如果效能不佳,請嘗試減少魚的數量來執行場景。
你會發現所有魚的游泳動作都同步,看起來很機械化。接下來我們要讓每條魚在週期中處於不同的位置,讓整個魚群更自然。
動畫化魚群
利用 cos 函式為魚做動畫的一大優點是,只需一個 time 參數。只要給每條魚一個不同的時間偏移,就能讓牠們的動作週期錯開。
我們只需要將每個實例的自訂值 INSTANCE_CUSTOM 加到 time 上即可。
float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
接下來要將值傳入 INSTANCE_CUSTOM。只要在前述 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);
你也可以像設定自訂值那樣,隨機指定每個實例的顏色。
這時你會發現雖然魚有動畫,但它們其實沒移動。你可以每一影格去更新每條魚的實例變換,雖然這比動態更新上千個 MeshInstance3D 要快,但仍然可能不夠流暢。
在下一個教學中,我們會介紹如何用 GPUParticles3D 來善用 GPU,不僅能獨立移動每條魚,也能維持實例化的效能優勢。