Files
skills/shader-dev/reference/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

28 KiB

2D SDF Detailed Reference

This file contains the complete step-by-step tutorial, mathematical derivations, detailed explanations, and advanced usage for SKILL.md.

Prerequisites

  • GLSL Basics: uniforms, varyings, built-in functions (length, dot, clamp, mix, smoothstep, step, sign, abs, max, min)
  • Vector Math: 2D vector operations, geometric meaning of dot and cross products
  • Coordinate Systems: conversion from screen coordinates to normalized device coordinates (NDC), aspect ratio correction
  • Signed Distance Field Concept: the function returns the signed distance to the shape boundary — negative inside, zero on the boundary, positive outside

Core Principles in Detail

The core idea of 2D SDF: for each pixel on screen, compute its shortest signed distance d to the target shape boundary.

  • d < 0: pixel is inside the shape
  • d = 0: pixel is exactly on the boundary
  • d > 0: pixel is outside the shape

Once you have the distance value d, use functions like smoothstep and clamp to map it to color/opacity, enabling:

  • Fill: color when d < 0
  • Anti-aliased edges: smoothstep(-aa, aa, d) for sub-pixel smoothing at the boundary
  • Stroke: apply smoothstep again on abs(d) - strokeWidth
  • Boolean operations: min(d1, d2) = union, max(d1, d2) = intersection, max(-d1, d2) = subtraction

Key mathematical 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
Union:        d = min(d1, d2)
Intersection: d = max(d1, d2)
Subtraction:  d = max(-d1, d2)
Smooth union: d = mix(d2, d1, h) - k*h*(1-h),  h = clamp(0.5 + 0.5*(d2-d1)/k, 0, 1)

Implementation Steps in Detail

Step 1: Coordinate Normalization and Aspect Ratio Correction

What: Convert screen pixel coordinates to normalized coordinates centered at the screen center, with the y range of [-1, 1].

Why: Pixel coordinates depend on resolution. After normalization, SDF parameters (such as radius) have resolution-independent physical meaning. Dividing by iResolution.y (not .x) ensures correct aspect ratio so circles don't become ellipses.

Code:

// Method 1: Origin at center, y range [-1, 1] (most common, standard practice)
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;

// Method 2: If you need to work in pixel space (suitable for fixed pixel-size UI)
vec2 p = fragCoord.xy;
vec2 center = iResolution.xy * 0.5;

// Method 3: [0, 1] range normalization (requires manual aspect ratio handling)
vec2 uv = fragCoord.xy / iResolution.xy;

Step 2: Defining SDF Primitive Functions

What: Write basic primitive functions that return signed distances. Each function takes the current point p and shape parameters, and returns a float distance value.

Why: These are the atomic building blocks for all 2D SDF graphics. Encapsulating them as independent functions allows free combination, transformation, and reuse.

Code:

// ---- Circle ----
// The most basic SDF: distance from point to center minus radius
float sdCircle(vec2 p, float radius) {
    return length(p) - radius;
}

// ---- Rectangle (optional rounded corners) ----
// halfSize is half-width and half-height, radius is the corner radius
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;
}

// ---- Line Segment ----
// Line segment from start to end, with width
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;
}

// ---- Triangle (exact signed distance) ----
// Three vertices p0, p1, p2, only one sqrt needed
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;

    // Squared distance to each edge (projection + clamp)
    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));

    // Determine inside/outside using cross product sign
    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);
}

// ---- Ellipse (approximate) ----
// Simplified ellipse SDF based on scaled space
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

What: Combine two SDF distance values using min/max operations to achieve union, subtraction, and intersection of shapes.

Why: This is the most powerful capability of SDFs — building arbitrarily complex shapes from simple primitives. min takes the smaller of the two field values to produce a union (since smaller distance means "closer" to the shape interior); max takes the larger value for intersection; max(a, -b) inverts b's inside/outside and intersects for subtraction.

Code:

// Union: take the nearest shape
float opUnion(float d1, float d2) {
    return min(d1, d2);
}

// Intersection: overlapping region of both shapes
float opIntersect(float d1, float d2) {
    return max(d1, d2);
}

// Subtraction: carve d1 out of d2
float opSubtract(float d1, float d2) {
    return max(-d1, d2);
}

// Smooth union: produces a rounded transition at the junction, 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);
}

// XOR: non-overlapping region of both shapes
float opXor(float d1, float d2) {
    return min(max(-d1, d2), max(-d2, d1));
}

Step 4: Coordinate Transforms

What: Transform coordinates before computing the SDF so that shapes appear at desired positions and angles.

Why: SDF functions define shapes centered at the origin by default. By transforming the input coordinates (rather than the shape itself), you can freely place and rotate multiple primitives in the scene without affecting the mathematical properties of the distance field.

Code:

// Translation: move the coordinate origin to position t
vec2 translate(vec2 p, vec2 t) {
    return p - t;
}

// Counter-clockwise rotation
vec2 rotateCCW(vec2 p, float angle) {
    mat2 m = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
    return p * m;
}

// Usage example: translate then rotate
float d = sdBox(rotateCCW(translate(p, vec2(0.5, 0.3)), iTime), vec2(0.2), 0.05);

Step 5: Distance Field Visualization and Rendering

What: Convert the SDF distance value to final color output. Includes fill, anti-aliasing, stroke, contour lines, and other visualization methods.

Why: The distance value itself is just a scalar that needs a mapping strategy to become a visual effect. smoothstep creates sub-pixel smooth transitions at the boundary, avoiding aliasing from hard edges. The fwidth function uses screen-space derivatives to automatically calculate pixel width, achieving resolution-independent anti-aliasing.

Code:

// ---- Method 1: clamp for simple alpha (most basic) ----
float t = clamp(d, 0.0, 1.0);
vec4 shapeColor = vec4(color, 1.0 - t);

// ---- Method 2: smoothstep anti-aliasing (recommended general approach) ----
// aa controls edge softness, typical value is pixel size px = 2.0/iResolution.y
float px = 2.0 / iResolution.y;                      // Adjustable: anti-aliasing width
float mask = smoothstep(px, -px, d);                  // 1.0 inside, 0.0 outside
vec3 col = mix(backgroundColor, shapeColor, mask);

// ---- Method 3: fwidth adaptive anti-aliasing (suitable for zooming scenes) ----
float anti = fwidth(d) * 1.0;                         // Adjustable: multiplier, larger = softer edges
float mask = 1.0 - smoothstep(-anti, anti, d);

// ---- Method 4: Classic distance field debug visualization ----
vec3 col = (d > 0.0) ? vec3(0.9, 0.6, 0.3)           // Outside: orange
                      : vec3(0.65, 0.85, 1.0);        // Inside: blue
col *= 1.0 - exp(-12.0 * abs(d));                     // Distance falloff
col *= 0.8 + 0.2 * cos(120.0 * d);                    // Contour lines, 120.0 adjustable: line density
col = mix(col, vec3(1.0), smoothstep(1.5*px, 0.0, abs(d) - 0.002)); // Zero contour highlight

Step 6: Stroke and Border Rendering

What: Use the absolute value of the distance field to extract the shape's outline, or render inner/outer borders separately.

Why: Strokes are a natural byproduct of SDFs — abs(d) gives unsigned distance, and subtracting the stroke width yields the "stroke shape" SDF. Unlike rasterized strokes that require geometry expansion, SDF strokes need only one line of math.

Code:

// ---- Fill mask ----
float fillMask(float d) {
    return clamp(-d, 0.0, 1.0);
}

// ---- Stroke rendering (fwidth adaptive) ----
// stroke is the stroke width (in distance field units)
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);
}

// ---- Inner border mask ----
float innerBorderMask(float d, float width) {
    return clamp(d + width, 0.0, 1.0) - clamp(d, 0.0, 1.0);
}

// ---- Outer border mask ----
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

What: Render multiple SDF shapes as layers with alpha channels, then blend them back-to-front using mix.

Why: Complex 2D scenes typically contain backgrounds, multiple shapes, strokes, and other visual layers. Rendering each SDF as an independent RGBA layer and compositing them layer by layer with standard alpha blending (mix(bottom, top, top.a)) is both intuitive and gives precise control over stacking order.

Code:

// Background layer
vec3 bgColor = vec3(1.0, 0.8, 0.7 - 0.07 * p.y) * (1.0 - 0.25 * length(p));

// Shape layer 1
float d1 = sdCircle(translate(p, pos1), 0.3);
vec4 layer1 = renderShape(d1, vec3(0.9, 0.3, 0.2), 0.02);

// Shape layer 2
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);   // Overlay shape 1
col = mix(col, layer2.rgb, layer2.a);   // Overlay shape 2

fragColor = vec4(col, 1.0);

Variant Detailed Descriptions

Variant 1: Solid Fill + Stroke Mode

Difference from the basic version: Instead of showing distance field debug colors, renders solid shapes with clean strokes, suitable for UI and icons.

Key modified code:

// Replace the distance field visualization section
vec3 shapeColor = vec3(0.32, 0.56, 0.53);
float strokeW = 0.015;   // Adjustable: stroke width
vec4 shape = render(d, shapeColor, strokeW);

vec3 col = bgCol;
col = mix(col, shape.rgb, shape.a);

Variant 2: Multi-Layer CSG Illustration

Difference from the basic version: Combines multiple SDF primitives through boolean operations into complex patterns (e.g., an umbrella, a logo), with each layer independently colored and composited layer by layer. Suitable for 2D illustrations and icon construction.

Key modified code:

// Build the body (ellipse intersection)
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);

// Build the handle (line segment + arc subtraction)
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);

// Composite
vec3 col = bgCol;
col = mix(col, layer0.rgb, layer0.a);
col = mix(col, layer1.rgb, layer1.a);

Variant 3: Hexagonal Grid Tiling

Difference from the basic version: Uses non-orthogonal coordinate system domain repetition to tile SDFs across the screen, with each cell having an independent ID for differentiated coloring. Suitable for background textures and geometric patterns.

Key modified code:

// Hexagonal grid function: returns (cellID.xy, edge distance, center distance)
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);
}

// Usage
#define HEX_SCALE 8.0          // Adjustable: grid density
vec4 h = hexagon(HEX_SCALE * p + 0.5 * iTime);
vec3 col = 0.15 + 0.15 * hash1(h.xy + 1.2);          // Different gray per cell
col *= smoothstep(0.10, 0.11, h.z);                   // Edge lines
col *= smoothstep(0.10, 0.11, h.w);                   // Center falloff

Variant 4: Organic Shapes (Polar Coordinate SDF)

Difference from the basic version: Uses polar coordinates (atan, length) to define shape boundary functions, enabling creation of hearts, petals, stars, and other non-polygonal organic shapes. Supports pulsing animations.

Key modified code:

// Heart SDF (polar coordinate algebraic curve)
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);  // Adjustable: sin frequency controls pulse count

// Rendering
vec3 col = mix(bgCol, heartCol, smoothstep(-0.01, 0.01, d - r));

Variant 5: Bezier Curve SDF

Difference from the basic version: Computes the exact signed distance from a point to a quadratic Bezier curve by solving a cubic equation (Cardano's formula). Suitable for curved text, path rendering, and similar scenarios.

Key modified code:

// Cubic equation solver (Cardano's formula)
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;
}

// Bezier SDF (three control points A, B, C)
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);   // signBezier uses barycentric coordinates to determine sign
}

Performance Optimization in Detail

1. Reducing sqrt Calls

In polygon SDFs (such as triangles), by comparing squared distance values first and only taking sqrt on the minimum distance at the end, multiple sqrt calls are reduced to one. This is the core optimization idea behind the triangle SDF implementation.

// Bad: sqrt on every edge
float d0 = length(v0 - e0 * h0);
float d1 = length(v1 - e1 * h1);
// Good: compare dot(v,v) squares, one sqrt at the end
float d0 = dot(proj0, proj0);
float d1 = dot(proj1, proj1);
return -sqrt(min(d0, d1)) * sign(...);

2. fwidth vs Fixed Pixel Width

fwidth(d) invokes screen-space partial derivatives. In simple scenes, a fixed px = 2.0/iResolution.y can replace it to reduce GPU derivative computation overhead. However, in scenes with coordinate scaling/distortion (such as the hexagonal grid's pos *= 1.2 + 0.15*length(pos)), fwidth must be used to ensure correct anti-aliasing width.

3. Avoiding Excessive Boolean Operation Nesting

Large amounts of min/max nesting are correct but computing distances for all primitives per pixel per frame can be expensive. You can skip distant primitives by checking rough bounding boxes:

// Only compute precisely when near the shape
if (length(p - shapeCenter) < shapeRadius + margin) {
    d = opUnion(d, sdComplexShape(p));
}

4. Supersampling AA Trade-off

Multiple samples (e.g., 2x2 supersampling) yield higher quality anti-aliasing but multiply the fragment shader computation by 4:

#define AA 2  // Adjustable: 1 = no supersampling, 2 = 4x, 3 = 9x
for (int m = 0; m < AA; m++)
for (int n = 0; n < AA; n++) {
    vec2 off = vec2(m, n) / float(AA);
    // ... computation ...
    tot += col;
}
tot /= float(AA * AA);

For most real-time scenes, single-pixel AA with smoothstep or fwidth is sufficient. Supersampling is mainly for offline rendering or showcase scenes.

5. Step Size Optimization for 2D Soft Shadows

In cone marching 2D soft shadows, use max(1.0, abs(sd)) instead of a fixed step size to take large leaps in open areas and small precise steps near shapes. Typically 64 steps can cover a large scene:

dt += max(1.0, abs(sd));  // Adaptive step size
if (dt > dl) break;       // Early exit after reaching the light source

Combination Suggestions in Detail

1. SDF + Noise Textures

Adding noise values to the distance field creates dissolve, erosion, and organic edge effects:

float d = sdCircle(p, 0.4);
d += noise(p * 10.0 + iTime) * 0.05;  // Organic jittery edges

2. SDF + 2D Lighting and Shadows

Cone marching based on the distance field implements real-time soft shadows and multi-light lighting for 2D scenes. The distance field provides "scene query" capability, using sceneDist() during ray marching to check occlusion:

// 2D soft shadow (see 4dfXDn for full implementation)
float shadow(vec2 p, vec2 lightPos, float radius) {
    vec2 dir = normalize(lightPos - p);
    float dl = length(p - lightPos);
    float lf = radius * dl;
    float dt = 0.01;
    for (int i = 0; i < 64; i++) {
        float sd = sceneDist(p + dir * dt);
        if (sd < -radius) return 0.0;
        lf = min(lf, sd / dt);
        dt += max(1.0, abs(sd));
        if (dt > dl) break;
    }
    lf = clamp((lf*dl + radius) / (2.0*radius), 0.0, 1.0);
    return smoothstep(0.0, 1.0, lf);
}

3. SDF + Normal Mapping / Bump Mapping

By computing normals via finite differences on the distance field, then applying standard lighting models, you can simulate 3D bump/highlight effects on 2D SDFs (as done in the DVD Bounce shader):

vec2 e = vec2(0.8, 0.0) / iResolution.y;
float fx = sceneDist(p) - sceneDist(p + e);
float fy = sceneDist(p) - sceneDist(p + e.yx);
vec3 nor = normalize(vec3(fx, fy, e.x / 0.1));  // 0.1 = bump factor, adjustable
// Standard Blinn-Phong lighting
vec3 lig = normalize(vec3(1.0, 2.0, 2.0));
float dif = clamp(dot(lig, nor), 0.0, 1.0);

4. SDF + Domain Repetition (Spatial Tiling)

Use fract or mod on coordinates for infinite repetition; use floor to get cell IDs for differentiated coloring. Suitable for background patterns, particle arrays, etc.:

vec2 cellSize = vec2(0.5);
vec2 cellID = floor(p / cellSize);
vec2 cellP = fract(p / cellSize) - 0.5;        // Local coordinate within cell
float d = sdCircle(cellP, 0.15 + 0.05 * sin(iTime + cellID.x * 3.0));

5. SDF + Animation

Distance field parameters (position, radius, rotation angle) naturally support continuous animation. Combine with sin/cos periodic motion, exp decay, mod looping, and other time functions:

// Bouncing
float y = abs(sin(iTime * 3.0)) * 0.5;
float d = sdCircle(translate(p, vec2(0.0, y)), 0.2);

// Pulse scaling
float pulse = 1.0 + 0.1 * sin(iTime * 6.28 * 2.0) * exp(-mod(iTime, 1.0) * 4.0);
float d = sdCircle(p / pulse, 0.3) * pulse;

// Rotation
float d = sdBox(rotateCCW(p, iTime), vec2(0.2), 0.03);

Extended 2D SDF Primitives Reference

sdRoundedBox — Rounded Box with Independent Corner Radii

Signature: float sdRoundedBox(vec2 p, vec2 b, vec4 r)

  • p: query point
  • b: half-size of the box
  • r: corner radii as vec4(top-right, bottom-right, top-left, bottom-left)

Selects the appropriate corner radius based on the quadrant of p, then computes a standard rounded box distance. Useful for UI elements where each corner needs a different rounding.

sdOrientedBox — Oriented Box

Signature: float sdOrientedBox(vec2 p, vec2 a, vec2 b, float th)

  • p: query point
  • a, b: endpoints defining the box's center axis
  • th: thickness (full width perpendicular to the axis)

Constructs a local coordinate frame aligned with segment a-to-b, then evaluates a standard box SDF. Useful for drawing thick line-like rectangles at arbitrary angles without manual rotation.

sdArc — Arc

Signature: float sdArc(vec2 p, vec2 sc, float ra, float rb)

  • p: query point
  • sc: vec2(sin, cos) of the half-aperture angle
  • ra: arc radius
  • rb: arc thickness

Computes distance to an arc segment. The aperture is symmetric about the y-axis. Combines angular clamping with radial distance.

sdPie — Pie / Sector

Signature: float sdPie(vec2 p, vec2 c, float r)

  • p: query point
  • c: vec2(sin, cos) of the half-aperture angle
  • r: radius

Returns the signed distance to a filled pie-slice (sector) shape. The sector is symmetric about the y-axis.

sdRing — Ring

Signature: float sdRing(vec2 p, vec2 n, float r, float th)

  • p: query point
  • n: vec2(sin, cos) of the half-aperture angle
  • r: ring radius
  • th: ring thickness

Similar to sdArc but with capped endpoints and full ring behavior within the aperture.

sdMoon — Moon Shape

Signature: float sdMoon(vec2 p, float d, float ra, float rb)

  • p: query point
  • d: distance between circle centers
  • ra: radius of outer circle
  • rb: radius of inner (subtracted) circle

Creates a crescent/moon shape by subtracting one circle from another. The two circles are offset by distance d along the x-axis.

sdHeart — Heart (Approximate)

Signature: float sdHeart(vec2 p)

  • p: query point (centered at origin, roughly unit scale)

An approximate heart SDF composed of two geometric regions stitched together. The shape extends roughly from (0,0) to (0,1) vertically.

sdVesica — Vesica / Lens Shape

Signature: float sdVesica(vec2 p, float w, float h)

  • p: query point
  • w: width of the vesica
  • h: height of the vesica

A lens-shaped figure (vesica piscis) formed by the intersection of two circles. Symmetric about both axes.

sdEgg — Egg Shape

Signature: float sdEgg(vec2 p, float he, float ra, float rb)

  • p: query point
  • he: half-height of the straight section
  • ra: radius at bottom
  • rb: radius at top

Produces an egg-like shape with different radii at top and bottom, connected by a straight vertical section.

sdEquilateralTriangle — Equilateral Triangle

Signature: float sdEquilateralTriangle(vec2 p, float r)

  • p: query point
  • r: side length / scale

An exact SDF for an equilateral triangle centered at the origin using symmetry folding.

sdPentagon — Pentagon

Signature: float sdPentagon(vec2 p, float r)

  • p: query point
  • r: circumscribed radius

Regular pentagon SDF using mirror-fold operations along pentagon edge normals. The constants encode cos/sin of 72-degree angles.

sdHexagon — Hexagon

Signature: float sdHexagon(vec2 p, float r)

  • p: query point
  • r: circumscribed radius

Regular hexagon SDF. Constants encode cos(30), sin(30), and tan(30). Uses a single mirror fold.

sdOctagon — Octagon

Signature: float sdOctagon(vec2 p, float r)

  • p: query point
  • r: circumscribed radius

Regular octagon SDF. Uses two mirror folds at 22.5-degree and 67.5-degree angles.

sdStar — N-Pointed Star

Signature: float sdStar(vec2 p, float r, int n, float m)

  • p: query point
  • r: outer radius
  • n: number of points
  • m: inner radius ratio (controls pointiness; typical range 2.0-6.0)

A general n-pointed star using angular repetition (mod(atan(...))) and edge projection. Higher m values produce sharper, thinner points.

sdBezier (Extended) — Quadratic Bezier Curve SDF

Signature: float sdBezier(vec2 pos, vec2 A, vec2 B, vec2 C)

  • pos: query point
  • A, B, C: control points of the quadratic Bezier

An alternative Bezier SDF formulation that solves for the closest point on the curve using the cubic formula. Returns unsigned distance (no sign). Note the different parameter order from the Variant 5 version.

sdParabola — Parabola

Signature: float sdParabola(vec2 pos, float k)

  • pos: query point
  • k: curvature coefficient (y = k * x^2)

Signed distance to a parabola. Uses a cubic root solution to find the closest point on the curve.

sdCross — Cross Shape

Signature: float sdCross(vec2 p, vec2 b, float r)

  • p: query point
  • b: half-extents of each arm (b.x = length, b.y = width)
  • r: corner rounding offset

A plus/cross shape formed by the union of two perpendicular rectangles, with an optional rounding parameter.

2D SDF Modifiers Reference

opRound2D — Rounding Modifier

Signature: float opRound2D(float d, float r)

Subtracts r from any SDF, effectively expanding the shape boundary outward by r and rounding all corners/edges. Apply to any existing SDF to add uniform rounding.

opAnnular2D — Annular (Hollowing) Modifier

Signature: float opAnnular2D(float d, float r)

Takes the absolute value of the distance and subtracts thickness r, converting any filled shape into a ring/outline version with wall thickness 2*r. Stackable: applying twice creates concentric rings.

opRepeat2D — Grid Repetition

Signature: vec2 opRepeat2D(vec2 p, float s)

Applies mod to fold coordinates into a repeating grid cell of size s. Apply to p before passing to any SDF to create infinite tiling. Use floor(p / s) to obtain cell IDs for per-cell variation.

opMirror2D — Arbitrary Mirror

Signature: vec2 opMirror2D(vec2 p, vec2 dir)

Mirrors coordinates across a line through the origin with direction dir (should be normalized). Any point on the negative side of the line is reflected to the positive side, effectively creating bilateral symmetry along any arbitrary axis.