Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

第一個遊戲

你已經決定開始編寫一個自訂 Spatial 著色器。或許你在網上看到一個很酷的著色器技巧,或許你發現 SpatialMaterial 並不能完全滿足你的需求。總之,你決定寫一個自己的,你想弄清楚從哪裡開始。

這個教學將說明如何編寫空間著色器, 並將涵蓋比 CanvasItem 更多的主題.

空間著色器比CanvasItem著色器有更多的內建功能. 對空間著色器的期望是:Godot為常見的用例提供了功能, 使用者僅需在著色器中設定適當的參數. 這對於PBR(基於物理的渲染)工作流來說尤其如此.

這是一個兩部分的教學. 在第一部分中, 我們將學習如何在頂點函式中使用高度圖的頂點位移來製作一個簡單的地形. 在 第二部分 中, 我們將採用本教學中的概念, 通過編寫一個海洋水著色器, 講解如何在片段著色器中設定自訂材質.

備註

這個教學假定你對著色器有一些基本的瞭解, 例如型別( vec2 , float , sampler2D ), 和函式. 如果你對這些概念摸不著頭腦, 那麼你在完成這個教學之前, 最好先從 著色器之書 <https://thebookofshaders.com/?lan=ch> 獲取一些基本知識.

在何處設定材質

在3D中, 物件是使用 Meshes 繪製的.Mesh是一種資源型別, 它以 "表面(surface)" 為單位儲存幾何體(物件的形狀)和材質(對象的顏色和對光線的反應). 一個Mesh可以有多個表面, 也可以只有一個. 通常情況下, 你會從另一個程式(如Blender)匯入一個Mesh. 但是Godot也有一些 PrimitiveMeshes 允許你在不匯入Mesh的情況下為場景新增基本幾何體.

你可以使用多種節點型別可以用來繪製Mesh. 主要的是 MeshInstance, 但你也可以使用 Particles, MultiMeshes (與 MultiMeshInstance 一起使用), 或其他.

通常情況下, 一個材質是與Mesh中的一個給定表面相關聯的, 但有些節點, 如MeshInstance, 允許你覆蓋一個特定的表面或所有表面的材質.

如果你在表面或Mesh本身上設定了材質, 那麼所有共用該Mesh的MeshInstance都共用該材質. 但是, 如果你想在多個Mesh實例中重用同一個Mesh, 但每個實例具有不同的材質, 那麼你應該在Meshinstance上設定材質.

在本教學中, 我們將材質設定在Mesh自身上, 不使用MeshInstance覆蓋材質的功能.

設定

向場景新增一個新的 MeshInstance 節點.

在屬性面板分頁中,點擊“Mesh”旁邊的“[空]”,然後選擇“新建 PlaneMesh”。然後點擊出現的平面的圖像。

這會在場景中新增一個 PlaneMesh .

然後,在視圖中,按一下左上角的“透視”按鈕。會出現一個功能表,在功能表中間找到如何顯示場景的選項。選擇“顯示線框”。

這將允許您查看構成平面的三角形.

../../../_images/plane.png

現在將 Subdivide WidthSubdivide Depth 設定為 32 .

../../../_images/plane-sub-set.webp

可以看到現在 Mesh 中有了更多的三角形. 這將為我們提供更多頂點, 便於新增更多細節.

../../../_images/plane-sub.png

表面,因此也僅有一個材質而非材質陣列。點擊“Material”旁邊的“[空]”,然後選擇“新建 ShaderMaterial”。然後點擊出現的球體。

現在點擊“Shader”旁邊寫著“[空]”的地方,選擇“新建 Shader”。

現在將彈出一個著色器編輯器, 你已經準備好編寫你的第一個空間著色器了!

著色器魔術

../../../_images/shader-editor.webp

注意到已經出現錯誤了嗎? 這是因為著色器編輯器會自動重新載入著色器. Godot著色器首先需要宣告它們是什麼型別的著色器. 因此, 我們將變數 shader_type 設定為 spatial , 因為它是一個空間著色器.

shader_type spatial;

接下來我們將定義 vertex() 函式. vertex() 函式決定你的 Mesh 在最終場景中的頂點位置. 我們用它來偏移每個頂點的高度, 使我們的平面看起來像一個小地形.

我們像這樣定義頂點著色器:

void vertex() {

}

vertex() 函式中沒有任何內容,Godot將使用其預設的頂點著色器. 我們可以簡單地通過新增一行進行更改:

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

新增此行後, 你應該會得到類似下方的圖像.

../../../_images/cos.png

好, 我們來解讀一下. VERTEXy 值正在增加. 我們將 VERTEXxz 分量作為參數傳遞給 cossin ;這樣就得到了在 xz 軸上呈現出波浪狀的圖像.

我們想要實作的是小山丘的外觀. 而 cossin 已經有點像山丘了. 我們便可以通過縮放 cossin 函式的輸入來實作.

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.png

看起來效果好了一些, 但它仍然過於尖銳和重複, 讓我們把它變得更有趣一點.

雜訊高度圖

雜訊是一種非常流行的偽造地形的工具. 可以認為它和餘弦函式一樣生成重複的小山, 只是在雜訊的影響下每個小山都擁有不同的高度.

Godot提供了 雜訊紋理 資源, 可以生成從著色器存取的雜訊紋理.

要在著色器中存取紋理,請在著色器頂部附近、vertex() 函式外部新增以下程式碼。

uniform sampler2D noise;

你可以用它將雜訊紋理發送給著色器。現在看看屬性面板中的材質。你應該會看到一個名為“Shader Params”(著色器參數)的區域。如果展開該區域,就會看到一個叫“noise”的部分。

點擊旁邊寫著“[空]”的地方,選擇“新建 NoiseTexture”。在你的 NoiseTexture 中,點擊旁邊的“Noise”,然後選擇“新建 OpenSimplexNoise”。

備註

NoiseTexture2D 使用 FastNoiseLite 來生成高度圖。

設定好後, 看起來應該像這樣.

../../../_images/noise-set.webp

現在, 使用 texture() 函式獲取雜訊紋理. texture() 將一個紋理作為第一個參數, 將在紋理上的位置 vec2 作為第二個參數. 我們用 VERTEXxz 通道來確定在紋理上的位置. 請注意 PlaneMesh 座標在 [-1,1] 範圍內 (大小為 2的情況下), 而紋理座標在 [0,1] 範圍內, 所以為了規範化, 我們將PlaneMesh的大小除以2.0並加上 0.5. texture() 返回一個目前位置 r, g, b, a 通道的 vec4 . 由於雜訊紋理是灰度的, 所有的值都相同, 所以我們可以使用任意一個通道作為高度. 本例中, 我們將使用 r , 或者說 x 通道.

void vertex() {
  float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
  VERTEX.y += height;
}

注意: xyzw 和GLSL中的 rgba 是相同的, 所以我們可以用 texture().x 代替上面的 texture().r . 詳情請參見 OpenGL 文件 .

使用此程式碼後, 你可以看到紋理建立了隨機外觀的山峰.

../../../_images/noise.png

目前它還很尖銳, 我們需要稍微柔化一下山峰. 這將用到uniform值. 你在之前已經使用了uniform 值來傳遞雜訊紋理, 現在讓我們來學習一下其中的工作原理.

Uniform

uniform值變數允許你把遊戲的變數傳遞到著色器. 它們對於控制著色器效果非常有用. 幾乎所有在著色器中使用的資料型別都可以作為uniform值. 要使用uniform值, 請在 Shader 中使用關鍵字 uniform 宣告它.

讓我們做一個改變地形高度的uniform.

uniform float height_scale = 0.5;

Godot讓你用一個值來初始化uniform;這裡, height_scale 被設定為 0.5 . 你可以通過在著色器對應的材質上呼叫函式 set_shader_param() 來從GDScript設定uniform . 從GDScript傳來的值優先於在著色器中用於初始化的值.

# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)

備註

更改uniform值時, 基於空間的節點與基於CanvasItem的節點使用的方法不同. 在這裡, 我們在PlaneMesh資源內設定材質. 在其他mesh資源中, 你可能要先呼叫 surface_get_material() 來獲取材質. 而在MeshInstance中, 則是用 get_surface_material()material_override 獲取材質.

請記住, 傳入 set_shader_param() 的字串必須與 Shader 中的uniform變數名稱相配對. 你可以在 Shader 中的任何地方使用這個uniform變數. 在這裡, 我們將用它來設定高度值, 而不是任意地乘以 0.5 .

VERTEX.y += height * height_scale;

現在它看起來好多了.

../../../_images/noise-low.png

使用 uniform,我們甚至可以在每一影格改變數值,以動畫化地形的高度。結合 Tween,這對簡單的動畫特別有用。

與光互動

首先關閉線框顯示。再次點擊視口左上角的“透視”字樣,選擇“顯示標準”。

../../../_images/normal.png

注意網格顏色是如何變得平滑的. 這是因為它的光線是平滑的. 讓我們加一盞燈吧!

首先, 我們將在場景中新增一個 OmniLight .

../../../_images/light.png

你會看到光線影響了地形, 但這看起來很奇怪. 問題是光線對地形的影響就像在平面上一樣. 這是因為光著色器使用 網格 中的法線來計算光.

法線儲存在網格中, 但是我們在著色器中改變網格的形狀, 所以法線不再正確. 為了解決這個問題, 我們可以在著色器中重新計算法線, 或者使用與我們的雜訊相對應的法線紋理.Godot讓這一切變得很簡單.

您可以在頂點函式中手動計算新的法線,然後只需設定法線 NORMAL。設定好 NORMAL 後,Godot 將為我們完成所有困難的光照計算。我們將在本教學的下一部分介紹這種方法,現在我們將從紋理中讀取法線。

相反, 我們將再次依靠雜訊來計算法線. 我們通過傳入第二個雜訊紋理來做到這一點.

uniform sampler2D normalmap;

把第二個 uniform 紋理設為另一個帶有單獨 OpenSimplexNoise 的 NoiseTexture。不過這一回,請取消勾選“As Normalmap”。

../../../_images/normal-set.webp

現在, 因為這是一個法線貼圖, 而不是每個頂點的法線, 我們將在 fragment() 函式中分配它. fragment() 函式將在本教學的下一部分中詳細解釋.

void fragment() {
}

當我們有對應某個特定頂點的法線時,就要設定 NORMAL,但如果你有一個來自紋理的法線貼圖,要使用 NORMALMAP 設定法線。這樣,Godot 將自動處理環繞網格的紋理。

最後, 為了確保我們從雜訊紋理和法線圖紋理的相同位置讀取資料, 我們將把 vertex() 函式中的 VERTEX.xz 座標傳遞給 fragment() 函式. 我們用variings來做這個.

vertex() 上面定義一個 vec2 叫做 tex_position . 在 vertex() 函式中, 將 VERTEX.xz 分配給 tex_position .

varying vec2 tex_position;

void vertex() {
  ...
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  ...
}

現在我們可以從 fragment() 函式中存取 tex_position .

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

法線就位後, 光線就會對網格的高度做出動態反應.

../../../_images/normalmap.png

我們甚至可以把燈拖來拖去, 燈光會自動更新.

../../../_images/normalmap2.png

以下是本教學的完整程式碼. 您可以看到,Godot會為您處理大多數繁瑣的事情, 本教學篇幅不會太長.

shader_type spatial;

uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;

varying vec2 tex_position;

void vertex() {
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  VERTEX.y += height * height_scale;
}

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

這就是這部分的全部內容. 希望您現在已瞭解Godot中頂點著色器的基本知識. 在本教學的下一部分中, 我們將編寫一個片段函式來配合這個頂點函式, 並且我們將介紹一種更高級的技術來將這個地形轉換成一個移動的波浪海洋.