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

Godot Version

4.5

Question

I’ve been looking at how plausible it is to convert my Unreal project over to Godot. However my post-process effects rely heavily on the ability to access the un-shaded scene geometry (so no lights/shadows in the scene), what is called the SceneColor in Unreal - and then be applied as an overlay to the lit scene.

I was wondering if anyone knew of a way to access the scene without any lighting or shading applied to it either in a GDShader or the Compositor, maybe some RenderTarget/Pass that would have the information?

I’ve tried some tests based on tutorials to get a feel for post-process in Godot, using both a GDShader and the compositor (just turning the scene greyscale), but lighting is always applied to the scene before the code is applied to it ( I hoped color_layer(0) might be pre-lighting but it wasn’t.

I presume this is because of it being Forwards vs Deffered rendering and that it might not be possible to get the unlit scene, but I figured I should ask.

Thanks :slight_smile:

1 Like

Your conclusion is correct. Unlike Unreal, Godot only does forward rendering so what you need cannot be gotten from the default rendering pipeline even via the Compositor.

You’ll have to do an additional custom rendering pass into an off-screen buffer (SubViewport in Godot terminology) and use the result as a texture in your final post processing shader.

Thanks for the reply.

Just to confirm, is an additional custom rendering pass something that is already supported from within Godots’ editor, or do you mean modifying the engine code to add in the additional pass and then using it through a SubViewport?

It’s fully supported in the editor. It may require a little bit of additional scene setup. The complexity of the setup would depend on your specific needs. If I understood your description correctly, you want just the unlit albedo?

Thanks for the reply, yes I did mean just the albedo.

So far, what I have done is to…

  1. Re-parent the scene items to all be attached to a SubViewport (so Node>SubViewport>Everything Else).

  2. I set the debug view on the SubViewport to “unshaded” giving me the effect I was after.

  3. I found an RTS cursor tutorial online which takes the main game camera, duplicates it for use with a SubViewport, and updates the transform - so whatever the main camera sees, the SubViewport sees as well. I modified the GDScript to instead use the Editor camera (instead of the in-game camera) so moving round the editor now updates the captured texture of the SubViewport.

  4. In the GDScript I got the Editors’ SubViewport size, and set this for the Unshaded SubViewports’ size.

There are a few things I am unsure of though:

I thought setting the SubViewport size to that of the editor, in addition to the camera being a duplicate of the editor camera would give a 1:1 perspective - however the capture is zoomed in as though the FOV isn’t the same (I tried getting/setting camera FOV but it made no difference). This means that objects visible in the editors viewport are cut off or missing in the SubViewport.

I’m unsure if it’s possible to specifically tell the SubViewport not to capture specific scene items. I know Godot supports Stencil Buffers, but can these be used as a way of excluding items from the capture? Or is the only method to give the SubViewport its own world, and modify the GDScript to duplicate items over from the main world to the SubViewport world, which may be a bit resource intensive in large scenes?

Is the unshaded debug view accessible in compiled games? I’m used to Unreal where if it’s debug it’s “Editor Only” so while it will often work in both Play In Editor and Standalone modes, it won’t work in a released build of a game.

I’ve also as-yet not found out how to pass the captured Render Target texture to a GDShader, though I did find information on how to pass it through to the Compositor.

You generally don’t need any scripting to set this up, except perhaps a line of code that copies the main viewport size to SubViewport size. But for a simple proof of concept prototype you can just enter the same numbers into SubViewport size and window size in the project settings.

The camera under the SubViewport needs to ape the main camera. To transfer the transforms use RemoteTransform3D node parented to the main camera, and to transfer fov, assign a shared CameraAttributesPhysical resource to attributes property of both cameras (assign a new object to one and copy-paste it to other). Lock the SubViewport camera node and manipulate only the main camera.

No need to maintain any copies of objects in this case. If you want to exclude something, Godot has visual instance rendering layers. Set your geometry’s layer property and then set camera’s cull_mask. The camera won’t see objects that are not on its cull_mask layers.

Afaik SubViewport debug rendering modes should work in the exported project. Make a test to be sure. If for some reason it doesn’t work, there are ways to render unlit albedo even without it.

To pass the SubViewport texture to a shader, declare a sampler2D uniform in the shader, then find the uniform in shader parameters in the inspector, assign ViewportTexture to it and pick your SubViewport.

Btw scene nodes don’t need to be under the SubViewport node if it shares the main world. The only node you need underneath is the camera.

1 Like

I just realized that you may have meant for this to be previewed in the editor. My previous post applies to runtime. Editor preview would in principle work the same but it’d require some additional scripting. It can be done with or without the Compositor. Do you really need the editor preview?

Yes, I did want it to be previewed in the editor, through the editors camera, because that way I know within the editor viewport that everything is functioning as expected without having to run anything first.

My use of the RTS script was to attempt to copy all the settings of the editor camera to a newly created and attached camera on the SubViewport, however this fails as the settings do not appear to transfer correctly (or at least, the FOV appears to remain different between the editor and sub viewports) but transforms are definitely updated.

@tool
extends SubViewport

var main_camera : Camera3D;
var sub_camera: Camera3D;
var EDV = EditorInterface.get_editor_viewport_3d();

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	main_camera = EDV.get_camera_3d();
	if main_camera == null:
		print ("ERROR")
		return;
	else:
		sub_camera = main_camera.duplicate(0);
		add_child(sub_camera);
		sub_camera.fov = main_camera.fov;
		sub_camera.projection = main_camera.projection;
		self.size = EDV.size;
		sub_camera.CameraAttributesPhysical = main_camera.CameraAttributesPhysical;

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	if sub_camera == null || main_camera == null:
		return;
	else:
		sub_camera.transform = main_camera.transform;
		sub_camera.fov = main_camera.fov;
		sub_camera.projection = main_camera.projection;

Is fov numerical value different between the cameras or things just look different?

Also, you should transfer global_transform instead of transform, just in case.

The following line makes no sense. It’ll likely produce an invalid property error:

sub_camera.CameraAttributesPhysical = main_camera.CameraAttributesPhysical

I made a quick test and it works as expected. The post processing shader splits the screen vertically. The right half is the unlit albedo from SubViewport.

Thanks for your help.

I’ve now managed to get a shader on a quad mesh that displays the output from the SubViewport.

Currently I have:

MyTree (root node in scene) then as children/grand-children:

Main_Cam - attached script transforms this to the same position as the editor camera.

|_ Quad Mesh - attached script gets the material override and sets the shader parameter to the texture of the SubViewport.

SubViewport - attached script gets the Editors viewport size and sets the SubViewport size to it.

|_ Camera - attached script uses the Main_Cams’ settings and transform to set its settings.

Also, the quad mesh is set to only Visual Layer 10, and the camera/subviewport set not to see it or it blanked out the entire screen even with the shader/parameter applied.

There are some issues remaining:

There was still a discrepency between the view of the cameras and the view of the SubViewport:

As you can see, the albedo texture is being stretched and isn’t 1:1 with the original scene. However I restarted the editor and it magically fixed itself, even though the editor had been restarted several times before, which I don’t like as it means I have no idea why it suddenly started working.

The Cameras can be selected by left clicking in the editor viewport, which leads to console spam: ERROR: scene/3d/camera_3d.cpp:482 - Condition “p.d == 0” is true. Returning: Point2()

I couldn’t get any method of the get_node functions to find the nodes, I tried multiple ways based on tutorials but I would always get either a null instance error, or the output would be a string of multiple editor related dialog names.

Thee only method I got to work which I know isn’t ideal was to use find_child: get_tree().root.find_child(“MainCam”, true, false));

Also ghosting is an issue, if you move the camera you see the two scenes separately.

Also is there any way to view the current framerate of a scene in the editor? I could only find information related to in-game framerates and not something to always display the framerate in the editor UI.

Not sure why this is happening. Your scripts might have not been updating something. Have in mind that if you change code in script’s _ready() it will only be re-executed when the nodes is added to the scene tree. For tool scripts that run in the editor you need to force that by closing and re-opening the scene.

Also set SubViewport’s render_target_update_mode to “Always”.

It’s likely caused by clicking on the object that has zero distance to the editor camera. In your case it’s SubViewport camera. Hide or lock the SubViewport camera node to make it unclickable. You may also want to lock the post processing quad node.

You’re probably using invalid paths. Post the specific case. Have in mind that node paths can be absolute or relative and that root is not the top node of your scene. The root is “invisible” viewport/window node that’s one level above that.

It’s likely caused by quad position updating with one frame delay in respect to editor camera which is updated separately from the scene tree. I’m not sure there’s a way around it when using a quad. This will not be the problem at runtime though as in that case the main camera will be the part of the scene tree.

Other possible glitches might appear due to rendering order in transparent pipeline. You might need to lower the render priority in quad’s material to force it to render before grid overlays. This priority will also have to be adjusted properly if your scene contains other objects that are rendered in the transparent pipeline.

You can see non-vsynched fps if you enable “View Frame Rate” in the 3d viewport menu (click on the word “Perspective” in top left corner)

In general you don’t need that many scripts. And you can set stuff up from gui without a need to call any _ready(). My test does everything from one script attached to SubViewport. It also works for both; editor and runtime camera. This is all of the script code I’m running:

@tool
extends SubViewport

func _process(delta: float) -> void:
	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()	
	self.size = vp_main.size
	%cam_albedo.global_transform = cam_main.global_transform
	%cam_albedo.fov = cam_main.fov
	%post_process_quad.global_transform = cam_main.global_transform.translated(-cam_main.global_basis.z * cam_main.near)

And the post processing quad runs this shader:

shader_type spatial;
render_mode cull_disabled, unshaded, depth_test_disabled;

uniform sampler2D screen: hint_screen_texture;
uniform sampler2D albedo_pass: source_color;

void vertex() {
	POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}

void fragment() {
	vec3 s = texture(screen, SCREEN_UV).rgb;
	vec3 a = texture(albedo_pass,SCREEN_UV).rgb;
	ALBEDO = mix(s, a, step(0.5, SCREEN_UV.x));
}
1 Like

Thanks for the reply, your script actually helped answer some of the issues/questions I was having and I now understand the named variable (%) since there’d been no mention of that in the tutorials I’d seen.

I see now that the PP Quad can be attached directly to the Editor camera, I had thought that the main_cam was a requirement (I took it for granted that this was an engine requirement), for simple testing of things this at least allows me to keep the scene that bit lighter on nodes (though naturally completely broken if I were to click play).

Also, I discovered that yes, the error messages were to do with selecting either the quad or camera through the viewport. I had been searching for disabling collision (to prevent their selection) not realising nodes could be locked. Once I found how to lock/unlock items from the editor (I would have thought it would have been the nodes’ right click menu in the Scene list, rather than a toolbar padlock icon) then the errors were fixed.

I also set up simple animations on some scene meshes (bouncing and rotating) and was able to confirm that if the camera is static, the ghosting doesn’t happen, even if the objects are fast moving. I also spotted that when moving round the editor viewport, you’d actually see the camera icon jumping from the previous position to the current one with quite a delay.

I had hunted throughout the settings for the frame rate, but never thought to click on the Perspective drop down menu - once I had the frame rate, my suspicions were confirmed in that the SubViewports capture is dropping my framerate by 30 fps, and increasing my GPU ms by another 5.

Also, for getting the nodes, I had been trying such things as:

#main_cam = get_tree().get_root().get_node(“MyTree“).get_node(“MainCam”);
#main_cam = get_tree().get_root().get_node(“MainCam”);
#print(get_node(“../MyTree”).name); (from the SubViewport script on this occasion, figuring it would go back one node, then forward).

etc. but I see from your script that for the editor you get_editor_viewport_3d, but for game you use the get_tree….. so I presume that my above method of getting nodes is only applicable to runtime and not in-editor use?

% prefix gets you a references to a scene unique node. It’s a mechanism for avoiding explicit node paths. Similarly $ followed by a node path is a shorthand for calling get_node()

It cannot really be attached but it can copy its transforms from the editor camera. As mentioned earlier there will be at least one frame update delay though. Because of that the quad might get out of sight when the camera is rapidly moving. Again, this will not happen at runtime. If you can’t live with this in the editor, you’ll have to implement the preview via the Compositor instead of using a quad.

Sounds reasonable although the absolute fps difference doesn’t mean much. If it becomes a bottleneck, you can always render the sub viewport at half or quarter resolution. Also try to eliminate every unneeded object from sub viewport rendering, like lights and world environment.

I’ll need to see a screenshot of your scene tree. Getting nodes is the same at runtime and in editor, but the tree is different. Since Godot’s editor is basically one big scene tree, if you call get_tree().get_root() in the editor, you’ll get the top of the hierarchy of the whole editor gui. So your scene will not be directly underneath that, while at runtime it will.

To get your scene’s top node in the editor you can use EditorInterface.get_edited_scene_root().

If you want to access nodes via same paths at runtime and in the editor, best to stick only to relative paths and not involve any get_root() calls.

Which I guess explains why I kept getting an error giving me a whole bunch of editor dialog node names in it as though it were reading a whole lot of internal nodes from somewhere.

It had all been working correctly, but just restarting the editor broke the scene (I changed nothing). I initally thought it was being on battery power as opposed to mains, but even restarting the computer on mains power didn’t fix it.

Also now FPS has dropped dramatically and the GPU time increased substantially as can be seen below:

Hard to tell without seeing your scene structure and all of your code.

It was odd, somehow the line of code that applies the SubViewport texture to the quad had been deleted, and it just never struck me it was missing, however much I stared at the GDScript/Shader code wondering what was going on.

It was still able to add my tint colour to the scene through the material, so changing the material did make changes to the scene as if everything was still connected.

No idea how I managed to wipe out the line, but at least I found out why it was happening, I guess there’s some fall-back texture in-play, because I can think of no other reason why there’d be the black diamond pattern otherwise.

Yeah I think this is a 4x4 px checker placeholder for when a Texture object is assigned to the uniform but there is no actual pixel data. It appears as blurry diamonds because of default linear filtering.

How would I go about passing the SubViewport texture through to the compositor?

The only information I found only suggested having to have a WorldEnvironment node in the scene tree, then using a script on the SubViewport to make a duplicate instance of the WE, and then pass the texture of the SubViewport through to the Compositor effect on the WE, and it also said it broke very easily, I don’t know if there’s a cleaner way of doing things, but information about the compositor and SubViewports was proving difficult to find.

I wondered if it were possible to entirely skip having an existing World Environment and try something like this in the _ready function of the SubViewport:

    #Create a new WorldEnvironment.
	var WE = WorldEnvironment.new();
	#Add it as a child to the SubViewport.
	add_child(WE);
	#Set its compositor.
	WE.compositor = load("res://Compositor_Examples/new_compositor.tres");	
	#Get the compositor effect we're using in the 0 index of its effects array.
	var Compos : compositor_greyscale = WE.compositor.compositor_effects[0];
	#Attach our loading script to the effect.
	Compos.set_script("Compositor_Loader_Script.gd");
	#Set the compositors texture variable.
	WE.compositor.SubViewport_Texture = self.get_texture();

Where I have an existing compositor as a file saved to disk, with the array already filled in with my compositor_greyscale class (still just using the tutorial I followed for now) and then attach the loading script to the effect and set the texture that way.

Downside is the “var Compos ….” line returns the error:

Trying to assign value of type ‘CompositorEffect’ to a variable of type ‘Compositor_Loader_Script.gd’.

I also don’t entirely know if the loading code is correct, I found a forum thread saying that the method for receiving the SubViewport texture in the loader was to use the RenderingServer.texture_get_rd_texture function to load the texture correctly, so this is how I currently have it, it compiles successfully but I’ve yet to actually get the unlit scene through to the glsl shader:

var SubViewport_Texture: Texture2D;
var SVT_RID := RID();

….

var unlit_scene := RDUniform.new();
unlit_scene.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE;
unlit_scene.binding = 2;
unlit_scene.add_id( RenderingServer.texture_get_rd_texture(SubViewport_Texture));

(for reference my glsl is basically just - binding 2 as 1 is the normal lit scene.):

layout(rgba16f, set = 0, binding = 2) uniform image2D unlit_scene;

and then imageload/store the unlit_scene for output.

Thanks for any help you can give :slight_smile:

Make a runtime version operational first to avoid messing with the editor. Once that’s running properly, you can do the editor version.

Attach a new Compositor object to your scene camera and add your CompositorEffect object to it. Do that via gui to avoid potentially messing up the code.
Make sure that SubViewport update mode is “Always”.

Can we see your complete compute shader code, and complete _render_callback() in your effect class? What’s the output you’re getting from the shader?