Save Expand icon

Ron Valstar
front-end developer

Experiment: procedural fire with sparks in GLSL

Here’s a hot shader: fire with sparks. The fire is Simplex noise, nothing special apart from some displacement to fake realism. But the sparks might need some explanation.

The shader is entirely procedural. So the sparks flying upward are calculated for each pixel. We could calculate the sparks in advance as particles and put them into the shader by uniform. But there’s no fun in that.
The technique I wanted to implement here is similar to one I previously used in plain JavaScript in an earlier experiment.

A grid, a prng and some trigonometry

The gist of it is as follows: calculate a grid, feed the grid positions into a pseudo random number generator (prng), use the random number to rotate a particle inside the grid.
Compared to the snow experiment the mental approach is a bit like coding it the other way around (we don’t really displace particles, we displace the input variables). Plus we cannot exceed the grid boundaries (we’re calculating pixels, not particles). If you click without dragging you will see the displaced grid. Notice the sparks are moving in circles within each grid cell.

First we create a grid. To do that we use modulo grid-size on the xy position: mod(gl_FragCoord,40.0), line 68. This will give us a grid of linear gradients (from zero to one) that we can use to create a spherical gradient.
If you change the very last line to: gl_FragColor = vec4(mod(gl_FragCoord,40.0)/40.0,0.0,1.0); you’ll see a pattern like this. The green and red are going from zero to one every 40 pixels.

modulo grid

The axes are divided by the size of the grid and floored to get the seeds for the random number. So the coordinates for the bottom left grid cell become 0,0 (the default origin for gl_FragCoord is the bottom left corner). Defining the grid index is done here.
Again a simple example: gl_FragColor = vec4(floor(gl_FragCoord.x/40.0)/4.0,floor(gl_FragCoord.y/40.0)/3.0,0.0,1.0); makes this:

grid indices

The floored indices are the numbers we’ll use as input for our prng. The prng we’ll be using is one by David Hoskins who says: The magic numbers were chosen by mashing the number pad, until it looked OK!. Haha, well fractional parts beats LCG and Mersenne Twister in this case.
This gets us the following gl_FragColor = vec4(prng(floor(gl_FragCoord.xy/40.0)),0.0,0.0,1.0);:

random grid indices

Let there be sparks

This random number is used to define the size of the spark as well as it’s rotation and it’s life expectancy. The input derived from gl_FragCoord is also displaced around a bit to create some flow direction and randomness.

And that is really all there is to it.
The source code is below. I’ve also ported this over to Shadertoy (with minor some adjustments to get it working there) so you can easily tweak and/or fork it.

#include "/static/glsl/noise3D.glsl"
#include "/static/glsl/noiseStack.glsl"
#include "/static/glsl/prng.glsl"

uniform float time;
uniform vec2 resolution;
uniform float size;
uniform float down;
uniform vec2 drag;
uniform vec2 offset;
float PI = 3.1415926535897932384626433832795;

vec2 noiseStackUV(vec3 pos,int octaves,float falloff,float diff){
  float displaceA = noiseStack(pos,octaves,falloff);
  float displaceB = noiseStack(pos+vec3(3984.293,423.21,5235.19),octaves,falloff);
  return vec2(displaceA,displaceB);
}

void main(){
  vec2 fragCoord = gl_FragCoord.xy;
  //
  float xpart = fragCoord.x/resolution.x;
  float ypart = fragCoord.y/resolution.y;
  //
  float clip = 210.0;
  float ypartClip = fragCoord.y/clip;
  float ypartClippedFalloff = clamp(2.0-ypartClip,0.0,1.0);
  float ypartClipped = min(ypartClip,1.0);
  float ypartClippedn = 1.0-ypartClipped;
  //
  float xfuel = 1.0-abs(2.0*xpart-1.0);//pow(1.0-abs(2.0*xpart-1.0),0.5);
  //
  float timeSpeed = 0.5;
  float realTime = timeSpeed*time;
  //
  vec2 coordScaled = 0.01*fragCoord - 0.02*vec2(offset.x,0.0);
  vec3 position = vec3(coordScaled,0.0) + vec3(1223.0,6434.0,8425.0);
  vec3 flow = vec3(4.1*(0.5-xpart)*pow(ypartClippedn,4.0),-2.0*xfuel*pow(ypartClippedn,64.0),0.0);
  vec3 timing = realTime*vec3(0.0,-1.7,1.1) + flow;
  //
  vec3 displacePos = vec3(1.0,0.5,1.0)*2.4*position+realTime*vec3(0.01,-0.7,1.3);
  vec3 displace3 = vec3(noiseStackUV(displacePos,2,0.4,0.1),0.0);
  //
  vec3 noiseCoord = (vec3(2.0,1.0,1.0)*position+timing+0.4*displace3)/1.0;
  float noise = noiseStack(noiseCoord,3,0.4);
  //
  float flames = pow(ypartClipped,0.3*xfuel)*pow(noise,0.3*xfuel);
  //
  float f = ypartClippedFalloff*pow(1.0-flames*flames*flames,8.0);
  float fff = f*f*f;
  vec3 fire = 1.5*vec3(f, fff, fff*fff);
  //
  // smoke
  float smokeNoise = 0.5+snoise(0.4*position+timing*vec3(1.0,1.0,0.2))/2.0;
  vec3 smoke = vec3(0.3*pow(xfuel,3.0)*pow(ypart,2.0)*(smokeNoise+0.4*(1.0-noise)));
  //
  // sparks
  float sparkGridSize = 30.0;
  vec2 sparkCoord = fragCoord - vec2(2.0*offset.x,190.0*realTime);
  sparkCoord -= 30.0*noiseStackUV(0.01*vec3(sparkCoord,30.0*time),1,0.4,0.1);
  sparkCoord += 100.0*flow.xy;
  if (mod(sparkCoord.y/sparkGridSize,2.0)<1.0) sparkCoord.x += 0.5*sparkGridSize;
  vec2 sparkGridIndex = vec2(floor(sparkCoord/sparkGridSize));
  float sparkRandom = prng(sparkGridIndex);
  float sparkLife = min(10.0*(1.0-min((sparkGridIndex.y+(190.0*realTime/sparkGridSize))/(24.0-20.0*sparkRandom),1.0)),1.0);
  vec3 sparks = vec3(0.0);
  if (sparkLife>0.0) {
    float sparkSize = xfuel*xfuel*sparkRandom*0.06;
    float sparkRadians = 999.0*sparkRandom*2.0*PI + 2.0*time;
    vec2 sparkCircular = vec2(sin(sparkRadians),cos(sparkRadians));
    vec2 sparkOffset = (0.5-sparkSize)*sparkGridSize*sparkCircular;
    vec2 sparkModulus = mod(sparkCoord+sparkOffset,sparkGridSize) - 0.5*vec2(sparkGridSize);
    float sparkLength = length(sparkModulus);
    float sparksGray = max(0.0, 1.0 - sparkLength/(sparkSize*sparkGridSize));
    sparks = sparkLife*sparksGray*vec3(1.0,0.3,0.0);
  }
  if (down==1.0&&length(drag)<10.0) sparks += vec3(sparkLife*mod(0.7*sparkGridIndex,1.0),0.0);
  //
  gl_FragColor = vec4(max(fire,sparks)+smoke,1.0);
}

comment send comment