22 KiB
3D Signed Distance Fields (3D SDF) Skill
Use Cases
- Real-time rendering of 3D geometry in ShaderToy / fragment shaders (no traditional meshes needed)
- Complex scenes composed from basic primitives (sphere, box, cylinder, torus, etc.)
- Smooth organic blending (character modeling, fluid blobs, biological forms)
- Infinitely repeating architectural/pattern structures (corridors, gear arrays, grids)
- Precise boolean operations (drilling holes, cutting, intersection) for sculpting geometry
Core Principles
An SDF returns the signed distance from any point in space to the nearest surface: positive = outside, negative = inside, zero = surface.
Sphere Tracing: advance along a ray, stepping by the current SDF value (the safe marching distance) at each step. The SDF guarantees no surface exists within that radius. A hit is registered when the distance falls below epsilon.
Key math:
- Sphere:
f(p) = |p| - r - Box:
f(p) = |max(|p|-b, 0)| + min(max(|p-b|), 0) - Union:
min(d1, d2)/ Subtraction:max(d1, -d2) - Smooth union:
min(d1,d2) - h^2/4k,h = max(k-|d1-d2|, 0) - Normal = SDF gradient:
n = normalize(gradient of f(p))(finite difference approximation)
Rendering Pipeline Overview
- SDF Primitive Library --
sdSphere,sdBox,sdEllipsoid,sdTorus,sdCapsule,sdCylinder - Boolean Operations --
opUnion/opSubtraction/opIntersection+ smooth variantssmin/smax - Scene Definition --
map(p)returnsvec2(distance, materialID), combining all primitives - Ray Marching --
raycast(ro, rd)sphere tracing loop (128 steps, adaptive thresholdSURF_DIST * t) - Normal Calculation -- tetrahedral differencing (4 map calls, ZERO macro to prevent inlining)
- Soft Shadows -- quadratic stepping with
k*h/tto estimate occlusion softness, Hermite smoothing - Ambient Occlusion -- 5-layer sampling along the normal, comparing SDF values with expected distances
- Camera + Rendering -- look-at matrix, multiple lights (sun + sky + SSS), gamma correction, fog
Full Code Template
Runs directly in ShaderToy. Includes multi-primitive scene, smooth blending, soft shadows, AO, and material system.
IMPORTANT: When using the vec2(distance, materialID) material system, smin needs to handle vec2 types. The template includes a vec2 smin(vec2 a, vec2 b, float k) overload that ensures the material ID is correctly passed through during smooth blending (taking the material of the closer distance).
// 3D SDF Full Rendering Pipeline Template - Runs in ShaderToy
#define AA 1 // Anti-aliasing (1=off, 2=4xAA, 3=9xAA)
#define MAX_STEPS 128
#define MAX_DIST 40.0
#define SURF_DIST 0.0001
#define SHADOW_STEPS 24
#define SHADOW_SOFTNESS 8.0
#define SMOOTH_K 0.3
#define ZERO (min(iFrame, 0))
// === SDF Primitives ===
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float sdEllipsoid(vec3 p, vec3 r) {
float k0 = length(p / r); float k1 = length(p / (r * r));
return k0 * (k0 - 1.0) / k1;
}
float sdTorus(vec3 p, vec2 t) {
return length(vec2(length(p.xz) - t.x, p.y)) - t.y;
}
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
vec3 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h) - r;
}
float sdCylinder(vec3 p, vec2 h) {
vec2 d = abs(vec2(length(p.xz), p.y)) - h;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
// === Extended SDF Primitives ===
float sdRoundBox(vec3 p, vec3 b, float r) {
vec3 q = abs(p) - b + r;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
}
float sdBoxFrame(vec3 p, vec3 b, float e) {
p = abs(p) - b;
vec3 q = abs(p + e) - e;
return min(min(
length(max(vec3(p.x, q.y, q.z), 0.0)) + min(max(p.x, max(q.y, q.z)), 0.0),
length(max(vec3(q.x, p.y, q.z), 0.0)) + min(max(q.x, max(p.y, q.z)), 0.0)),
length(max(vec3(q.x, q.y, p.z), 0.0)) + min(max(q.x, max(q.y, p.z)), 0.0));
}
float sdCone(vec3 p, vec2 c, float h) {
vec2 q = h * vec2(c.x / c.y, -1.0);
vec2 w = vec2(length(p.xz), p.y);
vec2 a = w - q * clamp(dot(w, q) / dot(q, q), 0.0, 1.0);
vec2 b = w - q * vec2(clamp(w.x / q.x, 0.0, 1.0), 1.0);
float k = sign(q.y);
float d = min(dot(a, a), dot(b, b));
float s = max(k * (w.x * q.y - w.y * q.x), k * (w.y - q.y));
return sqrt(d) * sign(s);
}
float sdCappedCone(vec3 p, float h, float r1, float r2) {
vec2 q = vec2(length(p.xz), p.y);
vec2 k1 = vec2(r2, h);
vec2 k2 = vec2(r2 - r1, 2.0 * h);
vec2 ca = vec2(q.x - min(q.x, (q.y < 0.0) ? r1 : r2), abs(q.y) - h);
vec2 cb = q - k1 + k2 * clamp(dot(k1 - q, k2) / dot(k2, k2), 0.0, 1.0);
float s = (cb.x < 0.0 && ca.y < 0.0) ? -1.0 : 1.0;
return s * sqrt(min(dot(ca, ca), dot(cb, cb)));
}
float sdRoundCone(vec3 p, float r1, float r2, float h) {
float b = (r1 - r2) / h;
float a = sqrt(1.0 - b * b);
vec2 q = vec2(length(p.xz), p.y);
float k = dot(q, vec2(-b, a));
if (k < 0.0) return length(q) - r1;
if (k > a * h) return length(q - vec2(0.0, h)) - r2;
return dot(q, vec2(a, b)) - r1;
}
float sdSolidAngle(vec3 p, vec2 c, float ra) {
vec2 q = vec2(length(p.xz), p.y);
float l = length(q) - ra;
float m = length(q - c * clamp(dot(q, c), 0.0, ra));
return max(l, m * sign(c.y * q.x - c.x * q.y));
}
float sdOctahedron(vec3 p, float s) {
p = abs(p);
float m = p.x + p.y + p.z - s;
vec3 q;
if (3.0 * p.x < m) q = p.xyz;
else if (3.0 * p.y < m) q = p.yzx;
else if (3.0 * p.z < m) q = p.zxy;
else return m * 0.57735027;
float k = clamp(0.5 * (q.z - q.y + s), 0.0, s);
return length(vec3(q.x, q.y - s + k, q.z - k));
}
float sdPyramid(vec3 p, float h) {
float m2 = h * h + 0.25;
p.xz = abs(p.xz);
p.xz = (p.z > p.x) ? p.zx : p.xz;
p.xz -= 0.5;
vec3 q = vec3(p.z, h * p.y - 0.5 * p.x, h * p.x + 0.5 * p.y);
float s = max(-q.x, 0.0);
float t = clamp((q.y - 0.5 * p.z) / (m2 + 0.25), 0.0, 1.0);
float a = m2 * (q.x + s) * (q.x + s) + q.y * q.y;
float b = m2 * (q.x + 0.5 * t) * (q.x + 0.5 * t) + (q.y - m2 * t) * (q.y - m2 * t);
float d2 = min(q.y, -q.x * m2 - q.y * 0.5) > 0.0 ? 0.0 : min(a, b);
return sqrt((d2 + q.z * q.z) / m2) * sign(max(q.z, -p.y));
}
float sdHexPrism(vec3 p, vec2 h) {
const vec3 k = vec3(-0.8660254, 0.5, 0.57735);
p = abs(p);
p.xy -= 2.0 * min(dot(k.xy, p.xy), 0.0) * k.xy;
vec2 d = vec2(length(p.xy - vec2(clamp(p.x, -k.z * h.x, k.z * h.x), h.x)) * sign(p.y - h.x), p.z - h.y);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float sdCutSphere(vec3 p, float r, float h) {
float w = sqrt(r * r - h * h);
vec2 q = vec2(length(p.xz), p.y);
float s = max((h - r) * q.x * q.x + w * w * (h + r - 2.0 * q.y), h * q.x - w * q.y);
return (s < 0.0) ? length(q) - r : (q.x < w) ? h - q.y : length(q - vec2(w, h));
}
float sdCappedTorus(vec3 p, vec2 sc, float ra, float rb) {
p.x = abs(p.x);
float k = (sc.y * p.x > sc.x * p.y) ? dot(p.xy, sc) : length(p.xy);
return sqrt(dot(p, p) + ra * ra - 2.0 * ra * k) - rb;
}
float sdLink(vec3 p, float le, float r1, float r2) {
vec3 q = vec3(p.x, max(abs(p.y) - le, 0.0), p.z);
return length(vec2(length(q.xy) - r1, q.z)) - r2;
}
float sdPlane(vec3 p, vec3 n, float h) {
return dot(p, n) + h;
}
float sdRhombus(vec3 p, float la, float lb, float h, float ra) {
p = abs(p);
vec2 b = vec2(la, lb);
float f = clamp((dot(b, b - 2.0 * p.xz)) / dot(b, b), -1.0, 1.0);
vec2 q = vec2(length(p.xz - 0.5 * b * vec2(1.0 - f, 1.0 + f)) * sign(p.x * b.y + p.z * b.x - b.x * b.y) - ra, p.y - h);
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0));
}
// Unsigned distance (exact)
float udTriangle(vec3 p, vec3 a, vec3 b, vec3 c) {
vec3 ba = b - a; vec3 pa = p - a;
vec3 cb = c - b; vec3 pb = p - b;
vec3 ac = a - c; vec3 pc = p - c;
vec3 nor = cross(ba, ac);
return sqrt(
(sign(dot(cross(ba, nor), pa)) +
sign(dot(cross(cb, nor), pb)) +
sign(dot(cross(ac, nor), pc)) < 2.0)
? min(min(
dot(ba * clamp(dot(ba, pa) / dot(ba, ba), 0.0, 1.0) - pa,
ba * clamp(dot(ba, pa) / dot(ba, ba), 0.0, 1.0) - pa),
dot(cb * clamp(dot(cb, pb) / dot(cb, cb), 0.0, 1.0) - pb,
cb * clamp(dot(cb, pb) / dot(cb, cb), 0.0, 1.0) - pb)),
dot(ac * clamp(dot(ac, pc) / dot(ac, ac), 0.0, 1.0) - pc,
ac * clamp(dot(ac, pc) / dot(ac, ac), 0.0, 1.0) - pc))
: dot(nor, pa) * dot(nor, pa) / dot(nor, nor));
}
// === Boolean Operations ===
vec2 opU(vec2 d1, vec2 d2) { return (d1.x < d2.x) ? d1 : d2; }
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / k;
}
vec2 smin(vec2 a, vec2 b, float k) {
// vec2 smin: x=distance (smooth blend), y=materialID (take material of closer distance)
float h = max(k - abs(a.x - b.x), 0.0);
float d = min(a.x, b.x) - h * h * 0.25 / k;
float m = (a.x < b.x) ? a.y : b.y;
return vec2(d, m);
}
float smax(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return max(a, b) + h * h * 0.25 / k;
}
// === Deformation Operators ===
// Round: soften edges of any SDF
// Usage: sdRound(sdBox(p, vec3(1.0)), 0.1)
float opRound(float d, float r) { return d - r; }
// Onion: hollow out any SDF into a shell
// Usage: opOnion(sdSphere(p, 1.0), 0.1) — sphere shell of thickness 0.1
float opOnion(float d, float t) { return abs(d) - t; }
// Elongate: stretch a shape along axes
// Usage: elongate a sphere into a capsule-like shape
float opElongate(in vec3 p, in vec3 h, in vec3 center, in vec3 size) {
// Generic elongation: subtract h from abs(p), clamp to 0
vec3 q = abs(p) - h;
// Then evaluate original SDF with max(q, 0.0)
// Return: sdOriginal(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0)
return sdBox(max(q, 0.0), size) + min(max(q.x, max(q.y, q.z)), 0.0); // example with box
}
// Twist: rotate around Y axis based on height
vec3 opTwist(vec3 p, float k) {
float c = cos(k * p.y);
float s = sin(k * p.y);
mat2 m = mat2(c, -s, s, c);
return vec3(m * p.xz, p.y);
}
// Cheap Bend: bend along X axis based on X position
vec3 opCheapBend(vec3 p, float k) {
float c = cos(k * p.x);
float s = sin(k * p.x);
mat2 m = mat2(c, -s, s, c);
vec2 q = m * p.xy;
return vec3(q, p.z);
}
// Displacement: add procedural detail to surface
float opDisplace(float d, vec3 p) {
float displacement = sin(20.0 * p.x) * sin(20.0 * p.y) * sin(20.0 * p.z);
return d + displacement * 0.02;
}
// === 2D-to-3D Constructors ===
// Revolution: rotate a 2D SDF around the Y axis to create a 3D solid of revolution
// sdf2d: any 2D SDF function, o: offset from axis
float opRevolution(vec3 p, float sdf2d_result, float o) {
vec2 q = vec2(length(p.xz) - o, p.y);
// Example: revolve a 2D circle to make a torus
// float d2d = length(q) - 0.3; // 2D circle as cross-section
// return d2d;
return sdf2d_result; // pass pre-computed 2D SDF of vec2(length(p.xz)-o, p.y)
}
// Extrusion: extend a 2D SDF along the Z axis with finite height
float opExtrusion(vec3 p, float d2d, float h) {
vec2 w = vec2(d2d, abs(p.z) - h);
return min(max(w.x, w.y), 0.0) + length(max(w, 0.0));
}
// Usage example: extruded 2D star
// float d2d = sdStar2D(p.xy, 0.5, 5, 2.0); // any 2D SDF
// float d3d = opExtrusion(p, d2d, 0.2); // extrude 0.2 units
// === Symmetry Operators ===
// Mirror across X axis (most common — bilateral symmetry)
// Place this at the beginning of map() to model only one half
vec3 opSymX(vec3 p) { p.x = abs(p.x); return p; }
// Mirror across X and Z (four-fold symmetry)
vec3 opSymXZ(vec3 p) { p.xz = abs(p.xz); return p; }
// Mirror across arbitrary direction
vec3 opMirror(vec3 p, vec3 dir) {
return p - 2.0 * dir * max(dot(p, dir), 0.0);
}
// === Scene ===
vec2 map(vec3 pos) {
vec2 res = vec2(pos.y, 0.0);
// Animated blob cluster
float dBlob = 2.0;
for (int i = 0; i < 8; i++) {
float fi = float(i);
float t = iTime * (fract(fi * 412.531 + 0.513) - 0.5) * 2.0;
vec3 offset = sin(t + fi * vec3(52.5126, 64.627, 632.25)) * vec3(2.0, 2.0, 0.8);
float radius = mix(0.3, 0.6, fract(fi * 412.531 + 0.5124));
dBlob = smin(dBlob, sdSphere(pos + offset, radius), SMOOTH_K);
}
res = opU(res, vec2(dBlob, 1.0));
float dBox = sdBox(pos - vec3(3.0, 0.4, 0.0), vec3(0.3, 0.4, 0.3));
res = opU(res, vec2(dBox, 2.0));
float dTorus = sdTorus((pos - vec3(-3.0, 0.5, 0.0)).xzy, vec2(0.4, 0.1));
res = opU(res, vec2(dTorus, 3.0));
// CSG subtraction: sphere minus box
float dCSG = sdSphere(pos - vec3(0.0, 0.5, 3.0), 0.5);
dCSG = max(dCSG, -sdBox(pos - vec3(0.0, 0.5, 3.0), vec3(0.3)));
res = opU(res, vec2(dCSG, 4.0));
return res;
}
// === Normals ===
vec3 calcNormal(vec3 pos) {
vec3 n = vec3(0.0);
for (int i = ZERO; i < 4; i++) {
vec3 e = 0.5773 * (2.0 * vec3((((i+3)>>1)&1), ((i>>1)&1), (i&1)) - 1.0);
n += e * map(pos + 0.0005 * e).x;
}
return normalize(n);
}
// === Shadows ===
float calcSoftshadow(vec3 ro, vec3 rd, float mint, float tmax) {
float res = 1.0, t = mint;
for (int i = ZERO; i < SHADOW_STEPS; i++) {
float h = map(ro + rd * t).x;
float s = clamp(SHADOW_SOFTNESS * h / t, 0.0, 1.0);
res = min(res, s);
t += clamp(h, 0.01, 0.2);
if (res < 0.004 || t > tmax) break;
}
res = clamp(res, 0.0, 1.0);
return res * res * (3.0 - 2.0 * res);
}
// === AO ===
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0, sca = 1.0;
for (int i = ZERO; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0;
float d = map(pos + h * nor).x;
occ += (h - d) * sca;
sca *= 0.95;
if (occ > 0.35) break;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0) * (0.5 + 0.5 * nor.y);
}
// === Ray Marching ===
vec2 raycast(vec3 ro, vec3 rd) {
vec2 res = vec2(-1.0);
float t = 0.01;
for (int i = 0; i < MAX_STEPS && t < MAX_DIST; i++) {
vec2 h = map(ro + rd * t);
if (abs(h.x) < SURF_DIST * t) { res = vec2(t, h.y); break; }
t += h.x;
}
return res;
}
// === Camera ===
mat3 setCamera(vec3 ro, vec3 ta, float cr) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = cross(cu, cw);
return mat3(cu, cv, cw);
}
// === Rendering ===
vec3 render(vec3 ro, vec3 rd) {
vec3 col = vec3(0.7, 0.7, 0.9) - max(rd.y, 0.0) * 0.3;
vec2 res = raycast(ro, rd);
float t = res.x, m = res.y;
if (m > -0.5) {
vec3 pos = ro + t * rd;
vec3 nor = (m < 0.5) ? vec3(0.0, 1.0, 0.0) : calcNormal(pos);
vec3 ref = reflect(rd, nor);
vec3 mate = 0.2 + 0.2 * sin(m * 2.0 + vec3(0.0, 1.0, 2.0));
if (m < 0.5) mate = vec3(0.15);
float occ = calcAO(pos, nor);
vec3 lin = vec3(0.0);
// Key light
{
vec3 lig = normalize(vec3(-0.5, 0.4, -0.6));
vec3 hal = normalize(lig - rd);
float dif = clamp(dot(nor, lig), 0.0, 1.0);
dif *= calcSoftshadow(pos, lig, 0.02, 2.5);
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0);
spe *= dif * (0.04 + 0.96 * pow(clamp(1.0 - dot(hal, lig), 0.0, 1.0), 5.0));
lin += mate * 2.20 * dif * vec3(1.30, 1.00, 0.70);
lin += 5.00 * spe * vec3(1.30, 1.00, 0.70);
}
// Sky light
{
float dif = sqrt(clamp(0.5 + 0.5 * nor.y, 0.0, 1.0)) * occ;
lin += mate * 0.60 * dif * vec3(0.40, 0.60, 1.15);
}
// Subsurface scattering approximation
{
float dif = pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 2.0) * occ;
lin += mate * 0.25 * dif;
}
col = lin;
col = mix(col, vec3(0.7, 0.7, 0.9), 1.0 - exp(-0.0001 * t * t * t));
}
return clamp(col, 0.0, 1.0);
}
// === Main Function ===
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 mo = iMouse.xy / iResolution.xy;
float time = 32.0 + iTime * 1.5;
vec3 ta = vec3(0.0, 0.0, 0.0);
vec3 ro = ta + vec3(4.5 * cos(0.1 * time + 7.0 * mo.x), 2.2,
4.5 * sin(0.1 * time + 7.0 * mo.x));
mat3 ca = setCamera(ro, ta, 0.0);
vec3 tot = vec3(0.0);
#if AA > 1
for (int m = ZERO; m < AA; m++)
for (int n = ZERO; n < AA; n++) {
vec2 o = vec2(float(m), float(n)) / float(AA) - 0.5;
vec2 p = (2.0 * (fragCoord + o) - iResolution.xy) / iResolution.y;
#else
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
#endif
vec3 rd = ca * normalize(vec3(p, 2.5));
vec3 col = render(ro, rd);
col = pow(col, vec3(0.4545));
tot += col;
#if AA > 1
}
tot /= float(AA * AA);
#endif
fragColor = vec4(tot, 1.0);
}
Common Variants
Variant 1: Dynamic Organic Body (Smooth Blob Animation)
vec2 map(vec3 p) {
float d = 2.0;
for (int i = 0; i < 16; i++) {
float fi = float(i);
float t = iTime * (fract(fi * 412.531 + 0.513) - 0.5) * 2.0;
d = smin(sdSphere(p + sin(t + fi * vec3(52.5126, 64.627, 632.25)) * vec3(2.0, 2.0, 0.8),
mix(0.5, 1.0, fract(fi * 412.531 + 0.5124))), d, 0.4);
}
return vec2(d, 1.0);
}
Variant 2: Infinite Repeating Corridor (Domain Repetition)
float repeat(float v, float c) { return mod(v, c) - c * 0.5; }
float amod(inout vec2 p, float count) {
float an = 6.283185 / count;
float a = atan(p.y, p.x) + an * 0.5;
float c = floor(a / an);
a = mod(a, an) - an * 0.5;
p = vec2(cos(a), sin(a)) * length(p);
return c;
}
vec2 map(vec3 p) {
p.z = repeat(p.z, 4.0);
p.x += 2.0 * sin(p.z * 0.1);
float d = -sdBox(p, vec3(2.0, 2.0, 20.0));
d = max(d, -sdBox(p, vec3(1.8, 1.8, 1.9)));
d = min(d, sdCylinder(p - vec3(1.5, -2.0, 0.0), vec2(0.1, 2.0)));
return vec2(d, 1.0);
}
Variant 3: Character/Creature Modeling
vec2 sdStick(vec3 p, vec3 a, vec3 b, float r1, float r2) {
vec3 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return vec2(length(pa - ba * h) - mix(r1, r2, h * h * (3.0 - 2.0 * h)), h);
}
vec2 map(vec3 pos) {
float d = sdEllipsoid(pos, vec3(0.25, 0.3, 0.25)); // body
d = smin(d, sdEllipsoid(pos - vec3(0.0, 0.35, 0.02),
vec3(0.12, 0.15, 0.13)), 0.1); // head
vec2 arm = sdStick(abs(pos.x) > 0.0 ? vec3(abs(pos.x), pos.yz) : pos,
vec3(0.18, 0.2, -0.05), vec3(0.35, -0.1, -0.15), 0.03, 0.05);
d = smin(d, arm.x, 0.04); // arms
d = smax(d, -sdEllipsoid(pos - vec3(0.0, 0.3, 0.15),
vec3(0.08, 0.03, 0.1)), 0.03); // mouth carving
return vec2(d, 1.0);
}
Variant 4: Symmetry Optimization
vec2 rot45(vec2 v) { return vec2(v.x - v.y, v.y + v.x) * 0.707107; }
vec2 map(vec3 p) {
float d = sdSphere(p, 0.12);
// Octahedral symmetry: 18-gear evaluations reduced to 4
vec3 qx = vec3(rot45(p.zy), p.x);
if (abs(qx.x) > abs(qx.y)) qx = qx.zxy;
vec3 qy = vec3(rot45(p.xz), p.y);
if (abs(qy.x) > abs(qy.y)) qy = qy.zxy;
vec3 qz = vec3(rot45(p.yx), p.z);
if (abs(qz.x) > abs(qz.y)) qz = qz.zxy;
vec3 qa = abs(p);
qa = (qa.x > qa.y && qa.x > qa.z) ? p.zxy : (qa.z > qa.y) ? p.yzx : p.xyz;
d = min(d, min(min(gear(qa, 0.0), gear(qx, 1.0)), min(gear(qy, 1.0), gear(qz, 1.0))));
return vec2(d, 1.0);
}
Variant 5: PBR Material Rendering
float D_GGX(float NoH, float roughness) {
float a = roughness * roughness; float a2 = a * a;
float d = NoH * NoH * (a2 - 1.0) + 1.0;
return a2 / (3.14159 * d * d);
}
vec3 F_Schlick(float VoH, vec3 f0) {
return f0 + (1.0 - f0) * pow(1.0 - VoH, 5.0);
}
vec3 pbrLighting(vec3 pos, vec3 nor, vec3 rd, vec3 albedo, float roughness, float metallic) {
vec3 lig = normalize(vec3(-0.5, 0.4, -0.6));
vec3 hal = normalize(lig - rd);
vec3 f0 = mix(vec3(0.04), albedo, metallic);
float NoL = max(dot(nor, lig), 0.0);
float NoH = max(dot(nor, hal), 0.0);
float VoH = max(dot(-rd, hal), 0.0);
vec3 spec = D_GGX(NoH, roughness) * F_Schlick(VoH, f0) * 0.25;
vec3 diff = albedo * (1.0 - metallic) / 3.14159;
float shadow = calcSoftshadow(pos, lig, 0.02, 2.5);
return (diff + spec) * NoL * shadow * vec3(1.3, 1.0, 0.7) * 3.0;
}
Performance & Composition
Performance Optimization Tips
- Bounding volume acceleration: test ray against AABB first to narrow
tmin/tmax, avoiding wasted steps in empty regions - Sub-scene bounding: in
map(), use a cheapsdBoxto check proximity before computing the precise SDF - Adaptive step size:
abs(h.x) < SURF_DIST * t-- looser tolerance at distance, stricter up close - Prevent compiler inlining:
#define ZERO (min(iFrame, 0))+ loop preventscalcNormalfrom inlining map 4 times - Exploit symmetry: fold into the fundamental domain, reducing 18 evaluations to 4
Common Composition Techniques
- Noise displacement:
d += 0.05 * sin(p.x*10.)*sin(p.y*10.)*sin(p.z*10.)adds organic detail; breaks the Lipschitz condition, so step size should be multiplied by 0.5~0.7 - Bump mapping: perturb only during normal calculation, leaving ray marching unaffected for better performance
- Domain transforms: warp coordinates before entering map (bending, polar coordinate transforms, etc.)
- Procedural animation: bone angles driven by time to position primitives,
sminensures smooth joints - Motion blur: multi-frame temporal sampling averaged
Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in reference