21 KiB
21 KiB
2D SDF Rendering Skill
Use Cases
- 2D shape rendering: circles, rectangles, triangles, ellipses, line segments, Bezier curves, etc.
- UI elements and icons: drawn with math functions, naturally resolution-independent
- Anti-aliased graphics, shape boolean operations, outlines and glow
- Motion graphics and animation, 2D soft shadows and lighting
Core Principles
For each pixel, compute the signed distance d to the shape boundary: d < 0 inside, d = 0 boundary, d > 0 outside.
Map to color via smoothstep/clamp:
- Fill: color when
d < 0 - Anti-aliasing:
smoothstep(-aa, aa, d) - Stroke: apply smoothstep to
abs(d) - strokeWidth - Boolean operations:
min(d1, d2)union,max(d1, d2)intersection,max(-d1, d2)subtraction
Key formulas:
Circle: d = length(p - center) - radius
Rectangle: d = length(max(abs(p) - halfSize, 0.0)) + min(max(abs(p).x - halfSize.x, abs(p).y - halfSize.y), 0.0)
Line segment: d = length(p - a - clamp(dot(p-a, b-a)/dot(b-a, b-a), 0, 1) * (b-a)) - width/2
Smooth union: d = mix(d2, d1, h) - k*h*(1-h), h = clamp(0.5 + 0.5*(d2-d1)/k, 0, 1)
Implementation Steps
Step 1: Coordinate Normalization
// Origin at center, y range [-1, 1] (standard approach)
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
// Pixel space (suitable for fixed pixel-size UI)
vec2 p = fragCoord.xy;
vec2 center = iResolution.xy * 0.5;
// [0, 1] range (requires manual aspect ratio handling)
vec2 uv = fragCoord.xy / iResolution.xy;
Step 2: SDF Primitive Functions
float sdCircle(vec2 p, float radius) {
return length(p) - radius;
}
// halfSize is half-width/half-height, radius is corner rounding
float sdBox(vec2 p, vec2 halfSize, float radius) {
halfSize -= vec2(radius);
vec2 d = abs(p) - halfSize;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius;
}
float sdLine(vec2 p, vec2 start, vec2 end, float width) {
vec2 dir = end - start;
float h = clamp(dot(p - start, dir) / dot(dir, dir), 0.0, 1.0);
return length(p - start - dir * h) - width * 0.5;
}
// Exact signed distance, requires only one sqrt
float sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) {
vec2 e0 = p1 - p0, v0 = p - p0;
vec2 e1 = p2 - p1, v1 = p - p1;
vec2 e2 = p0 - p2, v2 = p - p2;
float d0 = dot(v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0),
v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0));
float d1 = dot(v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0),
v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0));
float d2 = dot(v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0),
v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0));
float o = e0.x * e2.y - e0.y * e2.x;
vec2 d = min(min(vec2(d0, o * (v0.x * e0.y - v0.y * e0.x)),
vec2(d1, o * (v1.x * e1.y - v1.y * e1.x))),
vec2(d2, o * (v2.x * e2.y - v2.y * e2.x)));
return -sqrt(d.x) * sign(d.y);
}
// Approximate ellipse SDF
float sdEllipse(vec2 p, vec2 center, float a, float b) {
float a2 = a * a, b2 = b * b;
vec2 d = p - center;
return (b2 * d.x * d.x + a2 * d.y * d.y - a2 * b2) / (a2 * b2);
}
Step 3: CSG Boolean Operations
float opUnion(float d1, float d2) { return min(d1, d2); }
float opIntersect(float d1, float d2) { return max(d1, d2); }
float opSubtract(float d1, float d2) { return max(-d1, d2); }
float opXor(float d1, float d2) { return min(max(-d1, d2), max(-d2, d1)); }
// k controls transition width
float opSmoothUnion(float d1, float d2, float k) {
float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
return mix(d2, d1, h) - k * h * (1.0 - h);
}
Step 4: Coordinate Transforms
vec2 translate(vec2 p, vec2 t) { return p - t; }
vec2 rotateCCW(vec2 p, float angle) {
mat2 m = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
return p * m;
}
// Usage: translate first, then rotate
float d = sdBox(rotateCCW(translate(p, vec2(0.5, 0.3)), iTime), vec2(0.2), 0.05);
Step 5: Rendering and Anti-Aliasing
// smoothstep anti-aliasing (recommended)
float px = 2.0 / iResolution.y;
float mask = smoothstep(px, -px, d); // 1.0 inside, 0.0 outside
vec3 col = mix(backgroundColor, shapeColor, mask);
// fwidth adaptive anti-aliasing (suitable for scaled scenes)
float anti = fwidth(d) * 1.0;
float mask = 1.0 - smoothstep(-anti, anti, d);
// Classic distance field debug visualization
vec3 col = (d > 0.0) ? vec3(0.9, 0.6, 0.3) : vec3(0.65, 0.85, 1.0);
col *= 1.0 - exp(-12.0 * abs(d));
col *= 0.8 + 0.2 * cos(120.0 * d);
col = mix(col, vec3(1.0), smoothstep(1.5*px, 0.0, abs(d) - 0.002));
Step 6: Stroke and Border
// Fill + stroke rendering (fwidth adaptive)
vec4 renderShape(float d, vec3 color, float stroke) {
float anti = fwidth(d) * 1.0;
vec4 strokeLayer = vec4(vec3(0.05), 1.0 - smoothstep(-anti, anti, d - stroke));
vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d));
if (stroke < 0.0001) return colorLayer;
return vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}
float fillMask(float d) { return clamp(-d, 0.0, 1.0); }
float innerBorderMask(float d, float width) {
return clamp(d + width, 0.0, 1.0) - clamp(d, 0.0, 1.0);
}
float outerBorderMask(float d, float width) {
return clamp(d, 0.0, 1.0) - clamp(d - width, 0.0, 1.0);
}
Step 7: Multi-Layer Compositing
vec3 bgColor = vec3(1.0, 0.8, 0.7 - 0.07 * p.y) * (1.0 - 0.25 * length(p));
float d1 = sdCircle(translate(p, pos1), 0.3);
vec4 layer1 = renderShape(d1, vec3(0.9, 0.3, 0.2), 0.02);
float d2 = sdBox(translate(p, pos2), vec2(0.2), 0.05);
vec4 layer2 = renderShape(d2, vec3(0.2, 0.5, 0.8), 0.0);
// Composite back to front
vec3 col = bgColor;
col = mix(col, layer1.rgb, layer1.a);
col = mix(col, layer2.rgb, layer2.a);
fragColor = vec4(col, 1.0);
Full Code Template
// ===== 2D SDF Full Template (runs directly in ShaderToy) =====
#define AA_WIDTH 1.0 // Anti-aliasing width factor
#define STROKE_WIDTH 0.015 // Stroke width
#define SMOOTH_K 0.05 // Smooth union transition width
#define CONTOUR_FREQ 80.0 // Contour line frequency (for debugging)
#define ANIM_SPEED 1.0 // Animation speed multiplier
// --- SDF Primitives ---
float sdCircle(vec2 p, float r) { return length(p) - r; }
float sdBox(vec2 p, vec2 b, float r) {
b -= vec2(r);
vec2 d = abs(p) - b;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
}
float sdLine(vec2 p, vec2 a, vec2 b, float w) {
vec2 d = b - a;
float h = clamp(dot(p - a, d) / dot(d, d), 0.0, 1.0);
return length(p - a - d * h) - w * 0.5;
}
float sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) {
vec2 e0 = p1 - p0, v0 = p - p0;
vec2 e1 = p2 - p1, v1 = p - p1;
vec2 e2 = p0 - p2, v2 = p - p2;
float d0 = dot(v0 - e0 * clamp(dot(v0,e0)/dot(e0,e0),0.0,1.0),
v0 - e0 * clamp(dot(v0,e0)/dot(e0,e0),0.0,1.0));
float d1 = dot(v1 - e1 * clamp(dot(v1,e1)/dot(e1,e1),0.0,1.0),
v1 - e1 * clamp(dot(v1,e1)/dot(e1,e1),0.0,1.0));
float d2 = dot(v2 - e2 * clamp(dot(v2,e2)/dot(e2,e2),0.0,1.0),
v2 - e2 * clamp(dot(v2,e2)/dot(e2,e2),0.0,1.0));
float o = e0.x*e2.y - e0.y*e2.x;
vec2 dd = min(min(vec2(d0, o*(v0.x*e0.y-v0.y*e0.x)),
vec2(d1, o*(v1.x*e1.y-v1.y*e1.x))),
vec2(d2, o*(v2.x*e2.y-v2.y*e2.x)));
return -sqrt(dd.x) * sign(dd.y);
}
// --- CSG ---
float opUnion(float a, float b) { return min(a, b); }
float opSubtract(float a, float b) { return max(-a, b); }
float opIntersect(float a, float b) { return max(a, b); }
float opSmoothUnion(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);
}
float opXor(float a, float b) { return min(max(-a, b), max(-b, a)); }
// --- Coordinate Transforms ---
vec2 translate(vec2 p, vec2 t) { return p - t; }
vec2 rotateCCW(vec2 p, float a) {
return mat2(cos(a), sin(a), -sin(a), cos(a)) * p;
}
// --- Rendering Utilities ---
vec4 render(float d, vec3 color, float stroke) {
float anti = fwidth(d) * AA_WIDTH;
vec4 strokeLayer = vec4(vec3(0.05), 1.0 - smoothstep(-anti, anti, d - stroke));
vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d));
if (stroke < 0.0001) return colorLayer;
return vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}
float fillAA(float d, float px) { return smoothstep(px, -px, d); }
// --- Scene ---
float sceneDist(vec2 p) {
float t = iTime * ANIM_SPEED;
float c = sdCircle(translate(p, vec2(-0.6, 0.3)), 0.25);
float b = sdBox(translate(p, vec2(0.0, 0.3)), vec2(0.25, 0.18), 0.05);
vec2 tp = rotateCCW(translate(p, vec2(0.6, 0.3)), t * 0.5);
float tr = sdTriangle(tp, vec2(0.0, 0.25), vec2(-0.22, -0.12), vec2(0.22, -0.12));
float row1 = opUnion(c, opUnion(b, tr));
float c2 = sdCircle(translate(p, vec2(-0.5, -0.35)), 0.2);
float b2 = sdBox(translate(p, vec2(-0.3, -0.35)), vec2(0.15, 0.15), 0.0);
float smooth_demo = opSmoothUnion(c2, b2, SMOOTH_K);
float c3 = sdCircle(translate(p, vec2(0.15, -0.35)), 0.22);
float b3 = sdBox(translate(p, vec2(0.15, -0.35 + sin(t) * 0.15)), vec2(0.3, 0.08), 0.0);
float sub_demo = opSubtract(b3, c3);
float c4 = sdCircle(translate(p, vec2(0.65, -0.35)), 0.2);
float b4 = sdBox(translate(p, vec2(0.65, -0.35 + sin(t + 1.0) * 0.15)), vec2(0.3, 0.08), 0.0);
float xor_demo = opXor(b4, c4);
float row2 = opUnion(smooth_demo, opUnion(sub_demo, xor_demo));
return opUnion(row1, row2);
}
// --- Main Function ---
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
float px = 2.0 / iResolution.y;
float d = sceneDist(p);
vec3 bgCol = vec3(0.15, 0.15, 0.18) + 0.05 * p.y;
bgCol *= 1.0 - 0.3 * length(p);
vec3 col = (d > 0.0) ? vec3(0.9, 0.6, 0.3) : vec3(0.4, 0.7, 1.0);
col *= 1.0 - exp(-10.0 * abs(d));
col *= 0.8 + 0.2 * cos(CONTOUR_FREQ * d);
col = mix(col, vec3(1.0), smoothstep(1.5 * px, 0.0, abs(d) - 0.002));
col = mix(bgCol, col, 0.85);
// Uncomment to switch to solid rendering mode:
// vec3 shapeCol = vec3(0.2, 0.8, 0.6);
// float mask = fillAA(d, px);
// col = mix(bgCol, shapeCol, mask);
col = pow(col, vec3(1.0 / 2.2));
fragColor = vec4(col, 1.0);
}
Common Variants
Variant 1: Solid Fill + Stroke Mode
vec3 shapeColor = vec3(0.32, 0.56, 0.53);
float strokeW = 0.015;
vec4 shape = render(d, shapeColor, strokeW);
vec3 col = bgCol;
col = mix(col, shape.rgb, shape.a);
Variant 2: Multi-Layer CSG Illustration
float a = sdEllipse(p, vec2(0.0, 0.16), 0.25, 0.25);
float b = sdEllipse(p, vec2(0.0, -0.03), 0.8, 0.35);
float body = opIntersect(a, b);
vec4 layer1 = render(body, vec3(0.32, 0.56, 0.53), fwidth(body) * 2.0);
float handle = sdLine(p, vec2(0.0, 0.05), vec2(0.0, -0.42), 0.01);
float arc = sdCircle(translate(p, vec2(-0.04, -0.42)), 0.04);
float arcInner = sdCircle(translate(p, vec2(-0.04, -0.42)), 0.03);
handle = opUnion(handle, opSubtract(arcInner, arc));
vec4 layer0 = render(handle, vec3(0.4, 0.3, 0.28), STROKE_WIDTH);
vec3 col = bgCol;
col = mix(col, layer0.rgb, layer0.a);
col = mix(col, layer1.rgb, layer1.a);
Variant 3: Hexagonal Grid Tiling
vec4 hexagon(vec2 p) {
vec2 q = vec2(p.x * 2.0 * 0.5773503, p.y + p.x * 0.5773503);
vec2 pi = floor(q);
vec2 pf = fract(q);
float v = mod(pi.x + pi.y, 3.0);
float ca = step(1.0, v);
float cb = step(2.0, v);
vec2 ma = step(pf.xy, pf.yx);
float e = dot(ma, 1.0 - pf.yx + ca*(pf.x+pf.y-1.0) + cb*(pf.yx-2.0*pf.xy));
p = vec2(q.x + floor(0.5 + p.y / 1.5), 4.0 * p.y / 3.0) * 0.5 + 0.5;
float f = length((fract(p) - 0.5) * vec2(1.0, 0.85));
return vec4(pi + ca - cb * ma, e, f);
}
#define HEX_SCALE 8.0
vec4 h = hexagon(HEX_SCALE * p + 0.5 * iTime);
vec3 col = 0.15 + 0.15 * hash1(h.xy + 1.2);
col *= smoothstep(0.10, 0.11, h.z);
col *= smoothstep(0.10, 0.11, h.w);
Variant 4: Organic Shapes (Polar SDF)
// Heart SDF
p.y -= 0.25;
float a = atan(p.x, p.y) / 3.141593;
float r = length(p);
float h = abs(a);
float d = (13.0*h - 22.0*h*h + 10.0*h*h*h) / (6.0 - 5.0*h);
// Pulse animation
float tt = mod(iTime, 1.5) / 1.5;
float ss = pow(tt, 0.2) * 0.5 + 0.5;
ss = 1.0 + ss * 0.5 * sin(tt * 6.2831 * 3.0) * exp(-tt * 4.0);
vec3 col = mix(bgCol, heartCol, smoothstep(-0.01, 0.01, d - r));
Variant 5: Bezier Curve SDF
vec3 solveCubic(float a, float b, float c) {
float p = b - a*a/3.0, p3 = p*p*p;
float q = a*(2.0*a*a - 9.0*b)/27.0 + c;
float d = q*q + 4.0*p3/27.0;
float offset = -a/3.0;
if (d >= 0.0) {
float z = sqrt(d);
vec2 x = (vec2(z,-z) - q) / 2.0;
vec2 uv = sign(x) * pow(abs(x), vec2(1.0/3.0));
return vec3(offset + uv.x + uv.y);
}
float v = acos(-sqrt(-27.0/p3)*q/2.0) / 3.0;
float m = cos(v), n = sin(v) * 1.732050808;
return vec3(m+m, -n-m, n-m) * sqrt(-p/3.0) + offset;
}
float sdBezier(vec2 A, vec2 B, vec2 C, vec2 p) {
B = mix(B + vec2(1e-4), B, step(1e-6, abs(B*2.0-A-C)));
vec2 a = B-A, b = A-B*2.0+C, c = a*2.0, d = A-p;
vec3 k = vec3(3.*dot(a,b), 2.*dot(a,a)+dot(d,b), dot(d,a)) / dot(b,b);
vec3 t = clamp(solveCubic(k.x, k.y, k.z), 0.0, 1.0);
vec2 pos = A+(c+b*t.x)*t.x; float dis = length(pos-p);
pos = A+(c+b*t.y)*t.y; dis = min(dis, length(pos-p));
pos = A+(c+b*t.z)*t.z; dis = min(dis, length(pos-p));
return dis * signBezier(A, B, C, p);
}
Extended 2D SDF Library
// === Extended 2D SDF Library ===
// Rounded Box with independent corner radii (vec4 r = top-right, bottom-right, top-left, bottom-left)
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
r.xy = (p.x > 0.0) ? r.xy : r.zw;
r.x = (p.y > 0.0) ? r.x : r.y;
vec2 q = abs(p) - b + r.x;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
}
// Oriented Box (from point a to point b with thickness th)
float sdOrientedBox(vec2 p, vec2 a, vec2 b, float th) {
float l = length(b - a);
vec2 d = (b - a) / l;
vec2 q = (p - (a + b) * 0.5);
q = mat2(d.x, -d.y, d.y, d.x) * q;
q = abs(q) - vec2(l, th) * 0.5;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
}
// Arc (sc = vec2(sin,cos) of aperture angle, ra = radius, rb = thickness)
float sdArc(vec2 p, vec2 sc, float ra, float rb) {
p.x = abs(p.x);
return ((sc.y * p.x > sc.x * p.y) ? length(p - sc * ra) : abs(length(p) - ra)) - rb;
}
// Pie / Sector (c = vec2(sin,cos) of aperture angle)
float sdPie(vec2 p, vec2 c, float r) {
p.x = abs(p.x);
float l = length(p) - r;
float m = length(p - c * clamp(dot(p, c), 0.0, r));
return max(l, m * sign(c.y * p.x - c.x * p.y));
}
// Ring (n = vec2(sin,cos) of aperture, r = radius, th = thickness)
float sdRing(vec2 p, vec2 n, float r, float th) {
p.x = abs(p.x);
float d = length(p);
// If within aperture angle
if (n.y * p.x > n.x * p.y) {
return abs(d - r) - th;
}
// Cap endpoints
return min(length(p - n * r), length(p + n * r)) - th;
}
// Moon shape
float sdMoon(vec2 p, float d, float ra, float rb) {
p.y = abs(p.y);
float a = (ra * ra - rb * rb + d * d) / (2.0 * d);
float b2 = ra * ra - a * a;
if (d * (p.x * rb * rb - p.y * a * rb * rb - a * b2) > 0.0)
return length(p - vec2(a, sqrt(max(b2, 0.0))));
return max(length(p) - ra, -(length(p - vec2(d, 0.0)) - rb));
}
// Heart (approximate)
float sdHeart(vec2 p) {
p.x = abs(p.x);
if (p.y + p.x > 1.0)
return sqrt(dot(p - vec2(0.25, 0.75), p - vec2(0.25, 0.75))) - sqrt(2.0) / 4.0;
return sqrt(min(dot(p - vec2(0.0, 1.0), p - vec2(0.0, 1.0)),
dot(p - 0.5 * max(p.x + p.y, 0.0), p - 0.5 * max(p.x + p.y, 0.0)))) *
sign(p.x - p.y);
}
// Vesica (lens shape)
float sdVesica(vec2 p, float w, float h) {
p = abs(p);
float b = sqrt(h * h + w * w * 0.25) / w;
return ((p.y - h) * b * w > p.x * b * h)
? length(p - vec2(0.0, h))
: length(p - vec2(-w * 0.5, 0.0)) - b;
}
// Egg shape
float sdEgg(vec2 p, float he, float ra, float rb) {
p.x = abs(p.x);
float r = (p.y < 0.0) ? ra : rb;
return length(vec2(p.x, p.y - clamp(p.y, -he, he))) - r;
}
// Equilateral Triangle
float sdEquilateralTriangle(vec2 p, float r) {
const float k = sqrt(3.0);
p.x = abs(p.x) - r;
p.y = p.y + r / k;
if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
p.x -= clamp(p.x, -2.0 * r, 0.0);
return -length(p) * sign(p.y);
}
// Pentagon
float sdPentagon(vec2 p, float r) {
const vec3 k = vec3(0.809016994, 0.587785252, 0.726542528);
p.x = abs(p.x);
p -= 2.0 * min(dot(vec2(-k.x, k.y), p), 0.0) * vec2(-k.x, k.y);
p -= 2.0 * min(dot(vec2(k.x, k.y), p), 0.0) * vec2(k.x, k.y);
p -= vec2(clamp(p.x, -r * k.z, r * k.z), r);
return length(p) * sign(p.y);
}
// Hexagon
float sdHexagon(vec2 p, float r) {
const vec3 k = vec3(-0.866025404, 0.5, 0.577350269);
p = abs(p);
p -= 2.0 * min(dot(k.xy, p), 0.0) * k.xy;
p -= vec2(clamp(p.x, -k.z * r, k.z * r), r);
return length(p) * sign(p.y);
}
// Octagon
float sdOctagon(vec2 p, float r) {
const vec3 k = vec3(-0.9238795325, 0.3826834323, 0.4142135623);
p = abs(p);
p -= 2.0 * min(dot(vec2(k.x, k.y), p), 0.0) * vec2(k.x, k.y);
p -= 2.0 * min(dot(vec2(-k.x, k.y), p), 0.0) * vec2(-k.x, k.y);
p -= vec2(clamp(p.x, -k.z * r, k.z * r), r);
return length(p) * sign(p.y);
}
// Star (n-pointed, m = inner radius ratio)
float sdStar(vec2 p, float r, int n, float m) {
float an = 3.141593 / float(n);
float en = 3.141593 / m;
vec2 acs = vec2(cos(an), sin(an));
vec2 ecs = vec2(cos(en), sin(en));
float bn = mod(atan(p.x, p.y), 2.0 * an) - an;
p = length(p) * vec2(cos(bn), abs(sin(bn)));
p -= r * acs;
p += ecs * clamp(-dot(p, ecs), 0.0, r * acs.y / ecs.y);
return length(p) * sign(p.x);
}
// Quadratic Bezier curve SDF
float sdBezier(vec2 pos, vec2 A, vec2 B, vec2 C) {
vec2 a = B - A;
vec2 b = A - 2.0 * B + C;
vec2 c = a * 2.0;
vec2 d = A - pos;
float kk = 1.0 / dot(b, b);
float kx = kk * dot(a, b);
float ky = kk * (2.0 * dot(a, a) + dot(d, b)) / 3.0;
float kz = kk * dot(d, a);
float res = 0.0;
float p2 = ky - kx * kx;
float q = kx * (2.0 * kx * kx - 3.0 * ky) + kz;
float h = q * q + 4.0 * p2 * p2 * p2;
if (h >= 0.0) {
h = sqrt(h);
vec2 x = (vec2(h, -h) - q) / 2.0;
vec2 uv2 = sign(x) * pow(abs(x), vec2(1.0 / 3.0));
float t = clamp(uv2.x + uv2.y - kx, 0.0, 1.0);
res = dot(d + (c + b * t) * t, d + (c + b * t) * t);
} else {
float z = sqrt(-p2);
float v = acos(q / (p2 * z * 2.0)) / 3.0;
float m2 = cos(v);
float n2 = sin(v) * 1.732050808;
vec3 t = clamp(vec3(m2 + m2, -n2 - m2, n2 - m2) * z - kx, 0.0, 1.0);
res = min(dot(d + (c + b * t.x) * t.x, d + (c + b * t.x) * t.x),
dot(d + (c + b * t.y) * t.y, d + (c + b * t.y) * t.y));
}
return sqrt(res);
}
// Parabola
float sdParabola(vec2 pos, float k) {
pos.x = abs(pos.x);
float ik = 1.0 / k;
float p2 = ik * (pos.y - 0.5 * ik) / 3.0;
float q = 0.25 * ik * ik * pos.x;
float h = q * q - p2 * p2 * p2;
float r = sqrt(abs(h));
float x = (h > 0.0) ?
pow(q + r, 1.0 / 3.0) + pow(abs(q - r), 1.0 / 3.0) * sign(p2) :
2.0 * cos(atan(r, q) / 3.0) * sqrt(p2);
return length(pos - vec2(x, k * x * x)) * sign(pos.x - x);
}
// Cross shape
float sdCross(vec2 p, vec2 b, float r) {
p = abs(p); p = (p.y > p.x) ? p.yx : p.xy;
vec2 q = p - b;
float k = max(q.y, q.x);
vec2 w = (k > 0.0) ? q : vec2(b.y - p.x, -k);
return sign(k) * length(max(w, 0.0)) + r;
}
2D SDF Modifiers
// === 2D SDF Modifiers ===
// Round any 2D SDF
float opRound2D(float d, float r) { return d - r; }
// Create annular (ring) version of any 2D SDF
float opAnnular2D(float d, float r) { return abs(d) - r; }
// Repeat a 2D SDF in a grid
vec2 opRepeat2D(vec2 p, float s) { return mod(p + s * 0.5, s) - s * 0.5; }
// Mirror across arbitrary 2D direction
vec2 opMirror2D(vec2 p, vec2 dir) {
return p - 2.0 * dir * max(dot(p, dir), 0.0);
}
Performance & Composition Tips
Performance:
- In polygon SDFs, compare squared distances first; use a single
sqrtat the end - For simple scenes, use fixed
px = 2.0/iResolution.yinstead offwidth(d); usefwidthwhen coordinate scaling is involved - For many primitives, spatially partition and skip distant ones early
- Supersampling (2x2/3x3) only for offline rendering; for real-time, single-pixel AA with
smoothstep/fwidthis sufficient - For 2D soft shadow marching, use adaptive step size
dt += max(1.0, abs(sd))
Composition:
- SDF + Noise:
d += noise(p * 10.0 + iTime) * 0.05to create organic edges - SDF + 2D Lighting: cone marching for soft shadows, query occlusion via
sceneDist() - SDF + Normal Mapping: finite differences for normals + Blinn-Phong lighting to simulate bump effects
- SDF + Domain Repetition:
fract/modfor infinite repetition,floorfor cell ID - SDF + Animation: parameters driven by
sin/cosperiodic motion,expdecay,modlooping
Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in reference