INKHOW THE HEAD WORKS

Turning a head into
a page of pen strokes.

The pen engine draws whatever strokes it is handed. How the pen works covers that half — path, mark, deposit, render. This page is about where the strokes come from: a 3D head, a light, and a rule for turning tone into marks.

Nothing here is a filter over a rendered image. There is no image. The head is posed, rasterised into a buffer of surface facts, shaded down to a single channel of darkness, and then walked in rows by a hatcher that decides where a pen touches down, where it lifts, and how hard it presses at every step in between. The pen only ever sees the strokes.

Every head on this page is a live run of that pipeline, at the same 1080px the app renders at, so no figure can drift away from the code it describes. Where something has a slider, drag it.

00A HEAD, AND A WAY OF SHADING IT

Two things have to be settled before any of this runs: where the geometry comes from, and what kind of marks are going to describe it.

The geometry is 1, Google's parametric head model. A parametric model is the right kind of source here because it does not hand you a head, it hands you a mean head plus an 2 — a stack of shapes that deform it — so any particular face is a list of numbers saying how much of each to add. GNM's list is 253 long; this drawing resolves the first ten and leaves the rest at the mean. That is worth more than it sounds. The width of a skull is not something a hatch can adjust afterwards, and having it be a parameter means it can be chosen instead of lived with; section 08 comes back to exactly that. The numbers are resolved when the mesh is baked, so what the browser downloads is one specific head rather than the machinery to reach any of them.

The marks are 3: ruled rows, twenty pixels apart at 1080, all running in one direction. The shading is carried by the weight of each mark rather than by the spacing between rows — a distinction the whole of sections 05 and 06 turn on, and the single decision that most changes how the drawing reads. The finished page then goes through the same five-level 4 as everything on the pen page — 17, 77, 136, 196, 255. The head changes nothing about that.

FIG 00THE WHOLE THING, AT ITS DEFAULTS
THIS PIPELINE, AT THE SETTINGS THE APP OPENS WITH
Every section below is one stage of this picture. It is rendered at 1080px — the size every number on this page is quoted at — and the drawn area measures 302 × 447px, made of about thirty pen strokes in total.

01THE PIPELINE

Four stages sit in front of the pen's four. Each hands the next something narrower than it received: a skeleton becomes a posed mesh, a mesh becomes a 5, a G-buffer becomes one channel of darkness, and darkness becomes strokes. By the time the pen is involved there is no head left in the data at all.

FIG 01EIGHT STAGES, FOUR OF THEM NEW
POSEskin the rig1meshRASTER28k triangles2g-bufferLIGHTshade the normals3toneHATCHruns → strokes4strokesTHE PENfour more stagesCPU · TypeScript+ GPU
Click a stage to jump to it. The last box is the whole of the pen page.

Narrowing that hard, that early, is the point. It means the light can be changed without touching a triangle, the hatch can be changed without touching the light, and the pen can be swapped for a different pen without any of the three knowing. It also means each stage can be looked at on its own — the TONE toggle in the app shows the darkness field directly, because tuning a hatch against a field you cannot see is guesswork.

The whole head half runs in plain TypeScript on the CPU. The GPU only appears once there are strokes to rasterise.

02STAGE ONE · POSE

The asset is 14,008 vertices, 27,844 triangles and four joints — neck, head, and one for each eye — stored in metres relative to the head joint, and already carrying its identity. Posing it is 6, matching GNM's own implementation so the mesh deforms the way the model's authors intended.

Each joint gets a rotation as yaw, pitch and roll, composed Ry · Rx · Rz. 7 walks the chain from the neck outward, multiplying each local rotation onto its parent's world transform and accumulating the parent-relative offsets into a world translation. Turning the neck carries the head, and the head carries the eyes, because they are downstream of it.

Then each joint's transform is rebased — offset = worldT − world · jointBind — so that it rotates the mesh about that joint's own bind position rather than about the origin. Without it a neck yaw would swing the entire head around a point somewhere inside the skull.

Skinning itself is the average you would expect: for every vertex, every joint that influences it transforms it, and the results are summed by weight. Weights are stored as one byte per joint per vertex, renormalised at bake time so the sum is exactly 255. Normals go through the rotation only — no translation — and are renormalised afterwards, because a weighted average of unit vectors is not a unit vector.

02.1ONE SLIDER, TWO MECHANISMS

ROTATE HEAD is a single control and it drives two entirely different things. The neck and head joints take a fixed share of the rotation until they reach their limits, so a small turn is mostly articulation — the skeleton does the work it is there for, and the drawing shows a head turning on a neck. The camera orbits by whatever is left over: about a third of the turn from the first degree, and all of it once the joints have run out.

The limits are not arbitrary. Plain skinning pinches at the base of a turned neck, and the usual repair is a 8. GNM has a slot for one — pose_correctives_regressor, 36 × 53,463 floats — but every coefficient in it is zero in the v3.0 head this is baked from, so there is nothing to apply. The joints stop instead at the angles where skinning alone still holds up, and the camera takes the rest.

FIG 02HOW A TURN IS SHARED OUT
ROTATE HEAD -25°
−120−6060120−180°180°ROTATE HEAD
NECK JOINT±20°-7.5°
HEAD JOINT±30°-8.8°
CAMERA ORBITthe remainder-8.8°
Drag the slider. The neck takes 30% of the turn up to ±20°, the head 35% up to ±30°, and whatever is left over becomes camera orbit. Watch the flat tops: the neck saturates at ±67° and the head at ±86°, and past that every further degree is the camera moving.

One outright bug fell out of building it this way. Joint yaw and camera yaw had been written with opposite handedness, so posing the neck partly cancelled the orbit. Neither was wrong on its own, and it only surfaced when a single slider drove both.

03STAGE TWO · RASTER

The rasteriser is on the CPU, deliberately. The page already owns a WebGL context for the ink, and standing up a second one just to read a depth buffer back on every slider move costs more than the few milliseconds JS needs for a mesh this size — with no readback stall, and with the result sitting in plain arrays where the hatcher can read it directly.

Framing comes first. The camera is placed so the subject's 9 fills the requested fraction of the frame: dist = radius / (zoom · tan(fov/2)). That one line is what lets PERSPECTIVE and SIZE be independent controls — change the 10 and the camera moves in or out to keep the head the same size on the page, so the only thing that changes is how much the lens exaggerates depth.

FIG 03A LONG LENS AND A SHORT ONE
PERSPECTIVE 0 · 10° FOV
PERSPECTIVE 1 · 55° FOV
10° and 55° of vertical field of view, framed identically. The camera sits at twenty bounding-sphere radii for the long lens and three and a half for the wide one. The outline barely moves — that is the framing working — while the relationship between near and far features does not survive the trip.

Every vertex is then transformed once into screen space and cached — rotate by the camera matrix, take the camera-space distance d, and project with w = pixelsPerUnit / d. Screen y runs downward, so it is subtracted rather than added. Vertices closer than the near plane get a zero marker and any triangle touching one is dropped whole; the camera stays outside the bounding sphere at every setting the panel allows, so nothing visible is lost that way.

Then the triangle loop, forty-odd lines that do four things. 11 first, from the sign of the projected signed area — on a closed surface that discards half the mesh and nothing that would have been seen. Then a bounding box clipped to the buffer. Then, per pixel, three edge functions whose signs say whether the pixel is inside; the same three numbers, divided by the area, are the 12. Then a 13 against whatever was written there before.

Depth, 14 and ink allowance are all interpolated with those screen-space weights, which is affine rather than perspective-correct — strictly it is the wrong interpolation. At 27,844 triangles on a head that fills half the frame, a triangle spans a couple of pixels and the error is far below anything a 20px hatch row could express. It is a deliberate trade, not an oversight.

FIG 04WHAT THE RASTERISER HANDS OVER
DEPTH
distance from the camera
NORMALS
xyz as rgb — the only thing the light reads
INK ALLOWANCE
how much ink a vertex may carry
DARKNESS
the one channel the hatcher sees
Three arrays and a coverage mask, no triangles. Depth is only used for the visibility test; the drawing itself is decided entirely by the normals and the ink allowance. The fourth panel is the next stage's output, shown here because it is the only one anything downstream reads.

03.1FRAMING ON WHAT IS DRAWN

The mesh is stored relative to the head joint, which sits inside the skull with a neck stump hanging below it. Point the camera at the joint and the drawing rides 120px up the page: the joint is not the centre of what gets drawn, because the neck below it takes up frame without taking much ink. Technically centred, visually wrong.

So the framing is measured instead. At load time the bounds of every vertex that takes more than a token amount of ink are computed, and the camera looks at and orbits about the centre of that. The drawing lands centred on the page whatever the pose, and no manual nudge is needed anywhere in the settings.

FIG 05CENTRED ON THE JOINT, AND ON THE INK
CENTRED ON THE HEAD JOINT
CENTRED ON THE GEOMETRY THAT TAKES INK
Same pose, same lens, same light. The only difference is what the camera was told to look at, and it is worth 120px of vertical placement.

03.2THE TONE FIELD IS HALF SIZE

The G-buffer, and so the 15 that comes out of it, is rendered at half the canvas resolution — 540px against 1080 — clamped to between 384 and 768. Not as a performance compromise. A sharper field is measurably worse.

The reason is that a hatcher can only express detail by starting and stopping. Tone structure finer than the row pitch has nowhere else to go, so the only thing extra resolution can buy is breakage: marks chopped by features too small to be marks of their own. At a 20px pitch the effect is mild — the drawing gains a run or two, in places a draughtsman would not have lifted the pen — and it costs three times as much time to compute. Both directions of that trade point the same way. Section 05 comes back to what stray breaks do to a drawing.

FIG 06HALF-RESOLUTION TONE AGAINST FULL
HALF THE CANVAS · 540px what the app ships
TONE FIELD
THE SAME AREA DRAWN, AT 1:1
THE FULL CANVAS · 1080px four times the pixels, and no better
TONE FIELD
THE SAME AREA DRAWN, AT 1:1
The difference lives in the tone panels: the sharper field resolves a nostril and a lip edge at a scale the 20px row pitch cannot carry. The drawings on the right are nearly the same picture, which is the point — four times the pixels bought nothing the marks could use.

04STAGE THREE · LIGHT

This stage is thirty lines long and decides almost everything about whether the drawing reads as a face. It takes the G-buffer and produces one number per pixel: darkness, where 0 is bare paper and 1 is solid ink. Nothing downstream sees anything else.

The light itself is a direction, built from an azimuth and an elevation — there is no position, no falloff, no shadow map. What matters here is not simulating a room, it is producing a tone gradient a pen can follow. Four lines do that — wrapped Lambert, a rim darkening, a contrast curve and the neck fade — and they are worth stepping through in the order the code applies them. The figure below switches on three of them; contrast does nothing at its default, and has a section to itself.

FIG 07THE SHADING FUNCTION, TERM BY TERM
DARKNESS FIELD
WHAT THE PEN DRAWS FROM IT
float lambert = dot(normal, lightDir);
float lit = clamp((lambert + wrap) / (1.0 + wrap), 0.0, 1.0);
float d = 1.0 - lit;
d += rim * pow(1.0 - facing, 3.0);
d = PIVOT + (d - PIVOT) * contrast;
d *= fade;

The last multiply is not lighting at all. Each vertex carries an ink allowance that falls from 1 just under the jaw to 0 by the bottom of the neck stump, and the rasteriser interpolates it into the G-buffer alongside the normals. Multiplying the darkness by it means the tone reaches the hatcher already faded, so runs shorten and then stop — the drawing ends where the head does rather than at the edge of the paper.

The contrast line stays greyed throughout: at the default of 1.0 it is the identity. What it does when it is not is 04.3.

Both panels are real output: the darkness field on the left, and the drawing the hatcher makes from it on the right. Step 1 is textbook-correct and unusable.

04.1WHY LAMBERT FAILS, AND WHAT WRAP DOES

16 is the right model for a matte surface: how squarely it faces the light, clamped at zero where it turns away. The trouble is what it leaves for a pen. Half the sphere of surface directions maps onto a gradient and the other half maps onto a single value — solid black — so the head arrives at the hatcher as a bright region, a black region, and a 17 a pixel wide between them.

Rows crossing that either draw at full weight or not at all. There is no swelling and no thinning, because there is no gradient to swell across. The result is a silhouette with a hard edge through it, and no amount of tuning the hatch fixes a field with no information in it.

18 is the fix, and it is a cheat. Light is allowed past the terminator by a fixed amount and the whole range is rescaled so full brightness still means full brightness. Physically it stands in for light bouncing around a room; practically it converts the cliff into a ramp that runs from the lit profile edge all the way to the back of the skull. That ramp is the face. Everything legible in the drawing — the cheekbone, the brow, the plane change at the jaw — is the hatch following it.

FIG 08WRAP, PLOTTED AS THE HATCHER SEES IT
terminatorfacing awayfacing the lightsolidpaperN · LDARKNESS

The shaded band is the part of the sphere of directions that produces a tone the hatcher can vary a mark over — 68% of it here. At zero it is exactly half, and every surface past the terminator is the same solid black.

SOFTNESS 0.36
Dashed is plain Lambert. Drag SOFTNESS and watch the shaded band — the fraction of surface directions that produce a tone a mark can vary over — grow from half the head to nearly all of it.

04.2HOLDING THE SILHOUETTE

The second term is a small darkening wherever the surface turns away from the camera, taken from the 19 — the z component of the normal in view space, zero exactly at the silhouette. It is cubed, so it is nothing across the front of the face and only bites in the last few degrees before an edge.

Without it the lit side of the head fades to paper before it reaches the outline and the profile dissolves; the drawing loses its own boundary. It is set at 0.12 and it is the one term here that is doing a purely graphic job rather than an optical one.

04.3A TONE CURVE THAT HINGES LOW

Third is contrast, and the only interesting thing about it is where it pivots. d = PIVOT + (d − PIVOT) · contrast with PIVOT at 0.35, below mid grey.

A curve hinged at 0.5 lightens as much as it darkens, which is the wrong trade for this drawing. What it wants is the lit side held near paper while everything past the terminator is pushed toward solid — so the hinge sits low, and raising contrast spends most of its range on the shadow side.

FIG 09THE PIVOT
PIVOT 0.35hinged at mid greyDARKNESS INDARKNESS OUT

Below the pivot the curve is pulled toward paper, above it toward solid. Hinging at 0.35 rather than 0.5 means a lift in contrast spends most of its range darkening the shadow side and leaves the lit cheek alone, which is the trade the drawing wants.

CONTRAST 1.60
Solid is the curve as built; dashed is the same contrast hinged at mid grey instead. Note how much more of the lit range the mid-grey version bleaches away for the same darkening in shadow.

04.4WHERE THE DRAWING STOPS

The last multiply is not lighting at all. GNM ends at a neck stump: the subject is a head, and the stump is an artefact of where the model stops, not a shoulder. Cropping it at the frame edge only moves the problem — a drawing that runs off the bottom of the paper.

So every vertex carries an ink allowance instead, falling from 1 just below the jaw to 0 by the end of the stump, measured against the stump's own depth so it tracks the mesh rather than a magic number. The rasteriser interpolates it into the G-buffer alongside the normals, and the darkness is multiplied by it. Tone reaches the hatcher already faded, runs shorten and then stop, and the drawing ends where the head does.

05STAGE FOUR · HATCH

The hatcher is the piece that turns a continuous field into marks, and it is where a renderer stops being a renderer and starts being a draughtsman. It has one rule: one 20, one stroke.

Rows are laid out in their own coordinate frame — u along the row, v across it — so the row angle is a parameter rather than a special case, even though every figure here draws them horizontally. The canvas corners are projected into that frame to find how far the rows have to run. Each row then gets a jitter of up to 0.8px off its nominal line, a slope of up to 0.5px across its length, a random nib speed and a seed of its own.

Walking a row is a loop at a step of 12% of the row spacing — 2.4px at the default — fine enough that a run end lands within a nib width of the real boundary. At each step the darkness under the nib is sampled from the tone field, 21, because the field is 540px and the walk is in 1080px canvas coordinates.

The test is one line: if the tone is below the paper threshold — WHITE SPACE scaled into the top 70% of the tone range, so 0.091 at the default — the pen is off the page. Ink starts where the tone crosses that threshold and continues until it drops back below, and the entire run goes down as a single pen stroke — not a chain of dashes. Runs shorter than 3px are dropped. So every break in the finished drawing is somewhere the head got light enough to lift the nib: the eye socket, the front of the cheek, the gap under the jaw. None of them are arbitrary.

It is worth saying how few marks that is. At the defaults the whole head is about thirty strokes — a couple of dozen rows, most of them a single run from the profile edge to the back of the skull. There is no density of marks doing the work here. There is barely any drawing at all, in the sense of quantity. All of it is weight.

FIG 10ONE ROW, END TO END
ROW 25 OF 54 · y = 500px
WHITE SPACE 0.09110DARKNESS ALONG THE ROW · 0 RUNS
THE FINISHED DRAWING AROUND THAT ROW, AT THE SAME HORIZONTAL SCALE
The trace is the darkness along that scanline, sampled with the hatcher's own sampler. Shaded columns are where it sits above the threshold — the runs. The strip underneath is that same row cut out of the finished drawing at the same horizontal scale, so the marks line up with the trace above them. Drag the row down through the eyes and the nose and watch runs split and rejoin.

What this replaced matters, because the obvious approach is the wrong one. An earlier version chopped each row into jittered cells and inked a fraction of each, which is the usual recipe for a hand-drawn look and produces a convincing texture at a glance. But it puts a break wherever a cell happens to end, not where the head does, and those breaks read as noise laid on top of the form instead of as the form itself. Weight, not breakage, is what carries depth here — which is the next section.

FIG 11THE THREE HATCH CONTROLS

the row pitch — 20px at 1080, about the density a fineliner hatches at.

the widest the nib gets, where the tone is solid. Push it past the spacing and rows fuse.

the darkness a row must reach before the pen touches down.

MARKS 00MS
Row pitch, peak nib width and the paper threshold. INK WEIGHT is the widest the nib gets, on solid tone; the next section is everything it does on the way there. Take it past LINE SPACING and rows fuse into a solid; take WHITE SPACE up and the drawing retreats to the darkest parts of the head, then vanishes.

06WEIGHT ALONG THE MARK

This is the part that makes the drawing look drawn, and it is one line in the hatcher. The tone under the nib is not sampled once per stroke — it is sampled at every point along it, and handed to the pen as 22.

p = minPressure + (1 − minPressure) · tone^gamma, with a floor of 0.05 and a gamma of 1. That pressure travels with the stroke into the pen's own path stage, where it drives two separate things: the 23's half width, via PRESSURE → WIDTH, and its 24, via PRESSURE → FLOW. Darker tone means both a wider mark and a denser one.

Measuring a hatched drawing makes the case better than any argument. Slice across the bands and they run from about 3px to about 14px inside a single stroke, over and over. The nostril, the lip line and the eye socket are all far too small to be marks of their own at a 20px row pitch — they exist in the drawing only as the nib changing weight as it passes over them. That is most of what “detail” means in a hatched head, and it is why the tone is sampled per point rather than per stroke.

FIG 12TONE → PRESSURE → WIDTH
no mark drawn0px5px10px15px0.09p 0.140.25p 0.290.50p 0.530.75p 0.761.00p 1.00
INK WEIGHT is 14px, so the pen's nominal nib is 7.57px — the peak divided by 1 + 0.85. The swing runs from 2.9px at the threshold to 14.0px on solid tone, a ratio of 4.8 to one.
WEIGHT FROM TONE · PRESSURE → WIDTH 0.85
THE SAME RUNS AT ONE WEIGHT · PRESSURE → WIDTH 0
The curve is the real mapping, with the nib drawn at true scale at five points along it. Below, the same face at 1:1 with the weight modulation on and off — the runs, the tone and the row pitch are identical between the two panels, and only the nib's response to pressure differs.

Getting the swing right meant sizing the nib for its peak rather than its average. Tone pressure and the pen's own width jitter compound, so a nominal 10px nib was drawing 18px bands in shadow and fusing one row into the next, wiping out the row pitch entirely. INK WEIGHT is now defined as the peak, and the pen's nominal width is backed out of it — nibWidth = weight / (1 + pressureWidth). Set the peak, read the peak off the render.

07HANDING OVER TO THE PEN

What crosses the boundary is a list of strokes: points with position, pressure and a timestamp, plus a seed and a smoothing flag. Nothing else. From here the drawing is the pen page's problem — every stroke becomes a 25, every segment a quad, 26 as 27 into a float buffer and resolved into pixels once at the end.

Two details of that handoff matter. The smoothing flag is zero, because these paths are generated rather than sampled from a pointer and have no device jitter to remove. And the timestamps are synthetic: sampleAlong advances time by distance over a per-row nib speed, so the pen's speed-dependent terms — thinning, ink starvation — still have something meaningful to read.

The pen has no panel of its own in the head app. It is the calibrated fineliner with ten parameters overridden, and the reasons are all the same reason: a test sheet wants one weight end to end, and a head wants the weight to be the drawing.

PARAMETERFINELINERHEADWHY
nibWidth12.50weight / 1.85the slider sets the peak, not the nominal
pressureWidth0.120.85tone has to be able to move the nib most of its range
pressureFlow0.300.45darker tone should be denser as well as wider
widthJitter0.220.10tone now supplies the variation within a mark
endBlob0.120.02pooled ends fatten rows until they touch
dwell0.450.10nothing here ever stops moving
taperIn / taperOut2 / 36 / 9a run should die away at a tone boundary, not stop dead
widthVar0.40INK VARIANCEper-stroke variation is a control here, and off by default
edgeAmt1.55EDGE EROSIONsame parameter, exposed under a plainer name
levels5TONE LEVELSthe five greys, unchanged

Everything else — the capsule 28 model, canvas-anchored grain, the MAX blend within a stroke, 29 absorption, the five-level tone step — is untouched. The head does not need a special pen. It needs the same pen, driven.

08MEASURING IT

Tuning a hatched head by eye stalls even faster than tuning a pen does, because there are two things to get wrong at once — the tone and the marks — and they compensate for each other. So both are measured.

The marks are the easy half: slice across the bands and count pixels. tools/tone-match.mjs handles the tone, and the useful idea in it is that it compares two drawings by tonal structure rather than by pixels. Ink coverage is resampled onto a coarse grid laid over each image's own ink bounds, so framing and scale drop out of the comparison entirely — two renders of the same head at different sizes score as the same drawing, which is what you want when the thing you changed was the light.

The grid being coarse is the part that is easy to get wrong. Make it fine enough to resolve individual rows and it stops scoring tone and starts scoring the phase of the hatch: nudge every row half a pitch and a perfect drawing suddenly scores terribly. Coarser than the row pitch, and the measure sees what the eye sees. The tool also prints both grids as ASCII beside the number, which is more useful than the number, because a single figure tells you a change was worse and the grids tell you where.

A drawing against itself is 0.00, so the number is only ever meaningful as a comparison — this render against the one before the change. That is the whole workflow: change one parameter, re-score, keep or revert.

MEASUREWHAT IT TELLS YOUTHIS PIPELINE
subject boundsframing, independent of pose302 × 447px
row pitchthe hatch's own scale20px
blank scanlineshow much paper the drawing leaves23%
stroke weightthe swelling inside a mark3–15px, peak/floor 5.00
weight change per columnhow fast the nib is allowed to respond8.7%
run lengthhow often the pen liftsmedian 182px, p90 247px
grey levelsthe render pass's quantiser5 — 17 76 136 196 255

The greys are the five quarter-steps from section 00, give or take a rounding tie: mix(255, 17, ¾) is 76.5 exactly, so that level can land on either side depending on how the byte is rounded.

Stroke weight is the row to watch, because it is the one the eye is actually reading. The range and the peak-to-floor ratio are the swelling inside a mark, and a pipeline that got every other row on this table right while drawing at a constant width would measure well and look flat.

One number here cannot be fixed by any of the drawing controls, and it is worth knowing which. GNM's mean head measures 314px wide for this height; a narrower skull is a different shape, not a different hatch, and no amount of row pitch or ink weight will get you there. It is the clearest case on this page of a problem that has to be solved upstream — so it is, by resolving ten identity coefficients at bake time rather than shipping the mean head. Section 09 covers how.

09BAKING THE MESH

GNM ships as a 51MB .npz — 150MB of arrays, deflated — holding the identity and expression bases as well as the mesh, none of which has any business in a web bundle. tools/bake-head.mjs takes only what a rotating hatched head needs and writes 300KB.

STEPWHAT IT DOES
identityevaluates the identity basis at ten fixed coefficients — head_000 to head_009 — and adds the result to the mean mesh and to the joints, so a longer jaw carries the joint it hangs from
selectkeeps the skin and eye vertex groups; teeth and tongue sit behind the lips and never survive the depth test
reindexremaps to a compact vertex list — 14,008 vertices, which fits a uint16 index with room to spare
quantisepositions as int16 about the head joint, scaled by the mesh's own extent: 6.0µm per step
skinweights as one byte per joint, renormalised after rounding so the sum is exactly 255
windingchecks face normals point away from the centroid and flips the index order if not, rather than assuming
write128-byte header — magic, counts, scale, joint positions and parents — then three packed arrays

Resolving the identity here rather than shipping it is what keeps the asset the size it was. The alternative — ten basis shapes in the download so the browser could mix them — is about 840KB of vertex deltas to support a control nobody is turning. The cost is that changing the face means re-running the bake, which is the right trade for a drawing with one sitter.

Three things are computed at load rather than stored, because they are cheap and would otherwise be another thing that can go stale: area-weighted vertex normals, the neck ink fade, and the bounding sphere of the geometry that takes ink. None of them care which face arrived — they measure whatever mesh they are given, which is why a new identity re-frames itself with no other change. tools/npz.mjs reads the numpy archive directly, so the bake stays a plain node command with no Python involved.

GNM is copyright Google LLC and licensed under Apache 2.0. The licence, and a note recording exactly which version and variant this head was baked out of, sit beside the asset in public/head; the model itself is at github.com/google/GNM.

10WHAT IS NOT THERE

Identity at runtime, and expression at all. The face is fixed at bake time, so changing it is a command, not a slider; 09 has the arithmetic on why. Expression is out for a plainer reason — its basis is 82MB uncompressed, and a hatched portrait of a still sitter never asks for one.

Pose correctives. There is nothing to leave out — GNM's regressor is all zeros in the release this is baked from — and the joint limits are the workaround.

Cross-hatching. The row angle is already a parameter, and a second pass at a different angle over the same field would be a few lines. It is not in because it is not needed: one direction of rows plus weight already covers the tonal range a five-level quantiser can hold, and a second pass would be a different drawing rather than a better one.

Anything about the light being a light. No shadows, no falloff, no second source. A hatch resolves about five tones; spending complexity on a shading model that resolves more of them buys nothing that survives the quantiser.

11GLOSSARY

Every marked word on this page, numbered in the order it first appears. The last few belong to the pen rather than to the head, and are repeated here so the page reads on its own.

01GNM
Generative aNthropometric Model — Google's parametric head model, and pronounced like genome. A mean head, a basis of identity and expression shapes that deform it, and a four-joint skeleton with skinning weights. Uncompressed the two bases are 54MB and 82MB, so what is shipped here is a single head evaluated out of them, not the machinery to evaluate more.
02Identity basis
The set of shapes that turn a mean head into a particular one. Each is a displacement per vertex, and a face is a weighted sum of them — so an identity is a short list of numbers rather than a mesh. The weights are in standard deviations, ordered by how much variation each shape accounts for, which is why the first ten carry a recognisable face. There are 253 in all — 170 for the head, the rest for the teeth and the eyeballs.
SEE ALSO GNM1
03Hatching
Shading with ruled lines rather than with a wash: tone comes from how much of the paper the marks cover and how heavy they are. The whole problem of this page is turning a continuous tone into marks that read as that tone.
SEE ALSO Tone field15 · Run20
04Posterisation
Snapping a continuous range onto a small number of fixed steps. Doing it to opacity — at the very end, after everything else — is what puts a pen drawing's handful of greys on the page, and the speckled edge along with them.
SEE ALSO Coverage28
05G-buffer
A geometry buffer: the per-pixel facts about a surface — its distance, which way it faces, what it is made of — stored before any lighting is applied. Splitting the pipeline here means moving the light re-shades an existing buffer instead of re-rasterising the mesh.
SEE ALSO Surface normal14 · Depth test13 · Tone field15
06Linear blend skinning
Deforming a mesh by a skeleton: each vertex is transformed by every joint that influences it and the results are averaged by weight. Cheap, and the standard way a character mesh follows a rig. It pinches at large angles, which is why the joints here have limits.
07Forward kinematics
Walking a joint chain from the root outward, multiplying each joint's local rotation onto its parent's world transform. Turning the neck moves the head, the eyes and everything below them, because they are all downstream of it.
08Pose corrective
A learned fix-up applied on top of skinning to repair the shapes it gets wrong — most obviously the collapse at the base of a turned neck. GNM reserves an array for one, but every coefficient in it is zero in the released head, so there is none to apply and the joints stop where plain skinning still holds up.
09Bounding sphere
The smallest sphere containing a set of points, used here to place the camera: put it far enough back that the sphere fills the requested fraction of frame and the subject is framed at any field of view. Measured over the geometry that takes ink, not over the whole mesh.
SEE ALSO Field of view10
10Field of view
The angle the camera sees. A narrow one is a long lens: the head flattens, near and far features come out nearly the same size. A wide one exaggerates depth — the nose grows and the ears shrink. Changing it while keeping the subject framed means moving the camera in or out to match.
SEE ALSO Bounding sphere9
11Backface culling
Discarding triangles that face away from the camera, detected from the sign of their area once projected. It halves the work, and on a closed surface it throws away nothing that would have been visible.
SEE ALSO Depth test13
12Barycentric coordinates
A point inside a triangle written as a weighted blend of its three corners. The weights come out of the same cross products that test whether the pixel is inside at all, and they are what interpolates a normal, a depth or an ink allowance across the face.
SEE ALSO Surface normal14
13Depth test
Keeping, for each pixel, only the nearest surface written to it so far. It is what makes the far side of the skull disappear behind the near side without sorting the triangles first — and what makes the teeth and tongue not worth shipping.
SEE ALSO Backface culling11 · G-buffer5
14Surface normal
The direction a surface faces at a point, as a unit vector. Averaged from the faces around each vertex and interpolated across triangles, so a coarse mesh still shades smoothly. It is the only thing the lighting reads.
15Tone field
The single-channel image of darkness that the hatcher draws from, 0 for bare paper and 1 for solid ink. Everything about pose, lens and light has collapsed into it by this point; nothing downstream knows there was ever a head.
SEE ALSO G-buffer5 · Hatching3
16Lambertian shading
Brightness proportional to how squarely a surface faces the light — the dot product of the normal and the light direction, clamped at zero. Physically the right model for a matte surface, and on its own the wrong one for a hatched drawing, because it leaves nothing to draw with over most of the head.
17Terminator
The boundary between the lit and unlit sides of an object — where the surface turns exactly edge-on to the light. Under plain Lambertian shading it is a hard line; how much tone lives around it is what decides whether a hatch can describe form there.
18Wrap lighting
Letting light reach past the terminator by a fixed amount, then rescaling so full brightness still means full brightness. It is not physical, it is a cheat that stands in for the light bouncing around a real room — and it converts a two-region silhouette into a gradient a pen can follow.
19Facing ratio
How squarely a surface faces the camera rather than the light — the z component of its normal in view space. Zero exactly at the silhouette, so a function of it is a way to darken only the last few degrees before an edge.
SEE ALSO Surface normal14
20Run
An unbroken stretch of a row where the tone stays above the paper threshold — and the unit the hatcher works in. One run becomes exactly one pen stroke, so every gap in the finished drawing is somewhere the head got light enough to lift the nib.
SEE ALSO Hatching3 · Tone field15
21Bilinear sampling
Reading a value between the pixels of an image by blending the four nearest. It is what lets a 540px tone field be sampled at 1080px canvas positions without the marks stepping from one field pixel to the next.
SEE ALSO Tone field15
22Pressure
The pen model's input for how hard the nib is being pressed, 0 to 1, carried per point along a stroke. On a tablet it comes from the stylus. Here the hatcher synthesises it from the tone under the nib, which is what turns a flat row into a mark that swells and thins.
SEE ALSO Nib23 · Flow24
23Nib
The tip of the pen — the part touching the paper. Its width, and how much that width answers to pressure, is most of what separates a fineliner from a felt tip.
SEE ALSO Flow24
24Flow
How readily ink leaves the nib. Separate from width: pressing a ballpoint harder barely widens the line but pushes noticeably more ink out, so flow scales density rather than geometry.
SEE ALSO Density27 · Nib23
25Mark
The second stage of the pipeline: turning a path into geometry the GPU can rasterise. It decides where a stroke could possibly put ink, not how much — that is the next stage's job.
SEE ALSO Deposit26
26Deposit
The third stage: deciding how much pigment lands on each pixel. Paper texture, edge erosion and the pen's failure modes all apply here, and the answer accumulates as density.
SEE ALSO Density27 · Coverage28
27Density
How much pigment is sitting on a patch of paper, added up across every stroke that crossed it. It is not an opacity and not a colour, and it is unbounded — eight passes really can be eight times the first. Only the render stage turns it into something visible.
28Coverage
How much of a single pixel the mark covers, from 0 to 1. Read off the signed distance by a smooth ramp across the boundary; multiplied by flow, it becomes the density that gets deposited.
SEE ALSO Density27
29Beer–Lambert law
Light is absorbed exponentially with the amount of material it passes through. Applied to ink it means opacity approaches 1 without ever reaching it, so the first pass does nearly all the darkening and later ones add almost nothing — a saturation ceiling that falls out of the maths rather than being clamped in.
SEE ALSO Density27