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.

合成器(Compositor)

合成器是 Godot 4 的新功能,可讓你在渲染 檢視區 內容時,控制渲染管線。

你可以在 世界環境 節點上設定合成器,使其套用於所有檢視區,或是在 3D 攝影機 上設定,僅套用於該攝影機所屬的檢視區。

合成器 資源用於設定合成器。首先,請在對應的節點上建立新的合成器:

../../_images/new_compositor.webp

備註

目前合成器僅支援 Mobile(行動裝置)與 Forward+(前向加強)渲染器。

合成器特效

合成器特效讓你能在渲染管線的不同階段插入自訂邏輯。這是一項進階功能,使用時需要對渲染管線有高度的理解,才能發揮其最大效益。

由於合成器特效的核心邏輯會由渲染管線呼叫,因此會在執行渲染的執行緒中運作。請務必小心,避免產生執行緒相關問題。

為了說明如何使用合成器特效,我們將建立一個簡單的後處理特效,讓你可以撰寫自己的著色器程式,並透過運算著色器套用到全螢幕。你可以在這裡找到完成的展示專案:這裡

首先,建立一個名為 post_process_shader.gd 的新腳本。將其設為 tool 腳本,這樣就可以在編輯器中即時看到合成器特效的效果。這個腳本需要繼承自 CompositorEffect,並為其指定一個類別名稱。

post_process_shader.gd
@tool
extends CompositorEffect
class_name PostProcessShader

接著,我們要定義一個常數來存放著色器範本程式碼。這段程式碼是讓我們的運算著色器能夠運作的基礎。

const template_shader: String = """
#version 450

// Invocations in the (x, y, z) dimension
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(rgba16f, set = 0, binding = 0) uniform image2D color_image;

// Our push constant
layout(push_constant, std430) uniform Params {
    vec2 raster_size;
    vec2 reserved;
} params;

// The code we want to execute in each invocation
void main() {
    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
    ivec2 size = ivec2(params.raster_size);

    if (uv.x >= size.x || uv.y >= size.y) {
        return;
    }

    vec4 color = imageLoad(color_image, uv);

    #COMPUTE_CODE

    imageStore(color_image, uv, color);
}
"""

如需更多有關運算著色器運作方式的資訊,請參考 使用運算著色器

這裡最重要的是,螢幕上的每一個像素都會執行一次 main 函式,過程中會讀取該像素目前的顏色值、執行自訂程式碼,並將修改後的顏色寫回影像。

#COMPUTE_CODE 會被替換為我們撰寫的自訂程式碼。

為了設定自訂程式碼,我們需要一個 export 變數。同時,也會定義一些腳本變數以供使用:

@export_multiline var shader_code: String = "":
    set(value):
        mutex.lock()
        shader_code = value
        shader_is_dirty = true
        mutex.unlock()

var rd: RenderingDevice
var shader: RID
var pipeline: RID

var mutex: Mutex = Mutex.new()
var shader_is_dirty: bool = true

請注意,我們在程式中使用了 Mutex。大部分的實作都會由渲染引擎呼叫,因此會在渲染執行緒中執行。

我們必須確保在設定新的著色器程式碼並標記為已變更時,渲染執行緒不會同時存取這些資料。

接著初始化我們的特效。

# Called when this resource is constructed.
func _init():
    effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT
    rd = RenderingServer.get_rendering_device()

這裡的重點是設定 effect_callback_type,告訴渲染引擎要在渲染管線的哪個階段呼叫我們的程式碼。

備註

目前僅能存取 3D 渲染管線的各個階段!

我們同時也會取得渲染裝置的參考,這在後續會很實用。

我們還需要進行資源清理,這可以透過監聽 NOTIFICATION_PREDELETE 通知來完成:

# System notifications, we want to react on the notification that
# alerts us we are about to be destroyed.
func _notification(what):
    if what == NOTIFICATION_PREDELETE:
        if shader.is_valid():
            # Freeing our shader will also free any dependents such as the pipeline!
            rd.free_rid(shader)

請注意,雖然我們在渲染執行緒中建立著色器,但這裡並未使用 mutex。渲染伺服器的方法是執行緒安全的,free_rid 會等到目前正在渲染的所有畫面結束後,才會釋放著色器資源。

同時也請注意,這邊沒有手動釋放 pipeline。渲染裝置會自動追蹤相依性,pipeline 依賴著色器,因此當著色器被銷毀時 pipeline 也會自動釋放。

從這裡開始,我們的程式碼都會在渲染執行緒上執行。

接下來要實作一個輔助函式,當使用者的程式碼更動時,重新編譯著色器。

# Check if our shader has changed and needs to be recompiled.
func _check_shader() -> bool:
    if not rd:
        return false

    var new_shader_code: String = ""

    # Check if our shader is dirty.
    mutex.lock()
    if shader_is_dirty:
        new_shader_code = shader_code
        shader_is_dirty = false
    mutex.unlock()

    # We don't have a (new) shader?
    if new_shader_code.is_empty():
        return pipeline.is_valid()

    # Apply template.
    new_shader_code = template_shader.replace("#COMPUTE_CODE", new_shader_code);

    # Out with the old.
    if shader.is_valid():
        rd.free_rid(shader)
        shader = RID()
        pipeline = RID()

    # In with the new.
    var shader_source: RDShaderSource = RDShaderSource.new()
    shader_source.language = RenderingDevice.SHADER_LANGUAGE_GLSL
    shader_source.source_compute = new_shader_code
    var shader_spirv: RDShaderSPIRV = rd.shader_compile_spirv_from_source(shader_source)

    if shader_spirv.compile_error_compute != "":
        push_error(shader_spirv.compile_error_compute)
        push_error("In: " + new_shader_code)
        return false

    shader = rd.shader_create_from_spirv(shader_spirv)
    if not shader.is_valid():
        return false

    pipeline = rd.compute_pipeline_create(shader)
    return pipeline.is_valid()

在這個方法的一開始,我們會再次使用 mutex 來保護對使用者著色器程式碼及已變更標記的存取。如果使用者著色器程式碼已變更,就複製一份本地副本。

如果沒有新的程式碼片段,且已經有有效的 pipeline,就回傳 true。

如果有新的程式碼片段,則將其嵌入範本程式碼後進行編譯。

警告

這裡的程式碼會在執行時即時編譯新的程式碼。這對於原型設計很有幫助,因為可以立刻看到著色器變更的效果。

這樣做會導致無法預先編譯和快取著色器,這在某些平台(如遊戲主機)上可能會造成問題。請注意,展示專案中也有另一種做法:將完整運算著色器寫在 .glsl 檔案中,然後載入。Godot 可以透過這種方式來預先編譯並快取著色器。

最後要實作特效的回呼函式,渲染引擎會在正確的渲染階段呼叫它。

# Called by the rendering thread every frame.
func _render_callback(p_effect_callback_type, p_render_data):
    if rd and p_effect_callback_type == EFFECT_CALLBACK_TYPE_POST_TRANSPARENT and _check_shader():
        # Get our render scene buffers object, this gives us access to our render buffers.
        # Note that implementation differs per renderer hence the need for the cast.
        var render_scene_buffers: RenderSceneBuffersRD = p_render_data.get_render_scene_buffers()
        if render_scene_buffers:
            # Get our render size, this is the 3D render resolution!
            var size = render_scene_buffers.get_internal_size()
            if size.x == 0 and size.y == 0:
                return

            # We can use a compute shader here.
            var x_groups = (size.x - 1) / 8 + 1
            var y_groups = (size.y - 1) / 8 + 1
            var z_groups = 1

            # Push constant.
            var push_constant: PackedFloat32Array = PackedFloat32Array()
            push_constant.push_back(size.x)
            push_constant.push_back(size.y)
            push_constant.push_back(0.0)
            push_constant.push_back(0.0)

            # Loop through views just in case we're doing stereo rendering. No extra cost if this is mono.
            var view_count = render_scene_buffers.get_view_count()
            for view in range(view_count):
                # Get the RID for our color image, we will be reading from and writing to it.
                var input_image = render_scene_buffers.get_color_layer(view)

                # Create a uniform set.
                # This will be cached; the cache will be cleared if our viewport's configuration is changed.
                var uniform: RDUniform = RDUniform.new()
                uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
                uniform.binding = 0
                uniform.add_id(input_image)
                var uniform_set = UniformSetCacheRD.get_cache(shader, 0, [ uniform ])

                # Run our compute shader.
                var compute_list:= rd.compute_list_begin()
                rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
                rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
                rd.compute_list_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4)
                rd.compute_list_dispatch(compute_list, x_groups, y_groups, z_groups)
                rd.compute_list_end()

在此方法開始時,會檢查是否有渲染裝置、回呼類型是否正確,以及是否已經有著色器。

備註

檢查特效類型只是安全機制。我們已經在 _init 函式中設定,但使用者也有可能在 UI 中修改這個設定。

p_render_data 參數讓我們可以存取目前渲染畫面所用的資料物件。目前我們只需要用到場景渲染緩衝區,這會提供對渲染引擎所有內部緩衝區的存取權限。請注意,這裡我們會將其轉型為 RenderSceneBuffersRD 以使用完整 API。

接下來取得 internal size,也就是 3D 渲染緩衝區在升頻(如果有的話)前的解析度。升頻會在所有後處理效果結束後才執行。

根據 internal size 計算群組大小,詳見著色器範本程式碼中的 local size。

我們也會設定 push constant,讓著色器知道目前的大小。Godot 目前 尚未 支援 struct,因此我們用 PackedFloat32Array 來存放這些資料。請注意,這個陣列必須以 16 位元組對齊,也就是說長度必須是 4 的倍數。

現在我們要遍歷所有 view,這是為了支援多視圖渲染(例如 XR 立體渲染)。大多數情況下只會有一個 view。

備註

在這裡使用多視圖對後處理沒有效能上的好處,但分別處理各個 view 的方式,仍能讓 GPU 視需要使用平行運算。

接下來取得該 view 的顏色緩衝區,也就是 3D 場景渲染結果所儲存的緩衝區。

然後準備一組 uniform set,用來將顏色緩衝區傳遞給著色器。

請注意,我們使用 UniformSetCacheRD 快取,這樣每一幀都可以檢查目前的 uniform set。由於顏色緩衝區每幀都可能不同,uniform 快取也會在緩衝區被釋放時自動清理,這樣才能確保不會有記憶體洩漏或用到過期的 set。

最後,綁定 pipeline 與 uniform set,推送 push constant 資料,並對所有群組呼叫 dispatch,來組建運算清單。

完成合成器特效後,現在要將它加入合成器。

在合成器中展開「合成器特效」屬性,然後按下「新增元素」。

現在就可以新增我們的合成器特效:

../../_images/add_compositor_effect.webp

選取 PostProcessShader 後,請輸入自訂著色器程式碼:

float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721;
color.rgb = vec3(gray);

完成以上步驟後,畫面輸出就會變成灰階。

../../_images/post_process_shader.webp

備註

若想參考更進階的後處理特效範例,可以看看 Bastiaan Olij 製作的 基於徑向模糊的天空光暈 範例專案。