Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

螢幕閱讀著色器

前言

很多人想要讓著色器在寫螢幕的同時讀取該螢幕的資料。因為內部硬體限制,OpenGL 和 DirectX 等 3D API 都很難實作這一功能。GPU 是極其並行的,所以同時進行讀寫會導致各種快取和一致性問題。因此,即便是最新的硬體也對此進行無法正確的支援。

解決辦法是將螢幕或螢幕的一部分複製到一個背景緩衝區,然後在繪圖時從那裡讀取。Godot 提供了一些工具,可以使這一過程變得很容易。

匯入紋理

Godot 著色語言 有一個特殊的紋理, SCREEN_TEXTURE (在3D的情況下, DEPTH_TEXTURE 代表深度). 它以螢幕的UV作為參數, 並返回一個帶有顏色的RGB vec3. 一個特殊的內建變數.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 會自動為您計算模糊紋理。

警告

If the filter mode is not changed to a filter mode that contains mipmap in its name, textureLod with a LOD parameter greater than 0.0 will have the same appearance as with the 0.0 LOD parameter.

另一個範例:

螢幕紋理可以用來做很多事情。有一個針對*螢幕空間著色器*的特殊演示專案,你可以下載後查看學習。其中的一個例子就是用簡單的著色器來調整亮度、對比度以及飽和度:

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 中,第一次在即將繪製的節點中發現 hint_screen_texture 時,Godot 就會將整個螢幕拷貝到後臺緩衝之中。後續在著色器中使用它的節點將不會造成螢幕的複製,因為否則的話效率非常低。在 3D 中,進行螢幕拷貝的時機是在不透明幾何體階段之後、透明幾何體階段之前,所以透明的物體不會被捕捉到螢幕紋理之中。

因此,在 2D 中,如果使用 hint_screen_texture 的著色器存在覆蓋,那麼後一個著色器使用的就不是第一個著色器的結果,會導致意外的圖像:

../../_images/texscreen_demo1.png

在上圖中,第二個球體(右上)所使用的螢幕紋理和第一個球體所使用的螢幕紋理的來源是一致的,所以第一個球體會“消失”,或者說不可見。

在 2D 中,這個問題可以通過 BackBufferCopy 節點修正,在這兩個球體之間產生實體即可。BackBufferCopy 可以指定螢幕上的某個區域進行複製,也可以複製整個螢幕:

../../_images/texscreen_bbc.png

正確複製後臺緩衝之後,這兩個球體就能夠正確混合了:

../../_images/texscreen_demo2.png

警告

In 3D, materials that use hint_screen_texture are considered transparent themselves and will not appear in the resulting screen texture of other materials. If you plan to instance a scene that uses a material with hint_screen_texture, you will need to use a BackBufferCopy node.

在 3D 中,這個問題解決起來就沒有那麼靈活,因為螢幕紋理只會捕捉一次。在 3D 中使用螢幕紋理時請多加小心,因為它並不會捕獲到透明的物件,反而可能捕獲到位於使用螢幕紋理的物件之前的不透明物件。

要在 3D 中重現後臺緩衝的邏輯,可以建立一個 Viewport 並在物件的位置建立一個相機,然後就可以使用該 Viewport 的紋理來代替螢幕紋理。

後臺緩衝邏輯

好的,想要對後臺緩衝有更清晰的理解的話,Godot 在 2D 中後臺緩衝複製的原理是這樣的:

  • 如果某個節點使用了 hint_screen_texture,那麼繪製該節點之前就會將整個螢幕複製到後臺緩衝之中。只有第一次才會這麼做,後續的節點不會觸發。

  • 如果上述情況發生前遇到過 BackBufferCopy 節點(即便尚未使用過 hint_screen_texture),那麼也不會執行相關的行為。換句話說,自動複製整個螢幕的條件只有:某個節點中首次使用 hint_screen_texture 並且按照樹順序不存在更早的(未被禁用的)BackBufferCopy 節點。

  • 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;
}

法線-粗糙度紋理

備註

Normal-roughness texture is only supported in the Forward+ rendering method, not Mobile or Compatibility.

類似的,如果物件在深度預階段中進行了渲染,就可以用法線-粗糙度紋理來讀取該對象的法線和粗糙度。法線儲存在 .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;

匯入紋理

可以對多個 uniform 使用螢幕紋理提示(hint_screen_texturehint_depth_texturehint_normal_roughness_texture)。例如,你可能會想要使用不同的重複旗標和篩選旗標多次讀取該紋理。

下面的例子中,著色器在讀取螢幕空間法線時使用的就是線性篩選,而讀取螢幕空間粗糙度時使用的就是最鄰近篩選。

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;