
Learn Creative Coding (#33) - Drawing Shapes with Signed Distance Functions
Learn Creative Coding (#33) - Drawing Shapes with Signed Distance Functions

Last episode we built the bridge between JavaScript and GLSL -- uniforms. Resolution, time, mouse position. The three values that make a shader alive. We set up a reusable template, made pulsing rings, interactive glows, a breathing background. Good foundations.
But everything we drew was based on distance from a point. length(uv) gives you a circle. length(uv - mouse) gives you a circle around the mouse. And... that's kind of it? Rings, glows, gradients -- they're all variations of "how far from the center." If you want to draw a rectangle, a triangle, a star, a rounded box, a heart, literally anything that isn't a circle -- you need a different approach.
That approach is signed distance functions. SDFs. And once you get them, shaders stop being "fancy gradient generators" and start being a legitimate drawing tool. You can build anything.
What is a signed distance function?
Here's the concept. For any point in 2D space, a signed distance function returns a single number: the distance from that point to the nearest edge of a shape. But with a twist -- the sign tells you whether you're inside or outside.
- Positive = outside the shape
- Negative = inside the shape
- Zero = exactly on the boundary
That's it. One function, one number, and you know everything about that point's relationship to the shape. No polygon meshes, no fill algorithms, no edge cases (well, fewer edge cases). Just math.
Why is this so powerful for shaders? Because shaders already work per-pixel. For every pixel on screen, we already compute its position as uv. Now we just pass that uv to an SDF function, get back a distance value, and decide what color to show based on that distance. Inside the shape? One color. Outside? Another. Near the edge? Blend smoothly. That's anti-aliasing for free.
Let's start with the simplest SDF: the circle.
Circle SDF
A circle is defined by its center and radius. The SDF for a circle at the origin with radius r is:
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
That's the entire function. length(p) gives the distance from the origin to point p. Subtract the radius and you get:
- Points outside the circle:
length(p)>r, so the result is positive - Points inside:
length(p)<r, result is negative - Points on the edge:
length(p)==r, result is zero
Let's visualize it. First as a raw distance field -- every pixel shows its distance value as brightness:
precision mediump float;
uniform vec2 u_resolution;
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
float d = length(uv) - 0.3; // circle SDF, radius 0.3
// visualize raw distance: white = far outside, black = far inside
gl_FragColor = vec4(vec3(d), 1.0);
}
You'll see a gradient -- bright white far from the circle, fading to gray near the edge, and black inside. The edge itself (where d is exactly 0) sits at the transition from dark to light. It's not a sharp circle yet. It's a distance field. A topographic map of "how far from the shape."
Now let's turn it into a sharp shape using step():
precision mediump float;
uniform vec2 u_resolution;
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
float d = length(uv) - 0.3;
// hard edge: 0 inside, 1 outside
float shape = step(0.0, d);
gl_FragColor = vec4(vec3(shape), 1.0);
}
step(0.0, d) returns 0 when d < 0 (inside) and 1 when d >= 0 (outside). Black circle on white background. But look at the edge -- it's jagged. Aliased. Individual pixels are either 100% in or 100% out. No gradual transition.
The fix is smoothstep():
precision mediump float;
uniform vec2 u_resolution;
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
float d = length(uv) - 0.3;
// smooth edge: blend over a tiny distance range
float shape = smoothstep(0.0, 0.005, d);
gl_FragColor = vec4(vec3(shape), 1.0);
}
smoothstep(0.0, 0.005, d) creates a smooth transition from 0 to 1 over the range [0.0, 0.005]. That's about one pixel wide at typical resolutions. The edge is now smooth. Anti-aliased. And you didn't need to do anything clever -- the SDF already contains all the distance information, you just smoothed the threshold.
That 0.005 value depends on your coordinate scale. If your uv space spans -0.5 to 0.5 (like ours does with height normalization), then 0.005 is roughly one pixel. If you're working in a different coordinate space you'll need to adjust. A fancier approach uses fwidth(d) which automatically calculates the right smoothing width per-pixel, but for now, eyeballing it with 0.003 to 0.01 works fine.
Box SDF
A circle is one function. What about a rectangle? The SDF for an axis-aligned box centered at the origin:
float sdBox(vec2 p, vec2 halfSize) {
vec2 d = abs(p) - halfSize;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
This one is less obvious. Let me break it down.
abs(p) folds the space into the positive quadrant. Because the box is symmetric around the origin, we only need to think about one quarter of the space -- the math handles the other three quarters through symmetry. Same trick as folding a piece of paper to cut a symmetric shape.
abs(p) - halfSize gives you a signed distance in each axis. If the point is beyond the box edge on the X axis, d.x is positive. If it's inside, d.x is negative. Same for Y.
The tricky part is corners. When both d.x and d.y are positive (we're outside on both axes), the actual distance is length(d) -- the diagonal distance to the corner. When only one component is positive, the distance is that component. When both are negative (we're inside), the distance is the maximum (the least negative, which is closest to an edge).
length(max(d, 0.0)) handles the outside-corner case. min(max(d.x, d.y), 0.0) handles the inside case. Together they give a correct signed distance everywhere.
Let's see it in action:
precision mediump float;
uniform vec2 u_resolution;
float sdBox(vec2 p, vec2 halfSize) {
vec2 d = abs(p) - halfSize;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
float d = sdBox(uv, vec2(0.3, 0.2));
float shape = smoothstep(0.005, 0.0, d);
vec3 color = mix(vec3(0.05), vec3(0.9, 0.4, 0.2), shape);
gl_FragColor = vec4(color, 1.0);
}
Note the reversed smoothstep arguments -- smoothstep(0.005, 0.0, d) goes from 1 (inside) to 0 (outside). I flipped it so shape is 1 inside the box and 0 outside, which makes the mix() call more intuitive: mix from background color to shape color based on shape.
A 0.6 x 0.4 orange box on a near-black background. Smooth edges. Pure math. No vertices, no paths, no fill calls.
Rounded box
Want rounded corners? Just subtract a radius from the distance before thresholding:
float sdRoundedBox(vec2 p, vec2 halfSize, float radius) {
vec2 d = abs(p) - halfSize + radius;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius;
}
The + radius shrinks the box inward, then - radius at the end expands the distance field outward by the same amount. The net effect: corners that were sharp 90-degree angles are now rounded arcs with the given radius. Same technique works on any SDF -- subtract a constant from the result and you get rounded versions. It's called "offsetting" the distance field.
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
float d = sdRoundedBox(uv, vec2(0.3, 0.2), 0.05);
float shape = smoothstep(0.005, 0.0, d);
vec3 color = mix(vec3(0.05), vec3(0.3, 0.7, 0.5), shape);
gl_FragColor = vec4(color, 1.0);
}
Play with the radius value. At 0.0 it's a regular box. At 0.2 (equal to the smallest half-dimension), it becomes a stadium shape. Somewhere in between is the sweet spot for UI-style rounded rectangles.
Ring and outline
Here's a beautiful SDF trick. To turn any filled shape into an outline (ring), just take the absolute value of the distance before thresholding:
float d = sdCircle(uv, 0.3);
float ring = abs(d) - 0.01; // 0.01 = half the ring thickness
float shape = smoothstep(0.005, 0.0, ring);
abs(d) makes the distance zero at the boundary and positive everywhere else -- both inside AND outside. Subtracting a thickness value creates a thin band around the edge. This works for any SDF. Box outline? abs(sdBox(uv, size)) - thickness. Triangle outline? Same pattern. You never need a separate "stroke" function.
Let's draw concentric rings that shift over time:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
vec3 color = vec3(0.02, 0.02, 0.05);
for (int i = 0; i < 5; i++) {
float radius = 0.08 + float(i) * 0.08;
float d = sdCircle(uv, radius);
float ring = abs(d) - 0.003;
float shape = smoothstep(0.004, 0.0, ring);
// each ring gets a different hue
float hue = float(i) * 1.256 + u_time * 0.5;
vec3 ringColor = vec3(
sin(hue) * 0.5 + 0.5,
sin(hue + 2.094) * 0.5 + 0.5,
sin(hue + 4.189) * 0.5 + 0.5
);
color = mix(color, ringColor, shape);
}
gl_FragColor = vec4(color, 1.0);
}
Five concentric circles, each a thin outline with a shifting hue. The 2.094 and 4.189 offsets are the same 2*PI/3 spacing trick from episode 32 that creates smooth rainbow cycling. The whole thing hums gently as time advances.
Combining SDFs: boolean operations
This is where things get really interesting. SDFs combine with simple math operations:
Union (combine two shapes): min(sdfA, sdfB) -- for each point, take the closer shape. Result: the merged outline of both shapes.
Intersection (only where both shapes overlap): max(sdfA, sdfB) -- for each point, take the farther shape. Result: only the region inside both.
Subtraction (cut one shape from another): max(sdfA, -sdfB) -- negate B then intersect. Result: A with B's area removed.
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdBox(vec2 p, vec2 s) {
vec2 d = abs(p) - s;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
// two shapes
float circle = sdCircle(uv - vec2(-0.15, 0.0), 0.2);
float box = sdBox(uv - vec2(0.1, 0.0), vec2(0.18, 0.18));
// try different operations
float d;
// pick operation based on time (cycles every 9 seconds)
float phase = mod(u_time, 9.0);
if (phase < 3.0) {
d = min(circle, box); // union
} else if (phase < 6.0) {
d = max(circle, box); // intersection
} else {
d = max(circle, -box); // subtraction
}
float shape = smoothstep(0.005, 0.0, d);
vec3 bg = vec3(0.05);
vec3 fg = vec3(0.8, 0.3, 0.5);
vec3 color = mix(bg, fg, shape);
gl_FragColor = vec4(color, 1.0);
}
Watch it cycle. Union merges the circle and box into one blob. Intersection shows only the overlapping area -- a lens shape. Subtraction carves the box out of the circle, leaving a crescent. Three completely different shapes from the same two primitives, controlled by one function call each. This is constructive solid geometry (CSG) in 2D, and it runs at thousands of fps because it's literally just min and max per pixel.
Moving shapes is trivial too -- just subtract an offset from uv before passing it to the SDF. sdCircle(uv - vec2(0.2, 0.1), 0.15) draws a circle at position (0.2, 0.1). No transform matrices, no translate/rotate state management. You're just shifting the coordinate system.
Smooth blending
min() gives you hard union -- two shapes with sharp edges where they meet. But what if you want them to melt into each other? That's smooth minimum, and it's one of the most visually gorgeous SDF tricks:
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
The parameter k controls the blending radius. Higher k = smoother, more organic merging. At k = 0 it's identical to min(). At k = 0.3 shapes blob together like lava lamp fluid.
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
// two circles that move
vec2 p1 = vec2(sin(u_time * 0.7) * 0.2, cos(u_time * 0.5) * 0.15);
vec2 p2 = vec2(sin(u_time * 0.5 + 2.0) * 0.2, cos(u_time * 0.8) * 0.15);
float c1 = sdCircle(uv - p1, 0.12);
float c2 = sdCircle(uv - p2, 0.15);
// smooth blend
float d = smin(c1, c2, 0.15);
float shape = smoothstep(0.005, 0.0, d);
vec3 bg = vec3(0.03, 0.03, 0.06);
vec3 fg = vec3(0.4, 0.7, 0.9);
vec3 color = mix(bg, fg, shape);
gl_FragColor = vec4(color, 1.0);
}
Two circles floating around, melting into each other when they get close. The smin function handles the blending automatically based on proximity. When the circles are far apart, they look like two separate shapes. As they approach each other, a smooth bridge forms between them. When they overlap, they merge into one organic blob. All from that one function.
This is the technique behind every "metaball" effect you've ever seen. Those gooey, blobby physics simulations? SDFs with smooth minimum. Three lines of math.
Repetition
Want one circle? Write one SDF. Want infinite circles in a grid? Apply fract() to the coordinates:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
// repeat space every 0.2 units
float cellSize = 0.2;
vec2 cell = fract(uv / cellSize + 0.5) - 0.5;
cell *= cellSize;
// one circle SDF, but now in repeated space
float d = sdCircle(cell, 0.05 + sin(u_time + uv.x * 10.0) * 0.02);
float shape = smoothstep(0.005, 0.0, d);
vec3 bg = vec3(0.02);
vec3 fg = vec3(0.9, 0.5, 0.2);
vec3 color = mix(bg, fg, shape);
gl_FragColor = vec4(color, 1.0);
}
fract(uv / cellSize + 0.5) - 0.5 remaps the coordinates into repeating cells. Each cell goes from -0.5 to 0.5, and the SDF sees the same local coordinates in every cell. One circle becomes an infinte grid of circles. Multiply cell back by cellSize to restore the correct scale.
The sin(u_time + uv.x * 10.0) in the radius adds a wave effect -- circles pulse in a traveling wave across the grid. The uv.x part is the original (non-repeated) coordinate, so the wave is global even though the shapes are local. Mixing global and local coordinates is a common SDF pattern for adding variety to repeated elements.
But here's the catch with SDF repetition. When you combine repeated shapes with smin (smooth blending), only shapes in the same cell are aware of each other. Shapes in neighboring cells don't blend because each cell evaluates its SDF independently. To fix this, you'd need to check neighboring cells too -- sample the SDF in the current cell AND all 8 neighbors, then take the smooth minimum. It works but costs 9x more computation. For simple grids without blending, fract() is perfecly fine.
Building a scene
Let's combine everything into a complete SDF scene. Multiple shapes, boolean operations, smooth blending, coloring based on which shape is closest:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdBox(vec2 p, vec2 s) {
vec2 d = abs(p) - s;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
// animated positions
float t = u_time * 0.4;
vec2 p1 = vec2(sin(t) * 0.25, cos(t * 0.7) * 0.15);
vec2 p2 = vec2(cos(t * 0.6) * 0.2, sin(t * 0.9) * 0.2);
vec2 p3 = vec2(sin(t * 1.1 + 1.5) * 0.15, cos(t * 0.5 + 0.8) * 0.25);
// three shapes
float c1 = sdCircle(uv - p1, 0.1);
float c2 = sdCircle(uv - p2, 0.08);
float box = sdBox(uv - p3, vec2(0.07, 0.12));
// smooth blend all three
float d = smin(smin(c1, c2, 0.12), box, 0.1);
// color based on which shape is closest
vec3 col1 = vec3(0.9, 0.3, 0.3); // red
vec3 col2 = vec3(0.3, 0.9, 0.4); // green
vec3 col3 = vec3(0.3, 0.4, 0.9); // blue
// weight by inverse distance to each shape
float w1 = 1.0 / (abs(c1) + 0.01);
float w2 = 1.0 / (abs(c2) + 0.01);
float w3 = 1.0 / (abs(box) + 0.01);
float wTotal = w1 + w2 + w3;
vec3 shapeColor = (col1 * w1 + col2 * w2 + col3 * w3) / wTotal;
// edge glow
float glow = 0.003 / (abs(d) + 0.003);
glow = min(glow, 1.0);
vec3 glowColor = shapeColor * 1.5;
// compose
float shape = smoothstep(0.005, 0.0, d);
vec3 bg = vec3(0.02, 0.02, 0.04);
vec3 color = mix(bg, shapeColor * 0.6, shape);
color += glowColor * glow * 0.4;
gl_FragColor = vec4(color, 1.0);
}
Three shapes drifting around, smooth-blending when they get close, each with its own color. The inverse-distance weighting creates natural color transitions where shapes overlap -- the color blends between red, green, and blue based on proximity. The edge glow adds a soft neon outline. It looks like an organism under a microscope, or a lava lamp, or something alive. All from SDFs we defined in the last few minutes.
The color blending trick deserves explanation. For each pixel, we compute the distance to each individual shape. Then we weight the colors by 1.0 / (abs(distance) + epsilon). A pixel very close to shape 1 gets mostly color 1. A pixel equidistant from shapes 1 and 2 gets an even mix. The 0.01 epsilon prevents division by zero at the shape boundary. It's an approximation -- not physically correct blending -- but it looks natural and is very cheap to compute.
Rotation
To rotate an SDF, rotate the input coordinates:
vec2 rotate(vec2 p, float angle) {
float c = cos(angle);
float s = sin(angle);
return vec2(p.x * c - p.y * s, p.x * s + p.y * c);
}
Same rotation matrix from episode 13 (trig for artists). Apply it before the SDF:
float d = sdBox(rotate(uv, u_time * 0.5), vec2(0.2, 0.1));
A spinning rectangle. The SDF itself doesn't change. We just rotate the space it lives in. This is a general principle with SDFs -- instead of transforming the shape, you transform the coordinates. Want to move a shape? Subtract an offset. Want to rotate it? Rotate the input. Want to scale it? Divide the input (and multiply the output by the scale factor to keep correct distances).
This "transform the space, not the shape" mindset feels backwards at first. In Canvas API, you transform the drawing context. In SDFs, you transform the query point. Once it clicks though, it's actually simpler -- your shapes are always at the origin, and you're just asking "what would this point look like relative to the shape?"
The Inigo Quilez SDF catalog
I need to mention Inigo Quilez. He's basically the godfather of SDF art. His website (iquilezles.org) has a catalog of 2D and 3D distance functions that covers everything -- circles, boxes, triangles, hexagons, stars, hearts, arcs, bezier curves, parabolas, horseshoes. Every shape you can imagine, and the math is explained clearly with interactive demos.
I keep his 2D SDF page open in a tab pretty much permanently. When I need a new shape, I look it up there, copy the GLSL function, and plug it into my shader. You don't need to derive every SDF from scratch (though it's a good exercise). The catalog is your reference manual.
He's also written extensively about smooth minimum, domain repetition, and other SDF techniques we've touched on. If you want to go deeper than this episode covers, his articles are the definitive resource.
A creative exercise
Design an abstract logo or symbol using only SDF operations. Rules:
- At least 3 SDF primitives (circles, boxes, or both)
- At least one boolean operation (union, intersection, or subtraction)
- Must include some animation (rotation, pulsing, morphing)
- No textures, no images -- pure math
Here's my attempt -- an animated "eye" symbol:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdBox(vec2 p, vec2 s) {
vec2 d = abs(p) - s;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
vec2 rotate(vec2 p, float a) {
return vec2(p.x * cos(a) - p.y * sin(a), p.x * sin(a) + p.y * cos(a));
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
// outer eye shape: intersection of two offset circles (lens shape)
float top = sdCircle(uv - vec2(0.0, -0.18), 0.35);
float bottom = sdCircle(uv - vec2(0.0, 0.18), 0.35);
float lens = max(top, bottom);
// pupil
float pupilSize = 0.06 + sin(u_time * 2.0) * 0.015;
float pupil = sdCircle(uv, pupilSize);
// iris
float iris = sdCircle(uv, 0.12);
// iris ring
float irisRing = abs(iris) - 0.008;
// rotating detail lines inside iris
float details = 1.0;
for (int i = 0; i < 6; i++) {
float angle = float(i) * 0.524 + u_time * 0.2;
vec2 rv = rotate(uv, angle);
float line = sdBox(rv, vec2(0.12, 0.001));
float masked = max(line, -sdCircle(uv, 0.04)); // cut out center
masked = max(masked, iris); // keep inside iris
details = min(details, masked);
}
// compose layers
float shape = smoothstep(0.005, 0.0, lens);
float pupilShape = smoothstep(0.005, 0.0, pupil);
float irisShape = smoothstep(0.005, 0.0, iris);
float irisRingShape = smoothstep(0.004, 0.0, irisRing);
float detailShape = smoothstep(0.004, 0.0, details);
// colors
vec3 bg = vec3(0.02, 0.02, 0.04);
vec3 eyeWhite = vec3(0.85, 0.82, 0.78);
vec3 irisColor = vec3(0.2, 0.45, 0.35);
vec3 pupilColor = vec3(0.02);
vec3 color = bg;
color = mix(color, eyeWhite, shape);
color = mix(color, irisColor, irisShape * shape);
color = mix(color, irisColor * 0.7, detailShape * shape);
color = mix(color, irisColor * 1.3, irisRingShape * shape);
color = mix(color, pupilColor, pupilShape * shape);
// subtle catch light
float catchLight = sdCircle(uv - vec2(0.03, -0.03), 0.02);
float clShape = smoothstep(0.01, 0.0, catchLight);
color = mix(color, vec3(1.0), clShape * 0.6 * shape);
gl_FragColor = vec4(color, 1.0);
}
The outer eye shape is the intersection of two large circles offset vertically -- that creates the classic almond/lens shape. The iris has a ring outline and rotating detail lines (thin boxes masked to stay inside the iris). The pupil pulses slightly with time (that sin(u_time * 2.0) * 0.015). A small catch light circle adds the glint that makes it feel like a real eye.
All SDFs. No textures. No paths. No fill calls. Just distance functions, boolean operations, and careful layering. It's about 50 lines of GLSL and it renders at 60fps without breaking a sweat.
Why SDFs matter
We could draw a circle with Canvas API in two lines. We could draw a rectangle with one call. Why go through all this SDF math?
Three reasons.
First: performance. Every SDF evaluation is independent per pixel. The GPU runs them all in parallel. A scene with 50 shapes and smooth blending runs at 60fps because the GPU is processing millions of pixels simultaneously. Canvas API draws sequentially -- each shape call blocks until it's done. For complex scenes, SDFs on the GPU are orders of magnitude faster.
Second: flexibility. You can't smoothly blend two Canvas rectangles. You can't create a smooth transition between a circle and a box. You can't repeat a shape infinitely without a loop. SDFs do all of this with basic arithmetic. The building blocks are small, they compose freely, and the results are things Canvas physically cannot produce.
Third: this is the foundation of raymarching. In a few episodes we'll be using SDFs to build 3D worlds -- spheres, cubes, infinite corridors, fractal landscapes -- rendered entirely in a fragment shader. No 3D models, no mesh data, no geometry pipeline. Just SDFs evaluated in 3D space with a camera that marches along rays. Everything we learned today -- primitives, boolean operations, smooth blending, repetition, rotation -- applies directly to 3D. The step from 2D SDFs to 3D SDFs is surprisingly small once you understand the principles.
't Komt erop neer...
- A signed distance function returns the distance from any point to a shape's boundary -- negative inside, positive outside, zero on the edge
- Circle SDF:
length(p) - radius. Box SDF usesabs()for symmetry and handles corners withlength(max(d, 0))+ inside term smoothstep()converts raw distance into anti-aliased edges -- one function call, smooth shapesabs(distance) - thicknessturns any filled shape into an outline or ring- Boolean operations combine shapes:
min= union,max= intersection,max(a, -b)= subtraction - Smooth minimum (
smin) blends shapes organically -- the metaball technique in three lines of code fract()on coordinates creates infinite repetition from a single SDF- Transform the space, not the shape: subtract to move, rotate the input, divide to scale
- Inigo Quilez's SDF catalog (iquilezles.org) is your reference for every shape you could want
- SDFs are the foundation of raymarching -- everything we learned today extends directly to 3D
Next up we're looking at using SDFs to build repeating patterns -- stripes, grids, tiles, and the domain repetition techniques that turn a single shape into endless visual complexity. And from there, we bring noise onto the GPU where it really shines. The shader arc is just getting started :-)
Sallukes! Thanks for reading.
X
Estimated Payout
$0.13
Discussion
No comments yet. Be the first!