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.

你的第二個 3D 著色器

從高階來看,Godot 提供給使用者一組可以選擇設定的參數(如 AOSSS_StrengthRIM 等)。這些參數對應到不同的複雜效果(環境光遮蔽,次表面散射,邊緣光等)。如果沒有對這些參數賦值,對應的程式碼會在編譯前被移除,因此著色器不會產生額外的效能負擔。這讓使用者能夠輕鬆享有複雜且正確的 PBR 著色效果,而無需撰寫複雜的著色器。當然,Godot 也允許你完全忽略這些參數,直接撰寫完全自訂的著色器。

完整參數列表請參考 空間著色器 文件。

頂點函式和片段函式的差異在於,頂點函式是針對每個頂點運作並設定像是 VERTEX``(位置)和 ``NORMAL 等屬性,而片段著色器則是針對每個像素運作,最重要的是設定 MeshInstance3DALBEDO 顏色。

你的第一個空間片段函式

如同本教學前面所提到,在 Godot 中片段函式的標準用法是設定不同的材質屬性,剩下的則交由 Godot 處理。為了提供更大的彈性,Godot 也提供了所謂的算繪模式(render modes)。算繪模式設定在著色器最上方,緊接在 shader_type 下面,用來指定你希望著色器內建有哪些功能。

例如,如果你不希望燈光影響物件,可以將算繪模式設為 unshaded

render_mode unshaded;

你也可以同時套用多個算繪模式。例如,如果你想要用卡通著色而非寫實的 PBR 著色,可以把漫反射模式和高光模式都設為 toon:

render_mode diffuse_toon, specular_toon;

這種內建功能模型讓你只需調整幾個參數,就能寫出複雜的自訂著色器。

完整的算繪模式列表請參考 空間著色器參考 文件。

在本教學這一部分,我們會說明如何把前一部分的崎嶇地形變成海洋。

首先,來設定水的顏色。我們可以直接設定 ALBEDO

ALBEDO 是一個 vec3,用來表示物件的顏色。

我們來把它設成好看的藍色。

void fragment() {
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/albedo.png

我們把它設為非常深的藍色,因為水的藍色多半來自天空的反射。

Godot 使用的 PBR 模型主要依賴兩個參數:「金屬度」(METALLIC)與「粗糙度」(ROUGHNESS)。

ROUGHNESS 決定材質表面的平滑或粗糙程度。較低的 ROUGHNESS 會讓材質看起來像閃亮的塑膠,較高的 ROUGHNESS 則會讓顏色更為沉穩不反光。

METALLIC 指定物體的金屬感強度,建議設為接近 01。你可以把 METALLIC 想成是在反射與 ALBEDO 顏色間做平衡。高 METALLIC 幾乎完全忽略 ALBEDO,看起來就像天空的鏡子;低 METALLIC 則讓天空色與 ALBEDO 顏色都能表現出來。

ROUGHNESS (粗糙度)從左到右由 0 增加到 1METALLIC (金屬度)則從上到下由 0 增加到 1

../../../_images/PBR.png

備註

正確的 PBR 著色時,METALLIC 應接近 01,只有在材質混合時才設在兩者之間。

水不是金屬,所以我們把 METALLIC 設為 0.0。水的反射性很高,因此 ROUGHNESS 也要設得很低。

void fragment() {
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/plastic.png

現在我們得到的是一個光滑的塑膠表面。接著,我們要思考想要模擬哪些水的特殊屬性。有兩個主要特性能讓這個表面從奇怪的塑膠感變成漂亮的水面:第一是鏡面反射(Specular Reflection),也就是你看到太陽直射時水面出現的亮點;第二是菲涅耳反射(Fresnel Reflectance),也就是物體在較淺的觀察角度時反射性會變強。這就是為什麼你可以看清楚腳下的水,但遠一點就只能看到天空反射。

為了加強鏡面反射,我們會做兩件事。首先,把鏡面反射的算繪模式設為 toon,因為 toon 模式會有更明顯的高光區。

render_mode specular_toon;
../../../_images/specular-toon.png

再來,我們會加上邊緣光(rim lighting)。邊緣光會讓物件邊緣在掠射角度下更明亮。一般常用來模擬布料邊緣的透光,但在這裡能幫助我們做出漂亮的水面質感。

void fragment() {
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/rim.png

為了加入菲涅耳反射,我們會在片段著色器裡計算一個簡化版的菲涅耳項。這裡為了效能不採用完整公式,而是用 NORMALVIEW 向量的點積來近似。NORMAL 向量是指向網格表面外側,而 VIEW 向量則是從該點指向觀察者的方向。這兩個向量的點積可以幫我們判斷是正面看還是斜著看表面。

float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));

然後把它混合進 ROUGHNESSALBEDO。這也是 ShaderMaterial 相較於 StandardMaterial3D 的優勢。StandardMaterial3D 只能用紋理或單一數值來設定這些屬性,但有了著色器就能用任何你想得到的數學公式來設定。

void fragment() {
  float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01 * (1.0 - fresnel);
  ALBEDO = vec3(0.1, 0.3, 0.5) + (0.1 * fresnel);
}
../../../_images/fresnel.png

現在只需要五行程式碼,你就能做出複雜的水面。既然有了打光,這個水面看起來太亮了。我們把它調暗一點,只要把傳給 ALBEDOvec3 數值調小就行。設成 vec3(0.01, 0.03, 0.05) 吧。

../../../_images/dark-water.png

使用 TIME 製作動畫

回到頂點函式,我們可以用內建變數 TIME 來讓波浪動起來。

TIME 是內建變數,頂點和片段函式都能用。

上一個教學我們是用高度圖來計算高度。這次我們也一樣,只是把高度圖的程式碼寫在一個叫 height() 的函式裡。

float height(vec2 position) {
  return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
}

如果要在 height() 函式裡用 TIME,就必須把它傳進去。

float height(vec2 position, float time) {
}

記得在頂點函式裡正確傳遞這個參數。

void vertex() {
  vec2 pos = VERTEX.xz;
  float k = height(pos, TIME);
  VERTEX.y = k;
}

這次我們不是用法線貼圖來計算法線,而是在 vertex() 函式裡手動計算。請用下列程式碼。

NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));

我們要手動計算 NORMAL,因為下一節會用數學方式做出更複雜的波浪。

現在,我們要讓 height() 函式更複雜一點,將 positionTIME 的餘弦做偏移。

float height(vec2 position, float time) {
  vec2 offset = 0.01 * cos(position + time);
  return texture(noise, (position / 10.0) - offset).x;
}

這樣做出來的波浪會慢慢移動,但還不太自然。下一節會再深入一點,用更多數學函式讓波浪更真實。

進階效果:波浪

著色器的強大就在於你可以用數學實現各種複雜效果。為了說明這點,接下來我們會修改 height() 並加入一個新的 wave() 函式,讓波浪效果更上一層樓。

wave() 只有一個參數 position,和 height() 裡的一樣。

我們會在 height() 裡多次呼叫 wave(),來組合出各種波浪外觀。

float wave(vec2 position){
  position += texture(noise, position / 10.0).x * 2.0 - 1.0;
  vec2 wv = 1.0 - abs(sin(position));
  return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
}

一開始看起來很複雜,我們就一行一行來解釋。

position += texture(noise, position / 10.0).x * 2.0 - 1.0;

noise 紋理把位置做偏移,這樣波浪會彎曲,不會完全沿著網格變成直線。

vec2 wv = 1.0 - abs(sin(position));

sin()position 定義一個波浪函式。一般 sin() 波都是圓弧狀,我們用 abs() 絕對值讓它有明顯波峰,並限制在 0 到 1 的範圍,再用 1.0 減去它,把波峰推到上方。

return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);

將 x 方向的波和 y 方向的波相乘,並做冪次運算讓波峰更尖銳。再從 1.0 減去它,讓山脊變成波峰,並再次做冪次運算加強效果。

現在我們可以直接用 wave() 取代 height() 函式的內容。

float height(vec2 position, float time) {
  float h = wave(position);
  return h;
}

這樣你會得到:

../../../_images/wave1.png

正弦波的形狀太明顯了,我們把波形拉開一點,用縮放 position 來做到。

float height(vec2 position, float time) {
  float h = wave(position * 0.4);
  return h;
}

現在看起來好多了。

../../../_images/wave2.png

如果我們把多個波浪用不同頻率和振幅疊加在一起,效果會更自然。意思就是針對每個波浪調整位置的縮放(頻率),再用不同倍數調整波的高度(振幅)。

這裡有個例子:如何用四層波浪混合出更漂亮的波形。

float height(vec2 position, float time) {
  float d = wave((position + time) * 0.4) * 0.3;
  d += wave((position - time) * 0.3) * 0.3;
  d += wave((position + time) * 0.5) * 0.2;
  d += wave((position - time) * 0.6) * 0.2;
  return d;
}

注意:我們把時間加在兩個波浪上,另外兩個則減去時間,這樣波浪就會往不同方向移動產生複雜的效果。另外,所有振幅(也就是乘上的數字)加起來要等於 1.0,這樣波浪才會維持在 0-1 範圍內。

有了這段程式碼,你就能做出更複雜的波浪效果,只需加點數學運算!

../../../_images/wave3.png

更多關於空間著色器的資訊,請參閱 Shading Language 文件與 Spatial Shaders 文件。你也可以參考 著色 以及 3D 章節中的進階教學。