Need advice on Compositor workflow

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 RenderingServer and the global RenderingDevice in tandem; or does the RenderingDevice supercede the RenderingServer given that it’s lower level?
    • If this is the case, is that why a Viewport created via the RenderingServer provides an invalid texture; because it’s not defined at the appropriate level?
  • 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

What happens if you use texture_get_rd_texture(viewport_get_texture(...)) instead?

1 Like

Doing this does make the texture valid, but makes a new error pop up:

ERROR: Image (binding: 2, index 0) needs the TEXTURE_USAGE_STORAGE_BIT usage flag set in order to be used as uniform.

However, changing the uniform from an image2D to a sampler2D in the shader, and changing the uniform type, seems to fix the error.


Thanks for the tip on using RenderingServer.texture_get_rd_texture()!
I will report back if I get it all working.

Just out of curiosity, can you manually set that storage bit and use the viewport texture as an image?

I’m not sure how to manually set the storage bit. I had a similar issue with usage bits where using the color layer provided by the render buffers had invalid usage bits when making the CompositorEffect in C#.

Also, the viewport I’m creating appears to produce an all-black texture. I’m not sure what I need to tweak, but I’m tweaking.

Hm it appears that the flag can only be set at texture creation.

First set thing up with a texture provided by a subviwport node to make sure the sader side is getting the texture properly. Then proceed to using a manually created viewport instead.

As a guess you might need to assign a scenario (or a canvas for 2d) to the viewport using viewport_set_scenario() or viewport_attach_canvas(). You can get the scenario from the World3D object, either the default one attached to the main viewport or your custom one if you’re using a separate world for the viewport.

Yeah, I was thinking of using a SubViewport node as well, just to see if it actually works.
I tried to set the scenario by getting the world from the root viewport like so:

var root_viewport = Window.get_focused_window().get_viewport()
RenderingServer.viewport_set_scenario(zone_pass_viewport, root_viewport.world_3d.scenario)

Still no results.

Upon closer inspection though, it looks like I shouldn’t be using RenderingServer.viewport_get_texture() since this simply returns that last rendered frame – not the source texture for the viewport. I think I need to either use RenderingServer.viewport_get_render_target() or invoke RenderingServer.viewport_get_texture() whenever the compositor effect is executed (i.e. inside of _render_callback).

EDIT: Using RenderingServer.viewport_get_texture() inside of the _render_callback() does not seem to work. I will try using SubViewport nodes.

Yeah, you should get the texture rid every frame in _render_callback(), as your code creates a new compute list and bind the uniforms every frame. I never used render target rid so I’m not sure how it’s supposed to be used.

Doesn’t work in what way? Have you used texture_get_rd_texture() on the rid gotten from it. RenderingServer rids are difference form RenderingDevice rids.

# TESTING: VIEWPORT TEXTURE
var viewport_texture = RenderingServer.viewport_get_texture(zone_pass_viewport)
zone_pass_render_texture_rid = RenderingServer.texture_get_rd_texture(viewport_texture)

This is what I tried to do in the _render_callback() after configuring the viewport. The result is still a black texture when I render it directly to the screen.

Current CompositorEffect Version
@tool
class_name CompositorEffectTesting
extends CompositorEffect

var rd: RenderingDevice
var shader: RID
var pipeline: RID

# Render textures
var zone_pass_camera: RID
var zone_pass_viewport: RID
var zone_pass_render_texture_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_texture_rid.is_valid():
			RenderingServer.free_rid(zone_pass_render_texture_rid)
		if zone_pass_viewport.is_valid():
			RenderingServer.free_rid(zone_pass_viewport)
	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
	zone_pass_camera = RenderingServer.camera_create()
	#RenderingServer.camera_set_cull_mask(zone_pass_camera, ZONE_VISIBILITY_LAYER)
	
	zone_pass_viewport = RenderingServer.viewport_create()
	var root_viewport = Window.get_focused_window().get_viewport()
	var root_size = root_viewport.get_visible_rect().size
	RenderingServer.viewport_attach_camera(zone_pass_viewport, zone_pass_camera)
	RenderingServer.viewport_set_update_mode(zone_pass_viewport, RenderingServer.VIEWPORT_UPDATE_ALWAYS)
	RenderingServer.viewport_set_scenario(zone_pass_viewport, root_viewport.world_3d.scenario)
	#RenderingServer.viewport_set_parent_viewport(zone_pass_viewport, root_viewport.get_viewport())
	RenderingServer.viewport_set_size(zone_pass_viewport, root_size.x, root_size.y)
	
	print("Root viewport size: %s" % root_viewport.get_visible_rect().size)
	# 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 format: %s" % rd.texture_get_format(zone_pass_render_texture_rid).format)
		#print("Zone texture height: %s" % rd.texture_get_format(zone_pass_render_texture_rid).height)
		# 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])
				
				# TESTING: VIEWPORT TEXTURE
				var viewport_texture = RenderingServer.viewport_get_texture(zone_pass_viewport)
				zone_pass_render_texture_rid = RenderingServer.texture_get_rd_texture(viewport_texture)

				# UNIFORM: ZONE PASS TEXTURE (from viewport)
				uniform = RDUniform.new()
				uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_SAMPLER_WITH_TEXTURE
				uniform.binding = 2
				uniform.add_id(linear_sampler)
				uniform.add_id(zone_pass_render_texture_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

Is the viewport node update set to “always”?

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
	zone_pass_camera = RenderingServer.camera_create()
	#RenderingServer.camera_set_cull_mask(zone_pass_camera, ZONE_VISIBILITY_LAYER)
	
	zone_pass_viewport = RenderingServer.viewport_create()
	var root_viewport = Window.get_focused_window().get_viewport()
	var root_size = root_viewport.get_visible_rect().size
	RenderingServer.viewport_attach_camera(zone_pass_viewport, zone_pass_camera)
	RenderingServer.viewport_set_update_mode(zone_pass_viewport, RenderingServer.VIEWPORT_UPDATE_ALWAYS)
	RenderingServer.viewport_set_scenario(zone_pass_viewport, root_viewport.world_3d.scenario)
	#RenderingServer.viewport_set_parent_viewport(zone_pass_viewport, root_viewport.get_viewport())
	RenderingServer.viewport_set_size(zone_pass_viewport, root_size.x, root_size.y)

Yup.

Um that’s not for the viewport node.

Try to setup the pipeline to work only with the viewport node first, without creating viewports/cameras on the rendering server etc.

Oh, right. I misread. My bad. I’m in a speedy problem-solving mode right now. I just tried the above just to make sure.

I’m working on the viewport node setup right now.

1 Like

I’m getting the feeling this is a completely wrong way to approach the problem. The CompositorEffect can, obviously, not directly interact with nodes given that it’s not part of the scene tree. In order to pass the texture from the viewport node, I have to use ViewportTexture.get_image() which notes:

Note: This will fetch the texture data from the GPU, which might cause performance problems when overused. Avoid calling Texture2D.GetImage() every frame, especially on large textures.

I’ll do it anyway just to see if the shader actually works, but still. I feel like the correct solution has to do with the correct usage of the RenderingServer and/or RenderingDevice.

No, the compositor can work perfectly fine with the texture produced by a subviewport node. This texture lives on the rendering server, so does every other texture used by nodes. Nodes do their stuff using the servers. In a way, they are just additional abstractions over server api calls.

Make sure the subviewport node’s update is set to always and then get the rd texture rid via RenderingServer.texture_get_rd_texture(subviewport.get_texture().get_rid()) and pass it to the shader.

1 Like

That makes sense. But because of this, I also feel like I shouldn’t have to use the node(s).

In any case, I tried forwarding the viewport node’s texture RID as you described, and the texture is still black.

    public override void _Ready()
    {
        compositorMain.CompositorEffects[0].Set("zone_viewport_texture", this.GetTexture().GetRid());
    }

My whole project is in C# except for the compositor effect because of the aforementioned invalid usage bits present on the color layer (when using C#). That’s why you’re now seeing C# code.


I feel like I’m going crazy. I’m not sure what I’m doing wrong.
Do you have a resource or a previous project of yours that works like this?

…I tried this as well:

    public override void _Ready()
    {
        compositorMain.CompositorEffects[0].Set("zone_viewport_texture", RenderingServer.TextureGetRdTexture(this.GetTexture().GetRid()));
    }

You don’t need to use nodes whatsoever, but making a first iteration of the pipeline with nodes is easier to setup as it’s easy to miss some server call when setting things up. In the end, both versions work with the same stuff on the rendering server the difference is only in who makes the server calls, you or the node code.

So definitely set it up with the viewport node first. Going full server from there will be easy.

You need to pass the texture rid every frame when building and binding uniforms. At the time this _Ready() is invoked the texture may not even exist, as no frames will be rendered. You can check it by printing what rid is returned.

Best to get it every frame.