SubViewport texture look brighter than what the camera should show

Godot Version

v4.5.dev

Question

Hello :slightly_smiling_face: ,

I am currently working on some sort of portal for my 3D game, the idea is to have a door that will open when the player is close enough and act as a portal, the player can enter the door and should seemlessly be teleported on the other side like in the Portal games.

However, the render of my “portal” does not exactly look like what the camera would show on the other side : the color are a little too bright or are washed out :

To create this portal effect I am using a BoxMesh on which I added this shader :

shader_type spatial;
render_mode unshaded;

uniform sampler2D viewport_texture : source_color;
void fragment() {

	vec4 tex = texture(viewport_texture, SCREEN_UV);

	ALBEDO = tex.rgb;
}

Then for the “viewport_texture” I’m adding the SubViewport that is on this portal scene, it has a Camera3D as a child that is moved toward the exit portal paired with this one.

I tried to fiddle around the differents SubViewport parameters but I didn’t find anything that would make it look the same.

Finally, I also tried to tweak the sky parameters, most specificaly everything related to emission or energy multiplier, but I would still not find a solution.

The same issue can be seen in my debug room where the sky is almost the one by default for the engine :

When checking online the thing that seems to work for most peoples is putting the “source_color” flag on the sampler2d of the shader, but it didn’t work out for me.

If anyone know anything about this it would be of huge help :grinning_face_with_smiling_eyes: .

Just had the same problem trying to recreate portals. Tried a lot of stuff, but had to resort to fable for help:

1. The colour space problem

A normal SubViewport outputs an 8-bit texture that is already sRGB-encoded and clamped to 0..1. ALBEDO expects linear light, and the main pass encodes to sRGB again on its way to the screen. So the portal gets gamma-encoded twice, which lifts every midtone.

source_color is supposed to fix this by flagging the sampler as sRGB, but it is resolved for imported textures, and a render target doesn’t go through that path. On a ViewportTexture it often quietly does nothing.

2. The tonemap / glow problem

If your Environment uses anything other than the Linear tonemapper (Filmic, ACES, AgX) or has glow enabled, the portal camera applies it once, and then the main pass applies it again to the portal surface, because to the main pass it is just an unshaded quad. ACES applied twice lifts mid-grey by roughly half on its own. This one stays even if you fix the colour space perfectly.

The fix

Make the portal camera hand back raw linear light, and let the main pass do all the “look” work exactly once.

Step 1. On the SubViewport, enable use_hdr_2d = true. The docs say it best: the result “will not be clamped into the 0-1 range and can be used in 3D rendering without color space adjustments”. It becomes an RGBA16F target with no sRGB encode and no clamp.

Step 2. In the shader, drop source_color and sample straight through. fog_disabled stops the main pass from fogging the surface a second time.

shader_type spatial;
render_mode unshaded, fog_disabled;

uniform sampler2D viewport_texture : filter_linear, repeat_disable;

void fragment() {
    ALBEDO = texture(viewport_texture, SCREEN_UV).rgb;
}

Step 3. Give the portal camera a copy of the world Environment with the look stage switched off, plus neutral camera attributes so exposure and DOF aren’t doubled either.

func _ready() -> void:
    var world_env: Environment = get_world_3d().environment
    if world_env:
        var raw: Environment = world_env.duplicate()
        raw.tonemap_mode = Environment.TONE_MAPPER_LINEAR
        raw.tonemap_exposure = 1.0
        raw.glow_enabled = false
        raw.adjustment_enabled = false
        $SubViewport/Camera3D.environment = raw
    $SubViewport/Camera3D.attributes = CameraAttributesPractical.new()

duplicate() is shallow, so the sky is still shared. If you swap the WorldEnvironment at runtime, rebuild the copy.

After this the floor seen through the portal matches the floor beside it pixel for pixel, with ACES and glow enabled in the main environment.

If you’d rather not use HDR 2D

Keep the 8-bit viewport and decode manually in the shader instead of relying on source_color:

vec3 srgb_to_linear(vec3 c) {
    return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c));
}

That fixes problem 1 only, so you still need Step 3 for problem 2, and highlights above 1.0 get clamped.