進階後期處理
前言
本教學說明在 Godot 中進行進階後期處理的方法,特別是如何編寫會使用深度緩衝區的後期處理著色器。您應該已經熟悉後期處理的基本概念,尤其是 自訂後期處理教學 中介紹的方法。
全螢幕四邊形
製作自訂後期處理特效的一種方式是使用 Viewport。然而,使用 Viewport 有兩個主要缺點:
無法存取深度緩衝區
在編輯器中看不到後期處理著色器的效果
為了繞過無法使用深度緩衝區的限制,請使用 MeshInstance3D 並搭配 QuadMesh 基元。這樣我們就可以使用著色器來存取場景的深度紋理。接下來,請利用頂點著色器讓這個四邊形始終覆蓋螢幕,讓後期處理效果隨時生效,包含在編輯器內。
首先,新建一個 MeshInstance3D,並把它的網格設為 QuadMesh。這會建立一個以座標 (0, 0, 0) 為中心,寬高為 1 的四邊形。請將寬度和高度都設為 2,並勾選 Flip Faces。目前這個四邊形位於世界座標原點,但我們希望它能隨著相機移動,讓它始終覆蓋整個畫面。為達到這個目的,我們要繞過一般會將頂點位置轉換到不同座標空間的轉換流程,直接將頂點視為已在裁剪空間中。
頂點著色器預期的輸出座標應為裁剪空間座標,螢幕左下為 -1,右上為 1。這也是為什麼 QuadMesh 的寬高要設為 2。Godot 會自動處理從模型空間到視圖空間再到裁剪空間的轉換,因此我們需要消除這些自動轉換的影響,方法就是直接設定內建的 POSITION 變數。設定 POSITION 會略過內建的轉換流程,讓頂點座標直接呈現於裁剪空間。
shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;
void vertex() {
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}
備註
在 4.3 以前的 Godot 版本,這段程式碼建議使用 POSITION = vec4(VERTEX, 1.0);,這隱含假設裁剪空間的近裁剪面在 0.0。但在 4.3 之後,Godot 採用「反向 Z」深度緩衝區,近裁剪面為 1.0,因此這種寫法已不正確,無法在 4.3 以上版本運作。
即使有這樣的頂點著色器,四邊形還是會消失。這是因為視錐剔除是由 CPU 執行的。視錐剔除會使用相機矩陣和 Mesh 的 AABB 來判斷該 Mesh 是否可見,再決定是否傳送到 GPU。CPU 不知道我們如何處理頂點,所以它認為你指定的座標是世界座標,而不是裁剪空間,導致當視角偏離場景中心時 Godot 會剔除這個四邊形。為了避免這個問題,有幾種作法:
將 QuadMesh 加為相機的子節點,讓相機始終指向它
將 QuadMesh 的幾何屬性
extra_cull_margin設為盡量大
第二種方式可確保四邊形在編輯器中可見,第一種方式則能讓它即使相機離開剔除邊界也仍然可見。你也可以同時採用這兩種方式。
深度紋理
要讀取深度紋理,需先建立一個使用 hint_depth_texture 的紋理 uniform 來指向深度緩衝區。
uniform sampler2D depth_texture : hint_depth_texture;
定義後,即可用 texture() 函式讀取深度紋理。
float depth = texture(depth_texture, SCREEN_UV).x;
備註
類似於讀取螢幕紋理,只有從目前視口讀取時才能取得深度紋理。你無法從已渲染的其他視口取得深度紋理。
depth_texture 回傳的數值範圍為 1.0 到 0.0``(分別對應近裁剪面和遠裁剪面,因為採用「反向 Z」深度緩衝區),且這些值是非線性的。若直接顯示 ``depth_texture 的數值,除了極靠近相機的區域,其餘畫面幾乎全黑。若要讓深度值能對應到世界或模型座標,需要將其線性化。當投影矩陣套用到頂點時,z 值會變為非線性,因此要線性化時,我們需乘上投影矩陣的逆矩陣,這在 Godot 中可以用 INV_PROJECTION_MATRIX 取得。
首先,取得螢幕空間座標,並將其轉換為正規化裝置座標(NDC)。在 Vulkan 後端下,NDC 的 x 與 y 軸範圍為 -1.0 到 1.0,z 軸則為 0.0 到 1.0。可用 SCREEN_UV 重建 x、y 軸,z 軸則用深度值來重建。
void fragment() {
float depth = texture(depth_texture, SCREEN_UV).x;
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
}
備註
本教學假設你使用 Forward+ 或 Mobile 渲染器,這兩者都採用 Z 範圍為 [0.0, 1.0] 的 Vulkan NDC。相較之下,Compatibility 渲染器則採用 Z 範圍為 [-1.0, 1.0] 的 OpenGL NDC。若使用 Compatibility 渲染器,請改用以下的 NDC 計算方式:
vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
你也可以使用內建的 CURRENT_RENDERER 及 RENDERER_COMPATIBILITY 常數,在著色器內支援所有渲染器:
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
#endif
將 NDC 乘上 INV_PROJECTION_MATRIX 可轉換到視圖空間。回顧一下,視圖空間的座標是相對於相機的,因此 z 值就是該點到相機的距離。
void fragment() {
...
vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
view.xyz /= view.w;
float linear_depth = -view.z;
}
由於相機朝向負 z 軸,因此座標的 z 值會是負數。為了取得可用的深度值,需要把 view.z 取負號。
要從深度緩衝區重建世界座標,可使用下述程式碼,並以 INV_VIEW_MATRIX 將座標從視圖空間轉換到世界空間。
void fragment() {
...
vec4 world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
vec3 world_position = world.xyz / world.w;
}
範例著色器
只要加上一行輸出到 ALBEDO,就成為完整的著色器。根據你註解哪一行,這個著色器可以用來視覺化線性深度或世界座標。
shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;
uniform sampler2D depth_texture : hint_depth_texture;
void vertex() {
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}
void fragment() {
float depth = texture(depth_texture, SCREEN_UV).x;
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
view.xyz /= view.w;
float linear_depth = -view.z;
vec4 world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
vec3 world_position = world.xyz / world.w;
// Visualize linear depth
ALBEDO.rgb = vec3(fract(linear_depth));
// Visualize world coordinates
//ALBEDO.rgb = fract(world_position).xyz;
}
最佳化
你可以改用一個大三角形來取代全螢幕四邊形。原因可以參考 這裡。不過這樣的優勢其實很小,只有在片段著色器非常複雜時才較有幫助。
將 MeshInstance3D 的 Mesh 設為 ArrayMesh。ArrayMesh 可以讓你用頂點、法線、顏色等陣列輕鬆建構網格。
接著,將腳本掛載到 MeshInstance3D,並使用以下程式碼:
extends MeshInstance3D
func _ready():
# Create a single triangle out of vertices:
var verts = PackedVector3Array()
verts.append(Vector3(-1.0, -1.0, 0.0))
verts.append(Vector3(-1.0, 3.0, 0.0))
verts.append(Vector3(3.0, -1.0, 0.0))
# Create an array of arrays.
# This could contain normals, colors, UVs, etc.
var mesh_array = []
mesh_array.resize(Mesh.ARRAY_MAX) #required size for ArrayMesh Array
mesh_array[Mesh.ARRAY_VERTEX] = verts #position of vertex array in ArrayMesh Array
# Create mesh from mesh_array:
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_array)
備註
三角形的頂點座標是用正規化裝置座標(NDC)指定的。NDC 的 x、y 軸都是從 -1.0 到 1.0,也就是螢幕寬高各 2。為了用單一三角形覆蓋整個螢幕,需用一個寬高都是 4 的三角形(也就是寬高都加倍)。
套用上面相同的頂點著色器,畫面效果就會一樣。
用 ArrayMesh 取代 QuadMesh 的一個缺點是 ArrayMesh 在編輯器裡不會顯示,因為三角形要等到場景執行時才建立。若要解決這個問題,可以在 3D 建模軟體中建立一個三角形網格,然後在 MeshInstance3D 裡使用它。