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?