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...
合成器(Compositor)
合成器是 Godot 4 的新功能,可讓你在渲染 檢視區 內容時,控制渲染管線。
你可以在 世界環境 節點上設定合成器,使其套用於所有檢視區,或是在 3D 攝影機 上設定,僅套用於該攝影機所屬的檢視區。
合成器 資源用於設定合成器。首先,請在對應的節點上建立新的合成器:
備註
目前合成器僅支援 Mobile(行動裝置)與 Forward+(前向加強)渲染器。
合成器特效
合成器特效讓你能在渲染管線的不同階段插入自訂邏輯。這是一項進階功能,使用時需要對渲染管線有高度的理解,才能發揮其最大效益。
由於合成器特效的核心邏輯會由渲染管線呼叫,因此會在執行渲染的執行緒中運作。請務必小心,避免產生執行緒相關問題。
為了說明如何使用合成器特效,我們將建立一個簡單的後處理特效,讓你可以撰寫自己的著色器程式,並透過運算著色器套用到全螢幕。你可以在這裡找到完成的展示專案:這裡。
首先,建立一個名為 post_process_shader.gd 的新腳本。將其設為 tool 腳本,這樣就可以在編輯器中即時看到合成器特效的效果。這個腳本需要繼承自 CompositorEffect,並為其指定一個類別名稱。
@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,來組建運算清單。
完成合成器特效後,現在要將它加入合成器。
在合成器中展開「合成器特效」屬性,然後按下「新增元素」。
現在就可以新增我們的合成器特效:
選取 PostProcessShader 後,請輸入自訂著色器程式碼:
float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721;
color.rgb = vec3(gray);
完成以上步驟後,畫面輸出就會變成灰階。
備註
若想參考更進階的後處理特效範例,可以看看 Bastiaan Olij 製作的 基於徑向模糊的天空光暈 範例專案。