Skip to content

Commit

Permalink
Add Compositor Effects (Post-Processing) demo (#1058)
Browse files Browse the repository at this point in the history
Co-authored-by: Hugo Locurcio <[email protected]>
  • Loading branch information
BastiaanOlij and Calinou authored Jun 7, 2024
1 parent 785e321 commit 909331a
Show file tree
Hide file tree
Showing 14 changed files with 596 additions and 0 deletions.
39 changes: 39 additions & 0 deletions compute/post_shader/README.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions compute/post_shader/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions compute/post_shader/icon.svg.import
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions compute/post_shader/main.gd
Original file line number Diff line number Diff line change
@@ -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",
]

121 changes: 121 additions & 0 deletions compute/post_shader/main.tscn
Original file line number Diff line number Diff line change
@@ -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 added compute/post_shader/pattern.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions compute/post_shader/pattern.png.import
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions compute/post_shader/post_process_grayscale.gd
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions compute/post_shader/post_process_grayscale.glsl
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 909331a

Please sign in to comment.