Stylized 3D Fire and Smoke

Stylized toon-shaded 3D fire and smoke vfx. The fire is made with a shader that transforms a cylinder mesh into 3D fire. The Smoke is made with a combination of particle systems and custom shader material.

The code below is only for the 3D fire. The demo project has both fire and smoke shader setups

Shader code
shader_type spatial;
render_mode unshaded;

group_uniforms FlameShape;
uniform float top_radius : hint_range(0.0, 1.5);
uniform float bottom_radius : hint_range(0.0, 1.5);
uniform float center_radius : hint_range(0.0, 5.0);
uniform float center_position : hint_range(0.0, 1.0);
uniform float cylinder_height : hint_range(0.0, 10.0) = 1.0;
uniform float bottom_curve = 0.4;
uniform float _vibrate_amplitude = 0.015;
uniform float _vibrate_frequency = 200.0;
group_uniforms;

group_uniforms FlameAppearance;
uniform float _speed = 2.0;
uniform float _scale = 2.0;
group_uniforms;

varying float cylinderheight;

void vertex() {
    float topradius = top_radius;
    float bottomradius =  bottom_radius;
    float centerradius = center_radius;
    float centerposition = center_position;
    cylinderheight = cylinder_height;

    vec3 v_pos = VERTEX;
    float original_height = v_pos.y;
    float relative_height = (original_height + 1.0) / 2.0;

    float scaledHeight =  relative_height * cylinderheight;
    v_pos.y = scaledHeight + sin(TIME * _vibrate_frequency) * _vibrate_amplitude;

    float radius;
    if (relative_height <= centerposition) {
        float t = relative_height / centerposition;
		t = pow(t, bottom_curve);
        t = smoothstep(0.0, 1.0, t);
        radius = mix(bottom_radius, centerradius, t);
    } else {
        float t = (relative_height - centerposition) / (1.0 - centerposition);
        t = smoothstep(0.0, 1.0, t);
        radius = mix(centerradius, topradius, t);
    }

    float current_radius = length(vec2(v_pos.x, v_pos.z));
    if (current_radius > 0.0) {
        float scale_factor = radius / current_radius;
        v_pos.x *= scale_factor;
        v_pos.z *= scale_factor;
    }

    VERTEX = v_pos;
}

vec3 mod2893(vec3 x) {
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 mod289(vec4 x) {
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 permute(vec4 x) {
     return mod289(((x*34.0)+1.0)*x);
}

vec4 taylorInvSqrt(vec4 r)
{
  return 1.79284291400159 - 0.85373472095314 * r;
}

float snoise(vec3 v)
  {
  const vec2  C = vec2(1.0/6.0, 1.0/3.0) ;
  const vec4  D = vec4(0.0, 0.5, 1.0, 2.0);

// First corner
  vec3 i  = floor(v + dot(v, C.yyy) );
  vec3 x0 =   v - i + dot(i, C.xxx) ;

// Other corners
  vec3 g = step(x0.yzx, x0.xyz);
  vec3 l = 1.0 - g;
  vec3 i1 = min( g.xyz, l.zxy );
  vec3 i2 = max( g.xyz, l.zxy );

  //   x0 = x0 - 0.0 + 0.0 * C.xxx;
  //   x1 = x0 - i1  + 1.0 * C.xxx;
  //   x2 = x0 - i2  + 2.0 * C.xxx;
  //   x3 = x0 - 1.0 + 3.0 * C.xxx;
  vec3 x1 = x0 - i1 + C.xxx;
  vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
  vec3 x3 = x0 - D.yyy;      // -1.0+3.0*C.x = -0.5 = -D.y

// Permutations
  i = mod2893(i);
  vec4 p = permute( permute( permute(
             i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
           + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
           + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));

// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
  float n_ = 0.142857142857; // 1.0/7.0
  vec3  ns = n_ * D.wyz - D.xzx;

  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);  //  mod(p,7*7)

  vec4 x_ = floor(j * ns.z);
  vec4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)

  vec4 x = x_ *ns.x + ns.yyyy;
  vec4 y = y_ *ns.x + ns.yyyy;
  vec4 h = 1.0 - abs(x) - abs(y);

  vec4 b0 = vec4( x.xy, y.xy );
  vec4 b1 = vec4( x.zw, y.zw );

  //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
  //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
  vec4 s0 = floor(b0)*2.0 + 1.0;
  vec4 s1 = floor(b1)*2.0 + 1.0;
  vec4 sh = -step(h, vec4(0.0));

  vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
  vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;

  vec3 p0 = vec3(a0.xy,h.x);
  vec3 p1 = vec3(a0.zw,h.y);
  vec3 p2 = vec3(a1.xy,h.z);
  vec3 p3 = vec3(a1.zw,h.w);

//Normalise gradients
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
  p0 *= norm.x;
  p1 *= norm.y;
  p2 *= norm.z;
  p3 *= norm.w;

// Mix final noise value
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
  m = m * m;
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
                                dot(p2,x2), dot(p3,x3) ) );
  }


vec3 hsv2rgb(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.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

float getNoise(vec2 uv, float t){

    float TRAVEL_SPEED = _speed;

    //octave 1
    float SCALE = _scale;
    float noise = snoise( vec3(uv.x*SCALE ,uv.y*SCALE * 4. + t*TRAVEL_SPEED , 0.));

    //octave 2 - more detail
    SCALE = 6.0;
    noise += snoise( vec3(uv.x*SCALE + t*TRAVEL_SPEED,uv.y*SCALE , 0))* 0.2 ;

    //move noise into 0 - 1 range
    noise = (noise/2. + 0.5);

    return noise;

}

float getDepth(float n, float cutoff, float steps){

    //given a 0-1 value return a depth,

    //remap remaining non-cutoff region to 0 - 1
	float d = (n - cutoff) / (1. - cutoff);

    //step it
    d = floor(d*steps)/steps;

    return d;

}

float hash(vec2 p){
	vec3 p3  = fract(vec3(p.xyx) * .1031);
	p3 += dot(p3, p3.yzx + 33.33);
	return fract((p3.x + p3.y) * p3.z);
}

float luminance(vec3 color) {
	const vec3 magic = vec3(0.2125, 0.7154, 0.0721);
	return dot(magic, color);
}

void fragment() {
    float steps = 4.;
    float cutoff = 0.15; //depth less than this, show black

    vec2 uv = UV;
    uv.x *= 5.0;
    float t = TIME * 3.0;
    vec3 col = vec3(0.0);

    float noise = getNoise(uv, t);

    //shape cutoff to get higher further up the screen
    cutoff = 1.1 - uv.y * 2.5;

    if (noise < cutoff){
        col = vec3(0.);
    }else{
        //fire
        float d = pow(getDepth(noise, cutoff, steps),0.7);
        vec3 hsv = vec3(d *0.17,0.8 - d/4., d + 0.8);
        col = hsv2rgb(hsv);
     }

     ALPHA = col.r;

     ALBEDO = vec3(col) * 1.5;

}
Tags
3D fire, 3D smoke, fire vfx, Smoke VFX
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Sivabalan

3D Hover CanvasItem

Drawing board shader

Related shaders

Stylized fire (3D)

Stylized grass with wind and deformation

Omen’s Smoke Bomb from Valorant

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments