Fire Shader

fire shader in godot

This fire shader has some good options to make it both stylized & normal fire. You can download the project here to play with it and make changes.

Fire is quite an interesting thing in a game to add feeling. I was interested in making a fire shader since I saw a campfire in Commandos 2 game.

Making Noise Function

First we make a value noise function. It looks similar to Perlin noise but is technically different. The idea is to sample random numbers on grid and interpolate between them to get smooth transition.

The hash() function returns random numbers and noise() function does the actual value noise calculation. Finally, fbm() combines many layers of noise with varying properties and create a nice looking noise.

// 2D Value noise function
float hash(vec2 p) {
    p = fract(p * 0.3183099 + vec2(0.1, 0.1));
    p *= 17.0;
    return fract(p.x * p.y * (p.x + p.y));
}

// 2D Value noise function (smooth)
float noise(vec2 x) {
    vec2 p = floor(x);
    vec2 f = fract(x);

    float n = 
        hash(p)       * (1.0 - f.x) * (1.0 - f.y) +
        hash(p + vec2(1.0, 0.0)) * f.x * (1.0 - f.y) +
        hash(p + vec2(0.0, 1.0)) * (1.0 - f.x) * f.y +
        hash(p + vec2(1.0, 1.0)) * f.x * f.y;

    return n;
}

// Fractional Brownian Motion function (2D)
float fbm(vec2 p) {
	int octaves = noise_octaves;
 	float lacunarity = noise_lacunarity;
 	float gain = noise_gain;
 	float amplitude = noise_amplitude;
 	float frequency = noise_frequency;
	float total = 0.0;
	
    for (int i = 0; i < octaves; i++) {
        total += noise(p * frequency) * amplitude;
        frequency *= lacunarity;
        amplitude *= gain;
    }

    return total * 0.5;
}
Value noise as a result of above fbm() function

Animating Fire

The noise values made scrolling using the time. Also, we modified our original UVs to offset the fire position to be on desired position. Then we sampled the noise values by calling the function.

After that, the intensity of noise at different areas are calculated to give our fire a shape. Constant intensity means uniform fire. But since we need a water-drop-shaped fire, we used an exponent pow() function & passed UVs to compute intensity of fire on different places of our mesh.

pow()
pow() with offset
pow() + noise

For color calculation, the intensity of red channel is always more, followed by green and then blue channel. This is because greater values of exponent in pow() functions on values less than 1.0 make output even more pushed towards 0.0.

    vec3 fire_color = vec3(
		1.5 * fire_intensity1,
		1.5 * pow(fire_intensity1, 3.0),
		pow(fire_intensity1, 6.0)
	);

Finally, we compute alpha to be areas not fire. However we do leave some border between fire and alpha-ed area to render black color there, faking smoke:

// For alpha
float alpha = fire_intensity * (1.0 - pow(uv.y, 3.0));
// But for fire
noise * fire_intensity * (1.5 - pow(uv.y, 14.0));

// The exponents are different between fire and alpha portions; which appear blackish; therefore smoke.

Changing Fire Color

1st approach: Make a uniform variable named vec3 color_tint and mix or multiply it with final_color. This way you will have more control over the output color.

2nd approach: This involves swapping the final_color.rgb in different configurations to achieve some different colors. For example:

	if (colorval == 0)
	ALBEDO = final_color.rgb;
	else if (colorval == 1)
	ALBEDO = final_color.bgr;
	else if (colorval == 2)
	ALBEDO = final_color.brg;

Fire Shader Code

Godot fire shader code here:

shader_type spatial;

render_mode unshaded;

// fire uniforms
uniform float detail_strength = 3.0;
uniform float scroll_speed = 1.2;
uniform float fire_height = 1.0;
uniform float fire_shape = 1.5;
uniform float fire_thickness = 0.55;
uniform float fire_sharpness = 1.0;
uniform float intensity = 1.0;

// noise uniforms
uniform int noise_octaves = 6;
uniform float noise_lacunarity = 3.0;
uniform float noise_gain = 0.5;
uniform float noise_amplitude = 1.0;
uniform float noise_frequency = 1.5;

// 2D Value noise function
float hash(vec2 p) {
    p = fract(p * 0.3183099 + vec2(0.1, 0.1));
    p *= 17.0;
    return fract(p.x * p.y * (p.x + p.y));
}

// 2D Value noise function (smooth)
float noise(vec2 x) {
    vec2 p = floor(x);
    vec2 f = fract(x);

    float n = 
        hash(p)       * (1.0 - f.x) * (1.0 - f.y) +
        hash(p + vec2(1.0, 0.0)) * f.x * (1.0 - f.y) +
        hash(p + vec2(0.0, 1.0)) * (1.0 - f.x) * f.y +
        hash(p + vec2(1.0, 1.0)) * f.x * f.y;

    return n;
}

// Fractional Brownian Motion function (2D)
float fbm(vec2 p) {
	int octaves = noise_octaves;
 	float lacunarity = noise_lacunarity;
 	float gain = noise_gain;
 	float amplitude = noise_amplitude;
 	float frequency = noise_frequency;
	float total = 0.0;
	
    for (int i = 0; i < octaves; i++) {
        total += noise(p * frequency) * amplitude;
        frequency *= lacunarity;
        amplitude *= gain;
    }

    return total * 0.5;
}

void fragment() {
    vec2 uv = UV;

	// modified_uv for offset and animating UVs for fire    
    vec2 modified_uv = -uv;
	modified_uv.x = mod(modified_uv.x, 1.0) - 0.5;
    modified_uv.y += 0.84; // size vertical
    
	// fire noise scroll effect
	float scroll = scroll_speed * detail_strength * TIME;
	
	// sample noise for fire
	float noise = fbm(detail_strength * modified_uv - vec2(0.0, scroll));
    
	// controls the intensity of noise
	// at different points in uvs. it thus
	// controls the shape of fire
	float fire_intensity = intensity - 16.0 * fire_sharpness * pow(
		max(
			0.0,
			length(
				modified_uv * vec2((1.0 / fire_thickness) + modified_uv.y * fire_shape, 1.0 / fire_height)
			) - noise * max(0.0, modified_uv.y + 0.25)
		),
		1.2
	);
    float fire_intensity1 = noise * fire_intensity * (1.5 - pow(1.0 * uv.y, 14.0));
    fire_intensity1 = clamp(fire_intensity1, 0.0, 1.0);

	// compute fire color
	// red channel is most intense, green less than it
	// blue least intense because [pow of x while(x < 1) is less than x] 
    vec3 fire_color = vec3(
		1.5 * fire_intensity1,
		1.5 * pow(fire_intensity1, 3.0),
		pow(fire_intensity1, 6.0)
	);

	// add alpha to color
    float alpha = fire_intensity * (1.0 - pow(uv.y, 3.0));
    vec4 final_color = vec4(
		mix(vec3(0.0), fire_color, alpha),
		alpha
	);
    
	// apply to fragment
	ALBEDO = final_color.rgb;
    ALPHA = final_color.a;
}

More Reading

I wrote this in hurry & didn’t explain inner things in detail. But I’ll come back here after mids or maybe finals.