www.xbdev.net
xbdev - software development
Wednesday January 15, 2025
Home | Contact | Support | WebGPU Graphics and Compute ...
     
 

WebGPU/WGSL Tutorials and Articles

Graphics and Compute ...

 


Signed Distance Functions (SDF) - Basics Using 2D Examples


SDF functions (or Signed Distance Functions) are special functions that tell you how far you are from a shape. Imagine drawing a big circle on the ground (or any other shape) - the sdf function tells you how far you are from the surface of that shape!


Circle of
Circle of 'Trust' from the famous movie 'Meet the Parents'. Mixing in SDF functions to explain inside and outside the circle of trust.


As you get closer to the shape the distance gets smaller and as you move away the distance gets bigger. If you go inside the circle, it gives a negative number to show you're inside. This can be really useful for graphics and physics simulations (quick and easy way to calculate distances).

A signed distance function takes a point and returns the distance. The point represents the position in the world and the distance returned is the value (positive or negative) amount that you're from the shape - positive if it's outside the shape and negative if it's inside the surface.


Simple 2-dimensoinal SDF fragment shader - if sdf sphere function returns a negative value, then it
Simple 2-dimensoinal SDF fragment shader - if sdf sphere function returns a negative value, then it's inside the sphere and we draw it red - otherwise blue.


fn sdfSpheretestPoint:vec3<f32>, spherePos:vec3<f32>, sphereRadius:f32) -> f32 {
    return 
length(spherePos testPoint) - sphereRadius;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let nuv = ( uv 2.0 1.0 );
    
let p vec3<f32>( nuv0.0 );
    
    if ( 
sdfSpherevec3(0.00.00.0), 0.5 ) < 0.0 )
    {
        return 
vec4<f32>(1001); // red
    
}
    return 
vec4<f32>(0011); // white
}


SDF Fields


When working with SDF functions - instead of just working with true/false for inside or outside - we can use the distance value for visualizing what is happeing.

These distance values form what is known as a signed distance field - whihc we'll in numerous 2d examples to help explain and visualize what is happening.


Yoda and the
Yoda and the 'force' - but instead of 'force fields' - we do a play on 'sdf fields' - visualize the sdf field which shows the distance value for the sdf functions.


Color Gradients


A first approach is to use colors - with blue for outside and red for inside the shape - and of course, green (which means neutral) is for values close to or equal to zero. We can draw the sdf gradient field using a color gradient function - blending between blue and red, as shown below for a simple sphere (or circle in 2d).


Color gradient for the sdf distance.
Color gradient for the sdf distance.


fn colorGradient(valuef32) -> vec3<f32> {
    
// scale the values to emphasis colors around the 0.0
    
var powabs(value) ,0.2) * signvalue );
    
    
// Clamp the input value between -1 and 1
    
clamp(v, -1.01.0);

    
// Define color stops
    
let red vec3<f32>(1.00.00.0);      // -1.0
    
let orange vec3<f32>(1.00.50.0);   // -0.5
    
let white vec3<f32>(1.01.01.0);    //  0.0
    
let green vec3<f32>(0.01.00.0);    //  0.5
    
let blue vec3<f32>(0.00.01.0);     //  1.0

    // Calculate the color based on value ranges
    
if < -0.5 {
        
// Blend from red (-1.0) to orange (-0.5)
        
return mix(redorange, (1.0) / 0.5);
    } else if 
0.0 {
        
// Blend from orange (-0.5) to white (0.0)
        
return mix(orangewhite, (0.5) / 0.5);
    } else if 
0.5 {
        
// Blend from white (0.0) to green (0.5)
        
return mix(whitegreen0.5);
    } else {
        
// Blend from green (0.5) to blue (1.0)
        
return mix(greenblue, (0.5) / 0.5);
    }
}

// ---------------------------------

fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let rr randomsmoothtestPoint.xy 2.0 mytimer );
    
    
let d0 sdfCubetestPointvec3(0.00.00.0), vec3(0.3) );
    
    
let d1 sdfSpheretestPointvec3(0.00.30.0), 0.2 );
   
    var 
sdfUniond0d1 );

    return 
d;
}


@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let nuv uv 2.0 1.0;
 
    
let point vec3nuv0.0 );
    
    
let d sdfScenepoint );
    
    var 
color colorGradient);
}



Gradient Rings


Now color gradients are good - but they make it difficult to see the wave as they travel outwards from the shape - sort of like ripples on a pond. So instead of a solid color, we'll draw bands using a sine function. This causes the sdf value to oscillate between -1 to 1 - with the amplitude of the field represented by closer or wider color bands.

As we don't usually have negative color values - we take the
abs(..)
of the sine function - also the magitude of the sdf function can be small - so we scale it up a bit (e.g., multiply it by 50.0).

You get sdf gradient rings for the sdf field as follows:


A basic starting point - a simple circle (sphere cross section) - showing the gradient field.
A basic starting point - a simple circle (sphere cross section) - showing the gradient field.


fn sdfSpheretestPoint:vec3<f32>, spherePos:vec3<f32>, sphereRadius:f32) -> f32 {
    return 
length(spherePos testPoint) - sphereRadius;
}

fn 
sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let d sdfSpheretestPointvec3(0.0), 0.2 );
    return 
d;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let nuv uv 2.0 1.0;
 
    
let point vec3nuv0.0 );
    
    
let d sdfScenepoint );
    
    var 
color vec3abs(sin(d*50.0)) );
    
    if ( 
abs(d) < 0.01 )
    {
        
color vec3<f32>(1.00.00.0);
    }
    
      return 
vec4<f32>(color.xyz1.0);
};


Inside or Outside


While the gradient field helps to show the distances from the shape surface - it can be difficult to tell what is inside and what is outside of the shape - especially when we start adding and subtractig sdf functions.

To help make this easier, we update the visualization to color the inside of the shape a yellowish color.


Sdf color rings with a yellow color for the inside - and green ring for the edge.
Sdf color rings with a yellow color for the inside - and green ring for the edge.


@fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let nuv uv 2.0 1.0;
 
    
let point vec3nuv0.0 );
    
    
let d sdfScenepoint );
    
    var 
color vec3abs(sin(d*50.0)) );
    
    
// if inside the shape - mix inside color
    
if ( abs(d) < 0.005 )
    {
        
color vec3<f32>(0.00.80.0);
    }
    
    
// if on the surface - draw solid line
    
if ( 0.01 )
    {
         
color *= vec3<f32>(1.11.00.0);   
    }
    
      return 
vec4<f32>(color.xyz1.0);
};


Shapes


Very easy to create basic shapes with sdf functions, such as the cube and sphere - we'll go through some common ones here. Be aware, as you'll see in a bit, you can mix and match shapes to create complex structures (e.g., cogs and trees) using sdf unions (addition, difference and so on).

When we have two or more sdf functions - we need to add the result together - so we instead of two sdf outputs - they're combined into a single one! Single floating point distance value.

We can't just add the values together - instead we need to use the min(..) function to get the minimum value - this creates a new sdf function that's a combination of both. We can do this repeatedly adding more and more sdf functions together.

To make life easier - we call it the sdf union function:

// add together - called it sdfUnion - but could also be called 'sdfAdd(..)' or 'sdfCombine'
fn sdfUnion(a:f32b:f32) -> f32 {
    return 
min(ab);
}


Let's add a few more function definitions for some other shapes and throw them on screen.

// ---------------------------------
// sdf shapes
fn sdfSpheretestPoint:vec3<f32>, spherePos:vec3<f32>, sphereRadius:f32) -> f32 {
    return 
length(spherePos testPoint) - sphereRadius;
}

fn 
sdfCubetestPoint:vec3<f32>,  cubePos:vec3<f32>,  cubeDim:vec3<f32> ) ->f32
{
   var 
d:vec3<f32> = abs(cubePos testPoint) - cubeDim;
   return 
min(max(d.xmax(d.yd.z)), 0.0)
           + 
lengthmax(dvec3<f32>(0.0) ) );
}

fn 
sdfCylinder(testPointvec3<f32>, cylinderPosvec3<f32>, cylinderRadiusf32cylinderHeightf32) -> f32 {
    
let d vec2<f32>(
        
length(vec2<f32>(testPoint.cylinderPos.xtestPoint.cylinderPos.z)) - cylinderRadius,
        
abs(testPoint.cylinderPos.y) - cylinderHeight 0.5
    
);
    return 
min(max(d.xd.y), 0.0) + length(max(dvec2<f32>(0.0)));
}

fn 
sdfTorus(testPointvec3<f32>, torusPosvec3<f32>, torusRadiusvec2<f32>) -> f32 {
    
let q vec2<f32>(
        
length(vec2<f32>(testPoint.torusPos.x
                         
testPoint.torusPos.y)) - torusRadius.x,
                         
testPoint.torusPos.z
    
);
    return 
length(q) - torusRadius.y;
}

fn 
sdfPlane(testPointvec3<f32>, planeNormalvec3<f32>, planeDistf32) -> f32 {
    return 
dot(testPointnormalize(planeNormal)) + planeDist;
}

fn 
sdfEllipsoid(testPointvec3<f32>, ellipsoidPosvec3<f32>, ellipsoidRadiivec3<f32>) -> f32 {
    
let q = (testPoint ellipsoidPos) / ellipsoidRadii;
    return 
length(q) - 1.0;
}

fn 
sdfCapsule(testPointvec3<f32>, capsuleAvec3<f32>, capsuleBvec3<f32>, capsuleRadiusf32) -> f32 {
    
let pa testPoint capsuleA;
    
let ba capsuleB capsuleA;
    
let h clamp(dot(paba) / dot(baba), 0.01.0);
    return 
length(pa ba h) - capsuleRadius;
}

fn 
sdfCone(testPointvec3<f32>, coneTipvec3<f32>, coneDirvec3<f32>, coneAnglef32) -> f32 {
    
let q testPoint coneTip;
    
let cosTheta cos(coneAngle);
    
let sinTheta sin(coneAngle);
    
let d vec2<f32>(
        
length(coneDir dot(qconeDir)),
        
abs(dot(qconeDir))
    );
    return 
max(d.cosTheta d.sinTheta, -dot(qconeDir));
}


We'll only draw the cube, sphere and torus (aka donut) - as we're looking at things 2d at the moment - so most of the shapes will look the same (e.g., squares or just circles).


SDF Union of Shapes - Cube, Sphere and Torus.
SDF Union of Shapes - Cube, Sphere and Torus.


fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let d0 sdfCubetestPointvec3(0.00.50.0), vec3(0.2) );
    
    
let d1 sdfSpheretestPointvec3(0.0, -0.50.0), 0.2 );

    
let d2 sdfTorustestPointvec3(0.50.00.0), vec2(0.20.1) );
        
    
// Add the cube, sphere and torus together
    
var sdfUniond0d1 );
    
sdfUniondd2 );
    
    return 
d;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let nuv uv 2.0 1.0;
 
    
let point vec3nuv0.0 );
    
    
let d sdfScenepoint );
    
    var 
color vec3abs(sin(d*50.0)) );
    
    if ( 
abs(d) < 0.005 )
    {
        
color vec3<f32>(0.00.80.0);
    }
    
// if inside - use red rings for the gradients
    
if ( 0.01 )
    {
         
color *= vec3<f32>(1.11.00.0);   
    }
    
      return 
vec4<f32>(color.xyz1.0);
};


Union Operations


There is more than one way to combine sdf shapes - you can add them subtract them or even xor the shape overlaps!

The two most common union operations are the union add and the union difference.

// subtract one from the other
fn sdfDifference(a:f32b:f32) -> f32 {
    return 
max(a, -b);
}



Union and difference of a sphere and cube.
Union and difference of a sphere and cube.


Then we have the
sdfIntersection(..)
which keeps only the overlapping parts.

// Keep only the overlapping intersection parts
fn sdfIntersection(a:f32b:f32) -> f32 {
    return 
max(ab);
}



sdf intersection of the cube and sphere - only keeps the overlapping parts.
sdf intersection of the cube and sphere - only keeps the overlapping parts.



We can take it further with an XOR function - which keeps only the non-overlapping parts - and removes any overlapping pieces.

// keeps the parts that are NOT overlapping
fn sdfXORDifference(af32bf32) -> f32 {
    return 
max(min(ab), min(-a, -b));
}



XOR difference of the sphere and cube.
XOR difference of the sphere and cube.



Smooth Union Operations


We can make each of the union operations smoother - round the edges or make the transitions softer.

fn smoothunion(d1:f32d2:f32k:f32/*=1.0*/) -> f32 {
  
let h max(abs(d1 d2), 0.0) / k;
  return 
min(d1d2) - 0.25;
}



Smooth sdf union of two shapes - cube and sphere.
Smooth sdf union of two shapes - cube and sphere.


We also have a smoothdifference - when we difference two sdf functions.

fn smoothdifference(d1:f32d2:f32k:f32/*=1.0*/) -> f32 {
  
let h max(abs(d1 d2), 0.0) / k;
  return 
max(d1, -d2) - 0.25;
}



Sdf smooth difference of the sphere and cube.
Sdf smooth difference of the sphere and cube.


fn smoothintersection(d1:f32d2:f32k:f32/*=1.0*/) -> f32 {
  
let h max(abs(d1 d2), 0.0) / k;
  return 
max(d1d2) + 0.25;
}



Sdf smooth intersection
Sdf smooth intersection



Adding Noise



We can mix in a bit of smooth noise - for example, for the sphere - instead of it having a fixed radius - we use the smooth noise function.


Simple smooth noise function - interpolating between noise values.
Simple smooth noise function - interpolating between noise values.


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;
}



Adding smooth noise to the radius of the sdf sphere function.
Adding smooth noise to the radius of the sdf sphere function.


fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let rr randomsmoothtestPoint.xy 2.0 mytimer );
    
    
let d0 sdfCubetestPointvec3(0.00.00.0), vec3(0.3) );
    
    
let d1 sdfSpheretestPointvec3(0.00.30.0), 0.2 rr*0.2 );
   
    var 
sdfUniond0d1 );

    return 
d;
}


Add the noise in different ways - using unions, differences and intersections (sphere and cube example).


Adding noisy sphere to a cube in different ways.
Adding noisy sphere to a cube in different ways.



Splines



Simple sdf spline using bezier quatratic (64 iteration steps).
Simple sdf spline using bezier quatratic (64 iteration steps).


fn sdfBezierQuadratic(testPointvec3<f32>, p0vec3<f32>, p1vec3<f32>, p2vec3<f32>, radiusf32) -> f32 {
    
let maxSteps 64// Number of steps for approximation
    
var minDist 99999.0;

    
// Sample the Bézier curve at discrete points
    
for (var i:i32 0<= maxSteps+= 1) {
        
let t f32(i) / f32(maxSteps);
        
let curvePoint mix(mix(p0p1t), mix(p1p2t), t); // Quadratic Bézier interpolation
        
let distToCurve length(testPoint curvePoint);
        
minDist min(minDistdistToCurve);
    }

    
// Subtract the curve's radius
    
return minDist radius;
}

fn 
sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let d sdfBezierQuadratic(testPoint
                                
vec3<f32>(0,0,0),
                                
vec3<f32>(-0.90.90),
                                
vec3<f32>(0.70.50),
                                
0.1);
    
    return 
d;
}



Sdf bezier cube using 4 points (64 iteration steps).
Sdf bezier cube using 4 points (64 iteration steps).


fn sdfBezierCubic(testPointvec3<f32>, p0vec3<f32>, p1vec3<f32>, p2vec3<f32>, p3vec3<f32>, radiusf32) -> f32 {
    
let maxSteps 64// Number of steps for approximation
    
var minDist 99999.0;

    
// Sample the Bézier curve at discrete points
    
for (var i:i32 0<= maxSteps+= 1) {
        
let t f32(i) / f32(maxSteps);
        
let a mix(p0p1t);
        
let b mix(p1p2t);
        
let c mix(p2p3t);
        
let d mix(abt);
        
let curvePoint mix(dct); // Cubic Bézier interpolation
        
let distToCurve length(testPoint curvePoint);
        
minDist min(minDistdistToCurve);
    }

    
// Subtract the curve's radius
    
return minDist radius;
}

fn 
sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let d sdfBezierCubic(testPoint
                                
vec3<f32>(-0.5,  0.50),
                                
vec3<f32>(-0.5, -0.50),
                                
vec3<f32>( 0.5, -0.50),
                                
vec3<f32>( 0.5,  0.50),
                                
0.1);
    return 
d;
}


As the splines are generated using iterations - go along the bezier spline using small points - we can look show the iterations.


Sdf bezier spline for different iterations (for sdfBezierCubic with 4,8,16 and 32 iteration steps).
Sdf bezier spline for different iterations (for sdfBezierCubic with 4,8,16 and 32 iteration steps).



More SDF Noise


We can clamp the influence of the noise to a maximum radius - so it doesn't cause small ripples to propogate throughout the noise field - limiting noise to within a specific region. So for the sphere - we modified the radius - we limit the influence of the radius to a maximum of 1.0.


Noise radius limit (clamp range).
Noise radius limit (clamp range).


fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let d0 sdfCubetestPointvec3(0.0,-0.30.0), vec3(0.4) );
    
    
// noise
    
var rr randomsmoothtestPoint.xy 2.0 mytimer );
    
    
// clamp the noise to 0 to 1 - outside 1.0 noise rr is clamped to 0.0
    
var dist1 lengthvec3(0.00.30.0) - testPoint );
    
dist1 clamp(dist10.01.0); 
    
rr mixrr0.0dist1 );
    
    
let d1 sdfSpheretestPointvec3(0.00.30.0), 0.3 rr*0.3 );
   
    var 
sdfUniond0d1 );

    return 
d;
}


SDF Fractals


We can create a simple sdf fractal without loops but still capturing a fractal-like appearance by leveraging mathematical functions directly.

As it isn't a pure fractals, lets call it a pseudo-fractal - which we'll create by combining sin, cos and mathematical repetition.


Simple pseudo-fractal using sin/cos and mod function for repeating.
Simple pseudo-fractal using sin/cos and mod function for repeating.


fn fractalSDF(posvec3<f32>) -> f32 {
    
// Repeat space to simulate fractal structure
    
var pos;
    
let scale 2.0;

    
// Repeated folds and distortions
    
abs(p) - vec3<f32>(1.0);     // Box fold
    
= (scale) - 1.0 2.0;     // Space repetition

    // Combine sine and cosine waves for complexity
    
let wave sin(scale p.5.0) + cos(scale p.5.0) + sin(scale p.5.0);

    
// Combine distance and wave pattern, ensuring negative values are possible
    
let pp mixpos waveposclamp(length(pos)*0.70.01.0) );
    
let distance length(pp) - 0.5;

    
// Introduce a blending factor to balance wave impact
    
return distance;
}



MandelBox Fractal



SDF Mandelbox fractal.
SDF Mandelbox fractal.


fn mandelboxSDF(posvec3<f32>) -> f32 {
    
let scalef32 1.5;            // Scale factor
    
let bailoutf32 2.0;          // Escape radius
    
let iterationsi32 10;        // Number of iterations
    
let fixedRadiusf32 1.0;      // Fixed radius for folding
    
let minRadius2f32 fixedRadius fixedRadius;
    
let maxRadius2f32 bailout bailout;

    var 
zvec3<f32> = pos;
    var 
dzf32 1.0;               // Distance estimator

    
for (var ii32 0iterations1) {
        if (
dot(zz) > maxRadius2) {
            break;
        }

        
// Apply sphere fold
        
let r2f32 dot(zz);
        if (
r2 minRadius2) {
            
let factor maxRadius2 minRadius2;
            
*= factor;
            
dz *= factor;
        } else if (
r2 maxRadius2) {
            
let factor maxRadius2 r2;
            
*= factor;
            
dz *= factor;
        }

        
// Box fold
        
clamp(zvec3<f32>(-1.0), vec3<f32>(1.0)) * 2.0 z;

        
// Scale and translate
        
scale pos;
        
dz dz abs(scale) + 1.0;
    }

    return 
0.5 log(dot(zz)) * sqrt(dot(zz)) / dz;
}

fn 
sdfScenetestPoint:vec3<f32> ) -> f32
{
    var 
mandelboxSDFtestPoint );
    return 
d;
}


Repeated folds and distortions (structures of repeating and mirroring sdf patterns)


We can use mathematical transformations to encode complex shapes and patterns into simple equations. Repeated folds and distortions are key techniques in creating intricate structures like fractals. The box fold (`p = abs(p) - vec3(1.0);`) reflects all points into the positive quadrant of space, ensuring symmetry about each axis. For example, a point at `(-2.0, 0.5, -1.5)` becomes `(2.0, 0.5, 1.5)`, effectively mirroring space.

This operation generates sharp edges and a repeating symmetry characteristic of fractal patterns, particularly when applied recursively or in combination with other operations. After folding, subtracting `vec3(1.0)` offsets the space, adding further variation by centering the transformations around a different origin.

The space repetition transformation (`p = (p % scale) - scale / 2.0;`) introduces periodicity by remapping the position into a repeating cell structure. It achieves this by taking the modulus of each coordinate with respect to the `scale`, which ensures the position remains bounded within a range. For example, if `scale = 4.0`, a point at `p = (5.5, -3.2, 8.1)` maps to `(1.5, -3.2, 0.1)` after wrapping around.

Subtracting `scale / 2.0` centers each cell around the origin, creating a uniform tiling effect in space. When combined with box folds, this repetition builds a fractal-like appearance by breaking the space into self-similar patterns, while maintaining distance calculations for rendering or simulation purposes.

e.g.

abs(p) - vec3<f32>(1.0);       // Fold operation
= (scale) - scale 2.0;     // Space repetition





Box Fold Example


This example uses box folding to create a symmetrical structure around the origin.


Sdf box fold with spheres.
Sdf box fold with spheres.


fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    
// Apply box fold
    
let foldedP abs(p) - vec3<f32>(1.0);

    
// Sphere SDF after fold
    
let d sphereSDF(foldedPvec3(0.0), 1.7); // Sphere
    
    
return d;
}


Space Repetition Example


This example shows a grid of spheres created using space repetition.


Sdf repeating in the x direction only (animating the distance).
Sdf repeating in the x direction only (animating the distance).


fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    var 
testPoint;

    
let sepdist 1.0 abssin(mytimer*4.0) ); // animate the spheres moving appart and together
    
    
vec3<f32>( sign(p.x) * (p.sepdist) - sepdist/2p.yp.); // repeat in the x only
    
    
var sdfSphere(pvec3(0.00.00.0), 0.5);
    
    return 
d;
}


Flower (with Petals)


Flower with adjustable petals - simple sdf function for 2D flower shape in the x-y plane with radial symmetry for the petals


Sdf fractal flower with petals
Sdf fractal flower with petals


fn flowerSDF(pvec2<f32>, petalsf32radiusf32petalSharpnessf32) -> f32 {
    
// Convert position to polar coordinates
    
let r length(p);
    
let theta atan2(p.yp.x);

    
// Create petal modulation based on angle
    
let petalFactor cos(theta petals) * 0.5 0.5// 0 to 1

    // Define the flower shape
    
let petalRadius radius petalFactor petalSharpness;

    
// SDF of the flower
    
return petalRadius;
}

// ---------------------------------

fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    var 
flowerSDFtestPoint.xy5.01.00.9 );

    return 
d;
}


You can make the flowerSDF a 3d version with a couple of extra lines. However, in 2d it looks the same (only if you draw it in 3d).

fn flower3DSDF(pvec3<f32>, petalsf32radiusf32petalSharpnessf32) -> f32 {
    
let flowerDist flowerSDF(p.xypetalsradiuspetalSharpness); // 2D flower in x-y plane
    
let zDist abs(p.z) - 0.1// Thickness along z-axis
    
return max(flowerDistzDist); // Combine into a 3D shape
}



Sdf flower animated - the number of petals and their radius.
Sdf flower animated - the number of petals and their radius.



Ray-Marching (Ray-Tracing) with Lines


You can ray-trace an SDF scene - by shooting a ray into the scene and calculating the intersection point. To demonstrate this - let's draw some ray lines and plot their trajectory through the scene.

This includes both reflections and refections as they bounce-off and travel through shapes in the scene.

reflect(..)
function to calculate the reflection direction from the intersection point
refract(..)
function to calculate the refraction direction through the object (which uses Snell's law for bending due to the material density change)

To make things more interesting - we animate the fov for the rays - so you can see the reflection/refraction vectors change dynamically.

We draw the normals and the internal rays (travel through) the object in gray.


Visualizing the ray-tracing by drawing lines (rays) travelling through the scene - from the left side of the screen.
Visualizing the ray-tracing by drawing lines (rays) travelling through the scene - from the left side of the screen.


The following gives the raymarching code - which finds the intersection point for a ray with the sdf scene.

• Handle inside rays by using an inside flag to invert the sdf value
• WGSL does not support recursion - so a simple stack is created to emulate recursive nature of rays creating rays when colliding.

fn sdfScenetestPoint:vec3<f32> ) -> f32
{
    
let d0 sdfSpheretestPointvec3(0.00.00.0), 0.6 );
    
    
let d1 sdfSpheretestPointvec3(1.4, -1.00.0), 0.6 );
    
    
let d sdfUniond0d1 );
    
    return 
d;
}

fn 
raymarch(rovec3<f32>, rdvec3<f32>, maxStepsi32maxDistf32insidef32) -> f32 {
    var 
0.0// Distance traveled along the ray
    
var epsilonf32 0.001;
    for (var 
0maxSteps1) {
        
let p ro rd t// Current position along the ray
        
let d sdfScene(p) * inside// Distance to the scene's nearest surface

        
if (epsilon) { // Hit detected
            
return t// Return the distance to the hit
        
}

        if (
maxDist) { // Exit if the ray goes too far
            
break;
        }

        
+= d// Advance by the SDF value
    
}

    return -
1.0// No hit
}

// Check if the pixel lies on the line between ray origin and hit point
fn drawLine(uvvec2<f32>, startpointvec3<f32>, endpointvec3<f32>) -> bool {
    
let thickness:f32 0.009;
    
// Convert uv (which is 2D) into a 3D point by using the z component from the startpoint.
    
let pixelPos vec3<f32>(uv.xuv.ystartpoint.z);

    
// Direction of the line from startpoint to endpoint
    
let lineDir normalize(endpoint startpoint);

    
// Vector from the startpoint to the pixel position
    
let toPixel pixelPos startpoint;

    
// Compute the perpendicular distance from the point to the line
    
let perpDist length(cross(toPixellineDir)) / length(lineDir);

    
// Compute the projection of the point onto the line
    
let projection dot(toPixellineDir) / length(lineDir);

    
// Compute the distance from the startpoint to the endpoint (line segment length)
    
let lineLength length(endpoint startpoint);

    
// Check if the perpendicular distance is within the thickness
    // and if the projection is between 0 and the length of the line segment
    
return (perpDist <= thickness) && (projection >= 0.0) && (projection <= lineLength);
}

// Function to compute the normal of an SDF at a given point
fn sdfNormal(pvec3<f32>) -> vec3<f32> {
    
let epsilon 0.001// Small step for numerical differentiation
    
let dx vec3<f32>(epsilon0.00.0);
    
let dy vec3<f32>(0.0epsilon0.0);
    
let dz vec3<f32>(0.00.0epsilon);

    
// Approximate gradient using central differences
    
let normal vec3<f32>(
        
sdfScene(dx) - sdfScene(dx),
        
sdfScene(dy) - sdfScene(dy),
        
sdfScene(dz) - sdfScene(dz)
    );

    return 
normalize(normal); // Normalize the gradient to get the unit normal
}

struct RayStack {
    
rovec3<f32>,
    
rdvec3<f32>
};

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let d sdfScenepoint );

    var 
color vec3abs(sin(d*30.0)) );

    if ( 
abs(d) < 0.01 )
    {
        
color vec3<f32>(0.00.80.0);
    }
    
// if inside - use red rings for the gradients
    
if ( 0.01 )
    {
         
color *= vec3<f32>(1.11.00.0);   
    }
    
    
// ---------------------------------------
    // Ray-line tracer using 'stack'
    //
    // WGSL does not support recursive functions - however, we can emulate this
    // by adding out own little stack - push/pop information onto it as needed.

    
var rayStackCount:i32 0;
    var 
rayStack : array< RayStack32 >;
    
    for (var 
n:i32=-4n<=4n++)
    {
    
let dy = ( 0.1 sin(mytimer*10.0)*0.05 )*f32(n);
    
    
rayStackCount 0;
    
rayStackrayStackCount ].ro vec3<f32>(-2.00.00.0);
    
rayStackrayStackCount ].rd normalizevec3<f32>( 1.0dy0.0) ) ;
    
rayStackCount++;
    
    
    for (var 
i:i32=0i<3i++)
    {
        if ( 
rayStackCount <= ) { break; }
        
        
// pop it off the stack
        
rayStackCount--;
        
let ro rayStackrayStackCount ].ro;
        
let rd rayStackrayStackCount ].rd;
        
        
let hitDist raymarchrord324.01.0 );
        if ( 
hitDist 0.0 
        {
            
let hitpoint ro rd hitDist;
            if (
drawLine(nuvrohitpoint)) 
            {
                
color vec3<f32>(1.00.00.0); // Red
            
}

            
let normal sdfNormalhitpoint );
            if (
drawLine(nuvhitpointhitpoint+normal*0.25 )) 
            {
                
color vec3<f32>(0.00.01.0); 
            }
            
            
let reflectdir reflectrdnormal );
            {
                
rayStackrayStackCount ].ro hitpoint normal*0.01;
                
rayStackrayStackCount ].rd reflectdir;
                
rayStackCount++;
            }
            
            
let refractdir refractrdnormal1.0/1.2 ); // refract in
            
{
                
let ro2 hitpoint normal*0.01;
                
let rd2 refractdir;
                 
let hitDist2 raymarchro2rd2324.0, -1.0 );
                if ( 
hitDist2 0.0 )
                {
                    
let hitpoint2 ro2 rd2*hitDist2;
                    if (
drawLine(nuvro2hitpoint2))
                    {
                        
color vec3<f32>(0.50.51.0);
                    }
                    
                    
let normal2 sdfNormalhitpoint2 );
                      
let rd3 refractrd2, -normal21.2/1.0 ); // refract out
                      
let ro3 hitpoint2 normal2*0.01;
                    
                    if (
drawLine(nuvro3ro3+normal2*0.3 )) 
                    {
                        
color vec3<f32>(0.00.01.0); 
                    }
                    
                    
rayStackrayStackCount ].ro ro3;
                    
rayStackrayStackCount ].rd rd3;
                    
rayStackCount++;
                }
            }
        }
        else 
// no hit
        
{
            if (
drawLine(nuvroro+rd*2.0 )) 
            {
                
color vec3<f32>(0.30.00.0); 
            }
        }
    }
// end for i
   
    
}// end for n

      
return vec4<f32>(color.xyz1.0);
};


Next Comes Color!


We have shapes and we have gradients - and we're using 'color' to display information (e.g., inside, outside, edges and the field ripples). But each shape does not have its own color! For example, if we have two spheres or three spheres on screen - how do we assign a color to them? As at the moment, the
sdfScene(..)
function only returns a single parameter - a floating point number which represents the distance. There isn't any other information to help us identify which point belongs to which shape.

Now to fix this problem and add color information for each shape - we need to step back and modify our sdf functions - so they return extra information - a material as well as a distance. I say material and not just a color - as we can add lots of extra stuff inside the material, such as, reflection and refraction factors.

• Each SDF function returns both the distance and an identifier (materialId) for the shape.
• The scene function (sdfScene) determines which shape is closest to the given point and returns the corresponding distance and material ID.

Let's update the example sdf functions we've discussed upto now - but modify them to work with an
SDFResult
structure - which contains the distance and material.


SDF shapes are able to contain color details.
SDF shapes are able to contain color details.


struct Material {
    
colorvec3<f32// just a basic color
};

struct SDFResult {
    
distance:  f32,      // Signed distance value
    
material:  Material  // Identifier for the shape
};

// ---------------------------------
// sdf shapes
fn sdfSpheretestPoint:vec3<f32>, spherePos:vec3<f32>, sphereRadius:f32
             
material:Material) -> SDFResult {
    return 
SDFResultlength(spherePos testPoint) - sphereRadius,
                       
material );
}

// SDF functions updated to return SDFResult
fn sdfCube(testPointvec3<f32>, cubePosvec3<f32>, cubeDimvec3<f32>, materialMaterial) -> SDFResult {
    var 
dvec3<f32> = abs(cubePos testPoint) - cubeDim;
    
let distance min(max(d.xmax(d.yd.z)), 0.0) + length(max(dvec3<f32>(0.0)));
    return 
SDFResult(distancematerial);
}


fn 
sdfCylinder(testPointvec3<f32>, cylinderPosvec3<f32>, cylinderRadiusf32cylinderHeightf32materialMaterial) -> SDFResult {
    
let d vec2<f32>(
        
length(vec2<f32>(testPoint.cylinderPos.xtestPoint.cylinderPos.z)) - cylinderRadius,
        
abs(testPoint.cylinderPos.y) - cylinderHeight 0.5
    
);
    
let distance min(max(d.xd.y), 0.0) + length(max(dvec2<f32>(0.0)));
    return 
SDFResult(distancematerial);
}

fn 
sdfTorus(testPointvec3<f32>, torusPosvec3<f32>, torusRadiusvec2<f32>, materialMaterial) -> SDFResult {
    
let q vec2<f32>(
        
length(vec2<f32>(testPoint.torusPos.xtestPoint.torusPos.y)) - torusRadius.x,
        
testPoint.torusPos.z
    
);
    
let distance length(q) - torusRadius.y;
    return 
SDFResult(distancematerial);
}

fn 
sdfPlane(testPointvec3<f32>, planeNormalvec3<f32>, planeDistf32materialMaterial) -> SDFResult {
    
let distance dot(testPointnormalize(planeNormal)) + planeDist;
    return 
SDFResult(distancematerial);
}

fn 
sdfEllipsoid(testPointvec3<f32>, ellipsoidPosvec3<f32>, ellipsoidRadiivec3<f32>, materialMaterial) -> SDFResult {
    
let q = (testPoint ellipsoidPos) / ellipsoidRadii;
    
let distance length(q) - 1.0;
    return 
SDFResult(distancematerial);
}

fn 
sdfCapsule(testPointvec3<f32>, capsuleAvec3<f32>, capsuleBvec3<f32>, capsuleRadiusf32materialMaterial) -> SDFResult {
    
let pa testPoint capsuleA;
    
let ba capsuleB capsuleA;
    
let h clamp(dot(paba) / dot(baba), 0.01.0);
    
let distance length(pa ba h) - capsuleRadius;
    return 
SDFResult(distancematerial);
}

fn 
sdfCone(testPointvec3<f32>, coneTipvec3<f32>, coneDirvec3<f32>, coneAnglef32materialMaterial) -> SDFResult {
    
let q testPoint coneTip;
    
let cosTheta cos(coneAngle);
    
let sinTheta sin(coneAngle);
    
let d vec2<f32>(
        
length(coneDir dot(qconeDir)),
        
abs(dot(qconeDir))
    );
    
let distance max(d.cosTheta d.sinTheta, -dot(qconeDir));
    return 
SDFResult(distancematerial);
}

// ---------------------------------
// Union operation

// add together
fn sdfUnion(a:SDFResultb:SDFResult) -> SDFResult {
    if (
a.distance b.distance) {
        return 
a// Shape 'a' is closer
    

    return 
b// Shape 'b' is closer
}

// ---------------------------------

fn sdfScenetestPoint:vec3<f32> ) -> SDFResult
{
    
let red    Materialvec3(1.00.00.0) );
    
let green  Materialvec3(0.01.00.0) );
    
let blue   Materialvec3(0.00.01.0) );
    
let color0 Materialvec3(0.01.01.0) );
    
    
let d0 sdfSpheretestPointvec3(0.0,  0.00.0),  0.6red );
    
let d1 sdfSpheretestPointvec3(1.4, -1.00.0),  0.6green );
    
let d2 sdfCubetestPointvec3(0.0,-0.30.0), vec3(0.4), blue );
    
let d3 sdfTorustestPointvec3(1.0,0.80.0), vec2(0.40.1), color0 );
    
    var 
sdfUniond0d1 );
    
sdfUniondd2 );
    
sdfUniondd3 );

    return 
d;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let sdfResult sdfScenepoint );
    
let d sdfResult.distance;
    
let c sdfResult.material.color;

    var 
color vec3abs(sin(d*30.0)) );

    if ( 
abs(d) < 0.02 )
    {
        
color vec3<f32>(0.00.80.0);
    }
    
// if inside - use red rings for the gradients
    
if ( 0.0 )
    {
         
color *= c;//color *= vec3<f32>(1.1, 1.0, 0.0);   
    
}
    return 
vec4<f32>(color1.0); 
}


Back to Ray-Marching (With Colors)


Now that we've updated things so sdf function returns both a distance and a material color - we can integrate them into the simple ray-marching line algorithm we did earlier. So instead of using some constant colors for the rays (e.g., red for the primary ray).

When the line hits a red shape - the line turns red - so we can see a better picture of what's happening.


Ray-marching single ray - lines and color sdf functions. Brown lines show
Ray-marching single ray - lines and color sdf functions. Brown lines show 'no hit'; Small blue lines on the surfaces of the shapes indicate the collision normal.



Ray-marching with lines and color sdf functions.
Ray-marching with lines and color sdf functions.


@fragment
fn main(@location(0uvvec2<f32>) -> @location(0vec4<f32> {
    
let zoom 2.0;
    
let nuv = (uv 2.0 1.0) * zoom;

    
let point vec3(nuv0.0);
    
let sdfResult sdfScene(point);

    var 
color vec3(abs(sin(sdfResult.distance 30.0)));

    if (
abs(sdfResult.distance) < 0.01) {
        
color vec3<f32>(0.00.80.0);
    }
    
// If inside - use red rings for the gradients
    
if (sdfResult.distance 0.01) {
        
//color *= vec3<f32>(1.1, 1.0, 0.0);
        
color *= sdfResult.material.color;
    }

    
// ---------------------------------------
    // Ray-line tracer using 'stack'
    //
    // WGSL does not support recursive functions - however, we can emulate this
    // by adding our own little stack - push/pop information onto it as needed.

    
var rayStackCounti32 0;
    var 
rayStack: array<RayStack32>;

    for (var 
ni32 = -12<= 12n++) {
        
let dy = (0.03 sin(mytimer 0.0) * 0.05) * f32(n);

        
rayStackCount 0;
        
rayStack[rayStackCount].ro vec3<f32>(-2.00.00.0);
        
rayStack[rayStackCount].rd normalize(vec3<f32>(1.0dy0.0));
        
rayStackCount++;

        for (var 
ii32 03i++) {
            if (
rayStackCount <= 0) {
                break;
            }

            
// Pop it off the stack
            
rayStackCount--;
            
let ro rayStack[rayStackCount].ro;
            
let rd rayStack[rayStackCount].rd;

            
let hitResult raymarch(rord324.01.0);
            if (
hitResult.distance 0.0) {
                
let hitpoint ro rd hitResult.distance;
                if (
drawLine(nuvrohitpoint)) {
                    
//color = vec3<f32>(1.0, 0.0, 0.0); // Red
                    
color hitResult.material.color;
                }

                
let normal sdfNormal(hitpoint);
                if (
drawLine(nuvhitpointhitpoint normal 0.25)) {
                    
color vec3<f32>(0.00.01.0);
                }

                
let reflectdir reflect(rdnormal);
                {
                    
rayStack[rayStackCount].ro hitpoint normal 0.01;
                    
rayStack[rayStackCount].rd reflectdir;
                    
rayStackCount++;
                }
                
                
let ior 1.2;

                
let refractdir refract(rdnormalior); // Refract in
                
{
                    
let ro2 hitpoint normal 0.01;
                    
let rd2 refractdir;
                    
let hitResult2 raymarch(ro2rd2324.0, -1.0);
                    if (
hitResult2.distance 0.0) {
                        
let hitpoint2 ro2 rd2 hitResult2.distance;
                        
                        if (
drawLine(nuvro2hitpoint2)) {
                            
color vec3<f32>(0.50.51.0);
                        }

                        
let normal2 sdfNormal(hitpoint2);
                        
let rd3 refract(rd2, -normal21.0/ior); // Refract out
                        
let ro3 hitpoint2 normal2 0.01;

                        if (
drawLine(nuvro3ro3 normal2 0.3)) {
                            
color vec3<f32>(0.00.01.0);
                        }

                        
rayStack[rayStackCount].ro ro3;
                        
rayStack[rayStackCount].rd rd3;
                        
rayStackCount++;
                    }
                }
            } else { 
// No hit
                
if (drawLine(nuvroro rd 2.0)) {
                    
color vec3<f32>(0.30.00.0);
                }
            }
        } 
// End for i
    
// End for n

    
return vec4<f32>(color.xyz1.0);
}


Warning about the little function for the ray-marching line implementation - each fragment pixel is doing a lot of work! So be careful not to ramp things up too much - limited the number of lines and recursions - otherwise, it'll either kill your graphics card or crash your browser.

Taste of 3D


The simple scene using ray-machine with line - can be easily modified to use 3d rays instead of 2d lines. For example, the following shows the same scene but rendered in 3d with reflections and refractions (bit of lighting and shadows).


A look at the same example in 3-dimension - from lines to rays (each ray is a pixel).
A look at the same example in 3-dimension - from lines to rays (each ray is a pixel).


The following gives the body of the code - swapped out the line drawing code for the 2d side view for the 3d rays for each pixel.

So the scene is drawn from the same position - we place the camera on the left side facing right (so it was from the same view as the line we showed earlier).

We also added checkered ground plane to the bottom - to help with reflections - so you can see the reflected ground isntead of just a constant color.


@fragment
fn main(@location(0uvvec2<f32>) -> @location(0vec4<f32> {

    
let nuv = (uv 2.0 1.0);
    
    var 
color vec3<f32>(0,0,0);
        
    
let time mytimer 5.0;
    
let cam:Camera setCamera(nuv,
                               
vec3<f32>(-2.5*cos(time), 03.5*sin(time*0.5) ), 
                               
vec3<f32>( 0,0,0), 
                               
vec3<f32>(0,1,0), 60.0);

    
// ---------------------------------------
    // Ray-line tracer using 'stack'
    //
    // WGSL does not support recursive functions - however, we can emulate this
    // by adding our own little stack - push/pop information onto it as needed.

    
var rayStackCounti32 0;
    var 
rayStack: array<RayStack32>;
    
    
rayStackCount 0;
    
rayStack[rayStackCount].ro cam.ro;
    
rayStack[rayStackCount].rd cam.rd;
    
rayStack[rayStackCount].k  1.0// 100% from screen
    
rayStackCount++;

    for (var 
ii32 032i++) {
        if (
rayStackCount <= 0) {
            break;
        }

        
// Pop it off the stack
        
rayStackCount--;
        
let ro rayStack[rayStackCount].ro;
        
let rd rayStack[rayStackCount].rd;
        
let k  rayStack[rayStackCount].k;

        
let hitResult raymarch(rord12810.01.0);
        if (
hitResult.distance 0.0) {
            
            
let hitpoint ro rd hitResult.distance;
            
let normal sdfNormal(hitpoint);
            
//if (drawLine(nuv, ro, hitpoint)) {
                //color = vec3<f32>(1.0, 0.0, 0.0); // Red
            
                // Lighting
                
let lightPos vec3<f32>(2.05.52.0);
                
let lightDir normalizehitpoint lightPos );
                
let lightOrigin hitpoint normal 0.02;
                
let hitShadow raymarch(lightOriginlightDir648.01.0);

                
// Check for shadows
                
var isInShadow false;
                if ( 
hitShadow.distance 0.0) {
                    
isInShadow abshitShadow.distance length(hitpoint lightPos) ) > 0.2;
                }
            
                
let diffuse clamp(dot(normallightDir), 0.01.0) * hitResult.material.color;

                
let shininess 32.0;
                
let halfVector normalize(lightDir rd);
                
let specular pow(clamp(dot(normalhalfVector), 0.01.0), shininess) * vec3<f32>(1.01.01.0);
                var 
diffuseSpecular diffuse specular;
            
                if (
isInShadow) {
                    
diffuseSpecular *= 0.4// Reduce in shadow
                
}
            
//}
                
            // Clamp and normalize kr and kt
            
var kr clamp(hitResult.material.kr0.01.0);
            var 
kt clamp(hitResult.material.kt0.01.0);
            
let total kr kt;
            if (
total 1.0) {
                
kr /= total;
                
kt /= total;
            }
            
            
            var 
fresnelFactor fresnel(rdnormal);

            
// view the fresnel amount as a red value
            //color += vec3(1.0,0.0,0.0)*fresnelFactor;
            
kr *= fresnelFactor;
            
            
            
// Combine contributions
            
let totalReflectionRefraction kr kt;
            
let diffuseWeight clamp(1.0 totalReflectionRefraction0.01.0);
            
color += diffuseWeight diffuseSpecular k;// + reflectionColor + refractionColor;
            
            //if (drawLine(nuv, hitpoint, hitpoint + normal * 0.25)) {
            //    color = vec3<f32>(0.0, 0.0, 1.0);
            //}

            
let reflectdir reflect(rdnormal);
            
//var reflectionColor = vec3<f32>(0.0, 0.0, 0.0);
            
if ( kr 0.0 && 0.0// Reflection contribution
            
{
                
rayStack[rayStackCount].ro hitpoint normal 0.01;
                
rayStack[rayStackCount].rd reflectdir;
                
rayStack[rayStackCount].k  kr k;
                
rayStackCount++;
                
                
//reflectionColor = kr * hitResult.material.color; // Add reflection contribution
                //color += kr * hitResult.material.color; 
            
}
            
            
let ior 0.6;//1.2;

            // 4.0 is to fix a numerical issue - angle is almost parallel
            
var refractdir refract(rdnormal*3.0ior); // Refract in
            // on the edge of the shape (fresnel)
            
if ( length(refractdir) < 0.1 )
            {
                
//kr = clamp( 1.0 - fresnelReflectivity(rd, normal, kr), 0.0, 1.0);
                
                //let fix = abs(dot(normal, rd));
                //refractdir = rd;
                
refractdir normalizereflectdir );
                
//kt *= clamp( fix, 0.0, 1.0 );
            
}
            
///let fres = 1.0 - dot(normal, rd);
            // kt *= clamp( pow( fres, 0.2 ), 0.0, 1.0 );
            
            //var refractionColor = vec3<f32>(0.0, 0.0, 0.0);
            
if ( kt 0.0 && 0.0 // Refraction contribution
            
{
                
let ro2 hitpoint normal 0.03;
                
let rd2 refractdir;
                
let hitResult2 raymarch(ro2rd212810.0, -1.0);
                if (
hitResult2.distance >= -0.01 ) {
                    
let hitpoint2 ro2 rd2 hitResult2.distance;
                    
                    
//if (drawLine(nuv, ro2, hitpoint2)) {
                    //    color = vec3<f32>(0.5, 0.5, 1.0);
                    //}

                    
let normal2 sdfNormal(hitpoint2);
                    
let rd3 refract(rd2, -normal21.0/ior); // Refract out
                    
let ro3 hitpoint2 normal2 0.03;

                    
//if (drawLine(nuv, ro3, ro3 + normal2 * 0.3)) {
                    //    color = vec3<f32>(0.0, 0.0, 1.0);
                    //}

                    
rayStack[rayStackCount].ro ro3;
                    
rayStack[rayStackCount].rd rd3;
                    
rayStack[rayStackCount].k  kt k;
                    
rayStackCount++;
                }
            }
            

            
        } else { 
// No hit
            //if (drawLine(nuv, ro, ro + rd * 2.0)) {
            //    color = vec3<f32>(0.3, 0.0, 0.0);
            //}
            
color += (vec3<f32>(0.20.50.6) + vec3(-rd.y*1.5)) * k// Dark gray background
        
}
    } 
// End for i
    
    
return vec4<f32>(color1.0);
}


There are a few numerical tweaks in the 3d version which wouldn't have ben noticable in the 2d line version - such as, perpendicular and parallel lines for edges of shape surfaces. For example, the dot product of perpendicular vectors is zero, and if you try and normalize a zero - value you'll get an undefined.

We also pass a scaling factor down the stack - as the ray travels through the scene it's scaled by it's energy value (law of energy - sum of the incident, reflection and refraction ray value should sum up to 1.0).

Energy Conservation Law: The sum of the diffuse, reflection, and refraction contributions should not exceed 1.0.

We have the material property values: kd + kr + kt <= 1.0.

kd=diffuse factor, kr=reflection factor and kt is the transmission factor (refraction).

We usually only specify the kr and kt factors - as the kd can be calculated auomatically using: kd = 1.0 − kr − kt.


Resources & Links


Ray-Tracing Example (Code/Demo) [Version 0]

Ray-Tracing Example (Code/Demo) [Version 2]

Mixing Color with SDF Distance

Ray-Marching Example with Color SDF Functions [Version 3]

Taste of 3D using Ray-Marching























WebGPU Development Pixels - coding fragment shaders from post processing to ray tracing! 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 Development Cookbook - coding recipes for all your webgpu needs! 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 Ray-Tracing with WebGPU 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.