Godot Version
v4.6.stable.mono.official [89cea1439]
Question
I’ve been trying to create a masked full-screen blur effect where only parts of the screen are blurred. I initially achieved this using SubViewport nodes. However, given that I want a large blur radius for the effect, I need to optimize the blur algorithm. I’m thinking of doing a multi-pass Gaussian blur to start.
Now, because multi-pass effects are a little odd to achieve with nodes, I’ve been trying to use the Compositor. So far, I’ve got the multi-pass blur effect working. However, I’m not quite sure of how to define a new viewport and pass its render target to the shader with the uniform set so I can create the effect previously achieved with SubViewport nodes.
This is currently what I’m doing to configure the viewport:
func configure_additional_passes():
const ZONE_VISIBILITY_LAYER = 1 << 15
const VISION_VISIBILITY_LAYER = 1 << 16
# TODO: Register a camera and viewport setup that mimics the one currently in the camera scene (two viewports with one camera each).
# Configure camera and viewport for zone-based mask
var cam_rid = RenderingServer.camera_create()
RenderingServer.camera_set_cull_mask(cam_rid, ZONE_VISIBILITY_LAYER)
var viewport_rid = RenderingServer.viewport_create()
RenderingServer.viewport_attach_camera(viewport_rid, cam_rid)
RenderingServer.viewport_set_update_mode(viewport_rid, RenderingServer.VIEWPORT_UPDATE_ALWAYS)
#RenderingServer.viewport_set_size(viewport_rid, 1920, 1080)
zone_pass_render_target_rid = RenderingServer.viewport_get_render_target(viewport_rid)
# TODO: Configure viewport for vision-based (light from player) mask
pass
…and then pass the texture to the .glsl shader:
# UNIFORM: ZONE PASS TEXTURE (from viewport)
uniform = RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
uniform.binding = 2
uniform.add_id(zone_pass_render_target_rid)
var uniform_zone_set := UniformSetCacheRD.get_cache(shader, 2, [uniform])
# [...]
# Run directional blur shader (with X-direction)
var compute_list_x := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list_x, pipeline)
rd.compute_list_bind_uniform_set(compute_list_x, uniform_color_set, 0)
rd.compute_list_bind_uniform_set(compute_list_x, uniform_blur_set, 1)
rd.compute_list_bind_uniform_set(compute_list_x, uniform_zone_set, 2)
rd.compute_list_set_push_constant(compute_list_x, push_constant_x.to_byte_array(), push_constant_x.size() * 4)
rd.compute_list_dispatch(compute_list_x, x_groups, y_groups, z_groups)
rd.compute_list_end()
# Run directional blur shader (with Y-direction)
var compute_list_y := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list_y, pipeline)
rd.compute_list_bind_uniform_set(compute_list_y, uniform_color_set, 0)
rd.compute_list_bind_uniform_set(compute_list_y, uniform_blur_set, 1)
rd.compute_list_bind_uniform_set(compute_list_y, uniform_zone_set, 2)
rd.compute_list_set_push_constant(compute_list_y, push_constant_y.to_byte_array(), push_constant_y.size() * 4)
rd.compute_list_dispatch(compute_list_y, x_groups, y_groups, z_groups)
rd.compute_list_end()
Doing this produces the following error:
ERROR: Image (binding: 2, index 0) is not a valid texture.
I now sit with a couple of questions:
- Is it possible to retrieve a “valid” viewport texture for use in .glsl compute shaders?
- Is it okay to utilize
RenderingServerand the globalRenderingDevicein tandem; or does theRenderingDevicesupercede theRenderingServergiven that it’s lower level?- If this is the case, is that why a
Viewportcreated via theRenderingServerprovides an invalid texture; because it’s not defined at the appropriate level?
- If this is the case, is that why a
- Should I be using an entirely different approach?
If you have experience with the Compositor or low-level rendering, I’d greatly appreciate your advice.
Full CompositorEffect script
@tool
class_name CompositorEffectTesting
extends CompositorEffect
var rd: RenderingDevice
var shader: RID
var pipeline: RID
# Render textures
var zone_pass_render_target_rid: RID
const context_name = "blurCtx"
const blur_texture_name = "blurTex"
func _init() -> void:
effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT
rd = RenderingServer.get_rendering_device()
RenderingServer.call_on_render_thread(_initialize_compute)
configure_additional_passes()
# 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!
rd.free_rid(shader)
if zone_pass_render_target_rid.is_valid():
RenderingServer.free_rid(zone_pass_render_target_rid)
if (what == NOTIFICATION_EXTENSION_RELOADED):
print("Compositor effect reloaded!")
#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://shaders/glsl_shaders/selective_directional_blur.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)
func configure_additional_passes():
const ZONE_VISIBILITY_LAYER = 1 << 15
const VISION_VISIBILITY_LAYER = 1 << 16
# TODO: Register a camera and viewport setup that mimics the one currently in the camera scene (two viewports with one camera each).
# Configure camera and viewport for zone-based mask
var cam_rid = RenderingServer.camera_create()
RenderingServer.camera_set_cull_mask(cam_rid, ZONE_VISIBILITY_LAYER)
var viewport_rid = RenderingServer.viewport_create()
RenderingServer.viewport_attach_camera(viewport_rid, cam_rid)
RenderingServer.viewport_set_update_mode(viewport_rid, RenderingServer.VIEWPORT_UPDATE_ALWAYS)
#RenderingServer.viewport_set_size(viewport_rid, 1920, 1080)
zone_pass_render_target_rid = RenderingServer.viewport_get_render_target(viewport_rid)
# TODO: Configure viewport for vision-based (light from player) mask
pass
# 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():
print("Zone texture width: %s" % rd.texture_get_format(zone_pass_render_target_rid))
# 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: Vector2i = render_scene_buffers.get_internal_size()
if size.x == 0 and size.y == 0:
return
#region Create/Refresh Textures
if (render_scene_buffers.has_texture(context_name, blur_texture_name)):
var tf = render_scene_buffers.get_texture_format(context_name, blur_texture_name)
#if (tf.width != effect)
if (!render_scene_buffers.has_texture(context_name, blur_texture_name)):
var usage_bits = RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT | RenderingDevice.TEXTURE_USAGE_STORAGE_BIT
render_scene_buffers.create_texture(context_name, blur_texture_name, RenderingDevice.DATA_FORMAT_R8G8B8A8_UNORM, usage_bits, RenderingDevice.TEXTURE_SAMPLES_1, size, 1, 1, true, false)
#endregion
# 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_x := PackedFloat32Array([
size.x,
size.y,
size.x,
size.y,
1.0, # Blur direction (X)
0.0, # Blur direction (Y)
0.0,
0.0
])
var push_constant_y := PackedFloat32Array([
size.x,
size.y,
size.x,
size.y,
0.0, # Blur direction (X)
1.0, # Blur direction (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)
var blur_tex: RID = render_scene_buffers.get_texture_slice(context_name, blur_texture_name, view, 0, 1, 1)
var sampler_state = RDSamplerState.new()
sampler_state.min_filter = RenderingDevice.SAMPLER_FILTER_LINEAR
sampler_state.mag_filter = RenderingDevice.SAMPLER_FILTER_LINEAR
var linear_sampler = rd.sampler_create(sampler_state)
# Create a uniform set, this will be cached, the cache will be cleared if our viewports configuration is changed.
# UNIFORM: SCREEN TEXTURE
var uniform := RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
uniform.binding = 0
uniform.add_id(input_image)
var uniform_color_set := UniformSetCacheRD.get_cache(shader, 0, [uniform])
# UNIFORM: BLUR TEXTURE
uniform = RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
uniform.binding = 1
uniform.add_id(blur_tex)
var uniform_blur_set := UniformSetCacheRD.get_cache(shader, 1, [uniform])
# UNIFORM: ZONE PASS TEXTURE (from viewport)
uniform = RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
uniform.binding = 2
#uniform.add_id(linear_sampler)
uniform.add_id(zone_pass_render_target_rid)
var uniform_zone_set := UniformSetCacheRD.get_cache(shader, 2, [uniform])
# Run directional blur shader (with X-direction)
var compute_list_x := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list_x, pipeline)
rd.compute_list_bind_uniform_set(compute_list_x, uniform_color_set, 0)
rd.compute_list_bind_uniform_set(compute_list_x, uniform_blur_set, 1)
rd.compute_list_bind_uniform_set(compute_list_x, uniform_zone_set, 2)
rd.compute_list_set_push_constant(compute_list_x, push_constant_x.to_byte_array(), push_constant_x.size() * 4)
rd.compute_list_dispatch(compute_list_x, x_groups, y_groups, z_groups)
rd.compute_list_end()
# Run directional blur shader (with Y-direction)
var compute_list_y := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list_y, pipeline)
rd.compute_list_bind_uniform_set(compute_list_y, uniform_color_set, 0)
rd.compute_list_bind_uniform_set(compute_list_y, uniform_blur_set, 1)
rd.compute_list_bind_uniform_set(compute_list_y, uniform_zone_set, 2)
rd.compute_list_set_push_constant(compute_list_y, push_constant_y.to_byte_array(), push_constant_y.size() * 4)
rd.compute_list_dispatch(compute_list_y, x_groups, y_groups, z_groups)
rd.compute_list_end()
#endregion