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...
合成器
合成器是 Godot 4 中的新功能,能够用来控制 Viewport 渲染内容时所使用的渲染管线。
它可以在 WorldEnvironment 节点上进行配置,并应用于所有视口;也可以在 Camera3D 上进行配置,并仅应用于使用该相机的视口。
The Compositor resource is used to configure the compositor. To get started, create a new compositor on the appropriate node:

备注
目前只有移动渲染器和 Forward+ 渲染器支持合成器功能。
合成器效果
合成器效果允许你在渲染管线的各个阶段插入额外逻辑。这是一项高级功能,需要对渲染管线有很高的理解才能充分利用它。
由于合成器效果的核心逻辑是从渲染管线调用的,因此需要注意的是,该逻辑将在渲染发生的线程内运行。务必小心,以确保我们不会遇到线程问题。
为了说明如何使用合成器效果,我们将创建一个简单的后期处理效果,让你可以编写自己的着色器代码并通过计算着色器应用该全屏。你可以在这里找到完成的演示项目。
首先创建一个名为 post_process_shader.gd
的新脚本。我们将把它作为一个工具脚本,这样就可以在编辑器中看到合成器效果的工作情况。我们需要从 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_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)
请注意,即使我们在渲染线程内创建了着色器,我们也不会在此处使用互斥锁。我们的渲染服务器上的方法是线程安全的,并且 free_rid
将推迟清理着色器,将其推迟到当前正在渲染的所有帧都完成之后。
还要注意,我们无需释放管线。渲染设备会进行依赖跟踪,由于管线依赖于着色器,因此当着色器被销毁时,管线会被自动释放。
从此刻起,我们的代码将在渲染线程上运行。
我们的下一步是一个辅助函数,它将在用户代码发生更改时重新编译着色器。
# 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()
在这个方法的顶部,我们再次使用互斥锁来保护对用户着色器代码和脏标记的访问。如果我们的用户着色器代码脏了,我们会在本地线程中复制用户着色器代码。
如果我们没有新的代码片段,并且我们已经有一个有效的管线,我们就返回 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。
接下来,我们获得我们的内部尺寸
,即我们的 3D 渲染缓冲区在放大之前的分辨率(如果适用),放大发生在我们的后期处理运行之后。
根据我们的内部大小,我们计算出我们的分组大小,在我们的模板着色器中查看我们的局部大小。
我们还填充了推送常量,以便着色器知道大小。Godot 暂时不支持结构体,因此我们使用 PackedFloat32Array
来存储这些数据。请注意,我们必须用 16 字节对齐填充该数组。换句话说,我们的数组长度需要是 4 的倍数。
现在我们循环遍历视图,以防我们使用适用于立体渲染(XR)的多视图渲染。大多数情况下,我们只有一个视图。
备注
此处使用多视图进行后处理并没有性能优势,像这样单独处理视图仍然可以使 GPU 在有利的情况下使用并行性。
接下来我们获取该视图的颜色缓冲区。这是我们的 3D 场景被渲染到的缓冲区。
然后我们准备一个统一的集合,以便我们可以将颜色缓冲区传递给我们的着色器。
请注意我们使用 UniformSetCacheRD 缓存,以确保我们可以每帧检查 uniform 集。由于我们的颜色缓冲区可以逐帧更改,并且我们的 uniform 缓存会在缓冲区释放时自动清理 uniform 集,因此这是确保我们不泄漏内存或使用过时集的安全方法。
最后,我们通过绑定管线、绑定 uniform 集、推送推送常量数据、以及为我们的组调用调度,来构建我们的计算列表。
合成器效果完成后,我们现在需要将其添加到合成器中。
在合成器上,我们扩展合成器效果属性并按 Add Element
。
现在我们可以添加合成器效果:

选择 PostProcessShader
后,我们需要设置你的用户着色器代码:
float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721;
color.rgb = vec3(gray);
完成这一切后,我们的输出是灰度的。

备注
有关后期效果的更高级示例,请查看由 Bastiaan Olij 创建的基于径向模糊的天空光线示例项目。