Files
skills/shader-dev/techniques/sdf-2d.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

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 sqrt at the end
  • For simple scenes, use fixed px = 2.0/iResolution.y instead of fwidth(d); use fwidth when 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/fwidth is 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.05 to 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/mod for infinite repetition, floor for cell ID
  • SDF + Animation: parameters driven by sin/cos periodic motion, exp decay, mod looping

Further Reading

Full step-by-step tutorials, mathematical derivations, and advanced usage in reference