Raymarching in WebGL: Drawing Complex 3D Fractals inside Fragment Shaders
Master Raymarching and SDF rendering inside WebGL. Write an optimized GLSL fragment shader to render 3D mandelbox fractals at native speeds.

Master Raymarching and SDF rendering inside WebGL. Write an optimized GLSL fragment shader to render 3D mandelbox fractals at native speeds.
Raymarching in WebGL: Drawing Complex 3D Fractals inside Fragment Shaders
In standard 3D web graphics, we render objects using Polygons (triangles). We pass vertices to the GPU, rasterize them, and color the pixels. This works wonderfully for simple geometries but falls apart completely when attempting to render infinitely detailed, organic, or mathematical structures like 3D Fractals or morphing fluids.
To draw infinite complexity at a locked 60 FPS, we must bypass the polygon pipeline entirely and write a custom renderer inside a single Fragment Shader using Raymarching and Signed Distance Fields (SDFs).
In this guide, we will explore the mathematics behind Raymarching, understand SDFs, and implement an infinitely complex, interactive 3D Mandelbox Fractal running natively in the browser.
⚡ 1. The Raymarching Paradigm
In standard Raytracing, we trace rays from a camera and mathematically solve intersections with polygons. This is computationally expensive.
Raymarching is an iterative technique designed for GPU parallelism:
- 2.We cast a ray from the camera through each pixel.
- 4.Instead of calculating complex intersection algebra, we query a Signed Distance Function (SDF). The SDF tells us the minimum distance from our current point to the closest surface in the entire 3D scene.
- 6.We "march" the ray forward by exactly that distance (the "safety bubble").
- 8.We repeat this process. If the distance becomes extremely small (e.g., < 0.001), the ray has hit a surface! If the distance becomes very large (e.g., > 100.0) or we exceed a max step limit, the ray has escaped into empty space.
[Camera] ──(Ray Cast)──> (Safety Bubble 1) ──> (Safety Bubble 2) ──> [Surface Hit!]
│
[Query Scene SDF]
🏗️ 2. Coding a Signed Distance Field (SDF) in GLSL
An SDF takes a 3D coordinate point p and returns a single float representing its signed distance to a shape. If p is outside, it returns a positive value; if inside, a negative value.
Let's write a simple SDF for a sphere and a box, and blend them dynamically using smooth minimum (smin) functions to create a melting metal effect.
The GLSL SDF helpers:
glsl// Signed Distance Function to a Sphere float sdfSphere(vec3 p, float radius) { return length(p) - radius; } // Signed Distance Function to a Box float sdfBox(vec3 p, vec3 size) { vec3 d = abs(p) - size; return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0)); } // Smooth Minimum (Smin) to blend shapes organically float smin(float a, float b, float k) { float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); } // The global map function returning the closest scene distance float map(vec3 p) { // Animate a sphere bouncing through a box vec3 spherePos = p - vec3(sin(uTime) * 0.8, 0.0, 0.0); float sphere = sdfSphere(spherePos, 0.5); float box = sdfBox(p, vec3(0.5)); // Organically blend the bouncing sphere and box together return smin(sphere, box, 0.2); }
💻 3. The Core Raymarching Loop
With our scene map established, we implement the primary marching loop inside the fragment shader:
glsl#define MAX_STEPS 100 #define MAX_DIST 100.0 #define SURF_DIST 0.001 uniform vec2 uResolution; uniform float uTime; uniform vec2 uMouse; // Calculate normals (slopes) to compute lighting vec3 getNormal(vec3 p) { vec2 e = vec2(0.01, 0.0); float d = map(p); vec3 n = d - vec3( map(p - e.xyy), map(p - e.yxy), map(p - e.yyx) ); return normalize(n); } void main() { // Normalize screen coordinates (-1.0 to 1.0) vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution.xy) / uResolution.y; // 1. Define Camera Ray Origin (Ro) and Ray Direction (Rd) vec3 ro = vec3(0.0, 0.0, -3.0); vec3 rd = normalize(vec3(uv, 1.0)); // 2. Perform the Marching Loop float dO = 0.0; // Distance marched so far vec3 p; bool hit = false; for(int i = 0; i < MAX_STEPS; i++) { p = ro + rd * dO; float dS = map(p); // Query safety bubble dO += dS; // March safely forward if(dO >= MAX_DIST || dS < SURF_DIST) { if(dS < SURF_DIST) hit = true; break; } } // 3. Shading and Lighting vec3 color = vec3(0.02, 0.05, 0.1); // Deep space background if (hit) { vec3 normal = getNormal(p); // Setup simple diffuse light pointing from top-right vec3 lightPos = vec3(2.0, 4.0, -3.0); vec3 lightDir = normalize(lightPos - p); float diff = max(dot(normal, lightDir), 0.0); // Color based on normals and diffuse lighting color = vec3(diff) * vec3(0.0, 0.95, 0.99); } gl_FragColor = vec4(color, 1.0); }
🚀 4. Unleashing 3D Fractals: The Mandelbox
By recursively folding space inside the map function before evaluating the distance metric, we generate infinitely detailed 3D fractals like the Mandelbox:
glsl// Space fold helper for Mandlebox rendering void sphereFold(inout vec3 z, inout float dz) { float r2 = dot(z, z); float minRad2 = 0.25; float maxRad2 = 1.0; if (r2 < minRad2) { float temp = (maxRad2 / minRad2); z *= temp; dz *= temp; } else if (r2 < maxRad2) { float temp = (maxRad2 / r2); z *= temp; dz *= temp; } } void boxFold(inout vec3 z) { z = clamp(z, -1.0, 1.0) * 2.0 - z; } float mandelboxSDF(vec3 p) { vec3 z = p; float dr = 1.0; float scale = 2.0; for (int i = 0; i < 8; i++) { boxFold(z); sphereFold(z, dr); z = z * scale + p; dr = dr * abs(scale) + 1.0; } return length(z) / abs(dr); }
🏁 5. Conclusion
Raymarching transitions your graphics development from simple mesh manipulation to complex mathematical rendering. Running infinitely detailed 3D shapes and fractals directly inside a single fragment shader bypasses V8 CPU overhead entirely, unlocking elite, interactive visual styles natively on standard web screens.

SQLite on the Edge: Replicating Databases with LiteFS and Fly.io
A technical dive into distributed edge storage, exploring how LiteFS replicates SQLite databases across global Fly.io regions using FUSE and lease-based consensus.

Implementing Post-Quantum Cryptography in Next.js: Securing APIs against Future Decryption
Future-proof your web applications today. Learn how to secure Next.js API routes using Post-Quantum Cryptography (PQC) algorithms like ML-KEM and Kyber.