The Commission
The commission was strict: a gothic interior, raymarched in real time, drawn entirely as characters in a monospace grid on an ivory page. Dark glyphs on light paper, so the work reads as an engraving and never as a terminal. Scroll carries the camera from the west front, through the portal, and down the nave on an authored path. Below the walk, a charter, three further carvings, and a colophon, set like a luxury art book.
Everything on these pages is procedural. There are no photographs, no image files, no recordings: only two typefaces, three colors, and arithmetic run fresh at each opening.
The Press
The press runs two passes on the graphics card, sixty times a second. Pass one raymarches the building at glyph resolution: one ray per cell, roughly four thousand rays for the whole nave, each returning a luminance, a godray mass, and a depth. Pass two stamps every cell with one glyph from a measured alphabet. No pixels are computed that the page will not print.
The Scene
The cathedral is one signed-distance function. A pointed arch is two circle arcs; a nave is a vault shell with arch-shaped alcoves cut into it; the whole east end is four lines. The excerpt below is the working code from the plate you walked through.
// Signed 2D pointed-arch interior. q.x lateral, q.y from the arch floor.
// w half-width, s spring height, h apex rise. Positive inside.
float archIn(vec2 q, float w, float s, float h){
float k = (h*h - w*w) / (2.0*w);
float R = w + k;
return R - length(vec2(abs(q.x) + k, max(q.y - s, 0.0)));
}
float map(vec3 p){
float zl = mod(p.z, BAY) - BAY * 0.5; // mid-bay local z
float zc = mod(p.z + BAY * 0.5, BAY) - BAY * 0.5; // bay-boundary local z
float shell = NR - length(vec2(abs(p.x) + NK, max(p.y - NYS, 0.0)));
float air = shell;
// arcade openings into the aisles, spring 2.6 apex 4.3
float alc = min(archIn(vec2(zl, p.y), 2.0, 2.6, 1.7),
0.68 - abs(abs(p.x) - (NA + 0.47)));
air = max(air, alc);
// clerestory lancets (light enters here), sill 5.2 apex 7.3
float win = min(archIn(vec2(zl, p.y - 5.2), 0.75, 1.3, 0.8), p.y - 5.2);
win = min(win, 0.6 - abs(abs(p.x) - (NA + 0.25)));
air = max(air, win);
// engaged shafts at the bay boundaries
float rad = (p.y < 0.8) ? 0.56 : 0.44;
air = min(air, length(vec2(abs(p.x) - (NA - 0.12), zc)) - rad);
// transverse ribs riding the vault
if (p.y > 6.3) air = min(air, length(vec2(shell, zc)) - 0.15);
air = min(air, p.z + 4.5); // west wall
air = min(air, p.y); // floor
return air;
}
The Light
An engraver does not simulate light; an engraver assigns it. Arch voids print flat dark, window openings flat bright, walls stay pale, the vault is a dark canopy. The godrays are analytic: a point is inside a shaft if a line cast back along the sun's direction escapes through a clerestory lancet. Marching that test along each view ray, twenty-two samples per cell, gives the shafts their mass.
const vec3 SUN = vec3(-0.633, -0.773, 0.0603); // direction light travels
float shaftMask(vec3 p){
float tt = (NA + 0.35 - p.x) / 0.633; // back-cast to the window plane
if (tt < 0.0) return 0.0;
vec3 q = p - SUN * tt;
float zw = abs(mod(q.z, BAY) - BAY * 0.5);
float sy = smoothstep(5.25, 5.6, q.y) * (1.0 - smoothstep(7.0, 7.3, q.y));
float sz = 1.0 - smoothstep(0.5, 0.8, zw);
return pow(sy * sz, 1.5) * exp(-tt * 0.045);
}
// volumetric godrays, quantised later into the glyph field
float tv = min(t, 38.0);
float acc = 0.0;
for (int i = 0; i < 22; i++){
float ts = tv * (float(i) + jit) / 22.0;
acc += shaftMask(ro + rd * ts);
}
acc = min(acc * tv / 22.0 * 0.3, 0.55);
The Alphabet
The ramp is not guessed; it is measured. At press time every candidate mark is drawn once to a hidden canvas and its ink coverage counted, pixel by pixel. Sixteen marks are then chosen so their coverages step evenly from air to stone. No mark carries a descender, so the leading can sit near 1.0 and the field prints dense. Below, the alphabet this very page selected, with its measured weights.
const measured = RAMP_CANDIDATES.map((g) => ({ g, ink: measure(g) }))
.sort((a, b) => a.ink - b.ink);
const maxInk = measured[measured.length - 1].ink;
const ramp = [measured[0]];
for (let i = 1; i < K; i++) {
const target = (i / (K - 1)) * maxInk; // even steps of ink
let best = null;
for (const m of measured) {
if (ramp.includes(m)) continue;
if (!best || Math.abs(m.ink - target) < Math.abs(best.ink - target)) best = m;
}
if (best) ramp.push(best);
}
Pass two then maps tone to mark. A gentle S-curve snaps the lights to bare paper and lets the darks keep their gradations, and a Laplacian on the depth field inks a contour wherever one surface ends against another: the engraver's line, recovered from geometry.
// gentle S-curve: lights snap to paper, darks keep their gradations
float ink = pow(smoothstep(0.05, 0.95, clamp(1.0 - f.r, 0.0, 1.0)), uContrast);
// engraver's contour: a Laplacian on the depth field inks every silhouette
float lap = abs(dl + dr - 2.0 * f.b) + abs(du + dd - 2.0 * f.b);
float edge = smoothstep(0.014, 0.05, lap) * (1.0 - f.g * 0.7);
ink = max(ink, edge * 0.78);
float idx = floor(clamp(ink + jit, 0.0, 1.0) * (uGlyphN - 0.001));
vec2 auv = vec2((idx + intra.x) / uGlyphN, intra.y);
vec3 col = mix(uPaper, uInk, texture2D(uAtlas, auv).a);
The Camera
Scroll does not fight you. The page scrolls normally; the walk simply samples your progress and eases a dolly along seven authored stations, from the west front through the portal to the quire, with a Catmull-Rom spline between them. The stations are named at the plate's lower left as you pass. Under prefers-reduced-motion the carving becomes a single fixed frame, the west front already assembled, and scroll leaves the camera alone.
The pier turns by pointer drag or by arrow keys when the carving holds focus. The rose blooms once when it enters view, and again on request. The lamp in the undercroft never stops.
Provenance
Fully procedural. No image assets, no video, no libraries, no build step: one HTML page each for volume and guide, one stylesheet, three script files, WebGL 1. Type is Playfair Display and JetBrains Mono, served by Google Fonts. The palette is stone ivory, charcoal, and a line of bronze; the Society keeps no other colors. All buildings, cities, dates, and persons are fictional.
The Passes
- IThe first proofs printed murk. The scene was rebuilt to priory proportions so a landscape frame could hold arcade, clerestory, and vault at an honest lens; tone became assigned rather than simulated; a flat-tone debug view proved the scene before the type touched it.
- IIThe press went bold: 700-weight marks, tighter leading, an S-curve on ink, and contour lines from a depth Laplacian. Dust was let into the godrays, drifting one cell at a time.
- IIIThe visitors' book was sewn in after the colophon, the mobile fold recomposed at a coarser grid, and every caption checked against the survey for widows and stray dashes.
- IVThe society judged the first plate slow to read and ordered a recarving. The walk now opens outside, on a west front composed to hold at a glance and at thumbnail size, and on first load the type assembles course by course into the building, a demolition run in reverse. Under reduced motion the front simply stands, already carved.