你的第二個 3D 著色器
從高階來看,Godot 提供給使用者一組可以選擇設定的參數(如 AO、SSS_Strength、RIM 等)。這些參數對應到不同的複雜效果(環境光遮蔽,次表面散射,邊緣光等)。如果沒有對這些參數賦值,對應的程式碼會在編譯前被移除,因此著色器不會產生額外的效能負擔。這讓使用者能夠輕鬆享有複雜且正確的 PBR 著色效果,而無需撰寫複雜的著色器。當然,Godot 也允許你完全忽略這些參數,直接撰寫完全自訂的著色器。
完整參數列表請參考 空間著色器 文件。
頂點函式和片段函式的差異在於,頂點函式是針對每個頂點運作並設定像是 VERTEX``(位置)和 ``NORMAL 等屬性,而片段著色器則是針對每個像素運作,最重要的是設定 MeshInstance3D 的 ALBEDO 顏色。
你的第一個空間片段函式
如同本教學前面所提到,在 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);
}
我們把它設為非常深的藍色,因為水的藍色多半來自天空的反射。
Godot 使用的 PBR 模型主要依賴兩個參數:「金屬度」(METALLIC)與「粗糙度」(ROUGHNESS)。
ROUGHNESS 決定材質表面的平滑或粗糙程度。較低的 ROUGHNESS 會讓材質看起來像閃亮的塑膠,較高的 ROUGHNESS 則會讓顏色更為沉穩不反光。
METALLIC 指定物體的金屬感強度,建議設為接近 0 或 1。你可以把 METALLIC 想成是在反射與 ALBEDO 顏色間做平衡。高 METALLIC 幾乎完全忽略 ALBEDO,看起來就像天空的鏡子;低 METALLIC 則讓天空色與 ALBEDO 顏色都能表現出來。
ROUGHNESS (粗糙度)從左到右由 0 增加到 1 , METALLIC (金屬度)則從上到下由 0 增加到 1 。
備註
正確的 PBR 著色時,METALLIC 應接近 0 或 1,只有在材質混合時才設在兩者之間。
水不是金屬,所以我們把 METALLIC 設為 0.0。水的反射性很高,因此 ROUGHNESS 也要設得很低。
void fragment() {
METALLIC = 0.0;
ROUGHNESS = 0.01;
ALBEDO = vec3(0.1, 0.3, 0.5);
}
現在我們得到的是一個光滑的塑膠表面。接著,我們要思考想要模擬哪些水的特殊屬性。有兩個主要特性能讓這個表面從奇怪的塑膠感變成漂亮的水面:第一是鏡面反射(Specular Reflection),也就是你看到太陽直射時水面出現的亮點;第二是菲涅耳反射(Fresnel Reflectance),也就是物體在較淺的觀察角度時反射性會變強。這就是為什麼你可以看清楚腳下的水,但遠一點就只能看到天空反射。
為了加強鏡面反射,我們會做兩件事。首先,把鏡面反射的算繪模式設為 toon,因為 toon 模式會有更明顯的高光區。
render_mode specular_toon;
再來,我們會加上邊緣光(rim lighting)。邊緣光會讓物件邊緣在掠射角度下更明亮。一般常用來模擬布料邊緣的透光,但在這裡能幫助我們做出漂亮的水面質感。
void fragment() {
RIM = 0.2;
METALLIC = 0.0;
ROUGHNESS = 0.01;
ALBEDO = vec3(0.1, 0.3, 0.5);
}
為了加入菲涅耳反射,我們會在片段著色器裡計算一個簡化版的菲涅耳項。這裡為了效能不採用完整公式,而是用 NORMAL 和 VIEW 向量的點積來近似。NORMAL 向量是指向網格表面外側,而 VIEW 向量則是從該點指向觀察者的方向。這兩個向量的點積可以幫我們判斷是正面看還是斜著看表面。
float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
然後把它混合進 ROUGHNESS 和 ALBEDO。這也是 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);
}
現在只需要五行程式碼,你就能做出複雜的水面。既然有了打光,這個水面看起來太亮了。我們把它調暗一點,只要把傳給 ALBEDO 的 vec3 數值調小就行。設成 vec3(0.01, 0.03, 0.05) 吧。
使用 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() 函式更複雜一點,將 position 用 TIME 的餘弦做偏移。
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;
}
這樣你會得到:
正弦波的形狀太明顯了,我們把波形拉開一點,用縮放 position 來做到。
float height(vec2 position, float time) {
float h = wave(position * 0.4);
return h;
}
現在看起來好多了。
如果我們把多個波浪用不同頻率和振幅疊加在一起,效果會更自然。意思就是針對每個波浪調整位置的縮放(頻率),再用不同倍數調整波的高度(振幅)。
這裡有個例子:如何用四層波浪混合出更漂亮的波形。
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 範圍內。
有了這段程式碼,你就能做出更複雜的波浪效果,只需加點數學運算!
更多關於空間著色器的資訊,請參閱 Shading Language 文件與 Spatial Shaders 文件。你也可以參考 著色 以及 3D 章節中的進階教學。