www.xbdev.net
xbdev - software development
Tuesday April 1, 2025
Home | Contact | Support | Fractals Natures pattern... | Fractals Natures pattern...
>>
     
 

Fractals

Natures pattern...

 


Construct geometry and repeating patterns (fractals) using signed distance functions (sdf).
Construct geometry and repeating patterns (fractals) using signed distance functions (sdf).


Fractals > Signed Distance Function (SDF) Fractals


Lots of things around SDF functions and fractals, there are the key things we'll talk about about here:

1. What are SDF Functions?
2. Simple SDF (WebGL/GLSF) Example (Sphere)
3. 'Flat' Fractal (2D)
4. 3D Shapes (Solid Sphere, Hollow-Sphere, Shell, ...)
5. Adding function (create more complex shapes)
6. Multiple shapes (hard way vs smart way)
7. Multiple operations to build fractals
8. Scene full of fractals
9. Limitations, challenges (soft fractals without details)

What are 'Signed Distance Functions' (SDFs)?


A signed distance function is a mathematical concept used in computer graphics to describe the distance from a point in space to the closest surface of an object, with a positive or negative sign indicating whether the point is inside or outside the object.

Takes a 'point' (usually 3d point) and returns a single number (usually a float).

For example, in 3D sphere, the signed distance function at a point (x, y, z) would be the radius of the sphere minus the distance from (x, y, z) to the center if the point is inside the sphere, and negative if it's outside.

This function is valuable in rendering techniques like ray-marching, where it helps determine intersections with objects in a scene efficiently.

SDF Examples (Spheres)


(1SDF(p) = max(0.0length(p)-R)  // solid sphere, zero interior
(2SDF(p) = length(p)-R            // solid sphere, negative interior
(3SDF(p) = abs(length(p)-R)       // hollow sphere shell 


The SDF function requires a single parameter for the position that it's calculating the distance so the surface - other arguments can be passed as well - such as the radius, ..


SDF WebGL/GLSL for 'Circle'


The following provides a starting point - as the examples will work with WebGL - since it's easy to write and edit interactive fragment shaders for the SDF functions (which will run in a web-browser).

The starting point isn't very exciting - but gives you your first 'sdf' function.


Output for the 2-dimensional circle SDF function. Generates a circle using
Output for the 2-dimensional circle SDF function. Generates a circle using 'sdf' function - each pixel checks the sdf function to see if the point is inside or outside of the shape.


<!DOCTYPE html>
<
html lang="en">
<
head>
    <
meta charset="UTF-8">
    <
meta name="viewport" content="width=device-width, initial-scale=1.0">
    <
title>Simple Circle SDF</title>
    <
style>
        
body margin0; }
        
canvas displayblock; }
    </
style>
</
head>
<
body>
<
canvas id="canvas" width='256' height='256'></canvas>

<
script>
    const 
vertexShaderSource = `#version 100
        attribute vec2 a_position;
        void main() {
            gl_Position = vec4(a_position.xy, 0.0, 1.0);
        }
    
`;

    const 
fragmentShaderSource = `#version 100
        precision mediump float;

        uniform vec2 u_resolution;

        float circleSDF(vec2 p, float radius) {
            return length(p) - radius;
        }

        void main() {
            vec2 uv = gl_FragCoord.xy / u_resolution.xy;
            uv = uv * 2.0 - 1.0; // Map uv coordinates to range [-1, 1]

            float radius = 0.5; // Circle radius
            float sdf = circleSDF(uv, radius);
            
            if (sdf < 0.0) {
                gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Inside the circle, color red
            } else {
                gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // Outside the circle, color black
            }
        }
    
`;

    function 
createShader(gltypesource) {
        const 
shader gl.createShader(type);
        
gl.shaderSource(shadersource);
        
gl.compileShader(shader);
      
        
let status gl.getShaderParameter(shadergl.COMPILE_STATUS);
          
console.log('status:'status);
      

        if (!
status) {
            const 
shaderErr gl.getShaderInfoLog(shader);
            
console.log('compile error shaderErr:'shaderErr );
            
gl.deleteShader(shader);
            return 
null;
        }
        return 
shader;
    }

    function 
createProgram(glvertexShaderfragmentShader) {
        const 
program gl.createProgram();
        
gl.attachShader(programvertexShader);
        
gl.attachShader(programfragmentShader);
        
gl.linkProgram(program);
        if (!
gl.getProgramParameter(programgl.LINK_STATUS)) {
            
console.error('Unable to initialize the shader program: ' gl.getProgramInfoLog(program));
            return 
null;
        }
        return 
program;
    }

    function 
main() {
        const 
canvas document.getElementById('canvas');
        const 
gl canvas.getContext('webgl');

        if (!
gl) {
            
console.error('Unable to initialize WebGL. Your browser may not support it.');
            return;
        }

        
// Compile vertex and fragment shaders
        
const vertexShader createShader(glgl.VERTEX_SHADERvertexShaderSource);
        const 
fragmentShader createShader(glgl.FRAGMENT_SHADERfragmentShaderSource);

        
// Create shader program
        
const program createProgram(glvertexShaderfragmentShader);
        
gl.useProgram(program);

        
// Set up vertex buffer
        
const positionBuffer gl.createBuffer();
        
gl.bindBuffer(gl.ARRAY_BUFFERpositionBuffer);
        const 
positions = [
            -
1.0, -1.0,
             
1.0, -1.0,
            -
1.0,  1.0,
            -
1.0,  1.0,
             
1.0, -1.0,
             
1.0,  1.0,
        ];
        
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
        const 
positionAttributeLocation gl.getAttribLocation(program'a_position');
        
gl.enableVertexAttribArray(positionAttributeLocation);
        
gl.vertexAttribPointer(positionAttributeLocation2gl.FLOATfalse00);

        
// Set resolution uniform
        
const resolutionUniformLocation gl.getUniformLocation(program'u_resolution');
        
gl.uniform2f(resolutionUniformLocationcanvas.widthcanvas.height);

        
// Clear canvas
        
gl.clearColor(0.00.00.01.0);
        
gl.clear(gl.COLOR_BUFFER_BIT);

        
// Draw
        
gl.drawArrays(gl.TRIANGLES06);
    }

    
main();
</
script>
</
body>
</
html>


Extending the Example (Sphere to Mandelbrot Set Fractal)



Output for the 2-dimensional Mandelbrot Set SDF function .
Output for the 2-dimensional Mandelbrot Set SDF function .


<html lang="en">
<
head>
    <
meta charset="UTF-8">
    <
meta name="viewport" content="width=device-width, initial-scale=1.0">
    <
title>Mandelbrot Set Fractal</title>
    <
style>
        
body margin0; }
        
canvas displayblock; }
    </
style>
</
head>
<
body>
    <
canvas id="canvas" width='500' height='500'></canvas>

    <
script>
        const 
vertexShaderSource = `
            attribute vec2 a_position;
            void main() {
                gl_Position = vec4(a_position, 0.0, 1.0);
            }
        
`;

        const 
fragmentShaderSource = `
            precision mediump float;

            uniform vec2 u_resolution;

            void main() {
                vec2 uv = gl_FragCoord.xy / u_resolution.xy;
                
                // Map the range to the complex plane (-2, -1) to (1, 1)
                float aspectRatio = u_resolution.x / u_resolution.y;
                vec2 c = vec2((uv.x - 0.5) * 3.0 * aspectRatio, uv.y - 0.5 * 2.0);
                
                vec2 z = vec2(0.0);
                const int MAX_ITERATIONS = 30;
                int iterations = 0;

                for (int i = 0; i < MAX_ITERATIONS; i++) {
                    float x = z.x;
                    float y = z.y;

                    // Mandelbrot iteration formula: z = z^2 + c
                    float xtemp = x * x - y * y + c.x;
                    y = 2.0 * x * y + c.y;
                    x = xtemp;

                    z = vec2(x, y);

                    // Escape condition: |z| > 2
                    if (length(z) > 2.0) {
                        break;
                    }
                    
                    iterations++;
                }

                // Smooth coloring based on the number of iterations
                float smoothedIterations = float(iterations) + 1.0 - log(log(length(z))) / log(2.0);
                float colorValue = smoothedIterations / float(MAX_ITERATIONS);

                // Set color: inside of the Mandelbrot set is red
                if (iterations == MAX_ITERATIONS) {
                    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Red color for inside of the set
                } else {
                    gl_FragColor = vec4(colorValue, colorValue, colorValue, 1.0); // Grayscale color for outside of the set
                }

                // Set color
                //gl_FragColor = vec4(colorValue, colorValue, colorValue, 1.0);
            }
        
`;

        function 
createShader(gltypesource) {
            const 
shader gl.createShader(type);
            
gl.shaderSource(shadersource);
            
gl.compileShader(shader);
            if (!
gl.getShaderParameter(shadergl.COMPILE_STATUS)) {
                
console.error('An error occurred compiling the shaders: ' gl.getShaderInfoLog(shader));
                
gl.deleteShader(shader);
                return 
null;
            }
            return 
shader;
        }

        function 
createProgram(glvertexShaderfragmentShader) {
            const 
program gl.createProgram();
            
gl.attachShader(programvertexShader);
            
gl.attachShader(programfragmentShader);
            
gl.linkProgram(program);
            if (!
gl.getProgramParameter(programgl.LINK_STATUS)) {
                
console.error('Unable to initialize the shader program: ' gl.getProgramInfoLog(program));
                return 
null;
            }
            return 
program;
        }

        function 
main() {
            const 
canvas document.getElementById('canvas');
            const 
gl canvas.getContext('webgl');
            if (!
gl) {
                
console.error('Unable to initialize WebGL. Your browser may not support it.');
                return;
            }

            
// Compile vertex and fragment shaders
            
const vertexShader createShader(glgl.VERTEX_SHADERvertexShaderSource);
            const 
fragmentShader createShader(glgl.FRAGMENT_SHADERfragmentShaderSource);

            
// Create shader program
            
const program createProgram(glvertexShaderfragmentShader);
            
gl.useProgram(program);

            
// Set up vertex buffer
            
const positionBuffer gl.createBuffer();
            
gl.bindBuffer(gl.ARRAY_BUFFERpositionBuffer);
            const 
positions = [
                -
1.0, -1.0,
                 
1.0, -1.0,
                -
1.0,  1.0,
                -
1.0,  1.0,
                 
1.0, -1.0,
                 
1.0,  1.0,
            ];
            
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
            const 
positionAttributeLocation gl.getAttribLocation(program'a_position');
            
gl.enableVertexAttribArray(positionAttributeLocation);
            
gl.vertexAttribPointer(positionAttributeLocation2gl.FLOATfalse00);

            
// Set resolution uniform
            
const resolutionUniformLocation gl.getUniformLocation(program'u_resolution');
            
gl.uniform2f(resolutionUniformLocationcanvas.widthcanvas.height);

            
// Clear canvas
            
gl.clearColor(0.00.00.01.0);
            
gl.clear(gl.COLOR_BUFFER_BIT);

            
// Draw
            
gl.drawArrays(gl.TRIANGLES06);
        }

        
main();
    </
script>
</
body>
</
html>



SDF for a Sphere (3D)



<html lang="en">
<
head>
    <
meta charset="UTF-8">
    <
meta name="viewport" content="width=device-width, initial-scale=1.0">
    <
title>Simple Sphere SDF</title>
    <
style>
        
body margin0; }
        
canvas displayblock; }
    </
style>
</
head>
<
body>
    <
canvas id="canvas" width='500' height='500'></canvas>

    <
script>
        const 
vertexShaderSource = `
            attribute vec3 a_position;
            varying vec3 v_position;
            void main() {
                gl_Position = vec4(a_position, 1.0);
                v_position = a_position;
            }
        
`;

        const 
fragmentShaderSource = `
            precision highp float;

            uniform vec2 u_resolution;

            varying vec3 v_position;

            float sdfSphere( vec3 testPoint, vec3 spherePos, float sphereRadius) {
                return length(spherePos - testPoint) - sphereRadius;
            }

            float rayCast( vec3 position ) {
               float t = 0.0; // far distance
               t = sdfSphere(position, vec3(0.0,0,0), 1.0 );
               return t;
            }

            float rayTrace( vec3 rayPos, vec3 rayDir) {
               float t = 0.0;
               for (int i = 0; i < 200 ; i++)
               {
                   vec3 rayPosition = rayPos + rayDir * t;
                   t += rayCast(rayPosition);
                   if ( t < 0.0 || t > 1000.0 ) 
                   { 
                       return 0.0; // no hit
                   }
               }
               return t;
            }

            vec3 getNormal( vec3 pt ) {
                float d = rayCast( pt );
                float s = 0.01;
                vec3 a = vec3(pt.x + s, pt.y,     pt.z    );
                vec3 b = vec3(pt.x,     pt.y + s, pt.z    );
                vec3 c = vec3(pt.x,     pt.y,     pt.z + s);
                vec3 normal = normalize(vec3( rayCast(a) - d,
                                              rayCast(b) - d,
                                              rayCast(c) - d) );
                return normal;
            }

            void main() {
                vec2 uv = gl_FragCoord.xy / u_resolution.xy;
                uv = uv * 2.0 - 1.0; // Map uv coordinates to range [-1, 1]

                // Convert 2D UV to 3D position
                vec3 rayDir = normalize(vec3(uv, -1.0));
                vec3 rayPos = vec3(0.0, 0.0, 2.0);

                vec3 fragColor = vec3(0.2, 0, 0);
    
                float res = rayTrace(rayPos, rayDir);
                if (res > 0.0 )
                {
                    fragColor = vec3(1.0, 0.5, 0.0);
        
                    vec3 n = getNormal( rayPos + rayDir*res );
                    vec3 lightDirection = vec3(0.0, 0.0, 1.0);
                    fragColor = fragColor * min( dot( n, lightDirection ), 1.0 );
                }
                gl_FragColor = vec4( fragColor, 1.0);
            }
        
`;

        function 
createShader(gltypesource) {
            const 
shader gl.createShader(type);
            
gl.shaderSource(shadersource);
            
gl.compileShader(shader);
            if (!
gl.getShaderParameter(shadergl.COMPILE_STATUS)) {
                
console.error('An error occurred compiling the shaders: ' gl.getShaderInfoLog(shader));
                
gl.deleteShader(shader);
                return 
null;
            }
            return 
shader;
        }

        function 
createProgram(glvertexShaderfragmentShader) {
            const 
program gl.createProgram();
            
gl.attachShader(programvertexShader);
            
gl.attachShader(programfragmentShader);
            
gl.linkProgram(program);
            if (!
gl.getProgramParameter(programgl.LINK_STATUS)) {
                
console.error('Unable to initialize the shader program: ' gl.getProgramInfoLog(program));
                return 
null;
            }
            return 
program;
        }

        function 
main() {
            const 
canvas document.getElementById('canvas');
            const 
gl canvas.getContext('webgl');
            if (!
gl) {
                
console.error('Unable to initialize WebGL. Your browser may not support it.');
                return;
            }

            
// Compile vertex and fragment shaders
            
const vertexShader createShader(glgl.VERTEX_SHADERvertexShaderSource);
            const 
fragmentShader createShader(glgl.FRAGMENT_SHADERfragmentShaderSource);

            
// Create shader program
            
const program createProgram(glvertexShaderfragmentShader);
            
gl.useProgram(program);

            
// Set up vertex buffer
            
const positionBuffer gl.createBuffer();
            
gl.bindBuffer(gl.ARRAY_BUFFERpositionBuffer);
            const 
positions = [
                -
1.0, -1.00.0,
                 
1.0, -1.00.0,
                -
1.0,  1.00.0,
                -
1.0,  1.00.0,
                 
1.0, -1.00.0,
                 
1.0,  1.00.0,
            ];
            
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
            const 
positionAttributeLocation gl.getAttribLocation(program'a_position');
            
gl.enableVertexAttribArray(positionAttributeLocation);
            
gl.vertexAttribPointer(positionAttributeLocation3gl.FLOATfalse00);

            
// Set resolution uniform
            
const resolutionUniformLocation gl.getUniformLocation(program'u_resolution');
            
gl.uniform2f(resolutionUniformLocationcanvas.widthcanvas.height);

            
// Clear canvas
            
gl.clearColor(0.00.00.01.0);
            
gl.clear(gl.COLOR_BUFFER_BIT);

            
// Draw
            
gl.drawArrays(gl.TRIANGLES06);
        }

        
main();
    </
script>
</
body>
</
html>


Bit of Light


Without lighting the 3D sphere looks exactly the same as the 2D sphere - you can't see depth or changes in the surface - but we can write a simple function to calcualte the surface normal from the point.

To calculate the surface normal using the SDF function, you can approximate it by taking the gradient of the SDF at a given point.

vec3 getNormalvec3 pt ) {
    
float d rayCastpt );
    
float s 0.01;
    
vec3 a vec3(pt.spt.y,     pt.z    );
    
vec3 b vec3(pt.x,     pt.spt.z    );
    
vec3 c vec3(pt.x,     pt.y,     pt.s);
    
vec3 normal normalize(vec3rayCast(a) - d,
                                  
rayCast(b) - d,
                                  
rayCast(c) - d) );
    return 
normal;
}


The fragment shader code that calculates the normal for the surface using the SDF function and then use it to calculate lighting.

// Calculate surface normal
vec3 n getNormalrayPos rayDir*res );
vec3 lightDirection vec3(0.00.01.0);
// Calculate lighting intensity - angle between the normal andlight direction
fragColor fragColor mindotnlightDirection ), 1.0 );





Combining Shapes (Unions)

This is a crucial component - as you can easily add or subtract simple SDF functions to create new shapes. This is important for 'fractals' - as you'll see, it means you can combine shapes to make smaller sub-shapes.


For instance, it is possible to combine two distance fields using a simple `minimum(a,b)`` operator. As an example we could draw the union of two spheres.


simple sdf combining
Add or subtract SDF functions to construct new shapes.




function sdfCombine(vec3 p) {
    return 
minlength(p)-1.0 
                
length(p-vec3(2.0,0.0,0.0))-1.0 );
}


This would give us two spheres with unit radius, one centered at origo, and another at `(2,0,0)`. The same way it is possible to calculate the intersection of two objects, by taking the maximum value of the fields.

Finally, if you are using signed distance functions, it is possible to subtract one shape from another by inverting one of the fields, and calculating the intersection (i.e. taking `max(A, -B)`).


Take the concept of combining objects further - apply local transforms to create amazing effects (e.g., stretch, rotate, skew and then add them together - you can also do this in a 'loop'.


Construct a 'rayCast' function which can combine multiple different sdf functions in one place (transform, add, subtract, ...).

float rayCastvec3 position ) {
   
float t 0.0// far distance
   
sdfSphere(positionvec3(0.0,0,0), 1.0 );
   
   
mintsdfSphere(positionvec30.500.5), 0.5 ) );
   
   return 
t;
}



simple sdf combining
Combinging two spheres.




Other SDF Shapes


float sdfSphere(vec3 testPointvec3 spherePosfloat sphereRadius) {
    return 
length(spherePos testPoint) - sphereRadius;
}

float sdfCube(vec3 testPointvec3 cubePosvec3 cubeDim) {
    
vec3 d abs(cubePos testPoint) - cubeDim;
    return 
min(max(d.xmax(d.yd.z)), 0.0) + length(max(dvec3(0.0)));
}

float sdfCylinder(vec3 testPointvec3 cylinderPosfloat radiusfloat halfHeight) {
    
vec3 position cylinderPos testPoint;
    
vec2 d vec2(length(position.xz), abs(position.y)) - vec2(radiushalfHeight);
    return 
length(max(dvec2(0.0))) + min(max(d.xd.y), 0.0);
}

float sdfCapsule(vec3 testPointvec3 avec3 bfloat r) {
    
vec3 pa testPoint a;
    
vec3 ba a;
    
float h clamp(dot(paba) / dot(baba), 0.01.0);
    return 
length(pa ba h) - r;
}

float sdfTorus(vec3 testPointvec3 torusPosvec2 torusDim) {
    
vec3 p torusPos testPoint;
    
vec2 q vec2(length(p.xz) - torusDim.xp.y);
    return 
length(q) - torusDim.y;
}




mix of basic sdf shape functions
Example of different common sdf shape functions (cube, sphere, cyclinder, capsule, torus).




Cube-Sphere Union


float rayCast(vec3 ptin) {
    
float t 0.0;
    
sdfCube(ptinvec3(0.00.00.0), vec3(1.0));
    
min(tsdfSphere(ptinvec3(0.00.00.0), 1.2));
    return 
t;
}



union of sphere and cube - create cubesphere
Union of a cube and sphere - create 'cubesphere'.




If you use 'max' - to subtract the shapes, you'll get this:

float rayCast(vec3 ptin) {
    
float t 0.0;
    
sdfCube(ptinvec3(0.00.00.0), vec3(1.0));
    
max(tsdfSphere(ptinvec3(0.00.00.0), 1.2));
    return 
t;
}



union of sphere and cube - create cubesphere
Max (subtract) cube-sphere (instead of adding).




Cube Hole (Cube Cut Cube)


Subtract a cube from a cube to hollow out the inside - initial work for later on when we'll do this recursively to build a fractal shape.

This example, the values are hard coded - for easy reading - but we can calculate the offsets and sizes later on to 'chizzel' away at a shape to make it 'fractal'.

float rayCast(vec3 pin) {
    
sdfCubeptvec3(0), vec3(1.0) );
    
    
maxt, -sdfCubeptvec3(0), vec3(0.70.7,  2.0) ) );
    
    
maxt, -sdfCubeptvec3(0), vec3(0.72,    0.7) ) );
    
    
maxt, -sdfCubeptvec3(0), vec3(2,   0.7,  0.7) ) );

    return 
t;
}



sdf bar box
SDF Cube - but then we position and subtract rectangles for the front, top and side - the rectangle is long and thin. Think of it, like poking a hole through the shape..




Cube in Cube (Adding and Cutting Iteratively)


Iteratively add and cut to create shapes - for example, the following adds cube and cuts out the centre.

// Note - modulus % - wasn't available till 3.0 in GL3
float mod2(float afloat b) {
    return 
floor(b);
}
            
float sdfCubeCubevec3 pt )
{
    
float t 1000000.0;
    const 
int Iterations 24;
    const 
float Scale 0.04;

    
float s 1.0;
    for(
int j 0Iterationsj++)
    {
        if ( 
mod2float(j), 2.0) == 0.0 )
        {
           
mintsdfCubeptvec3(0), vec3(1.0)*) );
        }
        else 
        {
           
maxt, -sdfCubeptvec3(0), vec3(1.0,  1.0,   11.0)*s) );
           
maxt, -sdfCubeptvec3(0), vec3(1.0,  11.0,  1.0 )*s) );
           
maxt, -sdfCubeptvec3(0), vec3(11.01.0,   1.0 )*s) );
        }
        
Scale;
    }
    return 
t;
}



sdf bar box
SDF Cube in Cube



You can take the concept further by mixing on rotation - for each iteration add a small 'rotation'.


float sdfCubeCubevec3 pt )
{
    
float t 1000000.0;
    const 
int Iterations 44;
    const 
float Scale 0.05;

    
float s 1.0;
    for(
int j 0Iterationsj++)
    {
        
// Rotation 
        
float x float(j)*0.03;
        
float y float(j)*0.01;
        
mat4 Rx mat4(
            
vec4(cos(x), -sin(x), 0.00.0),
            
vec4(sin(x), cos(x), 0.00.0),
            
vec4(0.00.01.00.0),
            
vec4(0.00.00.01.0)
        );
        
mat4 Ry mat4(
            
vec4(cos(y), 0.0sin(y), 0.0),
            
vec4(0.01.00.00.0),
            
vec4(-sin(y), 0.0cos(y), 0.0),
            
vec4(0.00.00.01.0)
        );

        
vec3 pr = (vec4(pt1.0) * (Ry Rx)).xyz;
    
    
        if ( 
mod2float(j), 2.0) == 0.0 )
        {
           
mintsdfCubeprvec3(0), vec3(1.0)*) );
        }
        else 
        {
           
maxt, -sdfCubeprvec3(0), vec3(1.0,  1.0,   11.0)*s) );
           
maxt, -sdfCubeprvec3(0), vec3(1.0,  11.0,  1.0 )*s) );
           
maxt, -sdfCubeprvec3(0), vec3(11.01.0,   1.0 )*s) );
        }
        
Scale;
    }
    return 
t;
}



sdf cube in cube with a twist
SDF cube in cube operations with a small rotation causing a twist and warping effect of the inner bars.





Complexity (Lots of Shapes) - 'mod'


This is all nice, but even if you can create interesting structures, there are some limitations. The above method works fine, but scales very badly when the number of distance fields to be combined increases. Creating a scene with 1000 spheres by finding the minimum of the 1000 fields would already become too slow for real-time purposes. In fact ordinary ray tracing scales much better – the use of spatial acceleration structures makes it possible for ordinary ray tracers to draw scenes with millions of objects, something that is far from possible using the “find minimum of all object fields” distance field approach sketched above.

But fractals are all about detail, and endless complexity, so how do we proceed?

It turns out that there are some tricks, that makes it possible to add complexity in ways that scales much better.

First, it is possible to reuse (or instance) objects using e.g. the modulo-operator. Take a look at the following DE:

function sdf(vec3 z)
{
  
z.xy mod((z.xy),1.0)-vec3(0.5); // instance on xy-plane
  
return length(z)-0.3;             // sphere DE
}



Example - Rotating and Repeating Cube-Spheres


// build in one is called 'mod' - but have included my own 'mymod' here - just so you can 
// see what it's actually doing inside the function ;)
fn mymod(float xfloat y)
{
    return ( 
floor(x/y) );
}

float rayCast(vec3 ptin) {
    
// repeat across the x-z plane
    
float ssx 1.0;
    
float ssz 2.0;
    
vec3 position vec3(
        
mymod(ptin.ssx 2.0ssx 4.0) - ssx 2.0,
        
ptin.y,
        
mymod(ptin.ssz 2.0ssz 4.0) - ssz 2.0
    
);
    
float t 0.0;

    
// Rotation 
    
float x 0.2;
    
float y 0.7;
    
mat4 Rx mat4(
        
vec4(cos(x), -sin(x), 0.00.0),
        
vec4(sin(x), cos(x), 0.00.0),
        
vec4(0.00.01.00.0),
        
vec4(0.00.00.01.0)
    );
    
mat4 Ry mat4(
        
vec4(cos(y), 0.0sin(y), 0.0),
        
vec4(0.01.00.00.0),
        
vec4(-sin(y), 0.0cos(y), 0.0),
        
vec4(0.00.00.01.0)
    );

    
vec3 pt = (vec4(position1.0) * (Ry Rx)).xyz;
    
    
sdfCube(ptvec3(0.00.00.0), vec3(1.0));
    
min(tsdfSphere(ptvec3(0.00.00.0), 1.2));

    return 
t;
}




mix of basic sdf shape functions
Mod function to create many instances of the same cube-sphere union.






Recursive Tetrahedron Fractal


Let's bring it all together - combining objects to construct fractals.

A tetrahedron may be described as a polyhedron with vertices (1,1,1),(-1,-1,1),(1,-1,-1),(-1,1,-1). Now, for each point in space, lets us take the vertex closest to it, and scale the system by a factor of 2.0 using this vertex as center, and then finally return the distance to the point where we end, after having repeated this operation. Here is the code:

float sdfTetrahedron(vec3 z)
{

    
vec3 a1 vec3111);
    
vec3 a2 vec3(-1,-11);
    
vec3 a3 vec31,-1,-1);
    
vec3 a4 vec3(-11,-1);
    
    
vec3 c;
    
int n 0;
    
float dist;
    
float d;
    const 
int Iterations 10;
    const 
float Scale 2.0;

    for(
int j 0Iterationsj++)
    {
         
a1
         
dist length(z-a1);
         
         
length(z-a2); if (dist) { a2dist=d; }
         
length(z-a3); if (dist) { a3dist=d; }
         
length(z-a4); if (dist) { a4dist=d; }
         
Scale*z-c*(Scale-1.0);
         
         
n++;
    }
    
    return 
length(z) * pow(Scalefloat(-n)) - 0.01;
}


WebGL version of GLSL is limited e.g.
'while' This type of loop is not allowed




simple sdf combining
Recursive subtracting tetrahedrons to construct a fractal.



Even though we do not have an infinite number of objects, like the mod - example above, the number of objects grow exponentially as we crank up the number of iterations. In fact, the number of objects is equal to 4^Iterations. Just ten iterations will result in more than a million objects - something that is easily doable on a GPU in real-time!


SDF Folding Space


When constructing fractals by iteratively combining shapes, there is a smart trick that takes advantage of 'mirroring' (or folding). This clever trick utilizes the symmetries of the fractal. For instance, if you look at the tetrahedron fractal example, you'll notice it's symetrical across certain axis - so we can use that to optimize the implementation (use folds).

Instead of scaling about the nearest vertex, we mirror points in the symmetry plane of the tetrahedron, to make sure that we arrive at the same octant of the tetrahedron - and then always scale from the vertex it contains.

Here is the code - it produces the same result - but uses folds instead - so it's more compact and efficient.

float sdfTet(vec3 p )
{
    return (
max(
        
abs(p.x+p.y)-p.z,
        
abs(p.x-p.y)+p.z
    
)-1.)/sqrt(3.);
}

float sdfTetrahedron(vec3 zin)
{
    
vec3 z zin;
    
int n 0;
    const 
int Iterations 5;
    
float Scale 2.0;
    
vec3 Offset vec3(1);

    for(
int j 0Iterationsj++)
    {
         if ( (
z.x+z.y) < 0.0 z.xy = -z.yx// fold 1
         
if ( (z.x+z.z) < 0.0 z.xz = -z.zx// fold 2
         
if ( (z.y+z.z) < 0.0 z.zy = -z.yz// fold 3    
        
         
z*Scale Offset*(Scale-1.0);
        
         
n++;
    }
    
// sdf tetrahedron function
    
return sdfTet)  * pow(Scale, -float(n)) - 0.01// add 'chunky' factor
    
    // build up tetrahendron from spheres instead of sdftet function
    //return (length(z) ) * pow(Scale, -float(n)) - 0.01;
}


The folding operations is an interesting aspect of fractals with sdf, and builds up on 'mod' and 'combining' aspects - however, the really interesting stuff with folding happens when we throw rotations into the process. Imagine kaleidoscopic fractals - which produces fractals like the Menger Sponge.

Mixing in rotations, translations and scaling into the fold.


Lighting and SDF


Lighting helps make the fractals more beautiful - as it highlights the detail. Of course, there are more than one ways to light a scene (not just directional lighting).

Phong shading

Phong shading is a technique used to simulate the interaction between the light sources and surfaces, considering diffuse, specular, and ambient reflections, resulting in smooth shading with highlights and shadows.

Ambient Occlusion

Besides the ambient, diffuse, and specular light from Phong-shading, one thing that really improves the quality and depth illusion of a 3D scenes is ambient occlusion.

One approach is to count the number of ray steps as a very rough measure of how occluded the geometry.

Another approach is to sample the Distance Estimator at points along the normal of the surface and use this information to put together a measure for the Ambient Occlusion. This is a more intuitive method, but comes with some other shortcomings – i.e. new parameters are needed to control the distance between the samplings and their relative weights with no obvious default settings.

Glow

Glow can be added simply by mixing in a color based on the number of ray steps taken (points close to the fractal will use more ray steps, even if they miss the fractal, so pixels close to the object will glow).

Fog

Fog is also great for adding to the depth perception. Simply blend in the background color based on the distance from the camera.

Hard shadows

Hard shadows are also straight forward – check if the ray from the surface point to the light source is occluded. Hard shadows have crisply defined, sharp edges.

Soft shadows

Soft shadows are the softened shadows (feathered around the edge).

Reflections

Reflections are pretty much the same – reflect the camera ray in the surface normal, and mix in the color of whatever the reflected ray hits.


Colors


Colors can be use to visualize important information (edges or detail around a fractal).

Orbit Traps

Orbit traps is a popular way to color fractals. This method keeps track of how close the orbit comes to a chosen geometric object. Typical traps include keeping track of the minimum distance to the coordinate system center, or to simple geometric shapes like planes, lines, or spheres. In Fragmentarium, many of the systems use a 4-component vector to keep track of the minimum distance to the three x=0, y=0, and z=0 planes and to the distance from origo. These are mapped to color through the X,Y,Z, and R parameters in the ‘Coloring’ tab.

Iteration Count

The iteration count is the number of iterations it takes before the orbit diverges (becomes larger than the escape radius). Since this is an integer number it is prone to banding, which is discussed later in this post. One way to avoid this is by using a smooth fractional iteration count:

float smooth =  float(iteration
              + 
log(log(EscapeRadiusSquared))/log(Scale
              - 
log(log(dot(z,z)))/log(Scale);


Conditional Path Coloring


Some fractals use conditional logic inside the iteration loops (sometimes disguised as `abs` operator to avoid if statements). The Mandelbox is a good example: the sphere fold performs different actions depending on whether the length of the iterated point is smaller or larger than a set threshold. This makes it possible to keep track of a color variable, which is updated depending on the path taken.



Nutshell


Fusing signed distance functions (SDFs) with ray-tracing opens the door to a quick and powerful way to create complex interactive fractals. Combined with the processing power of the GPU; which can be executed in JavaScript using various API (like WebGL/GLSL or WebGPU/WGSL); there are no limits to what you can create!


Resources & Links


WebGL Mandelbrot Set Fractal

WebGPU Lab (100s of Fractals/Procedural Examples)

WebGL Examples

SDF Chess Shape (King) using WebGPU











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 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-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.