- 示例工程
【免费下载链接】godot-demo-projects
Demonstration and Template Projects
导读
本指南以 compute/post_shader 演示项目为依托,系统讲解 Godot 4 中全新的合成器特效(Compositor Effects)系统,即如何利用计算着色器(Compute Shader)在渲染管线末端实现全屏后处理。你将学会:如何创建CompositorEffect资源子类、如何将其挂载到WorldEnvironment或Camera3D的Compositor上、如何通过_render_callback在渲染线程提交计算指令,以及"模板注入式"与"文件预编译式"两种着色器组织方式的取舍。读完本文,你可以独立写出自己的全屏模糊、描边、色彩校正等后处理效果,并理解其与 2DCanvasItem着色器的本质差异。
适用前提:本功能仅在基于渲染设备(render device)的渲染器中可用,即 Forward+ 渲染器(项目配置见 project.godot 中的
config/features=PackedStringArray("4.7", "Forward Plus"))。演示语言为 GDScript。
一、合成器特效系统概述:为什么需要它
传统的 Godot 2D 后处理(如CanvasItem上的CanvasGroup、屏幕空间着色器)在 3D 渲染管线中缺乏统一的接入点。合成器特效系统解决了这一问题:它把"渲染完一帧画面后、在写回屏幕之前"的这段时间开放给开发者,允许你在该阶段提交任意渲染指令,包括计算着色器调度。
从 README.md 的技术描述可以提炼出三条核心规则:
- 特效即资源:一个后处理特效必须先实现为
CompositorEffect资源的子类。 - 挂载点有二:该资源实例可加入
Compositor,而Compositor既可以放在WorldEnvironment节点上,也可以放在Camera3D节点上(本演示使用前者,见 main.tscn 中WorldEnvironment节点的compositor = SubResource("Compositor_xxhi4"))。 - 回调即入口:在视口渲染过程中,资源的
_render_callback会在配置好的阶段(stage)被调用,开发者可在其中提交额外的渲染命令。
项目中的两个示例都以@tool脚本实现,因此在编辑器和运行时都能生效——你在编辑器中调整着色器代码,可以立即看到 3D 视口的变化,无需进入播放模式。
二、两种实现路径:模板注入 vs 文件预编译
演示项目同时提供了两个CompositorEffect子类,对应两种着色器组织策略,这是整个 demo 最值得理解的设计对比。
2.1 模板注入式:post_process_shader.gd
该方案将着色器代码保存在特效资源的属性中,运行时把用户代码注入到一段 GLSL 模板里再编译。核心代码如下:
- 脚本头部通过
class_name PostProcessShader extends CompositorEffect声明为合成器特效资源,并以@tool启用编辑器内运行; - 定义了一个
TEMPLATE_SHADER字符串常量,内含#version 450、#include "godot/scene_data_inc.glsl"、layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;等基础设施,并预留#COMPUTE_CODE占位符; - 模板中声明了三个关键绑定:
set = 0, binding = 0的场景数据统一缓冲(SceneDataBlock)、binding = 1的rgba16f颜色图像、binding = 2的深度纹理采样器,以及一组与脚本端推送常量严格对齐的Params(vec2 raster_size; float view; float pad;,注释明确要求"必须按 16 字节对齐"); - 用户代码通过
@export_multiline var shader_code: String暴露,其 setter 在加锁后把shader_is_dirty置为真,触发重新编译。
运行时代码注入与重编译流程(渲染线程区域#region Code in this region runs on the rendering thread):
_check_shader()在互斥锁保护下检查shader_is_dirty标记;- 若需要更新,用
TEMPLATE_SHADER.replace("#COMPUTE_CODE", new_shader_code)拼接完整着色器源码; - 调用
rd.shader_compile_spirv_from_source()把 GLSL 编译为 SPIR-V,若compile_error_compute非空则通过push_error输出错误; - 依次创建着色器 RID 与计算管线(
rd.compute_pipeline_create(shader))。
这种方式能在运行时随属性变化重新编译着色器(你可以拖动滑条或在代码中修改注入片段,效果实时更新)。但 README 明确指出了两个限制:
- 无法有效利用着色器缓存(shader caching);
- 某些平台可能不支持,例如要求着色器预编译的控制台平台(consoles)。
在 main.tscn 中,这个特效资源携带了一段默认注入代码——将深度反投影(unproject)后的世界坐标映射为 RGB 颜色的"伪深度可视化"片段:
// Unproject vec4 unproj = vec4(uv_norm * 2.0 - 1.0, depth, 1.0); mat4 inv_projection_matrix = scene_data_block.data.inv_projection_matrix_view[view]; vec4 vertex = inv_projection_matrix * unproj; vertex.xyz = vertex.xyz / vertex.w; color.rgb = clamp(vec3(vertex.x/20.0, vertex.y/20.0, -vertex.z/20.0), 0.0, 1.0);注意它通过scene_data_block访问了视图专属的逆投影矩阵,这正是模板中引入#include "godot/scene_data_inc.glsl"与SceneDataBlock的原因。
2.2 文件预编译式:post_process_grayscale.gd
该方案把着色器代码存成独立文件 post_process_grayscale.glsl,在初始化阶段一次性编译:
func _init() -> void: effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT rd = RenderingServer.get_rendering_device() RenderingServer.call_on_render_thread(_initialize_compute)- 通过
load("res://post_process_grayscale.glsl")加载着色器文件,其导入类型为RDShaderFile(见 post_process_grayscale.glsl.import 中的importer="glsl"、type="RDShaderFile"); - 调用
shader_file.get_spirv()直接取得引擎预编译好的 SPIR-V,再创建着色器与计算管线; - 编译过程发生在初始化阶段,因此编辑
glsl文件后需要重新加载场景才能看到效果。
该方式的优势正如 README 所述:Godot 可以对glsl文件进行预编译(precompile),从而获得更好的平台兼容性与启动性能。
两种方式的取舍可以总结为下表:
| 维度 | 模板注入式(post_process_shader.gd) | 文件预编译式(post_process_grayscale.gd) |
|---|---|---|
| 着色器来源 | 资源属性shader_code(@export_multiline) | 独立.glsl文件 |
| 编译时机 | 属性变化时运行时重编译 | 初始化时编译一次 |
| 修改生效方式 | 运行时实时生效 | 需重新加载场景 |
| 着色器缓存 | 无法有效利用 | 支持 Godot 预编译 |
| 平台兼容性 | 部分要求预编译的控制台平台可能不支持 | 更广 |
三、着色器与脚本的协作细节
3.1 灰度计算着色器剖析
post_process_grayscale.glsl 是一段标准的计算着色器,麻雀虽小五脏俱全:
#[compute] #version 450 layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; layout(rgba16f, set = 0, binding = 0) uniform image2D color_image; layout(push_constant, std430) uniform Params { vec2 raster_size; vec2 reserved; } params; void main() { ivec2 uv = ivec2(gl_GlobalInvocationID.xy); ivec2 size = ivec2(params.raster_size); // Prevent reading/writing out of bounds. if (uv.x >= size.x || uv.y >= size.y) { return; } // Read from our color buffer. vec4 color = imageLoad(color_image, uv); // Apply our changes. float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721; color.rgb = vec3(gray); // Write back to our color buffer. imageStore(color_image, uv, color); }要点说明:
- 工作组尺寸为
8 x 8 x 1,与脚本端_render_callback中的派发数量严格对应(见下文); rgba16f说明颜色缓冲是 16 位浮点格式,这是 HDR 渲染管线的标配;- 灰度权重
0.2125 / 0.7154 / 0.0721是标准的 Rec.709 亮度系数,比简单取平均更符合人眼感知; - 越界保护(
uv.x >= size.x || uv.y >= size.y)是必须的,因为(size.x - 1) / 8 + 1的向上取整可能导致工作组覆盖超出实际图像尺寸。
3.2 渲染线程回调:_render_callback 的完整调用链
两个脚本的_render_callback结构高度一致,这里以功能更完整的 post_process_shader.gd 为例梳理完整流程:
- 阶段校验:
p_effect_callback_type == EFFECT_CALLBACK_TYPE_POST_TRANSPARENT,即仅在"透明物体渲染后"的阶段触发(该枚举值4也固化在 main.tscn 的effect_callback_type = 4中); - 获取渲染缓冲:
p_render_data.get_render_scene_buffers()返回RenderSceneBuffers,注释提醒"不同渲染器的实现不同,因此需要类型转换"; - 读取内部尺寸:
render_scene_buffers.get_internal_size()得到的是3D 渲染分辨率(而非窗口分辨率),若为 0 则提前返回; - 计算工作组数量:
x_groups = (size.x - 1) / 8 + 1(向上取整),y_groups同理,z_groups = 1; - 构造推送常量:
PackedFloat32Array([size.x, size.y, 0.0, 0.0]),注释强调必须与着色器中Params的字段顺序一致并保持 16 字节对齐; - 创建最近邻采样器(
SAMPLER_FILTER_NEAREST),供深度纹理采样使用,只在无效时重建一次; - 遍历所有视图:
render_scene_buffers.get_view_count()兼容立体渲染(VR),单目渲染时循环只有一次、无额外开销; - 构建统一集合:使用
UniformSetCacheRD.get_cache(shader, 0, [...])——该缓存会在视口配置变化时自动清理; - 派发计算:
compute_list_begin → compute_list_bind_compute_pipeline → compute_list_bind_uniform_set → compute_list_set_push_constant → compute_list_dispatch → compute_list_end,这是 RenderingDevice 提交计算任务的完整序列。
此外,脚本在_notification中响应NOTIFICATION_PREDELETE,负责释放着色器 RID(注释说明释放着色器会连带释放依赖它的管线)与采样器 RID,这是避免渲染资源泄漏的关键细节。
3.3 两种脚本的差异
post_process_shader.gd额外绑定了深度纹理(binding 2),因此能实现依赖深度信息的后处理(如反投影可视化);post_process_grayscale.gd只绑定颜色图像(binding 0),结构更精简,也展示了"单图像读写"这一最常见的最小用例;- 前者使用
Mutex保护跨线程共享的shader_code状态;后者无需此机制,因为着色器在初始化时一次性编译完成。
四、场景组织与交互控制
4.1 场景树结构
main.tscn 展示了一个最小可复用的场景模板:
Main (Node3D, main.gd) ├── DirectionalLight3D (shadow_enabled) ├── WorldEnvironment │ └── compositor = Compositor(compositor_effects = [Grayscale特效, Shader特效]) ├── Camera3D (fov = 60) ├── Ground (PlaneMesh, 棋盘格纹理 pattern.png) ├── Sphere / Box (彩色棋盘格材质) ├── Info (Label) —— 显示两个特效的开关状态 └── Help (Label) —— 提示按键 G / S环境使用ProceduralSkyMaterial天空并开启glow_enabled,为后处理效果提供了足够丰富的色彩输入。两个CompositorEffect子资源在Compositor中按数组顺序排列:索引 0 是灰度特效(默认enabled = true),索引 1 是着色器注入特效(默认enabled = false)。
4.2 运行时开关
main.gd 通过输入动作切换两个特效的enabled属性:
extends Node3D @onready var compositor: Compositor = $WorldEnvironment.compositor func _input(input_event: InputEvent) -> void: if input_event.is_action_pressed(&"toggle_grayscale_effect"): compositor.compositor_effects[0].enabled = not compositor.compositor_effects[0].enabled update_info_text() if input_event.is_action_pressed(&"toggle_shader_effect"): compositor.compositor_effects[1].enabled = not compositor.compositor_effects[1].enabled update_info_text()对应的输入映射定义在 project.godot 的[input]段:toggle_grayscale_effect绑定物理键71(G 键)、toggle_shader_effect绑定物理键83(S 键)。Info标签会实时显示Grayscale effect: Enabled/Disabled与Shader effect: Enabled/Disabled,方便对比开启前后画面差异。
这种"通过compositor_effects[i].enabled动态启停"的机制意味着你可以在玩法代码中随时切换特效,例如只在低血量时开启画面灰度,或者按场景切换色调映射方案。
五、在编辑器中的可视化调试
由于两个脚本均以@tool标注,在编辑器中选中WorldEnvironment节点即可在 Inspector 中直接编辑shader_code多行文本,3D 视口会同步反映效果变化。项目截图(screenshots/post_process_shader.webp)展示了编辑器内启用灰度特效后的场景:棋盘格地面、立方体与球体整体呈现灰度,Inspector 中可看到PostProcessShader资源、Enabled开关与Effect Callback Type: Post Transparent等参数。
六、迁移到自己的项目:实操清单
如果你想把该方案复用到自己的 Godot 4.x 项目,按以下步骤操作:
- 确认渲染器:在项目设置中选择 Forward+(
config/features含"Forward Plus"),Mobile/Compatibility 等非渲染设备后端不支持此功能; - 编写特效脚本:新建脚本继承
CompositorEffect,在_init中设置effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT(或EFFECT_CALLBACK_TYPE_POST_OPAQUE等更早阶段),获取RenderingServer.get_rendering_device(); - 选择着色器组织方式:
- 原型调试 / 参数动态变化:采用模板注入式,把用户代码放入
@export_multiline属性; - 正式发布 / 追求缓存与平台兼容:采用文件预编译式,把 GLSL 存为
*.glsl文件并load()后get_spirv();
- 原型调试 / 参数动态变化:采用模板注入式,把用户代码放入
- 挂载合成器:在场景中创建
WorldEnvironment(或Camera3D),在属性面板添加Compositor,再向compositor_effects数组添加特效资源实例; - 编写回调:在
_render_callback中获取RenderSceneBuffers,以get_internal_size()计算派发组数,构造 16 字节对齐的推送常量,最后用UniformSetCacheRD+compute_list_*提交计算任务; - 记得清理:在
_notification(NOTIFICATION_PREDELETE)中释放着色器与采样器 RID。
七、小结
compute/post_shader演示的核心价值在于:它把 Godot 4 合成器特效系统的完整链路——资源子类、挂载方式、渲染回调、SPIR-V 编译、计算派发、双平台着色器策略——浓缩在两个约两百行的脚本中。无论你是要做风格化渲染、深度可视化还是性能分析工具,这条链路都是 3D 后处理扩展的起点。唯一需要牢记的限制是:它只服务于 Forward+ 这类基于渲染设备的渲染器,且模板注入方案在需要着色器预编译的平台上可能无法工作——选择文件预编译方案则能获得更广的兼容面。
如果你希望继续深入,可以在仓库中找到更多相关的计算着色器示例:compute/heightmap(计算着色器生成高度图)与 compute/texture(计算纹理处理),它们与本文共享RenderingDevice的编译与派发基础设施,可以交叉印证。
- 示例工程
【免费下载链接】godot-demo-projects
Demonstration and Template Projects
相关推荐
Godot-demo-projects中的着色器示例:2D与3D特效实现教程
Godot demo projects中的着色器示例:2D与3D特效实现教程 引言:告别特效实现困境 你是否还在为Godot引擎中着色器 Shader 的复杂语
示例工程godot-demo-projects 2D 示例合集:GDScript、着色器与渲染器选型的实战指南
godot demo projects 2D 示例合集:GDScript、着色器与渲染器选型的实战指南 本文基于 godot demo projects 仓库的
示例工程Godot 4 物理光照与相机单位实战:基于 godot-demo-projects 的 Physical Light and Camera Units 演示
Godot 4 物理光照与相机单位实战:基于 godot demo projects 的 Physical Light and Camera Units 演示
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考