螢幕取樣著色器
前言
我們常常希望著色器能同時從正在寫入的螢幕讀取資料。像 OpenGL 或 DirectX 這類 3D API,由於硬體本身的限制,這件事非常困難。GPU 是高度平行運作的,因此同時進行讀寫會造成快取與一致性等各種問題。因此,即使是最新的硬體也無法完全正確支援這一需求。
解決方式是將螢幕或螢幕的一部分複製到後臺緩衝區(back-buffer),然後在繪製時從該緩衝區讀取。Godot 提供了一些工具,可以簡化這個流程。
螢幕紋理
Godot 著色語言 提供一種特殊紋理,能夠存取已繪製到螢幕上的內容。宣告 sampler2D uniform 時,指定 hint_screen_texture 提示即可使用。內建變數 SCREEN_UV 則可取得螢幕相對座標的 UV。舉例來說,下方這個 canvas_item 片段著色器會讓物件隱形,因為它只顯示背後的內容:
shader_type canvas_item;
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;
void fragment() {
COLOR = textureLod(screen_texture, SCREEN_UV, 0.0);
}
這裡使用 textureLod,因為我們只想讀取底層的 mipmap。如果你想讀取紋理的模糊版本,只要增加 textureLod 的第三個參數,並將提示從 filter_nearest 改為 filter_nearest_mipmap (或其他啟用了 mipmap 的篩選器)。若使用啟用 mipmap 的篩選,Godot 會自動計算出模糊紋理。
警告
如果濾鏡模式沒有設為名稱中含有 mipmap 的類型,則 textureLod 的 LOD 參數大於 0.0 時,效果會與 LOD 為 0.0 相同。
螢幕紋理範例
螢幕紋理用途廣泛。有一個專門針對*螢幕空間著色器*的示範專案,歡迎下載學習。舉例來說,可以用簡單著色器調整畫面的亮度、對比度與飽和度:
shader_type canvas_item;
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;
uniform float brightness = 1.0;
uniform float contrast = 1.0;
uniform float saturation = 1.0;
void fragment() {
vec3 c = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
c.rgb = mix(vec3(0.0), c.rgb, brightness);
c.rgb = mix(vec3(0.5), c.rgb, contrast);
c.rgb = mix(vec3(dot(vec3(1.0), c.rgb) * 0.33333), c.rgb, saturation);
COLOR.rgb = c;
}
運作原理
這聽起來像是魔法,但其實不是。在 2D 中,當 Godot 首次在將要繪製的節點找到 hint_screen_texture 時,會將整個螢幕複製到後臺緩衝區。之後再有節點著色器使用它,就不會再次複製,避免效能浪費。在 3D 中,螢幕是在不透明幾何體階段結束後、透明幾何體開始前被複製,所以螢幕紋理不會包含透明物件。
因此,在 2D 中,若多個使用 hint_screen_texture 的著色器重疊時,後面的著色器無法取得前一個著色器的結果,可能產生意外的視覺效果:
在上圖中,第二個球體(右上)與第一個球體(下方)取樣自相同的螢幕紋理來源,因此第一個球體會「消失」或不可見。
在 2D 中,可以在兩個球體之間插入 BackBufferCopy 節點來修正此問題。BackBufferCopy 可以複製整個螢幕,也可指定螢幕區域:
正確處理後臺緩衝複製後,兩個球體就能正常混合顯示:
警告
在 3D 中,使用 hint_screen_texture 的材質本身會被視為透明,且不會出現在其他材質取得的螢幕紋理中。如果你要實例化一個使用 hint_screen_texture 材質的場景,則必須搭配 BackBufferCopy 節點。
在 3D 中,這類問題較難解決,因為螢幕紋理只會複製一次。在 3D 場景使用螢幕紋理時要特別注意:它不會捕捉透明物件,反而可能捕捉到位於螢幕紋理物件前方的不透明物件。
你可以在 3D 中透過建立 Viewport,並將相機放在與物件相同位置,之後直接取用 Viewport 的紋理來取代螢幕紋理,達到類似後臺緩衝的效果。
後臺緩衝區機制
以下更詳細說明 Godot 2D 後臺緩衝複製的機制:
如果某個節點使用了
hint_screen_texture,在繪製該節點前會先將整個螢幕複製到後臺緩衝區。這只會在首次出現時發生,後續節點不會重複複製。如果在上述情境前已經處理過 BackBufferCopy 節點(即使當時還沒用過
hint_screen_texture),那麼就不會自動複製螢幕。換句話說,只有在節點樹順序上首次遇到hint_screen_texture,且前面沒有啟用的 BackBufferCopy 節點時,才會自動複製整個螢幕。BackBufferCopy 可以選擇複製整個螢幕,也可只複製特定區域。如果只複製了區域,但著色器卻取樣到未被複製的像素,讀取結果就是未定義(通常是前一幀的垃圾資料)。換言之,雖然技術上可以只複製一區再取樣另一區,但請避免這樣做!
深度紋理
3D 著色器也能存取螢幕的深度緩衝,只需加上 hint_depth_texture 提示即可。這個紋理不是線性的,必須用逆投影矩陣進行轉換。
以下程式碼可取得目前繪製像素對應的 3D 位置:
uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;
void fragment() {
float depth = textureLod(depth_texture, SCREEN_UV, 0.0).r;
vec4 upos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth, 1.0);
vec3 pixel_position = upos.xyz / upos.w;
}
法線-粗糙度紋理
備註
法線-粗糙度紋理僅支援於 Forward+ 算繪管線,不支援行動或相容模式。
同理,法線-粗糙度紋理可用於讀取深度預算繪階段的物件之法線與粗糙度。法線資訊儲存在 .xyz 通道(範圍映射到 0-1),粗糙度則存於 .w 通道。
uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest;
void fragment() {
float screen_roughness = texture(normal_roughness_texture, SCREEN_UV).w;
vec3 screen_normal = texture(normal_roughness_texture, SCREEN_UV).xyz;
screen_normal = screen_normal * 2.0 - 1.0;
自訂螢幕紋理
螢幕紋理的提示(hint_screen_texture、hint_depth_texture、hint_normal_roughness_texture)可以套用在多個 uniform 上。例如,你可能會想用不同的重複旗標或篩選旗標,對同一紋理重複取樣。
以下範例展示一個著色器:它以線性篩選讀取螢幕空間法線,卻以最近點篩選讀取螢幕空間粗糙度。
uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest;
uniform sampler2D normal_roughness_texture2 : hint_normal_roughness_texture, repeat_enable, filter_linear;
void fragment() {
float screen_roughness = texture(normal_roughness_texture, SCREEN_UV).w;
vec3 screen_normal = texture(normal_roughness_texture2, SCREEN_UV).xyz;
screen_normal = screen_normal * 2.0 - 1.0;