161 lines
4.8 KiB
GDScript
161 lines
4.8 KiB
GDScript
@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
|