Level 4 · ●●○
Light, Shadow, and a Camera
Prerequisites: Level 3.
Level 3 left you with a white shape on black — a renderer, technically. This level is the crescendo: we will ask the field five more questions, each answerable with nothing but more field samples, and the silhouette will become the image on this site's front door. No new machinery arrives. That is the secret worth whispering twice: lighting is not a second engine bolted onto the first — it is the same march, aimed at new places.
Which way does the surface face?
Every lighting question starts with orientation. A surface facing the sun is bright; one edge-on is dim. The classical answer needs calculus — but a distance field will sell you the same information for four samples. Stand near the surface and ask the field its value a hair to your left and right: if the right-hand value is larger, the surface lies to your left. Do it on both axes (all three, in 3D) and the differences assemble into an arrow pointing most uphill — away from the surface. That arrow is the gradient, and normalized, it is the surface normal:
In the library this is calcNormalCentral — six samples in 3D, and the normalize absorbs the factor a mathematician would insist on. It is an approximation of the true gradient, and an honest one: on an exact field the error shrinks with , and Level 1's Lipschitz property is why the un-normalized arrow already has length ≈ 1. (Production marchers shave two samples with a tetrahedron trick — it ships in the library as calcNormal, and the code panel shows both.)
The renderer, assembled live
Here is the whole second act of this site in one figure. The scene is the landing page's hero, held still. Every stage below is a toggle; flip them in order, slowly, and watch each idea land. The captions here say what each toggle means — the code panel shows the handful of lines it is:
- Normals as color — the debug view every shader artist lives in: paint as RGB. Red faces +x, green faces up. If this looks smooth, the field is healthy.
- Diffuse light — brightness = how squarely the surface faces the light:
clamp(dot(n, lig), 0.0, 1.0). One dot product and the scene has volume. A cool sky term filled from above keeps shadows from going black. - Specular glint — a mirror-like highlight where the surface aligns with the half-vector between light and eye, sharpened by a power:
pow(clamp(dot(n, hal), 0.0, 1.0), 32.0). - Shadows — the move that earns raycasting its keep: from the surface point, march toward the light. Anything hit on the way means darkness. Try hard first; then switch to soft and read the Go deeper on the near-miss trick — shadows with penumbras for free.
- Ambient occlusion— crevices are dark because they see less sky. Step outward along the normal and compare the field's answers to the distance walked; the shortfall is occlusion.
- Depth fog— the march already knows how far every pixel's hit is; paint itself as atmosphere and the scene gains scale.
- Gamma — the unglamorous toggle that fixes everything: map linear light through because your eye is not linear. Flip it last and feel the midtones surface.
Take a moment with the “everything” button. A white cutout became a photograph, and every single stage was the same field, asked again: asked sideways (normals), asked toward the light (shadows), asked along the normal (AO), asked about (fog). One number per point — still.
A camera you steer
You have been dragging the camera already; here is what the drag does. A camera is three arrows and a number: a position , a forward direction toward the target, a right and an up vector completing the frame — and a focal length . Each pixel's ray direction is built from its screen coordinates :
Orbiting just moves on a sphere around the target; zooming changes the sphere's radius; the FOV slider trades perspective drama (wide, low ) against telephoto calm. The lens Go deeper unpacks why this is a pinhole camera.
Materials as functions of position
One question remains unanswered by geometry: what color is the surface? In triangle-land the answer is usually a picture stretched over the mesh. Here it is purer — a material is a function of position, evaluated where the ray landed. The checkerboard is a parity of floors; stripes are a sin thresholded; and a cosine palette turns coordinates directly into color. The generative thread from Level 2 never stopped:
Go deeper: soft shadows from one sample per step★★★
A physically correct soft shadow integrates an area light: many shadow rays per pixel, ruinously expensive. The raymarcher's trick — folklore credits the demoscene, the canonical write-up is Quilez's — is to reuse the samples the shadow march already takes. Marching from the surface toward the light, every step evaluates ; that value is how narrowly the ray is missing geometry at distance . Track the worst ratio:
A clean miss keeps the ratio large (full light); a near-miss at small darkens it sharply — exactly the geometry of a penumbra, where a thin sliver of light source peeks past an edge. sharpens or softens the falloff. It is an approximation (it assumes a point of view of the nearest occluder, ignores the light's true shape, and can over-darken when several near-misses stack) — and it looks right because the quantity it tracks, clearance over distance, is proportional to the angular size of the gap the light must squeeze through. One march, zero extra samples, penumbras included.
Go deeper: ambient occlusion as distance-vs-expectation★★☆
Walk outward from the surface along the normal. Above an open plain, after walking the field should read exactly — the surface you left is the nearest one. In a crevice, the field reads less than : some other wall is closer. The library's calcAO takes five such steps and accumulates the shortfall , geometrically downweighted:
The result approximates how much of the hemisphere above the point is blocked at short range — which is what makes corners read as corners even with no shadow ray pointed anywhere near them. It is not energy-correct (real AO is an integral over directions, not a walk along one), but it errs in a direction the eye forgives: it darkens exactly where contact darkening belongs.
Go deeper: the lens — why this is a pinhole camera★★☆
Put a sensor plane one focal length behind a pinhole and flip it in front instead (same picture, no inversion): a pixel at screen position sits at in camera space, and the ray from the pinhole through it is exactly the camRay formula. The in is the right triangle between the screen edge () and the forward axis. Wide FOV pushes the screen close to the pinhole — rays splay outward and perspective stretches; long FOV pulls it away and the world flattens. The library builds the camera frame with two cross products in cameraBasis: forward toward the target, right = forward × world-up, up completing the triad.
Go deeper: banding and other artifacts — and their fixes★★☆
Three artifacts account for most ugly raymarched renders:
- Step banding: visible contour-like rings on large flat surfaces, from rays all stopping at the same quantized ε distance. Fixes: shrink ε with distance (
SURF_EPS * t), or dither the starting offset per pixel. - Shadow acne: speckle where a surface shadows itself, because the shadow march starts at the surface where . The fix is in this site's own code: start at
p + n * 0.02— nudged off the surface along the normal — and begin the march atmint > 0. - Normal noise at creases: central differences straddle the crease and average two walls. Shrink (cost: floating-point noise) or accept it — smin welds exist precisely to keep creases rare.
A debugging habit worth stealing: when a render misbehaves, switch to normals-as-color first, step-cost second (Level 3). Nine artifacts in ten reveal themselves in one of those two views.
What you now know
- Normals come from sampling: central differences of the field point most-uphill; normalize and light away. No calculus, six samples.
- Every lighting effect is the same field, asked again — toward the light (shadows), along the normal (AO), about (fog), against the half-vector (specular).
- Soft shadows fall out of near-miss bookkeeping — during one shadow march.
- A camera is three arrows and a focal length; orbit, zoom, and FOV are all just choices of and .
- Materials are functions of position — checkers from
floor, stripes fromsin, palettes from cosines.
You now hold a complete renderer — the one drawing the front door of this site. Level 5 asks the only question left: how far can formulas go? (Spoiler: past infinity, and into a 4-kilobyte executable.)