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_offset uniform 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 speedstrength, and scale (wave size) of the sway directly in the Inspector.

 

How to Use:

 

  1. Apply the shader to a Sprite2D node.

  2. Set the time_offset uniform to a different random value on each instance (either in the Inspector or via GDScript) to de-synchronize the movement.

  3. Adjust sway_strength and sway_speed to 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;
}
Live Preview
Tags
2d, canvas_item, distortion, fire, flame, flicker, grass, leaves, noise, sway, uv displacement, wave, waving
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
CinemaLeo
CinemaLeo
1 month ago

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;