How can I access the unlit scene in either GDShaders or Compositor for PostProcess effects?

So here’s the setup. Runs smooth as butter in the editor. I excluded all boilerplate error checks and thread safety stuff for brevity. Once you get it running you can bring all that back in if needed.

Editor:

Runtime:

Scene:

Script in scene root (updates camera transform/fov and SubViewport texture rid in compositor effect):

@tool
extends Node3D

func _process(dt) :
	var vp_main: Viewport
	if  Engine.is_editor_hint():
		vp_main = EditorInterface.get_editor_viewport_3d()
	else:
		vp_main = get_tree().get_root()
	var cam_main: Camera3D = vp_main.get_camera_3d()	
	%vp.size = vp_main.size
	%cam_vp.global_transform = cam_main.global_transform
	%cam_vp.fov = cam_main.fov
	
	# needs to be done on startup and vp resize but we do it every frame for simplicity
	var ce: CompositorEffect = %env.compositor.compositor_effects[0]
	ce.vp_tex = RenderingServer.texture_get_rd_texture(%vp.get_texture());

Compositor effect:

@tool
class_name Effect extends CompositorEffect

var shader_file = preload("effect.glsl")
var rd: RenderingDevice
var shader: RID
var pipeline: RID
var vp_tex: RID


func _init():
	effect_callback_type = CompositorEffect.EFFECT_CALLBACK_TYPE_POST_SKY
	rd = RenderingServer.get_rendering_device()
	shader = rd.shader_create_from_spirv(shader_file.get_spirv())
	pipeline = rd.compute_pipeline_create(shader)


func _render_callback(callback_type, render_data):
	# get frame buffers
	var render_scene_buffers: RenderSceneBuffersRD = render_data.get_render_scene_buffers()

	# main render image uniform
	var u_main: RDUniform = RDUniform.new()
	u_main.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
	u_main.binding = 0
	u_main.add_id(render_scene_buffers.get_color_layer(0))
	# vp render image uniform
	var u_vp: RDUniform = RDUniform.new()
	u_vp.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
	u_vp.binding = 1
	u_vp.add_id(vp_tex)
	# uniform set
	var uniform_set = UniformSetCacheRD.get_cache(shader, 0, [u_main, u_vp])

	# calculate number of workgroups
	var image_size = render_scene_buffers.get_internal_size()
	var wgroups_count_x = (image_size.x - 1) / 8 + 1
	var wgroups_count_y = (image_size.y - 1) / 8 + 1

	# size uniform (for split screen mixing and invocation "crop")
	var push_constant: PackedFloat32Array = PackedFloat32Array()
	push_constant.push_back(image_size.x)
	push_constant.push_back(image_size.y)
	push_constant.push_back(0.0)
	push_constant.push_back(0.0)

	# execute 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, wgroups_count_x, wgroups_count_y, 1)
	rd.compute_list_end()

Compute shader:

#[compute]
#version 450

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(rgba16f, set = 0, binding = 0) uniform image2D main_image;
layout(rgba8, set = 0, binding = 1) uniform image2D vp_image;

layout(push_constant) uniform Params {
	vec2 image_size;
	vec2 padding;
} params;

void main() {
	ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
	if(uv.x >= params.image_size.x || uv.y >= params.image_size.y){
		return;
	}
	vec3 main = imageLoad(main_image, uv).rgb;
	vec3 vp = imageLoad(vp_image, uv).rgb;
	float mix_fac = step(params.image_size.x / 2, uv.x);
	vec4 out_color = vec4(mix(main, vp, mix_fac), 1.0);
	imageStore(main_image, uv, out_color);
}

To avoid dealing with individual cameras, the Compositor is attached to the WorldEnvironment node, but since that applies the effect to all viewports including the SubViewport, there’s a dummy Compositor assigned to SubViewport’s camera to avoid “infinite loop” feedback over frames (which causes cool looking ghosting, try it out). You also might want to enable SubViewport’s use_hdr_2d flag to keep everything linear.