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.

螢幕取樣著色器

前言

我們常常希望著色器能同時從正在寫入的螢幕讀取資料。像 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 的著色器重疊時,後面的著色器無法取得前一個著色器的結果,可能產生意外的視覺效果:

../../_images/texscreen_demo1.png

在上圖中,第二個球體(右上)與第一個球體(下方)取樣自相同的螢幕紋理來源,因此第一個球體會「消失」或不可見。

在 2D 中,可以在兩個球體之間插入 BackBufferCopy 節點來修正此問題。BackBufferCopy 可以複製整個螢幕,也可指定螢幕區域:

../../_images/texscreen_bbc.png

正確處理後臺緩衝複製後,兩個球體就能正常混合顯示:

../../_images/texscreen_demo2.png

警告

在 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_texturehint_depth_texturehint_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;