The primitive arsenal
Eight 2D primitives that between them cover most of what you will draw, and the one test that tells you a primitive is correct.
Code and working examples
Signed distance fields describe a shape by the distance to its edge. These notes use them to build characters, break shapes apart, and work with light and color.

Start with a figure
The explanation, the code and a working demo, together. 61 notes, grouped by what you are building.
Explore the figureEight 2D primitives that between them cover most of what you will draw, and the one test that tells you a primitive is correct.
The normal is usually reconstructed with four extra field evaluations. Most primitives know their gradient in closed form, and it composes through the operators.
k is the most load-bearing number in any shape built from primitives, and a constant k is wrong the moment anything tapers.
Exact for a sphere. Wrong at the seams for everything else, in the direction that oversteps.
smin does not know your skeleton. Blend every part with every other and a hand passing near a hip grows a web of material between them.
smin already computes the blend weight. Return it and your colors transition exactly where the geometry does, instead of near where it does.
max(a, -b) carves correctly and returns a field that is no longer a distance. The shape looks perfect; everything downstream of it does not.
Stepping a Bezier's parameter evenly does not step evenly along it. Anything placed by t bunches where the curve is slow and thins where it is fast.
Signed distance stored in a texture is a sampled field, and sampling rounds every sharp feature. That single fact is why direct outline evaluation exists, and it is the whole trade.
A character is a skeleton first and a distance field second. Joints in world space, primitives hung off them, blended by local radius.
Interpolating joint positions between two poses hits both poses exactly and shrinks every bone in between. Rotate the chain instead.
Interpolating a capsule's radius along its axis looks right and is not a distance function. The outline gives it away before the silhouette does.
Do not deform the shape. Deform the space it is measured in, and remember what that does to the distance.
Cloth is a curve you can evaluate, swept by a tapered capsule. The thing that makes it read is lag, not physics.
Armour and garments modeled as separate shapes drift off the body the moment it moves. Derive them from the body's own distance instead and they fit by construction.
Lerping two distance fields works and is subtly wrong. A smin crossfade keeps every intermediate a real shape.
A field has no instancing. Every pixel evaluates every figure, so a crowd is figure cost times crowd size times every pixel on screen.
Sphere tracing steps the full reported distance, which is safe only while the field never overreports. One non-uniform scale breaks that, and the surface fills with holes.
Dropping a raymarched object into a 2D scene works until it has to pass behind something. A 2D scene has no depth, so you have to assign one, and one number per layer is enough.
Break a shape without ever building geometry: carve each piece with its own cell, and evaluate the body in the piece's rest space.
Cracks run straight and branch. A random walk reads as scribble, and the difference is one line in the direction update.
Cutting a shape into pieces is easy. Keeping each piece rigid while it moves means transforming every term of its construction, and it is always a cut plane that gets left behind.
Where a field-rendered shape meets polygon geometry, both sides antialias their own edge and the background survives in between. Neither piece is wrong and the join is.
Splitting a shape gives you a new surface that was never meant to be seen. Shade it like the outside and the pieces read as smaller copies rather than as fragments.
A generic Voronoi shatter does not know it is cutting a body, so it slices limbs lengthwise and leaves shards with no thickness. Seed the cells from the skeleton instead.
A shard in a WebGL2 fragment shader costs about 0.007ms per megapixel on an M4 Max. The bounding-circle optimization everyone reaches for buys 22 percent, not an order of magnitude.
One exponential falloff gives you a blurry shape. A light needs three, at different radii, plus a crisp stroke.
A box blurred by a Gaussian has an exact closed form. Sampling it is solving an integral that was already solved.
Treating a tube light as a point source is correct far away and badly wrong close up. The line integral is a difference of two arctangents.
Bloom is neutral and symmetric. Halation is red, much wider, and gated to genuinely bright sources. They are different physics and they should not share a slider.
Two-dimensional global illumination is a gather over directions, and the ray count needed for a clean result is brutal. Radiance cascades exploit the fact that far light needs angular detail and near light needs spatial detail, never both.
Key to fill is the single dial that sets mood, and it is measured in stops because light is multiplicative. 1:1 is flat, 8:1 is a thriller.
The star pattern around a bright light is not a decoration, it is the Fourier transform of the hole the light came through. That gives you the streak count, the falloff and the color, none of which are taste.
Brute force is linear in seed count and jump flooding is flat, so there is a crossover. Measured on an M4 Max at 960 by 540, it sits around five hundred, which is far higher than the advice suggests.
A per-channel tonemapper compresses each channel independently, so the brightest one saturates first and the color rotates on its way to white. It is why neon reds go orange.
Saturated red and saturated blue on black appear to sit at different depths. It is an optical fact about your eye, not a design choice, and it makes text on a dark UI physically tiring.
Light text on dark and dark text on light are not equally readable, and the classic contrast ratio cannot tell you that because it is symmetric by construction.
Neon whitens at the core because a curve rolls the channels off in order. Clamp instead and you get a flat block of the wrong color.
Adding colors in display space makes every overlap too bright and shifts the hue. It is the most common rendering bug that nobody reports as a bug.
A dark gradient bands because 1/255 is a huge relative step down there. Half a code value of noise fixes it, and it has to go before the write.
White noise clumps, and the clumps are exactly the low-frequency structure the eye is built to find. Blue noise puts its energy where you cannot see it.
Supersampling a shrinking checker is throwing samples at a problem that has a closed form.
Film grain is not uniform noise. Its amplitude follows the exposure, peaking in the midtones and vanishing in both the shadows and the highlights.
Per-pixel noise is the first thing a video encoder throws away, so grain authored at one pixel arrives as blocky mush. Author it coarser, or add it after delivery.
Shutter coverage for a moving disc is a quadratic. Solve it and you get exact blur at one evaluation.
A cutscene that ships as video is rendered once, on your machine, with no deadline. Almost nobody uses that, and it is the largest free quality increase available in a game.
A streak is the shape swept along its velocity. The part people forget is that the same light is now spread over a longer smear, so fast things get dimmer, not brighter.
Putting the camera on a compass point and the props on an axis is half of it. The half that does the work is stopping there long enough for the arrangement to be read.
Quantize the time you sample at, not the simulation. And never step the whole frame at once.
You already know where the subject is, so you cannot see your own composition. A cheap saliency map answers where a first-time viewer looks, and it is often not where you staged.
A word that fades in as one block reads as a slide. Sixty milliseconds of stagger per letter and a settle instead of a ramp is most of what separates kinetic typography from a fade.
Out-of-focus highlights are not blurred blobs. They are images of the hole the light came through, and the rim is brighter than the middle.
Idle motion does not read as life because it moves. It reads as life because no two things share a period, so the relationship between them never repeats.
Rotated sines stack into a convincing fluid warp, stay seekable, and cost a handful of sin calls. The rotation between octaves is the whole trick.
Same peak, same duration, three envelopes. Linear reads mechanical, exponential reads physical, and a two-frame attack is the difference between a hit and a value being assigned.
Under about 100ms the eye sums light rather than measuring it, so a shorter flash must be brighter to read at all.
Three flashes a second over a quarter of the screen is the line the accessibility standards draw. Most attention-getting effects sit near it by accident, and there is almost always a version that reads the same without flashing.
Every pixel evaluates every primitive unless you stop it. A bounding early-out is the one change that moves the number in 2D.
Dropping to 50 percent render scale is a 4x saving and it costs almost nothing on soft content. What it destroys is thin features, and a field renderer is usually full of them.
Getting a distance field shader onto the screen in URP is a render feature, a material and a blit. The part that costs a day is a sign on the V coordinate that is correct on one graphics API and wrong on another.
Sharing distance field code between a web prototype and a Unity build is mostly a rename. The parts that are not a rename compile cleanly and draw the wrong picture.
One email when something new goes up. No newsletter, no schedule, nothing else.
Double opt-in, so watch for a confirmation email. Unsubscribe any time.