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

LearnWebGPU

Graphics and Compute ...

 


What do you see when you look up into the sky? Clouds? Clouds of all sizes and shapes - clouds that look like elephants and cars!

How could you go about creating such an effect?

You want to create that magical, fluffy soft look - like cotton candy!

Fluffy Cotton Candy Clouds (Animated)


In fact, it's really easy to generate these types of clouds! Yes! You can generate animated life-like cotton candy clouds that can shift into any shape or size you can imagine!

The secret is to combine signed distance functions (sdf) with a bit of random noise on the fragment shader.



Generating clouds is great! But animated clouds is even cooler! How could we make it even cooler? Making the clouds into shapes and spelling out words!



SDF Functions


The journey starts with SDF functions and doing a bit of `ray-tracing` in the fragment shader. Then we'll go step by step adding little featuers to make it more and more cloud-like - to the point that you're able to generated animated volumetric clouds that can be used for cloud-lettering or other effects.


Simple starting point - ray-trace red sphere - not very exciting?
Simple starting point - ray-trace red sphere - not very exciting?



Setup a SDF function for the scene - and for that scene we add a simple sphere. Using a fixed step we determine if a ray hits or doesn't hit the sphere (draw the result as a red circle).

As the ray-sphere intersection just returns a red color to confirm it works - the output looks like a flat 2d circle - but the math is 3d and is a sphere. You'll see later when we start adding in lighting that there is a depth.

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

fn 
sdfScenep:vec3<f32> ) -> f32
{
  var 
distance sdfSphere(p1.0vec3<f32>(000) );

  return -
distance;
}

fn 
rayMarchro:vec3<f32>,  rd:vec3<f32> ) -> vec4<f32>
{  
  
let MAX_STEPS   100;
  
let MARCH_SIZE  0.08;
  var 
depth       0.0;
  var 
ro depth rd;
  
  var 
res vec4<f32>(0.0);

  for (var 
i:i32 0MAX_STEPSi++) {
     var 
density sdfScene);

    
// We only draw the density if it's greater than 0
    
if (density 0.0) {
        
res += vec4<f32>(1,0,0,1);       
    }

    
depth += MARCH_SIZE;
    
ro depth rd;
  }

  return 
res;
}

@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy); // -1 to 1
    
    
var ro vec3<f32>(002); // ray origin that represents camera position
    
var rd normalize(vec3<f32>(uv, -1)); // ray direction
    
    
var fragColor rayMarchrord );

    
// Output to screen
    
return vec4<f32>(fragColor);
}


Add a bit of depth (color) - make the sphere look a bit more `fluffy` (use a grayscale cloudy color)


Modify the color calculation information in the
rayMarch(..)
function - so instead of a constant red color we'll mix the the depth with a gray and feather the edges.


Make the intersection color more feathery/cloud like - assuming a we
Make the intersection color more feathery/cloud like - assuming a we're looking to create a grayish type cloud.


fn rayMarchro:vec3<f32>,  rd:vec3<f32> ) -> vec4<f32>
{  
  
let MAX_STEPS   100;
  
let MARCH_SIZE  0.08;
  var 
depth       0.0;
  var 
ro depth rd;
  
  var 
res vec4<f32>(0.0);

  for (var 
i:i32 0MAX_STEPSi++) {
     var 
density sdfScene);

    
// We only draw the density if it's greater than 0
    
if (density 0.0) {
        
// Make the sphere more cloudy/feather-like around the edges
        
var color vec4(mix(vec3(1.0,1.0,1.0), 
                           
vec3(0.00.00.0), 
                           
density), density );
          
color vec4<f32>( color.rgb*color.acolor.);
          
res += color*(1.0-res.a);     
    }

    
depth += MARCH_SIZE;
    
ro depth rd;
  }

  return 
res;
}


Noise (on the shader)


Go into generating random noise on the shader using our own random number generator - for the seed it can either be the texture coordinates (x,y) or the world position (x,y,z).

The random number generator uses the chaotic values of the sin function for high frequencies. This random number function is cheap and cheerful - and works fine for our purposes - but there are lots and lots of different random number generates each with their own properties.




A simple test case is the plot the random pixels - see that the output is consistent - no blocks or rings in the random pixel output. Using the random number output for the red, green and blue (giving a grayscale color).

fn random(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}
  
@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy); 

    
// red==green==blue => 'gray' scale
    
var fragColor vec4<f32>( randomuv ), // red
                               
randomuv ), // green
                               
randomuv),  // blue
                               
1.0 );        // alpha
    // Output to screen
    
return vec4<f32>(fragColor);
}


If different random numbers are required for on the same GPU thread - the seed can be scaled and modified to generate multiple unique values.

For example, generating random pixels with color which requires 3 random numbers (red, green and blue color instead of just one value for the grayscale).





fn random(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}
  
@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy); 

    var 
fragColor vec4<f32>( randomuv ),        // red
                               
randomuv*2.32 ),   // green
                               
randomuv*7.21932), // blue
                               
1.0 );               // alpha
    // Output to screen
    
return vec4<f32>(fragColor);
}


Smooth Noise


Random numbers are great! But they're just too noisy and chaotic - we wanat the noise to change `gradually`. Actually very easy using a bit of linear interpolation and number rounding (taking the decimal and fractional part of the seed).

Add the
randomsmooth(..)
function which takes samples from the
random(..)
number function - but interpolates them - as the seed is 2-floating point values - the noise is interpolated in both dimensions (so if it's plotted you see a nice smoother noise).



Smooth noise - interpolating random values.
Smooth noise - interpolating random values.


fn random(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}

fn 
randomsmoothst:vec2<f32> ) -> f32 
{
    var 
floorst 2.0 ); // uv - 0,   1,   2,   3, 
    
var fractst 2.0 ); // uv - 0-1, 0-1, 0-1, 0-1

    // Four corners in 2D of a tile
    
var random(i);
    var 
random(vec2<f32>(1.00.0));
    var 
random(vec2<f32>(0.01.0));
    var 
random(vec2<f32>(1.01.0));

    
// Ease-in followed by an ease-out (tweening) for f
    // f = 0.5 * (1.0 - cos( 3.14 * f ) ); 
    // version without cos/sin
    // f = 3*f*f - 2*f*f*f;
    
    
3*f*2*f*f*f;
    
    
// bilinear interpolation to combine the values sampled from the 
    // four corners (a,b,c,d), resulting in a smoothly interpolated value.
   
    // Interpolate Along One Axis - interpolate between `a` and `b` using the fractional coordinate `f.x`, 
    // then interpolate between `c` and `d` using the same `f.x`. 
    // This gives two intermediate values, say `e` and `f`.

    // Interpolate Along the Other Axis - linearly interpolate between `e` and `f` 
    // using the fractional coordinate `f.y`. 
    // Final interpolation gives a moothly interpolated value across the square
    
    
var x1 mixabf.);
    var 
x2 mixcdf.);
    
    var 
y1 mixx1x2f.);
    
    return 
y1;
}
  
@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy); 

    
// red==green==blue => 'gray' scale
    
var fragColor vec4<f32>( randomsmoothuv ), // red
                               
randomsmoothuv ), // green
                               
randomsmoothuv),  // blue
                               
1.0 );        // alpha
    // Output to screen
    
return vec4<f32>(fragColor);
}



SDF Unions (new shapes)


SDF shapes can be combined very easily to create new shapes.

Because the sdf function describe geometric shapes by indicating the shortest distance from any point in space to the shape's surface.

To create new shapes, SDFs can be combined using operations such as addition and subtraction, where addition blends shapes together and subtraction cuts out one shape from another.

Unions of shapes are formed by taking the minimum value of the SDFs of the individual shapes, effectively merging them into a single continuous form.

Example - combining a cube with sphere:


Add the sphere and cube sdf functions to create a new shape.
Add the sphere and cube sdf functions to create a new shape.


fn sdfSphere(pvec3<f32>, rf32) -> f32 {
    return 
length(p) - r;
}

fn 
sdfCube(pvec3<f32>, bvec3<f32>) -> f32 {
    
let d abs(p) - b;
    return 
length(max(dvec3<f32>(0.0))) + min(max(d.xmax(d.yd.z)), 0.0);
}

fn 
rayCastpt:vec3<f32>) -> f32
{
    
maxtsdfCube(ptvec3<f32>( 0.000), vec3<f32>(1.0)) );
    
mintsdfSphere(ptvec3<f32>(0.0,0,0), 1.2 ) );

    return 
t;
}


SDF `Noise' Union (adding shapes to noise)


The concept of building new shapes by adding the sdf functions together can be expanded - create an sdf function that generates
noise
- this noise can be added to shapes - which will ultimately be used to create our fluffy cloud-like shape.



Combining sphere sdf function with a smooth noise.
Combining sphere sdf function with a smooth noise.



Take the result from the
randomsmooth(..)
function and add it to the
sdfsphere(..)
result to create a noisy meatball like sphere. It looks nice, but it doesn't really look like a cloud.


fn random(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}

fn 
randomsmoothst:vec2<f32> ) -> f32 
{
    var 
floorst 3.0 ); // uv - 0,   1,   2,   3, 
    
var fractst 3.0 ); // uv - 0-1, 0-1, 0-1, 0-1

    // Four corners in 2D of a tile
    
var random(i);
    var 
random(vec2<f32>(1.00.0));
    var 
random(vec2<f32>(0.01.0));
    var 
random(vec2<f32>(1.01.0));

    
// Ease-in followed by an ease-out (tweening) for f
    // f = 0.5 * (1.0 - cos( 3.14 * f ) ); 
    // version without cos/sin
    // f = 3*f*f - 2*f*f*f;
    
    
3*f*2*f*f*f;
    
    
// bilinear interpolation to combine the values sampled from the 
    // four corners (a,b,c,d), resulting in a smoothly interpolated value.
   
    // Interpolate Along One Axis - interpolate between `a` and `b` using the fractional coordinate `f.x`, 
    // then interpolate between `c` and `d` using the same `f.x`. 
    // This gives two intermediate values, say `e` and `f`.

    // Interpolate Along the Other Axis - linearly interpolate between `e` and `f` 
    // using the fractional coordinate `f.y`. 
    // Final interpolation gives a moothly interpolated value across the square
    
    
var x1 mixabf.);
    var 
x2 mixcdf.);
    
    var 
y1 mixx1x2f.);
    
    return 
y1;
}

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

fn 
sdfnoisep:vec3<f32> ) -> f32
{
  return 
randomsmoothp.xy )*0.3;
}        

fn 
sdfScenep:vec3<f32> ) -> f32
{
  var 
distance sdfSphere(p1.0vec3<f32>(000) );

  return -
distance sdfnoise);
}

fn 
rayMarchro:vec3<f32>,  rd:vec3<f32> ) -> vec4<f32>
{  
  
let MAX_STEPS   100;
  
let MARCH_SIZE  0.08;
  var 
depth       0.0;
  var 
ro depth rd;
  
  var 
res vec4<f32>(0.0);

  for (var 
i:i32 0MAX_STEPSi++) {
     var 
density sdfScene);

    
// We only draw the density if it's greater than 0
    
if (density 0.0) {
        var 
color vec4(mix(vec3(1.0,1.0,1.0), 
                           
vec3(0.00.00.0), 
                           
density), density );
          
color vec4<f32>( color.rgb*color.acolor.);
          
res += color*(1.0-res.a);     
    }

    
depth += MARCH_SIZE;
    
ro depth rd;
  }

  return 
res;
}

@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy); 

    var 
ro vec3<f32>(002); // ray origin that represents camera position
    
var rd normalize(vec3<f32>(uv, -1)); // ray direction
    
    
var fragColor rayMarchrord );
    
    
// Output to screen
    
return vec4<f32>(fragColor);
}


You can tweak the smoothnoise function a little to increase the noise frequency, these two lines at the top (isntead of `3` you could use `6` or higher). However, it might add more fidelity to the result - it not the result we're looking for - shifting the noise to be more coarse (less smooth).

fn randomsmoothst:vec2<f32> ) -> f32 
{
    var 
floorst 6.0 ); // Change 3 to 6
    
var fractst 6.0 ); //
    
...



Modifying the fidelity in the
Modifying the fidelity in the 'randomsmooth(..)' function.



Fractal Noise


The solution to our problems is fractals - fractals let us generate noise patterns that resemble noise in nature! There are lots of different types of fractal algorithms - the one that we'll use here is the `brownian` fractal noise algorithm - it works by taking multiple samples of the noise function.

Instead of using the smoothed noise function as it is - instead it will take multiple samples and combine them together (each sample will have a different seed offset). Creating a smoothed noise function but with fractal characteristics (repeating nested components).



Fractal noise combined with a sdf sphere.
Fractal noise combined with a sdf sphere.


First, let's extend the
smoothnoise(..)
so it takes a `vec3` instead of a `vec2` so can produce random noise in the x, y and z.

fn noisex:vec3<f32> ) -> f32
{
  var 
floor(1.0);
  var 
fract(1.0);
  
f*f*(3.0-2.0*f);

  var 
uv = (p.xy+vec2(37.0,239.0)*p.z) + f.xy;
  var 
texx randomsmoothuv );
  var 
texy randomsmooth2.182983*uv  );

  return 
mixtexxtexyf.) * 2.0 1.0;
}


Second, let's make our fractal brownian noise function (
fbm(..)
).

fn fbmp:vec3<f32> ) -> f32
{
  var 
uTime mytimer 0.8;
  var 
uTime vec3(0.2, -0.10.0);
  var 
noise();

  var 
0.0;
  var 
scale 0.25;
  var 
factor 2.02;

  for (var 
i:i32 06i++) {
      
+= scale noise();
      
*= factor;
      
factor += 0.21;
      
scale *= 0.5;
  }

  return 
f;
}


Animating the cloud can be done by adding a small offset to the seed - for example
mytimer
uniform is passed to the shader - this is incremented each frame. Adding it to the noise creates a scrolling effect - like the cloud is getting blown by the wind.


Adding Light


The clouds starting to look nice - it's fluffy and it's animated - but it lacks a bit of `depth` - which lighting will fix.


Add some lighting to the noise cloud.
Add some lighting to the noise cloud.


Add a constant to define the suns location - which we can use to calculate the direction from the point of intersection to the sun. This is done in two places - in the
rayMarch(..)
and in the final color calculation in
main(..)
.

....
const 
SUN_POSITION:vec3<f32> = vec3<f32>(1.00.00.0);

fn 
rayMarchro:vec3<f32>,  rd:vec3<f32> ) -> vec4<f32>
{  
  
let MAX_STEPS   100;
  
let MARCH_SIZE  0.08;
  var 
depth       0.0;
  var 
ro depth rd;
  
  var 
res vec4<f32>(0.0);

  for (var 
i:i32 0MAX_STEPSi++) {
     var 
density sdfScene);

    
// We only draw the density if it's greater than 0
    
if (density 0.0) {
        
// Directional lighting - diffuse lighting
        
var sunDirection normalize(SUN_POSITION);
        var 
diffuse clamp((sdfScene(p) - sdfScene(0.3 sunDirection)) / 0.30.01.0 );
        var 
lin vec3(0.60,0.60,0.75) * 1.1 0.8 vec3(1.0,0.6,0.3) * diffuse;
        var 
color vec4(mix(vec3(1.01.01.0), vec3(0.00.00.0), density), density );
        
color vec4<f32>( color.rgb*lincolor.);
        
color vec4<f32>( color.rgb*color.acolor.);
        
res += color*(1.0-res.a);     
    }

    
depth += MARCH_SIZE;
    
ro depth rd;
  }

  return 
res;
}

@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy); 

    var 
ro vec3<f32>(002); // ray origin that represents camera position
    
var rd normalize(vec3<f32>(uv, -1)); // ray direction
    
    // Sun and Sky
    
var sunDirection normalize(SUN_POSITION);
    var 
sun clamp(dot(sunDirectionrd), 0.01.0 );
    
// Base sky color
    
var color vec3(0.7,0.7,0.90);
    
// Add vertical gradient
    
color -= 0.3 vec3(0.90,0.75,0.90) * rd.y;
    
// Add sun color to sky
    
color += 0.5 vec3(1.0,0.5,0.3) * pow(sun10.0);

    var 
fragColor rayMarchrord );

    
fragColor vec4<f32>( color.xyz * (1.0-fragColor.a) + fragColor.rgb1.0 );
    
    
// Output to screen
    
return vec4<f32>(fragColor);
}



Shape of Clouds


Up until now, we've just been adding the noise to a sphere - to create a `roundish` cloud shape - however, the real beautiy of the algorithm is we can swap in other sdf functions or build or won. For example, instead of the sphere - lets create a sdf function for the letter A. This can be constructed from simpler SDF functions (box and capsules).



SDF function for the letter
SDF function for the letter 'A' - cloud in this shape instead of a sphere. The left side shows the SDF function with 'no' nois added- the right shows with the noise - to create the cloud-like effect.



...
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 
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 
sdfLetterA(pvec3<f32>) -> f32 {
    var 
leftDiagonal sdfCapsule(pvec3<f32>(-0.7, -1.00.0), vec3<f32>(0.01.00.0), 0.3);
    var 
rightDiagonal sdfCapsule(pvec3<f32>(0.7, -1.00.0), vec3<f32>(0.01.00.0), 0.3);
    var 
horizontalBar sdfRoundedBox(p-vec3(0,-0.5,0), vec3<f32>(0.60.10.1), 0.12);

    return 
min(leftDiagonalmin(rightDiagonalhorizontalBar));
}

fn 
sdfScenep:vec3<f32> ) -> f32
{
  var 
distance sdfLetterA);

  return -
distance fbm);
}
...


Clouds Changing Shape


We can take things a little further by having the cloud shape morph into different shapes as time progresses.

Just a matter of interpolating between SDF functions.



Cloud morphing to different shapes in real-time. Starts off as a sphere, then morphs into a torus (donut), cross and a capsule.
Cloud morphing to different shapes in real-time. Starts off as a sphere, then morphs into a torus (donut), cross and a capsule.




This is the complete code with the extra functions for blending between shapes (mymod, ...).

@group(0) @binding(2) var <uniformmytimer f32;


const 
PI:f32 3.14159265359;
    
fn 
rotate2Da:f32 ) -> mat2x2<f32
{
  var 
sin(a);
  var 
cos(a);
  return 
mat2x2<f32>(c, -s
                     
s,  c);
}

fn 
mymod(x:f32y:f32) -> f32
{
    return ( 
floor(x/y) );
}

fn 
nextStep(  ti:f32,  len:f32,  smo:f32 ) -> f32 
{
  var 
ti smo;
  var 
tt mymod(tlen);
  var 
stp floor(len) - 1.0;
  return 
smoothstep(0.0smott) + stp;
}

fn 
random(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}

fn 
randomsmoothst:vec2<f32> ) -> f32 
{
    var 
floorst 2.0 ); // uv - 0,   1,   2,   3, 
    
var fractst 2.0 ); // uv - 0-1, 0-1, 0-1, 0-1

    // Four corners in 2D of a tile
    
var random(i);
    var 
random(vec2<f32>(1.00.0));
    var 
random(vec2<f32>(0.01.0));
    var 
random(vec2<f32>(1.01.0));

    
// Ease-in followed by an ease-out (tweening) for f
    // f = 0.5 * (1.0 - cos( 3.14 * f ) ); 
    // version without cos/sin
    // f = 3*f*f - 2*f*f*f;
    
    
3*f*2*f*f*f;
    
    
// bilinear interpolation to combine the values sampled from the 
    // four corners (a,b,c,d), resulting in a smoothly interpolated value.
   
    // Interpolate Along One Axis - interpolate between `a` and `b` using the fractional coordinate `f.x`, 
    // then interpolate between `c` and `d` using the same `f.x`. 
    // This gives two intermediate values, say `e` and `f`.

    // Interpolate Along the Other Axis - linearly interpolate between `e` and `f` 
    // using the fractional coordinate `f.y`. 
    // Final interpolation gives a moothly interpolated value across the square
    
    
var x1 mixabf.);
    var 
x2 mixcdf.);
    
    var 
y1 mixx1x2f.);
    
    return 
y1;
}

fn 
noisex:vec3<f32> ) -> f32
{
  var 
floor(1.0);
  var 
fract(1.0);
  
f*f*(3.0-2.0*f);

  var 
uv = (p.xy+vec2(37.0,239.0)*p.z) + f.xy;
  var 
texx randomsmoothuv );
  var 
texy randomsmooth2.182983*uv  );

  return 
mixtexxtexyf.) * 2.0 1.0;
}


fn 
fbmp:vec3<f32> ) -> f32
{
  var 
uTime mytimer 0.8;
  var 
uTime vec3(0.2, -0.10.0);
  var 
noise();

  var 
0.0;
  var 
scale 0.25;
  var 
factor 2.02;

  for (var 
i:i32 06i++) {
      
+= scale noise);
      
*= factor;
      
factor += 0.21;
      
scale *= 0.5;
  }

  return 
f;
}

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

fn 
sdfTorus(pvec3<f32>, rvec2<f32>) -> f32 {
    var 
length(p.xy) - r.x;
    return 
length(vec2<f32>(xp.z)) - r.y;
}

fn 
sdfCrossp:vec3<f32>,  s:f32 ) -> f32 
{
  var 
da max(abs(p.x), abs(p.y));
  var 
db max(abs(p.y), abs(p.z));
  var 
dc max(abs(p.z), abs(p.x));

  return 
min(damin(dbdc)) - s;
}

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 
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 
sdfLetterA(pvec3<f32>) -> f32 {
    var 
leftDiagonal sdfCapsule(pvec3<f32>(-0.7, -1.00.0), vec3<f32>(0.01.00.0), 0.3);
    var 
rightDiagonal sdfCapsule(pvec3<f32>(0.7, -1.00.0), vec3<f32>(0.01.00.0), 0.3);
    var 
horizontalBar sdfRoundedBox(p-vec3(0,-0.5,0), vec3<f32>(0.60.10.1), 0.12);

    return 
min(leftDiagonalmin(rightDiagonalhorizontalBar));
}

fn 
sdfScenep:vec3<f32> ) -> f32
{
  var 
uTime mytimer*0.7;
    
  var 
p1 p;
  var 
xz p1.xz rotate2D(-PI 0.2);
  
p1.+= xz.x;
  
p1.+= xz.y;
    
  var 
yz p1.yz rotate2D(PI 0.4);
  
p1.+= yz.x;
  
p1.+= yz.y;
  
  var 
s1 sdfSphere(p1.5vec3<f32>(000) );
  var 
s2 sdfTorus(p1vec2(2.61.2));
  var 
s3 sdfCross(p1 1.00.8);
  var 
s4 sdfCapsule(pvec3(-2.0, -1.50.0), vec3(2.01.50.0), 1.0);

  var 
mymod(nextStep(uTime3.01.2), 4.0);

  var 
distance s1;
  
distance mix(distances2clamp(1.00.01.0));
  
distance mix(distances3clamp(2.00.01.0));
  
distance mix(distances4clamp(3.00.01.0));
  
distance mix(distances1clamp(4.00.01.0));

  return -
distance fbm(p);
}

const 
SUN_POSITION:vec3<f32> = vec3<f32>(1.00.00.0);

fn 
rayMarchro:vec3<f32>,  rd:vec3<f32> ) -> vec4<f32>
{  
  
let MAX_STEPS   100;
  
let MARCH_SIZE  0.08;
  var 
depth       0.0;
  var 
ro depth rd;
  
  var 
res vec4<f32>(0.0);

  for (var 
i:i32 0MAX_STEPSi++) {
     var 
density sdfScene);

    
// We only draw the density if it's greater than 0
    
if (density 0.0) {
        
// Directional lighting - diffuse lighting
        
var sunDirection normalize(SUN_POSITION);
        var 
diffuse clamp((sdfScene(p) - sdfScene(0.3 sunDirection)) / 0.30.01.0 );
        var 
lin vec3(0.60,0.60,0.75) * 1.1 0.8 vec3(1.0,0.6,0.3) * diffuse;
        var 
color vec4(mix(vec3(1.01.01.0), vec3(0.00.00.0), density), density );
        
color vec4<f32>( color.rgb*lincolor.);
        
color vec4<f32>( color.rgb*color.acolor.);
        
res += color*(1.0-res.a);     
    }

    
depth += MARCH_SIZE;
    
ro depth rd;
  }

  return 
res;
}

  
@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy); 

    var 
ro vec3<f32>(005); // ray origin that represents camera position
    
var rd normalize(vec3<f32>(uv, -1)); // ray direction
    
    // Sun and Sky
    
var sunDirection normalize(SUN_POSITION);
    var 
sun clamp(dot(sunDirectionrd), 0.01.0 );
    
// Base sky color
    
var color vec3(0.7,0.7,0.90);
    
// Add vertical gradient
    
color -= 0.3 vec3(0.90,0.75,0.90) * rd.y;
    
// Add sun color to sky
    
color += 0.5 vec3(1.0,0.5,0.3) * pow(sun10.0);

    var 
fragColor rayMarchrord );

    
fragColor vec4<f32>( color.xyz * (1.0-fragColor.a) + fragColor.rgb1.0 );
    
    
// Output to screen
    
return vec4<f32>(fragColor);
}



This is only the beginning of the journey - you can keep enhancing the lighting and style of the clouds - also optimizing the algorithm so it runs smoother.



Cloud writing - letter
Cloud writing - letter 'B'.



Things to Try


• Other noise functions
• Other fractal algorithms
• Cloud colors (not just gray but other mixes)
• Mixing in `lightning` effects so the cloud flashes
• Multiple clouds (e.g., `mod` function)
• Complex SDF functions - e.g., cloud that looks like a car or a human head
• Mouse interaction - add forces to the direction of the movement
• Add the full alphabet of letters (capitals) - spell out words in clouds
• Try other lighting algorithms




Links and Resources


• WebGPU Clouds 1 [LINK]
• WebGPU Clouds 2 [LINK]
• WebGPU Clouds 3 [LINK]
• WebGPU Clouds 4 [LINK]
• WebGPU Cloud Writing [LINK]

The algorithm can also be modified to other noise functions, such as `fire` and `flames` using a different fractal noise function and colors, example shown here:
• WebGPU Fire 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.