KeiruaProd

I help my clients acquire new users and make more money with their web businesses. I have ten years of experience with SaaS projects. If that’s something you need help with, we should get in touch!
< Back to article list

godot pixel shader transition

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.

Creating the transition scene component

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).

Using the transition component

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()

The transition code

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()