How cool would it be to generate 'volumetric' flames! Not only flames, but also control them so you can have the flames morph into shapes like teddy bears, numbers or cars. What is more, the flame is animated so it flickers and moves like a real flame!! Cool? Very!
And there is more! Oh yeah! This 3d flame effect runs in real-time in your web-browser on a web-page using just JavaScript and the WebGPU API! Can you beleive it! We truly live in an age of magic that a web-page can generate volumetric effects without breaking a sweat.
The figure shows the effect being used to generate the letters for the word 'xbdev'. Cool eh? NO? ... even better ...'Super Cool!'
Flames and Fire Effects
The algorithm works using the fragment shader to ray-trace a simple signed distance function. For example, a `sphere` - this is very simple and efficient - however, the algorithm mixes 'fractal noise' on top of the shape to introduce a `flame-like effect`.
Fractals are used for the noise to create a pattern that is random but also has the characteristics of 'fire'.
What are SDF functions? Why are they so great in ray-tracing?
If you've never heard of SDF functions - Signed Distance Functions (SDFs) are mathematical representations that define the distance from any point in space to the closest surface of a shape. They return a single number which is positive outside the shape, zero or negative (ouside, on the surface or inside the shape).
This simple concpet makes SDFs highly efficient for geometric queries and surface evaluations.
SDFs are great in ray-tracing as they enable efficient and precise intersection testing. Instead of complex intersection algorithms, rays can be marched incrementally by the distance returned by the SDF, significantly simplifying calculations.
As you'll see later on, SDFs are versatile for a wide variety of shapes and operations, including smooth blends and boolean operations (construct complex shapes from simple shapes). This flexibility will allow us to create complex models for our flame effect with ease.
SDF Function (Circle)
Minimal working example that we can use as a starting point - setting up the output in the fragment shader. The vertex shader is a full screen quad - the texture coordinates are the corners of the quad (0 to 1 for the u and v).
In the fragment shader we use the uv coordinates to construct the ray tracer origin and ray direction - which is used to find the color of each pixel.
@group(0) @binding(0) var mySampler: sampler; @group(0) @binding(1) var myTexture: texture_2d<f32>; @group(0) @binding(2) var <uniform> mytimer : f32;
fn raymarch(ro: vec3<f32>, rd: vec3<f32>) -> vec4<f32> { var p = ro; var d = 0.0; let eps = 0.02;
for (var i=0; i<64; i=i+1) { var d = scene( ro + rd*f32(i)*eps ); if ( d < eps ) { return vec4<f32>( d * 50.0 ); } } return vec4<f32>(0.0); }
@fragment fn main(@location(0) uvs : vec2<f32>) -> @location(0) vec4<f32> { var v = -1.0 + 2.0 * uvs.xy / 1.0;
var ro = vec3<f32>(0., 0.0, -2.0); var rd = normalize(vec3<f32>(v.x*1.0, -v.y, 1.0));
var fragColor = raymarch(ro, rd);
fragColor.w = 1.0;// no alpha return fragColor; }
You can use an adaptive size step size for the ray-tracer - however, a fixed step size simplifies the implementation and reduce computational overhead compared to adaptive step sizes values. Fixed step size ensures consistent performance and predictable behavior, avoiding the complexities of dynamically adjusting steps. It prevents potential overshooting in regions where the SDF gradient changes rapidly, ensuring stable traversal through the scene. Also allows us to customize the result - more/less steps for accuracy and resolution.
Noise (Random Pixels)
Noise is more than just `random` values - it embodies a whole subject area! Noise comes in all sorts of flavors; each having their own unique characteristics.
Fractal Noise (Blobs)
Lets look at basic noise on the GPU. There is two ways of getting noise onto the GPU threads; (1) load an external texture containing random numbers - or (2) generate the random number on the GPU in code (using some unique seed value to that thread).
When we talk about `noise` functions, we're not just talking about random numbers but creating a specific type of noise pattern.
fn noise(p : vec3<f32>) -> f32 { var i = floor(p); var a = dot(i, vec3<f32>(1., 57., 21.)) + vec4<f32>(0., 57., 21., 78.); var f = cos((p-i)*acos(-1.))*(-.5)+.5; a = mix(sin(cos(a)*a),sin(cos(1.+a)*(1.+a)), f.x); var bb = mix(a.xz, a.yw, f.y); a.x = bb.x; a.y = bb.y; return mix(a.x, a.y, f.z); }
@fragment fn main(@location(0) uvs : vec2<f32>) -> @location(0) vec4<f32> { var v = -1.0 + 2.0 * uvs.xy / 1.0;
var ro = vec3<f32>(0., 0.0, -2.0); var rd = normalize(vec3<f32>(v.x*1.0, -v.y, 1.0));
var fragColor = vec4<f32>( noise( vec3(v*3.0, 0.0) ) );
fragColor.w = 1.0;// no alpha return fragColor; }
To animate the noise so that it scrolls upwards slowly, we simply add a timer constant to the seed, e.g.:
direction - so it moves upwards! If we wanted we could mix other functions in with the timer (e.g., sin/cos to oscillate the movement - wiggles around).
Mixing SDF Functions
Let's take a moment to review the basics of SDF shape unions (combinations). An sdf function returns a floating point number that can be combined in different ways to (add or subtract the two forms).
SDF Function and Noise (Fire is Born)
As we discussed - it's very easy to combine SDF functions together to construct new shapes - however, instead of using an sdf function, we'll create an sdf noise function. This will modulate the shape (circle in this case) so it's more like the shape of a flame.
Mixing the noise directly with the sphere (adding noise) gives a wiggly `blob` that moves up and down - like a large flame moves - but it still lacks movement.
// Simple noise mixed with the sdfSphere fn scene(p: vec3<f32>) -> f32 { let distance = sdfSphere( p, 1.0 );
Without lighting - the animated volume might look interesting - but it doesn't call out the words fire, Fire FIRE!! The secret is to add a of color.
We have to tweak things a little to extract some extra information - other than just inside and outside - we'll add a hack value to detect the 'glow' - inner edges so they can be used to add specular effect.
fn raymarch(ro: vec3<f32>, rd: vec3<f32>) -> vec4<f32> { var p = ro; let eps = 0.025; var glow = -9999.0;
for (var i=0; i<64; i=i+1) { var d = scene( p ) + eps; p += d*rd; if ( d > eps ) { if (flame(p) < 0.0 || glow >= -9999.0) { glow = f32(i)/64.0; } } } return vec4<f32>(p, glow); }
@fragment fn main(@location(0) uvs : vec2<f32>) -> @location(0) vec4<f32> { var v = -1.0 + 2.0 * uvs.xy / 1.0;
var ro = vec3<f32>(0., 0.0, -3.0); var rd = normalize(vec3<f32>(v.x*1.0, -v.y, 1.0));
var p = raymarch(ro, rd);
let glow = p.w;
return vec4<f32>( vec3(glow), 1 ); }
The example shows the 'glow' value - but we mix it to a higher power to focus it on the shape surface and lose all of those ripples.
Add a small scale and take it to the power to give it emphasis.
return vec4<f32>( vec3( pow(glow*2, 4) ), 1 );
But we still don't have color!! It looks nice, but fire needs some blue and red!
We can take two colors and mix them based on the 'y' value - then combine them with the glow factor to create a nice looking flame shading.
@fragment fn main(@location(0) uvs : vec2<f32>) -> @location(0) vec4<f32> { var v = -1.0 + 2.0 * uvs.xy / 1.0;
var ro = vec3<f32>(0., 0.0, -3.0); var rd = normalize(vec3<f32>(v.x*1.0, -v.y, 1.0));
var p = raymarch(ro, rd);
// show the color range - orange to blue var col = mix(vec4<f32>(1.,.5,.1,1.), vec4<f32>(0.1,.5,1.,1.), p.y*0.02+0.4);
// mix in the 'glow' factor here in the next step
col.w = 1.0;// no alpha return col; }
Taking this color gradient and mixing it with the glow factor - so the color is mapped onto the flame.
@fragment fn main(@location(0) uvs : vec2<f32>) -> @location(0) vec4<f32> { var v = -1.0 + 2.0 * uvs.xy / 1.0;
var ro = vec3<f32>(0., 0.0, -3.0); var rd = normalize(vec3<f32>(v.x*1.0, -v.y, 1.0));
var p = raymarch(ro, rd);
let glow = p.w;
var col = mix(vec4<f32>(1.0,0.5,0.1,1.0), vec4<f32>(0.1,0.5,1.0,1.0), p.y*0.02+0.4);
var fragColor = mix( vec4<f32>(0), col, pow( glow*2,4 ) );
fragColor.w = 1.0;// no alpha return fragColor; }
Burning Letters (Letter A)
Swap out the SDF function for a sphere with a more complex one - use an sdf function that spells out the the letter 'A' (function
sdfLetterA(..)
).
fn scene(p: vec3<f32>) -> f32 { //let distance = sdfSphere( p, 1.0 ); // take out the sphere let distance = sdfLetterA( p ); // plug in the letter A sdf function
let f = (distance + flame(p)*0.8 );
return min( 100.0-length(p), abs(f) ); }
The full code for the letter A with the few extra SDF functions to build the letter A from capsules and rectangles.
@group(0) @binding(2) var <uniform> mytimer : f32;
fn noise(p : vec3<f32>) -> f32 { var i = floor(p); var a = dot(i, vec3<f32>(1., 57., 21.)) + vec4<f32>(0., 57., 21., 78.); var f = cos((p-i)*acos(-1.))*(-.5)+.5; a = mix(sin(cos(a)*a),sin(cos(1.+a)*(1.+a)), f.x); var bb = mix(a.xz, a.yw, f.y); a.x = bb.x; a.y = bb.y; return mix(a.x, a.y, f.z); }
fn scene(p: vec3<f32>) -> f32 { //let distance = sdfSphere( p, 1.0 ); let distance = sdfLetterA( p );
let f = (distance + flame(p)*0.8 );
return min( 100.0-length(p), abs(f) ); }
fn raymarch(ro: vec3<f32>, rd: vec3<f32>) -> vec4<f32> { var p = ro; let eps = 0.002; var glow = -9999.0;
for (var i=0; i<64; i=i+1) { var d = scene( p ) + eps; p += d*rd; if ( d > eps ) { if (flame(p) < 0.0 || glow >= -9999.0) { glow = f32(i)/64.0; } } } return vec4<f32>(p, glow); }
@fragment fn main(@location(0) uvs : vec2<f32>) -> @location(0) vec4<f32> { var v = -1.0 + 2.0 * uvs.xy / 1.0;
var ro = vec3<f32>(0., 0.0, -3.0); var rd = normalize(vec3<f32>(v.x*1.0, -v.y, 1.0));
var p = raymarch(ro, rd);
let glow = p.w;
var col = mix(vec4<f32>(1.,.5,.1,1.), vec4<f32>(0.1,.5,1.,1.), p.y*0.02+0.4);
var fragColor = mix( vec4<f32>(0), col, pow( glow*2,4 ) );
fragColor.w = 1.0;// no alpha return fragColor; }
You can swap out the letter A and insert any other SDF function - creating all sorts of shapes with a flame effect. In fact, you can add lots of objects to a scene some with and some without the effect - the opportunities are unlimited.
What's more, you can even customize the flame effect for different objects (size, color, speed or shape) - very simple and very powerful effect!
Summary
At the end of the day - the underlying principles for the flame effect area really simple - just a matter of generating a simple oscillating noise pattern (blobby and smooth) that you can mix with an sdf shape (desplaces the surfaces). Mixing in a bit of color and you've got a nice looking flame effect.
As the noise is a smooth pattern - adding an increment in the y direction (moves upwards) you get the flame to animate (move).
Tweaking the constants (noise values, colors and speed of the updates) you're able to create a sweet effect.
Overlaying the flame effect on different shapes you can create burning candles, burning words or even burning ice-cubes.
Things to Try
• Other colors (also try adding more steps)
• Different noise generators
• Fractal patterns
• Flame style (small flames instead of a single large flame)
• Animation speed
• User input - control the direction of the flame - moves towards mouse
• Other SDF shapes for the flame
• Mixing smoke and sparks with the flame effect
• Multiple instances of the flame - using 'mod' effect
Advanced
• Develop editor for the flame (including load/save/share flame designs)
• Animate the flame flying around the screen
• Add a scene - construct a cool 3d scene from sdf objects (e.g., candle stick, walls, floor, ...)
• Mix the flame effect with an image in the background (transparency)
• Path tracing the flame effect (e.g., Monte Carlo approximation with the rays going from the light instead of the camera)
• Generate the flame in a 3d scene (extract the camera data, orientation and direction, generate the flame and insert it into the scene as a billboard)
• Metaballs - they can be used to generate an amazing effect on their own - however, their generated sdf surface can be combined with the flame effect
• Use the marching cube algorithm to create 'triangle' mesh version of the flame (save this out to a file)
Extreme Advanced
• Record a short video of a candle flame (or similar flame) - use tools to analyse the colors/speed/shape, extract this information and configure/tune the paramters in the flame shader to emulate the recorded version.
Resources and Links
• WebGPU Demo Letter A [LINK]
• WebGPU Demo Word 'XBDEV' [LINK]
• Flame (Sphere) [LINK]
From flames to clouds - same concept discussed here but we focus on a 'cloud' type effect.
• Generating Clouds, Shapes and Letters [LINK]
Retro fire effect using pixel manipulation
• 2D Flame (Pixel Effect) [LINK]
Visitor:
Copyright (c) 2002-2024 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.