SDF โ€” Agent Context for Generative Signed-Distance Graphics

Purpose: a compact, copy-paste-ready mental model for an agent (or human) that generates 2D/3D graphics by combining SDF primitives with boolean/domain operations. This is the "skin" that turns the scattered references (Inigo Quilez's distance-function articles, the ShaderToy SDF playlist) into a working recipe. Drop this into any graphics-generation prompt.

Sources synthesized here:


1. Core idea (the skin)

An SDF map(p) returns the distance from point p to the nearest surface.

  • d < 0 โ†’ inside
  • d = 0 โ†’ on surface
  • d > 0 โ†’ outside

Rendering = raymarch: step along a ray by d each time until |d| โ‰ˆ 0.

float map(vec3 p){ return <combined primitives + ops>; }
vec3 ro = ...; vec3 rd = ...;   // ray origin, ray direction
float t = 0.0;
for(int i=0;i<96;i++){
  vec3 p = ro + rd*t;
  float d = map(p);
  if(d < 0.001) break;   // hit
  if(t > 50.0) break;    // miss
  t += d;                 // safe step (Lipschitz-1)
}

Rule of thumb: only use t += d when every operation in map() is distance-preserving (exact primitives + exact booleans). Domain repetitions, twists, and non-uniform scaling break Lipschitz-1 โ†’ divide d by a factor.


2. Primitives (copy these)

3D (from iQ distfunctions)

float sdSphere(vec3 p, float r){ return length(p) - r; }

float sdBox(vec3 p, vec3 b){
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

float sdTorus(vec3 p, vec2 t){
  vec2 q = vec2(length(p.xz) - t.x, p.y);
  return length(q) - 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, float h, float r){
  vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

float sdPlane(vec3 p, vec3 n, float h){ return dot(p,n) + h; }

2D (from iQ distfunctions2d)

float sdCircle(vec2 p, float r){ return length(p) - r; }
float sdBox(vec2 p, vec2 b){
  vec2 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,q.y),0.0);
}
float sdSegment(vec2 p, vec2 a, vec2 b){
  vec2 pa = p-a, ba = b-a;
  float h = clamp(dot(pa,ba)/dot(ba,ba), 0.0, 1.0);
  return length(pa - ba*h);
}

Smooth versions (smin) are what make SDF art look organic โ€” see ยง3.


3. Operations (combine primitives)

Boolean (exact, distance-preserving)

float opUnion(float a, float b){ return min(a,b); }
float opSubtract(float a, float b){ return max(-a, b); }     // remove b from a
float opIntersect(float a, float b){ return max(a,b); }

Smooth union (organic blends)

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);
}
// opSmoothUnion(a,b,k) = smin(a,b,k)

Domain ops (warp space BEFORE evaluating)

// repetition (infinite grid) โ€” divide d by cell count to stay marchable
vec3 q = p;
q.x = mod(p.x+0.5*s, s) - 0.5*s;   // repeat every s along x

// rotation about Y
mat2 rot(float a){ float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }

// twist (non-uniform โ†’ reduce step size: t += d*0.5)
float twist(vec3 p){ float c=cos(p.y),s=sin(p.y); mat2 m=mat2(c,-s,s,c);
  vec3 q=vec3(m*p.xz, p.y); return map(q); }

Skin memory: every domain op that isn't a pure rotation/translation needs a step-size divisor (t += d * 0.5 or smaller) or the ray overshoots.


4. The workbench recepy (recombine)

GoalCombine
Blobby creaturesmin(sphere A, sphere B, k) + smin(.., capsule, k)
Mechanical partopSubtract(box, cylinder) (bolt holes)
Endless fieldmod() repetition of one primitive
Ring / donutsdTorus directly
Double-orbtwo sdSpheres via opUnion/smin (see ShaderToy 4lyfzw)
Box-with-holeopSubtract(sdBox, sdSphere)
Twisted columntwist(p) โ†’ sdBox

ShaderToy studied here: 4lyfzw (double-sphere), Ml3fWj (sdBox field), Mt3BDj (torus). The MlcBDj/ltcfDj/lt3BW2/Xds3zN pages are untitled stubs on ShaderToy (no code) โ€” treat as empty references.


5. Lighting / shading (make it read as 3D)

vec3 calcNormal(vec3 p){
  vec2 e = vec2(0.001, 0.0);
  return normalize(vec3(
    map(p+e.xyy)-map(p-e.xyy),
    map(p+e.yxy)-map(p-e.yxy),
    map(p+e.yyx)-map(p-e.yyx)));
}
// Lambert + a rim term:
vec3 n = calcNormal(p);
float diff = clamp(dot(n, lightDir), 0.0, 1.0);
vec3 col = baseColor * (0.2 + 0.8*diff);

6. Drop-in agent instruction (paste into a generation prompt)

"Generate a raymarched SDF scene. Define map(p) from primitives (sdSphere, sdBox, sdTorus, sdCapsule) combined with exact booleans (opUnion/opSubtract/opIntersect) and smin for organic blends. Use domain repetition/mod() for fields and rot() for orientation. Keep all ops distance-preserving; if you twist or non-uniformly scale, divide the march step by ~0.5. Shade with a normal-based Lambert + rim. Output GLSL."


7. Why this "skill" helps an agent

  • Closures, not raw links: the agent gets primitives + ops + the one gotcha (Lipschitz step size) instead of a reading list.
  • Composable: any object = primitives ร— booleans ร— domain warps.
  • Portable: the ยง6 block is self-contained for a text-to-shader prompt.
Built with LogoFlowershow