Add Compositor Effects (Post-Processing) demo (#1058)

Co-authored-by: Hugo Locurcio <hugo.locurcio@hugo.pro>
This commit is contained in:
Bastiaan Olij
2024-06-08 06:19:25 +10:00
committed by GitHub
parent 785e3213d5
commit 909331ac51
14 changed files with 596 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
# Compositor Effects (Post-Processing)
This demo shows how to use compositor effects to create a post process.
This functionality only works in render device based renderers such as the Forward+ renderer.
Language: GDScript
Renderer: Forward+
> Note: this demo requires Godot 4.3 or later
## Screenshots
![Screenshot](screenshots/post_process_shader.webp)
## Technical description
This demo shows the use of the new compositor effect system to add a compute shader based post process.
A compositor effect needs to first be implemented as a subclass of the `CompositorEffect` resource.
An instance of this resource can then be added to the `Compositor`
either as part of a `WorldEnvironment` node or as part of a `Camera3D` node.
During rendering of a viewport the `_render_callback` on this resource will be called
at the configured stage and additional rendering commands can be submitted.
The two examples in this project both add a compute call to apply a full screen effect.
Both are designed as tool scripts so they work both in editor and in runtime.
`post_process_shader.gd` shows an example where a template shader is used into which user code
is injected. The user code is stored in a property of the compositor effect.
This approach is able to recompile the shader as the property changes in runtime.
This approach is not able to make efficient use of shader caching and may not be supported on certain
platforms, such as certain consoles, that require precompiling of shaders.
`post_process_grayscale.gd` show an example where the shader code is stored in a file,
namely `post_process_grayscale.glsl` and is compiled on initialisation.
For editing a project this means that the shader is compiled once when the effect is loaded.
Making changes to the `glsl` file will require reloading the scene.
The advantage of this approach is that Godot can precompile the `glsl` file.

View File

@@ -0,0 +1 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z" fill="#478cbf"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

After

Width:  |  Height:  |  Size: 949 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ckgggpfd707sy"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,23 @@
extends Node3D
@onready var compositor: Compositor = $WorldEnvironment.compositor
func _input(event: InputEvent) -> void:
if event.is_action_pressed(&"toggle_grayscale_effect"):
compositor.compositor_effects[0].enabled = not compositor.compositor_effects[0].enabled
update_info_text()
if event.is_action_pressed(&"toggle_shader_effect"):
compositor.compositor_effects[1].enabled = not compositor.compositor_effects[1].enabled
update_info_text()
func update_info_text() -> void:
$Info.text = """Grayscale effect: %s
Shader effect: %s
""" % [
"Enabled" if compositor.compositor_effects[0].enabled else "Disabled",
"Enabled" if compositor.compositor_effects[1].enabled else "Disabled",
]

View File

@@ -0,0 +1,121 @@
[gd_scene load_steps=17 format=3 uid="uid://bpfg1l8j4i08u"]
[ext_resource type="Script" path="res://main.gd" id="1_o0pyp"]
[ext_resource type="Texture2D" uid="uid://br4k6sn2rvgj" path="res://pattern.png" id="1_r22bv"]
[ext_resource type="Script" path="res://post_process_shader.gd" id="1_rkpno"]
[ext_resource type="Script" path="res://post_process_grayscale.gd" id="2_pwabc"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_lnmx8"]
sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
[sub_resource type="Sky" id="Sky_guc0r"]
sky_material = SubResource("ProceduralSkyMaterial_lnmx8")
[sub_resource type="Environment" id="Environment_fjaix"]
background_mode = 2
sky = SubResource("Sky_guc0r")
tonemap_mode = 2
glow_enabled = true
[sub_resource type="CompositorEffect" id="CompositorEffect_d6jju"]
resource_local_to_scene = false
resource_name = ""
enabled = true
effect_callback_type = 4
needs_motion_vectors = false
needs_normal_roughness = false
script = ExtResource("2_pwabc")
[sub_resource type="CompositorEffect" id="CompositorEffect_ek4c3"]
resource_local_to_scene = false
resource_name = ""
enabled = false
effect_callback_type = 4
needs_motion_vectors = false
needs_normal_roughness = false
script = ExtResource("1_rkpno")
shader_code = " // Invert color.
color.rgb = vec3(1.0 - color.r, 1.0 - color.g, 1.0 - color.b);
"
[sub_resource type="Compositor" id="Compositor_xxhi4"]
compositor_effects = Array[CompositorEffect]([SubResource("CompositorEffect_d6jju"), SubResource("CompositorEffect_ek4c3")])
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_xlpoj"]
albedo_color = Color(0, 0.684707, 0.148281, 1)
albedo_texture = ExtResource("1_r22bv")
texture_filter = 5
[sub_resource type="PlaneMesh" id="PlaneMesh_82vj7"]
material = SubResource("StandardMaterial3D_xlpoj")
size = Vector2(10, 10)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_aqyxc"]
albedo_color = Color(0.946837, 0.315651, 0.66999, 1)
albedo_texture = ExtResource("1_r22bv")
texture_filter = 5
[sub_resource type="SphereMesh" id="SphereMesh_iuyuf"]
material = SubResource("StandardMaterial3D_aqyxc")
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_188mc"]
albedo_color = Color(0.436357, 0.305476, 0.999959, 1)
albedo_texture = ExtResource("1_r22bv")
texture_filter = 5
[sub_resource type="BoxMesh" id="BoxMesh_h605a"]
material = SubResource("StandardMaterial3D_188mc")
[node name="Main" type="Node3D"]
script = ExtResource("1_o0pyp")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(-0.866025, -0.433013, 0.25, 0, 0.5, 0.866025, -0.5, 0.75, -0.433013, 0, 0, 0)
shadow_enabled = true
shadow_bias = 0.04
directional_shadow_mode = 0
directional_shadow_fade_start = 1.0
directional_shadow_max_distance = 15.0
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_fjaix")
compositor = SubResource("Compositor_xxhi4")
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(0.866025, -0.129409, 0.482963, -1.54268e-08, 0.965926, 0.258819, -0.5, -0.224144, 0.836516, 1, 1.2, 2)
fov = 60.0
[node name="Ground" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -3.01202)
mesh = SubResource("PlaneMesh_82vj7")
[node name="Sphere" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -0.796)
mesh = SubResource("SphereMesh_iuyuf")
[node name="Box" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.333, 0.5, -0.392)
mesh = SubResource("BoxMesh_h605a")
[node name="Info" type="Label" parent="."]
offset_left = 24.0
offset_top = 24.0
offset_right = 64.0
offset_bottom = 47.0
theme_override_constants/outline_size = 4
text = "Grayscale effect: Enabled
Shader effect: Disabled"
[node name="Help" type="Label" parent="."]
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 24.0
offset_top = -47.0
offset_right = 175.0
offset_bottom = -24.0
grow_vertical = 0
theme_override_constants/outline_size = 4
text = "G: Toggle grayscale effect
S: Toggle shader effect"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://br4k6sn2rvgj"
path="res://.godot/imported/pattern.png-888ea151ee9fa7a079d3252596260765.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://pattern.png"
dest_files=["res://.godot/imported/pattern.png-888ea151ee9fa7a079d3252596260765.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View File

@@ -0,0 +1,89 @@
@tool
extends CompositorEffect
class_name PostProcessGrayScale
var rd: RenderingDevice
var shader: RID
var pipeline: RID
func _init() -> void:
effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT
rd = RenderingServer.get_rendering_device()
RenderingServer.call_on_render_thread(_initialize_compute)
# System notifications, we want to react on the notification that
# alerts us we are about to be destroyed.
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
if shader.is_valid():
# Freeing our shader will also free any dependents such as the pipeline!
RenderingServer.free_rid(shader)
#region Code in this region runs on the rendering thread.
# Compile our shader at initialization.
func _initialize_compute() -> void:
rd = RenderingServer.get_rendering_device()
if not rd:
return
# Compile our shader.
var shader_file := load("res://post_process_grayscale.glsl")
var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
shader = rd.shader_create_from_spirv(shader_spirv)
if shader.is_valid():
pipeline = rd.compute_pipeline_create(shader)
# Called by the rendering thread every frame.
func _render_callback(p_effect_callback_type: EffectCallbackType, p_render_data: RenderData) -> void:
if rd and p_effect_callback_type == EFFECT_CALLBACK_TYPE_POST_TRANSPARENT and pipeline.is_valid():
# 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 := p_render_data.get_render_scene_buffers()
if render_scene_buffers:
# Get our render size, this is the 3D render resolution!
var size: Vector2i = render_scene_buffers.get_internal_size()
if size.x == 0 and size.y == 0:
return
# We can use a compute shader here.
@warning_ignore("integer_division")
var x_groups := (size.x - 1) / 8 + 1
@warning_ignore("integer_division")
var y_groups := (size.y - 1) / 8 + 1
var z_groups := 1
# Create push constant.
# Must be aligned to 16 bytes and be in the same order as defined in the shader.
var push_constant := PackedFloat32Array([
size.x,
size.y,
0.0,
0.0,
])
# Loop through views just in case we're doing stereo rendering. No extra cost if this is mono.
var view_count: int = render_scene_buffers.get_view_count()
for view in view_count:
# Get the RID for our color image, we will be reading from and writing to it.
var input_image: RID = render_scene_buffers.get_color_layer(view)
# Create a uniform set, this will be cached, the cache will be cleared if our viewports configuration is changed.
var uniform := 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()
#endregion

View File

@@ -0,0 +1,34 @@
#[compute]
#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);
// 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);
}

View File

@@ -0,0 +1,14 @@
[remap]
importer="glsl"
type="RDShaderFile"
uid="uid://y08qi2a3t16m"
path="res://.godot/imported/post_process_grayscale.glsl-74f080e1b01e56b39260b8709956a520.res"
[deps]
source_file="res://post_process_grayscale.glsl"
dest_files=["res://.godot/imported/post_process_grayscale.glsl-74f080e1b01e56b39260b8709956a520.res"]
[params]

View File

@@ -0,0 +1,160 @@
@tool
extends CompositorEffect
class_name PostProcessShader
const template_shader := """#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.
// Must be aligned to 16 bytes, just like the push constant we passed from the script.
layout(push_constant, std430) uniform Params {
vec2 raster_size;
vec2 pad;
} 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);
}"""
@export_multiline var shader_code := "":
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.new()
var shader_is_dirty := true
func _init() -> void:
effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT
rd = RenderingServer.get_rendering_device()
# System notifications, we want to react on the notification that
# alerts us we are about to be destroyed.
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
if shader.is_valid():
# Freeing our shader will also free any dependents such as the pipeline!
RenderingServer.free_rid(shader)
#region Code in this region runs on the rendering thread.
# Check if our shader has changed and needs to be recompiled.
func _check_shader() -> bool:
if not rd:
return false
var new_shader_code := ""
# 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.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()
# Called by the rendering thread every frame.
func _render_callback(p_effect_callback_type: EffectCallbackType, p_render_data: RenderData) -> void:
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 := p_render_data.get_render_scene_buffers()
if render_scene_buffers:
# Get our render size, this is the 3D render resolution!
var size: Vector2i = render_scene_buffers.get_internal_size()
if size.x == 0 and size.y == 0:
return
# We can use a compute shader here.
@warning_ignore("integer_division")
var x_groups := (size.x - 1) / 8 + 1
@warning_ignore("integer_division")
var y_groups := (size.y - 1) / 8 + 1
var z_groups := 1
# Create push constant.
# Must be aligned to 16 bytes and be in the same order as defined in the shader.
var push_constant := PackedFloat32Array([
size.x,
size.y,
0.0,
0.0,
])
# Loop through views just in case we're doing stereo rendering. No extra cost if this is mono.
var view_count: int = render_scene_buffers.get_view_count()
for view in view_count:
# Get the RID for our color image, we will be reading from and writing to it.
var input_image: RID = render_scene_buffers.get_color_layer(view)
# Create a uniform set, this will be cached, the cache will be cleared if our viewports configuration is changed.
var uniform := 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()
#endregion

View File

@@ -0,0 +1,44 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Compositor Effects (Post-Processing)"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.3", "Forward Plus")
config/icon="res://icon.svg"
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
[input]
toggle_grayscale_effect={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":71,"physical_keycode":0,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
]
}
toggle_shader_effect={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
]
}
[rendering]
lights_and_shadows/directional_shadow/soft_shadow_filter_quality=3
textures/default_filters/anisotropic_filtering_level=4
anti_aliasing/quality/msaa_3d=2

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB