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...
進階後期處理
前言
本教學說明在 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(3.0, -1.0, 0.0))
verts.append(Vector3(-1.0, 3.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 裡使用它。