www.xbdev.net
xbdev - software development
Friday May 8, 2026
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 sdfSphere( testPoint:vec3<f32>, spherePos:vec3<f32>, sphereRadius:f32) -> f32 {
    return length(spherePos - testPoint) - sphereRadius;
}

@fragment
fn main(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> 
{
    let nuv = ( uv * 2.0 - 1.0 );
    let p = vec3<f32>( nuv, 0.0 );
    
    if ( sdfSphere( p , vec3(0.0, 0.0, 0.0), 0.5 ) < 0.0 )
    {
        return vec4<f32>(1, 0, 0, 1); // red
    }
    return vec4<f32>(0, 0, 1, 1); // 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(value: f32) -> vec3<f32> {
    // scale the values to emphasis colors around the 0.0
    var v = pow( abs(value) ,0.2) * sign( value );
    
    // Clamp the input value between -1 and 1
    v = clamp(v, -1.0, 1.0);

    // Define color stops
    let red = vec3<f32>(1.0, 0.0, 0.0);      // -1.0
    let orange = vec3<f32>(1.0, 0.5, 0.0);   // -0.5
    let white = vec3<f32>(1.0, 1.0, 1.0);    //  0.0
    let green = vec3<f32>(0.0, 1.0, 0.0);    //  0.5
    let blue = vec3<f32>(0.0, 0.0, 1.0);     //  1.0

    // Calculate the color based on value ranges
    if v < -0.5 {
        // Blend from red (-1.0) to orange (-0.5)
        return mix(red, orange, (v + 1.0) / 0.5);
    } else if v < 0.0 {
        // Blend from orange (-0.5) to white (0.0)
        return mix(orange, white, (v + 0.5) / 0.5);
    } else if v < 0.5 {
        // Blend from white (0.0) to green (0.5)
        return mix(white, green, v / 0.5);
    } else {
        // Blend from green (0.5) to blue (1.0)
        return mix(green, blue, (v - 0.5) / 0.5);
    }
}

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

fn sdfScene( testPoint:vec3<f32> ) -> f32
{
    let rr = randomsmooth( testPoint.xy * 2.0 + mytimer );
    
    let d0 = sdfCube( testPoint, vec3(0.0, 0.0, 0.0), vec3(0.3) );
    
    let d1 = sdfSphere( testPoint, vec3(0.0, 0.3, 0.0), 0.2 );
   
    var d = sdfUnion( d0, d1 );

    return d;
}


@fragment
fn main(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> 
{
    let nuv = uv * 2.0 - 1.0;
 
    let point = vec3( nuv, 0.0 );
    
    let d = sdfScene( point );
    
    var color = colorGradient( d );
}



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 sdfSphere( testPoint:vec3<f32>, spherePos:vec3<f32>, sphereRadius:f32) -> f32 {
    return length(spherePos - testPoint) - sphereRadius;
}

fn sdfScene( testPoint:vec3<f32> ) -> f32
{
    let d = sdfSphere( testPoint, vec3(0.0), 0.2 );
    return d;
}

@fragment
fn main(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> 
{
    let nuv = uv * 2.0 - 1.0;
 
    let point = vec3( nuv, 0.0 );
    
    let d = sdfScene( point );
    
    var color = vec3( abs(sin(d*50.0)) );
    
    if ( abs(d) < 0.01 )
    {
        color = vec3<f32>(1.0, 0.0, 0.0);
    }
    
      return vec4<f32>(color.xyz, 1.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(0) uv : vec2<f32>) -> @location(0) vec4<f32> 
{
    let nuv = uv * 2.0 - 1.0;
 
    let point = vec3( nuv, 0.0 );
    
    let d = sdfScene( point );
    
    var color = vec3( abs(sin(d*50.0)) );
    
    // if inside the shape - mix inside color
    if ( abs(d) < 0.005 )
    {
        color = vec3<f32>(0.0, 0.8, 0.0);
    }
    
    // if on the surface - draw solid line
    if ( d < 0.01 )
    {
         color *= vec3<f32>(1.1, 1.0, 0.0);   
    }
    
      return vec4<f32>(color.xyz, 1.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:f32, b:f32) -> f32 {
    return min(a, b);
}


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

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

fn sdfCube( testPoint:vec3<f32>,  cubePos:vec3<f32>,  cubeDim:vec3<f32> ) ->f32
{
   var d:vec3<f32> = abs(cubePos - testPoint) - cubeDim;
   return min(max(d.x, max(d.y, d.z)), 0.0)
           + length( max(d, vec3<f32>(0.0) ) );
}

fn sdfCylinder(testPoint: vec3<f32>, cylinderPos: vec3<f32>, cylinderRadius: f32, cylinderHeight: f32) -> f32 {
    let d = vec2<f32>(
        length(vec2<f32>(testPoint.x - cylinderPos.x, testPoint.z - cylinderPos.z)) - cylinderRadius,
        abs(testPoint.y - cylinderPos.y) - cylinderHeight * 0.5
    );
    return min(max(d.x, d.y), 0.0) + length(max(d, vec2<f32>(0.0)));
}

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

fn sdfPlane(testPoint: vec3<f32>, planeNormal: vec3<f32>, planeDist: f32) -> f32 {
    return dot(testPoint, normalize(planeNormal)) + planeDist;
}

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

fn sdfCapsule(testPoint: vec3<f32>, capsuleA: vec3<f32>, capsuleB: vec3<f32>, capsuleRadius: f32) -> f32 {
    let pa = testPoint - capsuleA;
    let ba = capsuleB - capsuleA;
    let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h) - capsuleRadius;
}

fn sdfCone(testPoint: vec3<f32>, coneTip: vec3<f32>, coneDir: vec3<f32>, coneAngle: f32) -> f32 {
    let q = testPoint - coneTip;
    let cosTheta = cos(coneAngle);
    let sinTheta = sin(coneAngle);
    let d = vec2<f32>(
        length(q - coneDir * dot(q, coneDir)),
        abs(dot(q, coneDir))
    );
    return max(d.x * cosTheta - d.y * sinTheta, -dot(q, coneDir));
}


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 sdfScene( testPoint:vec3<f32> ) -> f32
{
    let d0 = sdfCube( testPoint, vec3(0.0, 0.5, 0.0), vec3(0.2) );
    
    let d1 = sdfSphere( testPoint, vec3(0.0, -0.5, 0.0), 0.2 );

    let d2 = sdfTorus( testPoint, vec3(0.5, 0.0, 0.0), vec2(0.2, 0.1) );
        
    // Add the cube, sphere and torus together
    var d = sdfUnion( d0, d1 );
    d = sdfUnion( d, d2 );
    
    return d;
}

@fragment
fn main(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> 
{
    let nuv = uv * 2.0 - 1.0;
 
    let point = vec3( nuv, 0.0 );
    
    let d = sdfScene( point );
    
    var color = vec3( abs(sin(d*50.0)) );
    
    if ( abs(d) < 0.005 )
    {
        color = vec3<f32>(0.0, 0.8, 0.0);
    }
    // if inside - use red rings for the gradients
    if ( d < 0.01 )
    {
         color *= vec3<f32>(1.1, 1.0, 0.0);   
    }
    
      return vec4<f32>(color.xyz, 1.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:f32, b: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:f32, b:f32) -> f32 {
    return max(a, b);
}



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(a: f32, b: f32) -> f32 {
    return max(min(a, b), 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:f32, d2:f32, k:f32/*=1.0*/) -> f32 {
  let h = max(k - abs(d1 - d2), 0.0) / k;
  return min(d1, d2) - h * h * k * 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:f32, d2:f32, k:f32/*=1.0*/) -> f32 {
  let h = max(k - abs(d1 - d2), 0.0) / k;
  return max(d1, -d2) - h * h * k * 0.25;
}



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


fn smoothintersection(d1:f32, d2:f32, k:f32/*=1.0*/) -> f32 {
  let h = max(k - abs(d1 - d2), 0.0) / k;
  return max(d1, d2) + h * h * k * 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(uv: vec2<f32>) -> f32 {
    return fract(sin(dot(uv, vec2<f32>(12.9898, 78.233))) * 43758.5453);
}

fn randomsmooth( st:vec2<f32> ) -> f32 
{
    var i = floor( st * 3.0 ); // uv - 0,   1,   2,   3, 
    var f = fract( st * 3.0 ); // uv - 0-1, 0-1, 0-1, 0-1

    // Four corners in 2D of a tile
    var a = random(i);
    var b = random(i + vec2<f32>(1.0, 0.0));
    var c = random(i + vec2<f32>(0.0, 1.0));
    var d = random(i + vec2<f32>(1.0, 1.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;
    
    f = 3*f*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 = mix( a, b, f.x );
    var x2 = mix( c, d, f.x );
    
    var y1 = mix( x1, x2, f.y );
    
    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 sdfScene( testPoint:vec3<f32> ) -> f32
{
    let rr = randomsmooth( testPoint.xy * 2.0 + mytimer );
    
    let d0 = sdfCube( testPoint, vec3(0.0, 0.0, 0.0), vec3(0.3) );
    
    let d1 = sdfSphere( testPoint, vec3(0.0, 0.3, 0.0), 0.2 + rr*0.2 );
   
    var d = sdfUnion( d0, d1 );

    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(testPoint: vec3<f32>, p0: vec3<f32>, p1: vec3<f32>, p2: vec3<f32>, radius: f32) -> 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; i <= maxSteps; i += 1) {
        let t = f32(i) / f32(maxSteps);
        let curvePoint = mix(mix(p0, p1, t), mix(p1, p2, t), t); // Quadratic Bézier interpolation
        let distToCurve = length(testPoint - curvePoint);
        minDist = min(minDist, distToCurve);
    }

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

fn sdfScene( testPoint:vec3<f32> ) -> f32
{
    let d = sdfBezierQuadratic(testPoint, 
                                vec3<f32>(0,0,0),
                                vec3<f32>(-0.9, 0.9, 0),
                                vec3<f32>(0.7, 0.5, 0),
                                0.1);
    
    return d;
}



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


fn sdfBezierCubic(testPoint: vec3<f32>, p0: vec3<f32>, p1: vec3<f32>, p2: vec3<f32>, p3: vec3<f32>, radius: f32) -> 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; i <= maxSteps; i += 1) {
        let t = f32(i) / f32(maxSteps);
        let a = mix(p0, p1, t);
        let b = mix(p1, p2, t);
        let c = mix(p2, p3, t);
        let d = mix(a, b, t);
        let curvePoint = mix(d, c, t); // Cubic Bézier interpolation
        let distToCurve = length(testPoint - curvePoint);
        minDist = min(minDist, distToCurve);
    }

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

fn sdfScene( testPoint:vec3<f32> ) -> f32
{
    let d = sdfBezierCubic(testPoint, 
                                vec3<f32>(-0.5,  0.5, 0),
                                vec3<f32>(-0.5, -0.5, 0),
                                vec3<f32>( 0.5, -0.5, 0),
                                vec3<f32>( 0.5,  0.5, 0),
                                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 sdfScene( testPoint:vec3<f32> ) -> f32
{
    let d0 = sdfCube( testPoint, vec3(0.0,-0.3, 0.0), vec3(0.4) );
    
    // noise
    var rr = randomsmooth( testPoint.xy * 2.0 + mytimer );
    
    // clamp the noise to 0 to 1 - outside 1.0 noise rr is clamped to 0.0
    var dist1 = length( vec3(0.0, 0.3, 0.0) - testPoint );
    dist1 = clamp(dist1, 0.0, 1.0); 
    rr = mix( rr, 0.0, dist1 );
    
    let d1 = sdfSphere( testPoint, vec3(0.0, 0.3, 0.0), 0.3 + rr*0.3 );
   
    var d = sdfUnion( d0, d1 );

    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(pos: vec3<f32>) -> f32 {
    // Repeat space to simulate fractal structure
    var p = pos;
    let scale = 2.0;

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

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

    // Combine distance and wave pattern, ensuring negative values are possible
    let pp = mix( pos * wave, pos, clamp(length(pos)*0.7, 0.0, 1.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(pos: vec3<f32>) -> f32 {
    let scale: f32 = 1.5;            // Scale factor
    let bailout: f32 = 2.0;          // Escape radius
    let iterations: i32 = 10;        // Number of iterations
    let fixedRadius: f32 = 1.0;      // Fixed radius for folding
    let minRadius2: f32 = fixedRadius * fixedRadius;
    let maxRadius2: f32 = bailout * bailout;

    var z: vec3<f32> = pos;
    var dz: f32 = 1.0;               // Distance estimator

    for (var i: i32 = 0; i < iterations; i = i + 1) {
        if (dot(z, z) > maxRadius2) {
            break;
        }

        // Apply sphere fold
        let r2: f32 = dot(z, z);
        if (r2 < minRadius2) {
            let factor = maxRadius2 / minRadius2;
            z *= factor;
            dz *= factor;
        } else if (r2 < maxRadius2) {
            let factor = maxRadius2 / r2;
            z *= factor;
            dz *= factor;
        }

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

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

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

fn sdfScene( testPoint:vec3<f32> ) -> f32
{
    var d = mandelboxSDF( testPoint );
    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.

<?php
p = abs(p) - vec3<f32>(1.0);       // Fold operation
p = (p % 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 sdfScene( testPoint:vec3<f32> ) -> f32
{
    // Apply box fold
    let foldedP = abs(p) - vec3<f32>(1.0);

    // Sphere SDF after fold
    let d = sphereSDF(foldedP, vec3(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 sdfScene( testPoint:vec3<f32> ) -> f32
{
    var p = testPoint;

    let sepdist = 1.0 + abs( sin(mytimer*4.0) ); // animate the spheres moving appart and together
    
    p = vec3<f32>( sign(p.x) * (p.x % sepdist) - sepdist/2, p.y, p.z ); // repeat in the x only
    
    var d = sdfSphere(p, vec3(0.0, 0.0, 0.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(p: vec2<f32>, petals: f32, radius: f32, petalSharpness: f32) -> f32 {
    // Convert position to polar coordinates
    let r = length(p);
    let theta = atan2(p.y, p.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 r - petalRadius;
}

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

fn sdfScene( testPoint:vec3<f32> ) -> f32
{
    var d = flowerSDF( testPoint.xy, 5.0, 1.0, 0.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(p: vec3<f32>, petals: f32, radius: f32, petalSharpness: f32) -> f32 {
    let flowerDist = flowerSDF(p.xy, petals, radius, petalSharpness); // 2D flower in x-y plane
    let zDist = abs(p.z) - 0.1; // Thickness along z-axis
    return max(flowerDist, zDist); // 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 sdfScene( testPoint:vec3<f32> ) -> f32
{
    let d0 = sdfSphere( testPoint, vec3(0.0, 0.0, 0.0), 0.6 );
    
    let d1 = sdfSphere( testPoint, vec3(1.4, -1.0, 0.0), 0.6 );
    
    let d = sdfUnion( d0, d1 );
    
    return d;
}

fn raymarch(ro: vec3<f32>, rd: vec3<f32>, maxSteps: i32, maxDist: f32, inside: f32) -> f32 {
    var t = 0.0; // Distance traveled along the ray
    var epsilon: f32 = 0.001;
    for (var i = 0; i < maxSteps; i = i + 1) {
        let p = ro + rd * t; // Current position along the ray
        let d = sdfScene(p) * inside; // Distance to the scene's nearest surface

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

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

        t += 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(uv: vec2<f32>, startpoint: vec3<f32>, endpoint: vec3<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.x, uv.y, startpoint.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(toPixel, lineDir)) / length(lineDir);

    // Compute the projection of the point onto the line
    let projection = dot(toPixel, lineDir) / 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(p: vec3<f32>) -> vec3<f32> {
    let epsilon = 0.001; // Small step for numerical differentiation
    let dx = vec3<f32>(epsilon, 0.0, 0.0);
    let dy = vec3<f32>(0.0, epsilon, 0.0);
    let dz = vec3<f32>(0.0, 0.0, epsilon);

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

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

struct RayStack {
    ro: vec3<f32>,
    rd: vec3<f32>
};

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

    var color = vec3( abs(sin(d*30.0)) );

    if ( abs(d) < 0.01 )
    {
        color = vec3<f32>(0.0, 0.8, 0.0);
    }
    // if inside - use red rings for the gradients
    if ( d < 0.01 )
    {
         color *= vec3<f32>(1.1, 1.0, 0.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< RayStack, 32 >;
    
    for (var n:i32=-4; n<=4; n++)
    {
    let dy = ( 0.1 + sin(mytimer*10.0)*0.05 )*f32(n);
    
    rayStackCount = 0;
    rayStack[ rayStackCount ].ro = vec3<f32>(-2.0, 0.0, 0.0);
    rayStack[ rayStackCount ].rd = normalize( vec3<f32>( 1.0, dy, 0.0) ) ;
    rayStackCount++;
    
    
    for (var i:i32=0; i<3; i++)
    {
        if ( rayStackCount <= 0 ) { break; }
        
        // pop it off the stack
        rayStackCount--;
        let ro = rayStack[ rayStackCount ].ro;
        let rd = rayStack[ rayStackCount ].rd;
        
        let hitDist = raymarch( ro, rd, 32, 4.0, 1.0 );
        if ( hitDist > 0.0 ) 
        {
            let hitpoint = ro + rd * hitDist;
            if (drawLine(nuv, ro, hitpoint)) 
            {
                color = vec3<f32>(1.0, 0.0, 0.0); // Red
            }

            let normal = sdfNormal( hitpoint );
            if (drawLine(nuv, hitpoint, hitpoint+normal*0.25 )) 
            {
                color = vec3<f32>(0.0, 0.0, 1.0); 
            }
            
            let reflectdir = reflect( rd, normal );
            {
                rayStack[ rayStackCount ].ro = hitpoint + normal*0.01;
                rayStack[ rayStackCount ].rd = reflectdir;
                rayStackCount++;
            }
            
            let refractdir = refract( rd, normal, 1.0/1.2 ); // refract in
            {
                let ro2 = hitpoint - normal*0.01;
                let rd2 = refractdir;
                 let hitDist2 = raymarch( ro2, rd2, 32, 4.0, -1.0 );
                if ( hitDist2 > 0.0 )
                {
                    let hitpoint2 = ro2 + rd2*hitDist2;
                    if (drawLine(nuv, ro2, hitpoint2))
                    {
                        color = vec3<f32>(0.5, 0.5, 1.0);
                    }
                    
                    let normal2 = sdfNormal( hitpoint2 );
                      let rd3 = refract( rd2, -normal2, 1.2/1.0 ); // refract out
                      let ro3 = hitpoint2 + normal2*0.01;
                    
                    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;
                    rayStackCount++;
                }
            }
        }
        else // no hit
        {
            if (drawLine(nuv, ro, ro+rd*2.0 )) 
            {
                color = vec3<f32>(0.3, 0.0, 0.0); 
            }
        }
    }// end for i
   
    }// end for n

      return vec4<f32>(color.xyz, 1.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.


<?php
struct Material {
    color: vec3<f32> // just a basic color
};

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

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

// SDF functions updated to return SDFResult
fn sdfCube(testPoint: vec3<f32>, cubePos: vec3<f32>, cubeDim: vec3<f32>, material: Material) -> SDFResult {
    var d: vec3<f32> = abs(cubePos - testPoint) - cubeDim;
    let distance = min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, vec3<f32>(0.0)));
    return SDFResult(distance, material);
}


fn sdfCylinder(testPoint: vec3<f32>, cylinderPos: vec3<f32>, cylinderRadius: f32, cylinderHeight: f32, material: Material) -> SDFResult {
    let d = vec2<f32>(
        length(vec2<f32>(testPoint.x - cylinderPos.x, testPoint.z - cylinderPos.z)) - cylinderRadius,
        abs(testPoint.y - cylinderPos.y) - cylinderHeight * 0.5
    );
    let distance = min(max(d.x, d.y), 0.0) + length(max(d, vec2<f32>(0.0)));
    return SDFResult(distance, material);
}

fn sdfTorus(testPoint: vec3<f32>, torusPos: vec3<f32>, torusRadius: vec2<f32>, material: Material) -> SDFResult {
    let q = vec2<f32>(
        length(vec2<f32>(testPoint.x - torusPos.x, testPoint.y - torusPos.y)) - torusRadius.x,
        testPoint.z - torusPos.z
    );
    let distance = length(q) - torusRadius.y;
    return SDFResult(distance, material);
}

fn sdfPlane(testPoint: vec3<f32>, planeNormal: vec3<f32>, planeDist: f32, material: Material) -> SDFResult {
    let distance = dot(testPoint, normalize(planeNormal)) + planeDist;
    return SDFResult(distance, material);
}

fn sdfEllipsoid(testPoint: vec3<f32>, ellipsoidPos: vec3<f32>, ellipsoidRadii: vec3<f32>, material: Material) -> SDFResult {
    let q = (testPoint - ellipsoidPos) / ellipsoidRadii;
    let distance = length(q) - 1.0;
    return SDFResult(distance, material);
}

fn sdfCapsule(testPoint: vec3<f32>, capsuleA: vec3<f32>, capsuleB: vec3<f32>, capsuleRadius: f32, material: Material) -> SDFResult {
    let pa = testPoint - capsuleA;
    let ba = capsuleB - capsuleA;
    let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    let distance = length(pa - ba * h) - capsuleRadius;
    return SDFResult(distance, material);
}

fn sdfCone(testPoint: vec3<f32>, coneTip: vec3<f32>, coneDir: vec3<f32>, coneAngle: f32, material: Material) -> SDFResult {
    let q = testPoint - coneTip;
    let cosTheta = cos(coneAngle);
    let sinTheta = sin(coneAngle);
    let d = vec2<f32>(
        length(q - coneDir * dot(q, coneDir)),
        abs(dot(q, coneDir))
    );
    let distance = max(d.x * cosTheta - d.y * sinTheta, -dot(q, coneDir));
    return SDFResult(distance, material);
}

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

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

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

fn sdfScene( testPoint:vec3<f32> ) -> SDFResult
{
    let red    = Material( vec3(1.0, 0.0, 0.0) );
    let green  = Material( vec3(0.0, 1.0, 0.0) );
    let blue   = Material( vec3(0.0, 0.0, 1.0) );
    let color0 = Material( vec3(0.0, 1.0, 1.0) );
    
    let d0 = sdfSphere( testPoint, vec3(0.0,  0.0, 0.0),  0.6, red );
    let d1 = sdfSphere( testPoint, vec3(1.4, -1.0, 0.0),  0.6, green );
    let d2 = sdfCube( testPoint, vec3(0.0,-0.3, 0.0), vec3(0.4), blue );
    let d3 = sdfTorus( testPoint, vec3(1.0,0.8, 0.0), vec2(0.4, 0.1), color0 );
    
    var d = sdfUnion( d0, d1 );
    d = sdfUnion( d, d2 );
    d = sdfUnion( d, d3 );

    return d;
}

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

    var color = vec3( abs(sin(d*30.0)) );

    if ( abs(d) < 0.02 )
    {
        color = vec3<f32>(0.0, 0.8, 0.0);
    }
    // if inside - use red rings for the gradients
    if ( d < 0.0 )
    {
         color *= c;//color *= vec3<f32>(1.1, 1.0, 0.0);   
    }
    return vec4<f32>(color, 1.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(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let zoom = 2.0;
    let nuv = (uv * 2.0 - 1.0) * zoom;

    let point = vec3(nuv, 0.0);
    let sdfResult = sdfScene(point);

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

    if (abs(sdfResult.distance) < 0.01) {
        color = vec3<f32>(0.0, 0.8, 0.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 rayStackCount: i32 = 0;
    var rayStack: array<RayStack, 32>;

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

        rayStackCount = 0;
        rayStack[rayStackCount].ro = vec3<f32>(-2.0, 0.0, 0.0);
        rayStack[rayStackCount].rd = normalize(vec3<f32>(1.0, dy, 0.0));
        rayStackCount++;

        for (var i: i32 = 0; i < 3; i++) {
            if (rayStackCount <= 0) {
                break;
            }

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

            let hitResult = raymarch(ro, rd, 32, 4.0, 1.0);
            if (hitResult.distance > 0.0) {
                let hitpoint = ro + rd * hitResult.distance;
                if (drawLine(nuv, ro, hitpoint)) {
                    //color = vec3<f32>(1.0, 0.0, 0.0); // Red
                    color = hitResult.material.color;
                }

                let normal = sdfNormal(hitpoint);
                if (drawLine(nuv, hitpoint, hitpoint + normal * 0.25)) {
                    color = vec3<f32>(0.0, 0.0, 1.0);
                }

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

                let refractdir = refract(rd, normal, ior); // Refract in
                {
                    let ro2 = hitpoint - normal * 0.01;
                    let rd2 = refractdir;
                    let hitResult2 = raymarch(ro2, rd2, 32, 4.0, -1.0);
                    if (hitResult2.distance > 0.0) {
                        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, -normal2, 1.0/ior); // Refract out
                        let ro3 = hitpoint2 + normal2 * 0.01;

                        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;
                        rayStackCount++;
                    }
                }
            } else { // No hit
                if (drawLine(nuv, ro, ro + rd * 2.0)) {
                    color = vec3<f32>(0.3, 0.0, 0.0);
                }
            }
        } // End for i
    } // End for n

    return vec4<f32>(color.xyz, 1.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(0) uv: vec2<f32>) -> @location(0) vec4<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), 0, 3.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 rayStackCount: i32 = 0;
    var rayStack: array<RayStack, 32>;
    
    rayStackCount = 0;
    rayStack[rayStackCount].ro = cam.ro;
    rayStack[rayStackCount].rd = cam.rd;
    rayStack[rayStackCount].k  = 1.0; // 100% from screen
    rayStackCount++;

    for (var i: i32 = 0; i < 32; i++) {
        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(ro, rd, 128, 10.0, 1.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.0, 5.5, 2.0);
                let lightDir = normalize( hitpoint - lightPos );
                let lightOrigin = hitpoint + normal * 0.02;
                let hitShadow = raymarch(lightOrigin, lightDir, 64, 8.0, 1.0);

                // Check for shadows
                var isInShadow = false;
                if ( hitShadow.distance > 0.0) {
                    isInShadow = abs( hitShadow.distance - length(hitpoint - lightPos) ) > 0.2;
                }
            
                let diffuse = clamp(dot(normal, lightDir), 0.0, 1.0) * hitResult.material.color;

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

            // 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 - totalReflectionRefraction, 0.0, 1.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(rd, normal);
            //var reflectionColor = vec3<f32>(0.0, 0.0, 0.0);
            if ( kr > 0.0 && k > 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(rd, normal*3.0, ior); // 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 = normalize( reflectdir );
                //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 && k > 0.0 ) // Refraction contribution
            {
                let ro2 = hitpoint - normal * 0.03;
                let rd2 = refractdir;
                let hitResult2 = raymarch(ro2, rd2, 128, 10.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, -normal2, 1.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.2, 0.5, 0.6) + vec3(-rd.y*1.5)) * k; // Dark gray background
        }
    } // End for i
    
    return vec4<f32>(color, 1.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























101 WebGPU Programming Projects. 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 wgsl compute graphics all in one 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 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-2026 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.