2D Flame, Leaves, Wavy Sway / Gentle Horizontal Flicker Shader
A simple yet effective canvas_item shader designed to add a gentle, perpetual horizontal “sway” or “flicker” to any 2D texture, perfect for fire, flags, heat haze, or stylized energy effects.
Key Features:
-
UV Displacement: Uses a time-based noise function to displace the texture’s UV coordinates horizontally, creating a smooth, waving motion.
-
Anti-Synchronization: Includes a
time_offsetuniform that allows you to assign a unique random phase to each instance. This prevents multiple sprites using the same shader from moving in perfect unison, essential for realistic-looking groups of flames or candles. -
Customizable: Exposed uniform variables let you easily control the speed, strength, and scale (wave size) of the sway directly in the Inspector.
How to Use:
-
Apply the shader to a
Sprite2Dnode. -
Set the
time_offsetuniform to a different random value on each instance (either in the Inspector or via GDScript) to de-synchronize the movement. -
Adjust
sway_strengthandsway_speedto fine-tune the intensity and pace of the flicker.
Use it with this script to randomize the time_offset. – Remember to mark each node using this shader as unique for this to work.
extends Sprite2D
func _ready():
# Check if the material is a ShaderMaterial
if material is ShaderMaterial:
# Generate a random float between 0.0 and 100.0
# This acts as the unique starting point in time for the noise cycle.
var random_offset = randf() * 100.0
# Set the uniform parameter in the shader
(material as ShaderMaterial).set_shader_parameter("time_offset", random_offset)
Shader code
shader_type canvas_item;
// The speed of the horizontal flame movement (sway)
uniform float sway_speed : hint_range(0.1, 5.0) = 1.0;
// How far the texture is displaced horizontally
uniform float sway_strength : hint_range(0.0, 0.05) = 0.01;
// Scales the noise to control the size of the waves/flickers
uniform float noise_scale : hint_range(1.0, 20.0) = 5.0;
// NEW: This uniform will hold the time offset for this specific flame instance.
uniform float time_offset : hint_range(0.0, 100.0) = 0.0;
// Simple noise function (adapted from common GLSL techniques)
float noise1(vec2 p, float t) { // Pass the time 't' as an argument
// We use the passed-in time 't' instead of the global TIME
float n = sin(p.x * 2.0 + t * sway_speed) * 0.5;
n += sin(p.y * 5.0 + t * sway_speed * 1.5) * 0.3;
return n;
}
void fragment() {
// 1. Calculate the INSTANCE TIME: TIME + offset
float instance_time = TIME + time_offset;
vec2 distorted_uv = UV;
// 2. Call the noise function with the instance-specific time
float displacement = noise1(UV * noise_scale, instance_time) * sway_strength;
// 3. Apply the displacement
distorted_uv.x += displacement;
// 4. Sample the original texture
vec4 color = texture(TEXTURE, distorted_uv);
COLOR = color;
}

Wonderful shader!
I was having trouble in 4.5 for some reason, with the colours all being forced to white after processing.
I got it working again by changing the final line to this:
// 5. Multiply by COLOR to apply Godot’s modulation (font color, etc.)
COLOR = color * COLOR;