Procedural Torch & Candle Shader (Fire + Smoke + Sparks)

🔥 Procedural Torch & Candle Flame Shader (v1.0 – Smoke & Sparks Update)

This canvas_item shader, built for Godot 4.x, generates a fully procedural animated flame without any textures or particles.
It simulates fire, smoke, and sparks entirely through mathematical functions, resulting in a dynamic and stylized torch or candle effect ideal for pixel-art, fantasy scenes, or UI decorations.

The shader includes an optional pixelation control for retro or low-resolution aesthetics.

 

Designed for small-scale applications, this shader delivers smooth, organic motion and flickering light with full color control — all within a single draw call.

🌟 Key Features

  • Procedural Fire Simulation:
    Creates a convincing vertical flame shape with realistic heat distortion and smooth fading.

  • Integrated Smoke and Sparks:
    Adds drifting smoke trails and sharp glowing sparks, both affected by adjustable wind force.

  • Hue & Saturation Control:
    Allows subtle color shifting to simulate magical or stylized flames.

  • Dynamic Flicker:
    Simulates natural fire flicker using time-based pseudo-random noise.

  • Aspect Ratio and Positioning:
    Works seamlessly on sprites, control nodes, or full-screen overlays with tunable offset and scaling.

  • Adjustable Pixelation:
    Pixelate the output for a stylized or retro appearance, or disable for smooth, realistic blending.

 

✨ New in v1.0: Smoke & Sparks Update

Feature Description
Sparks Simulation Adds a customizable layer of glowing embers that rise above the flame and fade naturally.
Smoke Layer Introduces procedural smoke puffs that drift upwards and sideways, influenced by wind.
Wind Force Control Global horizontal movement that bends flame, sparks, and smoke in sync.
Hue & Saturation Adjustments Modify fire color tone dynamically for stylized effects (e.g. blue fire, magical fire).
Performance Tweaks Streamlined loops, better per-pixel efficiency, and stable brightness handling.

⚙️ Usage & Parameters

Usage:
Apply this shader to a ColorRect, Sprite2D, or TextureRect node with a transparent background.
Scale the node to roughly match your intended flame size (e.g., 128×256).
For best results, place over a dark or semi-transparent background.

Parameter Description Default Range
particleCount Number of flame particles; higher = smoother fire. 128 16–512
speed Animation playback speed multiplier. 1.0 0.1–5.0
pixelSize Pixelation level for a stylized low-res effect. 0.015 0.001–0.1
brightness Base brightness of the fire core. 0.001 0.0001–0.01
fireShift Horizontal randomness of the flame. 0.15 0.0–0.5
fireShiftFrequency Frequency of flame oscillation. 5.0 1.0–10.0
fromColor Base color (hottest point). (0.9, 0.2, 0.1)
toColor Top color (cooler, smokier). (0.4, 0.35, 0.2)
flickerStrength Intensity of brightness flicker. 0.15 0.0–0.5
sparkCount Number of glowing sparks emitted. 18 1–50
sparkSize Spark size (smaller = sharper points). 0.004 0.0005–0.005
sparkSpeed Spark movement speed. 0.15 0.1–3.0
smokeCount Number of smoke puffs simulated. 20 5–50
smokeSize Size of smoke puffs. 0.015 0.001–0.05
smokeDrift Horizontal drift of smoke. 0.2 0.0–0.5
windForce Horizontal wind influence on all particles. 0.0 -0.5–0.5
hueShift Shifts color hue for stylized flames. 0.0 -1.0–1.0
saturationFactor Multiplies color saturation. 1.0 0.0–2.0
aspectRatio Adjusts for non-square nodes (width/height). 1.0 0.01–5.0
posOffset Moves the flame’s origin relative to the node. (0.0, -0.5)
alpha Global transparency multiplier. 0.85 0.0–1.0

💬 Notes & Performance Tips

  • The shader is optimized for small areas (like torches, candles, and campfires).
    Using it full-screen or in many simultaneous instances may impact performance.

  • On modern GPUs, one or two flames are negligible — hundreds may be costly.

  • You can dynamically lower particleCount and smokeCount when far from the camera.

  • Works with additive or alpha blending; experiment with modulate for visual variety.

⚡ Licensing & Attribution

Feel free to use, remix, or modify this shader for your own projects.
If you post your own variations or optimizations, please consider sharing them back on godotshaders.com

 

 

Shader code
//  Procedural Torch & Candle Shader (Fire + Smoke + Sparks)
//  Author: CaptainLaptop
//  License: CC0 / MIT
//  Engine: Godot 4.x
//
//  Description:
//  A fully procedural, particle-free fire simulation that
//  generates animated flames, sparks, and smoke using only math.
//
//  Ideal for candles, torches, and other small light sources.
//  Avoid large full-screen use as it can be fillrate-heavy.
//
//  Controls include flicker, wind, hue shift, color, and size.

/* USAGE:
1. Apply this shader to a CanvasItem (e.g., a ColorRect or a Sprite2D).
2. The effect is drawn from the center of the node, growing upwards (Y-axis inverted).
3. Adjust 'aspectRatio' to match your node's width/height ratio if not 1:1.
*/
shader_type canvas_item;



// =============================================================================
// GLOBAL CONTROL UNIFORMS
// =============================================================================

// Controls the overall speed of particle movement and animation
uniform float speed : hint_range(0.1, 5.0) = 1.0;
// Defines the size of the 'pixels' or blocks the fire is rendered in
uniform float pixelSize : hint_range(0.001, 0.1) = 0.015;
// Overall intensity multiplier for the colors
uniform float alpha : hint_range(0.0, 1.0) = 0.95; 
// Strength of the overall light flicker effect
uniform float flickerStrength : hint_range(0.0, 0.5) = 0.15; 

// =============================================================================
// COLOR & HUE UNIFORMS
// =============================================================================

// Color near the top (smoke/darker)
uniform vec3 toColor : source_color = vec3(0.4, 0.35, 0.2);  
// Color near the base (hottest/brightest)
uniform vec3 fromColor : source_color = vec3(0.9, 0.2, 0.1); 

// Shifts the base color hue (HSV 'H' value)
uniform float hueShift : hint_range(-1.0, 1.0) = 0.0; 
// Multiplier for saturation (HSV 'S' value)
uniform float saturationFactor : hint_range(0.0, 2.0) = 1.0; 

// =============================================================================
// FIRE SIMULATION UNIFORMS
// =============================================================================

// The number of individual fire particles calculated
uniform float particleCount : hint_range(16.0, 512.0) = 128.0;
// Base light brightness applied to particles
uniform float brightness : hint_range(0.0001, 0.01) = 0.001;

// Horizontal influence of particle noise, determines flame width
uniform float fireShift : hint_range(0.0, 0.5) = 0.15;
// Frequency of the noise wave for horizontal flickering/wobble
uniform float fireShiftFrequency : hint_range(1.0, 10.0) = 5.0;

// x: Max horizontal span, y: Max vertical height of the fire
uniform vec2 size = vec2(0.05, 0.75);
// x: inner glow start, y: outer glow end (used in smoothstep for falloff)
uniform vec2 glow = vec2(0.001, 0.04); 

// =============================================================================
// SPARKS UNIFORMS
// =============================================================================

uniform float sparkCount : hint_range(1.0, 50.0) = 18.0; 
uniform float sparkSize : hint_range(0.0005, 0.005) = 0.004; 
uniform float sparkSpeed : hint_range(0.1, 3.0) = 0.15; 
uniform vec3 sparkColor : source_color = vec3(1.0, 0.7, 0.0); 

// =============================================================================
// SMOKE UNIFORMS
// =============================================================================

uniform float smokeCount : hint_range(5.0, 50.0) = 20.0;
uniform float smokeSize : hint_range(0.001, 0.05) = 0.015; 
uniform float smokeDrift : hint_range(0.0, 0.5) = 0.2; 
uniform vec3 smokeColor : source_color = vec3(0.05, 0.05, 0.05); 

// =============================================================================
// ENVIRONMENT UNIFORMS
// =============================================================================

// Constant horizontal force applied to all particles (wind)
uniform float windForce : hint_range(-0.5, 0.5) = 0.0; 
// Node's Width/Height ratio for correct aspect correction
uniform float aspectRatio : hint_range(0.01, 5.0) = 1.0; 
// Position offset (shifts the origin of the flame source)
uniform vec2 posOffset = vec2(0.0, -0.5); 


// =============================================================================
// HELPER FUNCTIONS
// =============================================================================

// 1D Hash function for generating particle randomness
float Hash1(float t) {
    return fract(cos(t * 124.97) * 248.842) - 0.5;
}

// Equivalent to GLSL's 'saturate' (clamps between 0.0 and 1.0)
float saturate(float x) {
    return clamp(x, 0.0, 1.0);
}

// --- HSV Utility Functions (Custom implementation for portability) ---

// Converts RGB color space to HSV color space
vec3 rgb_to_hsv(vec3 c) {
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.w - q.y) / (6.0 * d + e) + q.z, d / (q.x + e), q.x);
}

// Converts HSV color space to RGB color space
vec3 hsv_to_rgb(vec3 c) {
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.w);
    return c.z * mix(K.xxx, clamp(p - K.x, 0.0, 1.0), c.y);
}


// =============================================================================
// PARTICLE SIMULATION FUNCTIONS
// =============================================================================

// Core fire particle simulation
vec3 Simulate(vec2 uv, float t) {
    vec3 res = vec3(0.0);
    
    for (float i = 0.0; i < particleCount; i++) {
        // ct: Current time through the particle's lifecycle (0.0 to 1.0)
        float ct = fract(t + (i + 1.0) / particleCount);
        
        // seed: Unique randomness based on ID and current cycle
        float seed = Hash1((i + 1.0) * (t - ct));
        
        // dir: Base direction vector. WindForce affects X (horizontal).
        vec2 dir = vec2(0.0, size.y); 
        dir.x += windForce; 
        
        // Fire wobble/flicker (Horizontal noise based on time and seed)
        dir.x += (cos(t * seed) * sin(ct * fireShiftFrequency)) * mix(0.0, fireShift, log(ct));
        
        // cb: Current brightness, fading out as particle ages (ct increases)
        float cb = saturate(mix(brightness, 0.0, ct));
        
        // off: Initial horizontal offset randomized by seed
        vec2 off = vec2(seed * size.x, 0.0); 
        
        // Accumulate color: mix base color to top color based on age (ct)
        res += mix(fromColor * abs(seed), toColor, ct) 
               // Apply glow/falloff using smoothstep on inverse distance
               * smoothstep(glow.x, glow.y, cb / length((uv - off - (dir * ct))));
    }
    
    return res;
}

// Simulation for sharp, fast-moving sparks
vec3 SimulateSparks(vec2 uv, float t) {
    vec3 sparkRes = vec3(0.0);
    
    t *= sparkSpeed; // Adjust time scale for spark speed
    
    for (float i = 0.0; i < sparkCount; i++) {
        float particleId = (i + 1.0) * 133.0; 
        float ct = fract(t + particleId / 100.0); // Lifecycle time
        float seed = Hash1(particleId); 
        
        // High vertical velocity (2.5x flame height)
        vec2 dir = vec2(seed * 0.4, size.y * 2.5); 
        dir.x += windForce * 2.0; // Sparks are pushed harder by wind
        
        // Current particle center position
        vec2 particle_center = vec2(seed * size.x * 0.3, 0.0) + (dir * ct); 
        
        float dist = length(uv - particle_center);
        
        // Create an extremely sharp, localized glow
        float particle_influence = saturate(1.0 - dist / (sparkSize * 5.0)); 
        float glow_falloff = pow(particle_influence, 15.0); // High power = sharp falloff
        float lifespan_fade = saturate(1.0 - ct * 2.0); 

        // Add contribution (boost intensity for visibility)
        sparkRes += sparkColor * glow_falloff * lifespan_fade * 100.0; 
    }
    return sparkRes;
}

// Simulation for slow-moving, diffuse smoke
vec3 SimulateSmoke(vec2 uv, float t) {
    vec3 smokeRes = vec3(0.0);
    
    for (float i = 0.0; i < smokeCount; i++) {
        float particleId = (i + 1.0) * 111.0;  
        float ct = fract(t * 0.5 + particleId / 100.0); // Slower cycle
        float seed = Hash1(particleId); 
        
        // Slower vertical velocity than fire, with horizontal drift
        vec2 dir = vec2(seed * smokeDrift, size.y * 1.5); 
        dir.x += windForce * 0.8; // Smoke is pushed by wind
        
        float cb = saturate(mix(brightness * 10.0, 0.0, ct * 1.5)); 
        
        // Starts near the top of the fire/smoke base
        vec2 off = vec2(seed * size.x * 0.5, size.y * 0.6); 

        // Add contribution: uses a larger smoothstep range for a diffused look
        smokeRes += smokeColor * smoothstep(smokeSize, smokeSize * 2.0,
            cb / length((uv - off - (dir * ct))));
    }
    return smokeRes;
}


// =============================================================================
// FRAGMENT MAIN
// =============================================================================

void fragment() {
    // 1. Coordinate Setup
    vec2 frag_coords = UV * 2.0 - 1.0; // [-1, 1] range
    frag_coords.x *= aspectRatio;      // Correct X-axis for aspect ratio
    frag_coords.y *= -1.0;             // Invert Y so fire grows upwards
    frag_coords -= posOffset;          // Apply source offset
    
    // 2. Pixelation effect
    vec2 pixelated_frag = floor(frag_coords / pixelSize) * pixelSize;
    
    // 3. Run Simulations
    float base_time = TIME + 100.0;
    
    vec3 fire_color = Simulate(pixelated_frag, base_time * speed);
    vec3 spark_color = SimulateSparks(pixelated_frag, base_time);
    vec3 smoke_color = SimulateSmoke(pixelated_frag, base_time);

    vec3 result_color = fire_color + spark_color + smoke_color; 
    
    // 4. Hue & Saturation Control
    vec3 hsv = rgb_to_hsv(result_color);
    
    hsv.x = fract(hsv.x + hueShift); // Apply hue shift (wrap around)
    hsv.y *= saturationFactor;       // Apply saturation factor
    
    result_color = hsv_to_rgb(hsv);
    
    // 5. Apply Flicker
    float flicker = (Hash1(base_time * 5.0) + 0.5) * flickerStrength; 
    float intensity_mod = 1.0 + flicker; 
    vec3 final_color = result_color * intensity_mod;

    // 6. Alpha Calculation
    // Use the max color channel intensity to define alpha (transparency)
    float intensity = max(max(result_color.r, result_color.g), result_color.b);
    
    // Smoothstep ensures sharp cut-off on edges
    float final_alpha = smoothstep(0.01, 0.5, intensity) * alpha;
    
    // Final output color
    COLOR = vec4(final_color * alpha, final_alpha);
}
Tags
campfire, candle, Ember, fantasy, fire, flame, flicker, glow, Heat, lighting, Magical, Particle-Free, pixel, pixel-art, Procedural, shader, Smoke, Sparks, torch, vfx
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.

More from CaptainLaptop

Pulsing Screen Scale Shader (Heartbeat & Sine Modes)

Rhythmic Color Pulse Shader (Sine Wave Brightness and Tint)

LED Grid Overlay Shader (Adjustable Aspect Ratio) v.2.0

Related shaders

Fireball or Candle fire shader

Lighter/Candle flame

3D – Candle Flame

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments