I’ve added a nice shader transition in a Godot game I’m working on using code I found on GodotShaders. The code only is a big help (this one came with an article explaining how the shader works), but that’s still was pretty raw and it is nice to build a nice structure. Here is what I came up with.
I have a custom DiamondTransition
scene that I want to be able to reuse:
.DiamondTransition
├── AnimationPlayer
└── ColorRect
The shader code is attached to a ShaderMaterial
on the Material
of the ColorRect
.
This scene drives the shader using an AnimationPlayer
, that has 2 animations (fade_in
and fade_out
), both with keyframes attached to the property material:shader_parameter/progress
. progress
is a property of the shader we will declare later on. In both cases, progress
goes from 0 to 1 in 1 second, the shader code deals
It’s possible to code this animation using a Tween
instead and to listen to the finished
signal. It’s probably more modular as the duration of the animation can be changed more easily, but that would involve writing and maintaining more code. That’s one of the beauty of Godot though ! Depending on your preferences, you can do things in different ways, either with a code-first approach (Tween
), or with a node first approach (using AnimationPlayer
).
The core idea is to have a simple component that can be reused with a very small footprint.
Once it’s added to another scene, we can:
In this example, I’ll transition from the CardBack scene to CardFront at the press of a key:
.ShaderTest
├── DiamondTransition
├── CardBack
└── CardFront
The ShaderTest
listens to the event fade_in_finished
from the transition scene, and calls on_diamond_transition_fade_in_finished
when it is triggered.
extends Node2D
@onready var card_back = $card_back
@onready var card_front = $card_front
# Called when the node enters the scene tree for the first time.
func _ready():
card_back.visible = true
card_front.visible = false
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
if Input.is_action_just_pressed("ui_accept"):
$DiamondTransition.play_fade_in()
func _on_diamond_transition_fade_in_finished():
card_back.visible = false
card_front.visible = true
$DiamondTransition.play_fade_out()
Onto the code. First, the full fragment code, with improvements based on the comments on GodotShaders:
// shaders/diamond_in.fragment
shader_type canvas_item;
// initial version (with explanation) from
// https://ddrkirby.com/articles/shader-based-transitions/shader-based-transitions.html
// using optimizations from
// https://godotshaders.com/shader/diamond-based-screen-transition/#comment-403
// An input into the shader from our game code.
// Ranges from 0 to 1 over the course of the transition.
// We use this to actually animate the shader.
uniform float progress : hint_range(0, 1);
// Size of each diamond, in pixels.
uniform float diamondPixelSize = 10.f;
float when_lt(float x, float y) {
return max(sign(y - x), 0.0);
}
void fragment() {
float xFraction = fract(FRAGCOORD.x / diamondPixelSize);
float yFraction = fract(FRAGCOORD.y / diamondPixelSize);
float xDistance = abs(xFraction - 0.5);
float yDistance = abs(yFraction - 0.5);
COLOR.a *= when_lt(xDistance + yDistance + UV.x + UV.y, progress * 4.0f);
}
It’s possible to write the reverse transition (left as an exercise in the original article) by simply changing the last line. Read the original article to get a feel for why this works.
// shaders/diamond_out.fragment
void fragment() {
// ...
COLOR.a *= when_lt(xDistance + yDistance + (1.0f-UV.x) + (1.0f-UV.y), (1.0f-progress) * 4.0f);
}
Then, we need to drive this shader. The CanvasLayer
it’s attached on has its own script, whose code is below.
We simply want to expose two functions (fade_in
, fade_out
) that can be called from outside.
When the fade_in transition is over, the AnimationPlayer
triggers an event which is listened to, so that the scene can trigger another dedicated fade_in_finished
event which can then, in turn, be listened by the scene that uses this effect.
(it is possible to emit the fade_in_finished
signal directly from the AnimationPlayer
by running code directly inside the animation : again, one of the various possibilities of Godot).
Note the 2 @export
variables, so that we can customize this effect directly from the game editor.
extends CanvasLayer
@export var transition_color: Color = Color("2c84b7")
@export var diamond_pixel_size: float = 10.0
signal fade_in_finished
func _ready():
$ColorRect.material.set("shader_parameter/progress", 0)
pass
func set_shader_material_to(material):
$ColorRect.material = ShaderMaterial.new()
$ColorRect.material.set("shader", material)
$ColorRect.material.set("shader_parameter/progress", 0)
$ColorRect.material.set("shader_parameter/diamondPixelSize", diamond_pixel_size)
func play_fade_in():
var material = load("res://shaders/diamonds_in.gdshader")
$ColorRect.color = transition_color
set_shader_material_to(material)
# It might be possible to use a tween instead of an AnimationPlayer:
# https://docs.godotengine.org/en/stable/classes/class_tween.html
# https://www.reddit.com/r/godot/comments/u4yy9k/comment/jjqum4q/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
$AnimationPlayer.play("fade_in")
func play_fade_out():
var material = load("res://shaders/diamonds_out.gdshader")
$ColorRect.color = transition_color
set_shader_material_to(material)
$AnimationPlayer.play("fade_out")
func _on_animation_player_animation_finished(anim_name):
if anim_name == "fade_in":
fade_in_finished.emit()