wrote a Godot 4 scene transition system that swaps shaders — black fades were embarrassing me

298 views 0 replies

Every transition in my game was a plain black fade. Fine for a jam, embarrassing past that. Spent a few evenings building a small autoload singleton that handles scene changes with swappable shader-based transitions — the shader is just a parameter, so anything that reads a progress uniform drops in without touching the manager code.

The setup: a CanvasLayer at z-index 100 with a ColorRect covering the screen and a transparent Control node that swallows input during the animation.

# TransitionManager.gd — add as autoload
extends CanvasLayer

signal transition_finished

@onready var overlay: ColorRect = $Overlay
@onready var blocker: Control = $InputBlocker

var _active: bool = false

func transition_to(scene_path: String, mat: ShaderMaterial = null) -> void:
    if _active:
        return
    _active = true
    if mat:
        overlay.material = mat
    overlay.visible = true
    blocker.mouse_filter = Control.MOUSE_FILTER_STOP
    var tween := create_tween()
    tween.tween_method(_set_progress, 0.0, 1.0, 0.4)
    await tween.finished
    get_tree().change_scene_to_file(scene_path)
    tween = create_tween()
    tween.tween_method(_set_progress, 1.0, 0.0, 0.4)
    await tween.finished
    overlay.visible = false
    blocker.mouse_filter = Control.MOUSE_FILTER_IGNORE
    _active = false
    transition_finished.emit()

func _set_progress(v: float) -> void:
    (overlay.material as ShaderMaterial).set_shader_parameter("progress", v)

Default shader on the ColorRect is a radial wipe, set it in the scene so it's always there as the fallback:

shader_type canvas_item;
uniform float progress : hint_range(0.0, 1.0) = 0.0;

void fragment() {
    float dist = distance(UV, vec2(0.5));
    COLOR = vec4(0.0, 0.0, 0.0, step(dist, progress * 0.8));
}

Swap in whatever you want. I have a pixelate-out and a scanline wipe I cycle between depending on the scene context. The progress param convention means any new shader just works without touching the manager.

The thing I haven't cracked: transitions where the destination scene should peek through before the overlay lifts, a reveal where the new scene shows through before it's fully clear. Right now the overlay is fully opaque during the out-phase and clears once the new scene loads. Doing it properly probably means pre-rendering the destination to a SubViewport and sampling it in the shader, but that feels heavyweight for what it is. Has anyone done a clean version of this in Godot?

Moonjump
Forum Search Shader Sandbox
Sign In Register