www.xbdev.net
xbdev - software development
Tuesday October 15, 2024
Home | Contact | Support | LearnWebGPU Graphics and Compute ...
     
 

LearnWebGPU

Graphics and Compute ...

 


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'.


Figures illustrating the key concepts of the algorithm, shapes (like 3d letters), smooth noise, fractal noise, two primary colo...
Figures illustrating the key concepts of the algorithm, shapes (like 3d letters), smooth noise, fractal noise, two primary colors (top/bottom flame), various sdf shapes for the flame.


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.


Simple sdf sphere function - instead of drawing  a constant color - draw the distance steps so you can see the rings.
Simple sdf sphere function - instead of drawing a constant color - draw the distance steps so you can see the rings.



@group(0) @binding(0) var  mySamplersampler;
@
group(0) @binding(1) var  myTexturetexture_2d<f32>;
@
group(0) @binding(2) var <uniformmytimer f32;

fn 
sdfSpherep:vec3<f32>, r:f32) -> f32
{
  return 
length(p) - r;
}

fn 
scene(pvec3<f32>) -> f32
{
    
let distance sdfSpherep1.0 );
    
    return 
distance;
}

fn 
raymarch(rovec3<f32>, rdvec3<f32>) -> vec4<f32>
{
  var 
p    ro;
  var 
d    0.0;
  
let eps  0.02;

  for (var 
i=0i<64i=i+1)
  {
    var 
scenero rd*f32(i)*eps );
    if ( 
eps )
    {
        return 
vec4<f32>( 50.0 );
    }
  }
  return 
vec4<f32>(0.0);
}

@
fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
= -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.y1.0));
  
  var 
fragColor raymarch(rord);

  
fragColor.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.


Noise function that generates smooth noise using a 3 value seed - the resulting noise is `clumpy` blobs.
Noise function that generates smooth noise using a 3 value seed - the resulting noise is `clumpy` blobs.


fn noise(vec3<f32>) -> f32
{
  var 
floor(p);
  var 
dot(ivec3<f32>(1.57.21.)) + vec4<f32>(0.57.21.78.);
  var 
cos((p-i)*acos(-1.))*(-.5)+.5;
  
mix(sin(cos(a)*a),sin(cos(1.+a)*(1.+a)), f.x);
  var 
bb mix(a.xza.ywf.y);
  
a.bb.x;
  
a.bb.y;
  return 
mix(a.xa.yf.z);
}

@
fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
= -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.y1.0));
  
  var 
fragColor vec4<f32>( noisevec3(v*3.00.0) ) );

  
fragColor.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.:

  var fragColor vec4<f32>( noisevec3(v*3.0 vec2(0,mytimer), 0.0) ) );


We only add a value to the
y
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.


Noise on top of a sphere - creating a bopping (up/down) blob.
Noise on top of a sphere - creating a bopping (up/down) blob.


// Simple noise mixed with the sdfSphere
fn scene(pvec3<f32>) -> f32
{
    
let distance sdfSpherep1.0 );
    
    return ( 
distance noisevec3(0,mytimer,0) )*0.3 );
}


Let's put the noise function into its own function - which we can tweak and modify its contents for the flame noise effect.

Add in some tweaks to the noise - add multiple noise together (fractal-like fashion) - to introduce sub-harmonic characteristics to the noise pattern.


Similar to the `noise` function but the extra calculations imporove the `movement`; flame dances more like a flame than Homer S...
Similar to the `noise` function but the extra calculations imporove the `movement`; flame dances more like a flame than Homer Simpson's tummy while dancing!


fn flame(pvec3<f32>) -> f32
{
  return (
noise(p+vec3<f32>(0.0,mytimer*2.0,0.0)) + noise(p*3.0)*0.5)*0.25*(p.y);
}

fn 
scene(pvec3<f32>) -> f32
{
    
let distance sdfSpherep1.0 );
    
    return ( 
distance flame(p) );
}



Colors and Light (Actually Looks Like Fire)


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.


Sphere
Sphere 'glow' hack - make the flame look sweeter.


fn scene(pvec3<f32>) -> f32
{
    
let distance sdfSpherep1.0 );
    
    
let f = (distance flame(p)*1.4);
    
    return 
min100.0-length(p), abs(f) );
}

fn 
raymarch(rovec3<f32>, rdvec3<f32>) -> vec4<f32>
{
  var 
p      ro;
  
let eps    0.025;
  var 
glow   = -9999.0;
    
  for (var 
i=0i<64i=i+1)
  {
    var 
scene) + eps;
    
+= d*rd;
    if ( 
eps )
    {
        if (
flame(p) < 0.0 || glow >= -9999.0)
        {
            
glow f32(i)/64.0;
        }
    }
  }
  return 
vec4<f32>(pglow);
}

@
fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
= -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.y1.0));
  
  var 
raymarch(rord);
    
  
let glow p.w;
    
  return 
vec4<f32>( vec3(glow), );
}


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.


Taking the glow factor to a power of 4 shift emphasis onto key areas.
Taking the glow factor to a power of 4 shift emphasis onto key areas.


  return vec4<f32>( vec3pow(glow*24) ), );


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.



Two colors which mix and transition using the
Two colors which mix and transition using the 'y' value. Debug you can see the color range (bottom orange top is light blue).


@fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
= -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.y1.0));
  
  var 
raymarch(rord);
    
  
// 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.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.


Combing the glow number with a color gradient (light orange and blue).
Combing the glow number with a color gradient (light orange and blue).


@fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
= -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.y1.0));
  
  var 
raymarch(rord);
    
  
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 mixvec4<f32>(0), colpowglow*2,) );
    
  
fragColor.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(..)
).


Burning letter
Burning letter 'A' - so easy to make any other SDF functions burn - you just plug them into the code.


fn scene(pvec3<f32>) -> f32
{
    
//let distance = sdfSphere( p, 1.0 ); // take out the sphere
    
let distance sdfLetterA);       // plug in the letter A sdf function
    
    
let f = (distance flame(p)*0.8 );
    
    return 
min100.0-length(p), abs(f) );
}




You can play around scaling and modifying the noise value to get a variety of effects - depending on how you want your flame to...
You can play around scaling and modifying the noise value to get a variety of effects - depending on how you want your flame to look. Simiarly, you scale the timer uniform value to speed up/slow down the flame animation.


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 <uniformmytimer f32;

fn 
noise(vec3<f32>) -> f32
{
  var 
floor(p);
  var 
dot(ivec3<f32>(1.57.21.)) + vec4<f32>(0.57.21.78.);
  var 
cos((p-i)*acos(-1.))*(-.5)+.5;
  
mix(sin(cos(a)*a),sin(cos(1.+a)*(1.+a)), f.x);
  var 
bb mix(a.xza.ywf.y);
  
a.bb.x;
  
a.bb.y;
  return 
mix(a.xa.yf.z);
}

fn 
sdfSpherep:vec3<f32>, r:f32) -> f32
{
  return 
length(p) - r;
}

fn 
sdfRoundedBox(pvec3<f32>, bvec3<f32>, rf32) -> f32 {
    var 
abs(p) - b;
    return 
length(max(qvec3<f32>(0.0))) + min(max(q.xmax(q.yq.z)), 0.0) - r;
}

fn 
sdfCapsule(pvec3<f32>, avec3<f32>, bvec3<f32>, rf32) -> f32 {
    var 
ab a;
    var 
ap a;

    var 
dot(abap) / dot(abab);
    
clamp(t0.01.0);

    var 
ab;

    var 
length(c) - r;

    return 
d;
}

fn 
sdfLetterA(pvec3<f32>) -> f32 {
    var 
leftDiagonal sdfCapsule(pvec3<f32>(-0.81.00.0), vec3<f32>(0.0, -1.00.0), 0.3);
    var 
rightDiagonal sdfCapsule(pvec3<f32>(0.81.00.0), vec3<f32>(0.0, -1.00.0), 0.3);
    var 
horizontalBar sdfRoundedBox(vec3(0,0.6,0), vec3<f32>(0.80.20.1), 0.03);
    var 
letterA min(leftDiagonalmin(rightDiagonalhorizontalBar));
    return -
letterA;
}

fn 
flame(pvec3<f32>) -> f32
{
  return (
noise(p+vec3<f32>(0.0,mytimer*2.0,0.0)) + noise(p*3.0)*0.5)*0.25*(p.y);
}

fn 
scene(pvec3<f32>) -> f32
{
    
//let distance = sdfSphere( p, 1.0 );
    
let distance sdfLetterA);
    
    
let f = (distance flame(p)*0.8 );
    
    return 
min100.0-length(p), abs(f) );
}

fn 
raymarch(rovec3<f32>, rdvec3<f32>) -> vec4<f32>
{
  var 
p      ro;
  
let eps    0.002;
  var 
glow   = -9999.0;
    
  for (var 
i=0i<64i=i+1)
  {
    var 
scene) + eps;
    
+= d*rd;
    if ( 
eps )
    {
        if (
flame(p) < 0.0 || glow >= -9999.0)
        {
            
glow f32(i)/64.0;
        }
    }
  }
  return 
vec4<f32>(pglow);
}

@
fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
= -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.y1.0));
  
  var 
raymarch(rord);
    
  
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 mixvec4<f32>(0), colpowglow*2,) );

  
fragColor.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]












WebGPU Development Cookbook - coding recipes for all your webgpu needs! WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Games WGSL 2d 3d interactive web-based fun learning WebGPU Compute WebGPU API - Owners WebGPU & WGSL Essentials: A Hands-On Approach to Interactive Graphics, Games, 2D Interfaces, 3D Meshes, Animation, Security and Production Kenwright graphics and animations using the webgpu api 12 week course kenwright learn webgpu api kenwright programming compute and graphics applications with html5 and webgpu api kenwright real-time 3d graphics with webgpu kenwright webgpu for dummies kenwright webgpu api develompent a quick start guide kenwright webgpu by example 2022 kenwright webgpu gems kenwright webgpu interactive compute and graphics visualization cookbook kenwright wgsl webgpu shading language cookbook kenwright WebGPU Shader Language Development: Vertex, Fragment, Compute Shaders for Programmers Kenwright wgsl webgpugems shading language cookbook kenwright WGSL Fundamentals book kenwright WebGPU Data Visualization Cookbook kenwright Special Effects Programming with WebGPU kenwright WebGPU Programming Guide: Interactive Graphics and Compute Programming with WebGPU & WGSL kenwright



 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2024 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.