# Carve Night: build notes

A pumpkin carving simulator. Cut a lid, scoop out the goop and seeds, carve or etch a face, set
candles inside, and line the pumpkins up on a porch. Then turn the porch lights out.

Everything on the porch is drawn in the browser with Three.js r185: the pumpkins, the goop, the porch
boards, the siding, the newspaper and the leaves are all made by code when the page starts. Since 1.2 two
things come from other GameLab games: Seven, the robot from Starlight Menagerie, who carves next to you,
and the places you can set the pumpkins out in.

## The shell

A pumpkin is one function, `P(u, v)` in `src/shape.js`. `u` runs once round the waist and `v`
runs from the stem to the blossom end. The ribs are a warped `|sin(n·θ/2)|` raised to a power, so
the lobes are broad and the grooves are narrow. They fade out near the poles. The stem sits in a
dish, the base is softly flattened, and each pumpkin gets its own lean, lumps and rib phases from
its seed.

The inner wall is the same surface with the ribs a third as deep, pulled in along its own normal
by the wall thickness. It is thicker at the base and round the stem, as real ones are.

Seven varieties set the size, rib count, groove depth, wall thickness, colours and features:
Howden Field, Cinderella, Lumina, Knucklehead, Jarrahdale, Sugar Pie and Big Max.

## Carving is painting

Each pumpkin keeps three pictures of itself, unrolled like a world map (`src/masks.js`):

- **keep**: white where the shell is still there. Every cut paints black here.
- **etch**: white where the skin is untouched, grey where it has been peeled.
- **goop**: white where pulp still clings inside.

The skin and the inner wall both use keep as an alpha map, so a hole goes straight through. The
shadow materials use it too, so a candle throws the carved face onto the porch.

### Cut walls

A hole needs a wall: the pale, wet face of the cut. `src/contour.js` runs marching squares over
the keep mask in 64-pixel tiles and finds every outline to a fraction of a pixel. Each outline
point becomes two vertices, one on the skin and one on the inner wall, at the same `(u, v)`. Only
the tiles touched by the last stroke are redone, so the walls keep up while you drag.

The wall shader colours the cut across its depth: a thin rind of skin, then pale flesh, then the
stringy orange of the inside. Candlelight hits it nearly edge-on, so it glows the most.

### Plugs and the lid

When a knife loop closes, or a stencil is pressed, the piece inside is copied into a small mask of
its own and rebuilt as a patch of skin, inner wall and cut wall (`src/piece.js`). It pops out of
the hole and falls onto the table. A loop round the stem makes the lid instead. The lid is cut a
hair smaller than its hole, so a thin line of light shows round it when it is back on.

### Etching

Etching peels the rind without cutting through. Shallow etching leaves flecks of rind; deep
etching shows clean flesh. The skin shader bevels the edge of the peel and lets candlelight
through in proportion to how deep it goes, so etched patterns glow softly while the cuts blaze.

## Goop

The pulp is a third surface inside the inner wall, as thick as a few centimetres at the base and
thin near the top. Scraping reduces the goop mask; the surface sinks back to the wall as it goes.
Up to 220 seeds ride the surface and vanish when their spot is scraped. Fibres are thin tubes
between nearby points on the pulp, with a few long ones across the hollow, rebuilt as you scoop.
Every scoopful flies to the bowl and joins a pile there.

When the lid comes off, strings of goop stretch between the lid and the pulp and snap one by one.

## Skin and subsurface light (1.1)

A pumpkin's rind is a thin waxy cuticle over a layer packed with carotenoid pigment, with orange flesh
under that. Very little light bounces straight off. Most of it goes in, the pigments absorb the blues
and greens, and what comes back out has travelled a few millimetres sideways. Three things follow, and
the shaders now do all three:

- **The highlight is broad and soft.** Wax, not varnish: no clear coat, a lifted roughness range, a low
  specular level and a faint dusty sheen at grazing angles.
- **Diffuse light wraps, by colour.** The direct diffuse term in three's physical lighting is replaced
  (`SSS_LIGHTS` in `src/materials.js`) with wrap lighting per channel: red wraps furthest past the
  shadow line, blue hardly at all. Each channel uses its own normal: red follows the smooth surface,
  green half the bump map, blue all of it. Small bumps show in the highlight and melt in the diffuse, and
  the terminator glows a deeper orange. Ambient light that enters the rind comes back warmer too.
- **Flesh is far more translucent than rind.** Peeled skin, the cut walls, the inside wall and the goop
  wrap further and soften their normals more.

Light from a candle inside comes through the shell with the translucency term from three.js's
`SubsurfaceScatteringShader` example (Barre-Brisebois, GDC 2011, "Approximating Translucency"): a
distorted half vector toward the light, raised to a power, scaled by how thin the wall is (peeled or
etched flesh is thin, rind is nearly opaque). three's version multiplies by the shadowed light colour,
which is zero behind a closed shell, so the skin shader reads the candle positions directly.

This also follows the real-time skin approximations in GPU Gems, chapter 16 (wrap lighting with a scatter
band at the terminator). Thickness-based transmission is left to the candle glow terms, which already
know how far the light has to travel.

### Depth in the carving

The hollow is enclosed, so it should be dark. The inside wall, the goop and the seeds now get only a
small share of the sky and the room (`cnHollow()`), more near the top with the lid off. The face of a
cut gets the full room at the skin and very little at its inner edge. What lights the inside is what
comes through the openings and the candle. A carved face now reads as holes into a dark, lit-from-within
space instead of flat yellow shapes.

### Stems with an age

A stem dries from the cut tip toward the shoulder as the pumpkin cures: green, then yellow-green, then
tan, beige or woody brown, ridged and split along its length. Each pumpkin gets an age and a dried
colour from its seed, so some come straight off the vine and some are fully cured. Cinderella,
Jarrahdale and Big Max are *C. maxima*: their stems are fat, round and spongy and dry to grey or beige
cork, and the geometry follows.

## Light

- **Skin textures** are baked on the GPU at load (`src/bake.js`). The colour shader repeats the rib
  formula so the dark grooves fall in the mesh's grooves. It adds stem-to-blossom streaks, a mottle,
  a green-gold collar round the stem, the pale ground spot where it sat in the field, cork scars,
  lenticels, warts for the Knucklehead and field dirt. Roughness rides in the colour map's alpha;
  a cavity term rides in the normal map's alpha.
- **Candles** are tea lights or pillars with a shader flame and a real point light. The pool of
  lights is fixed in size, so lighting a candle never recompiles a shader. Shadow maps are redrawn
  only when something moves.
- **Glow through the flesh.** Each pumpkin's surfaces know where its candles are. Peeled skin, cut
  walls, the inner wall and the goop add light in proportion to the candle's irradiance, with a
  softened falloff so nothing blows out next to the flame.
- **The porch** has three moods: the clamp lamp on for carving, the porch light on for arranging,
  and lights out, where only the moon, a dim window and the candles are left.
- **Post**: bloom, a neutral tone curve, then a film pass with vignette, grain and a little fringe.

## Seven (1.2)

Seven is the FW-7 field robot from Starlight Menagerie: the same GLB (`assets/seven/fieldwork-characters.glb`),
clips and tread rig that Island V5 and FIELDWORK use, at 17% of his usual size so he fits on the end of the
table. He has a Jack Be Little of his own (a mini variety only he gets) on a chopping board, a paring knife
in his right hand and a scoop in his left.

He copies you (`src/seven.js`). Every finished step on your pumpkin is sent to him as an event: the lid,
a scoop session, a stencil (with its id, size, tilt and where it went, relative to the side of your pumpkin
facing the camera), a knife, gouge or etch stroke (the whole path), a drill hole, a candle, the lid going back
on. He queues them and works through them one at a time:

1. **Peek.** He leans over, turns his head and squints, and a cyan scan beam runs from his visor to your
   pumpkin. If the camera looks straight at him, or you tap him, he's caught: he snaps upright, looks at the
   sky, whistles and says something innocent. The job waits and he tries again a few seconds later.
2. **Work.** He turns to his pumpkin and does the same operation, carved on the side facing him, with one
   thing wrong. The lid is crooked, tiny, enormous or zigzagged. The stencil is upside down, huge, tiny, on
   the back, the wrong one, one eye of a pair, too high, stamped twice, only etched or tilted. Strokes come
   out wobbly, flipped, bigger or shifted. The goop flies over his shoulder, or onto his own visor, or half
   of it stays in. Candles: three of them, four, one on top of the pumpkin, or the lid back on upside down.
   He never picks the same mistake twice running.
3. **Reveal.** He spins his pumpkin round to face you and reacts: proud (Celebrate) or not sure (Concern),
   with a chirp or his chatter and a short line in a speech bubble.

His carving uses the same Pumpkin class as yours, so his cuts have walls, his plugs pop out and his candles
glow through the shell. The clips (idle, look, celebrate, concern, interact) play from the GLB; the peek, the
sawing arm and the scooping arm are added on top each frame, after resetting those joints to rest so the
offsets never accumulate.

## The Seven cam (1.3)

`state.sevenCam` swaps the carving camera for one at his end of the table (`sevenCamTarget()` in `src/main.js`):
it orbits the point between him and his pumpkin, and the usual drag and zoom still work within tighter limits.
Seven is told the camera is hidden (`env.hidden`), so `isWatched()` never catches him through it, and he
presents his pumpkin toward the carving camera you left (`env.eye`), not toward the hidden one. Copies of
stencils and strokes use the same remembered eye (`carveEye`) to work out which side of your pumpkin was the front.

## The trailer (1.3)

The trailer is shot from the game itself (`tools/trailer.mjs`, `tools/trailer-shots.mjs`). Headless Chrome loads
the page with `tools/vclock.js` injected first: it freezes `performance.now`, `Date.now`, `requestAnimationFrame` and
the timers, and seeds `Math.random`, so the game moves exactly one twenty-fourth of a second per frame however long
the frame takes to draw. A session loads the game once and plays its takes in order; Seven's jobs are held back
between takes and released when a take wants him to act. `TL.sounds` logs every sound the game asks for with its
time, and the edit puts real sounds in the same places.

Shooting it found five bugs. Candles crowded into one pumpkin added up without limit (four in a Jack Be Little
blew the night shots out), and small pumpkins glowed far hotter than big ones: both are fixed in `CandleRig.update`
and `feedCandles`. Seven worked out which side of his pumpkin faced him before turning it back from showing you,
so every copy after the first went on the back; `startWork()` turns it first now. And a copy could land inside his
own lid hole after an enormous lid; copies are kept below the rim. And the sea at Kelda Head and Lumen Reach
never drew: its fragment shader used fog but the vertex shader never passed the fog depth, so the program failed
to link. `SEA_V` includes the fog chunks now.

## Places to set them out (1.2)

The carving always happens on the porch. The display can move (`src/places.js`), and each place is built
from another game's models:

| Place | From | What's used |
| --- | --- | --- |
| Kelda Head | Prism Light | the lighthouse at a third size with a sweeping beam, reef rocks, bushes, the wreckers' false light, a wreck and a bell buoy in the cove, the far isles |
| Lumen Reach | Island V5 | basalt terrace, steps and columns, boulders, glassblooms, the lumen pylon, the dock and its path lamps, the field ship, the stone arch |
| The garden deck | Starlight Menagerie | deck plating and wall panelling textures, bench, crates, barrels, hay, pallet, cart, toolbox, lamp post, placard, a spruce; a ringed planet in the window |
| Firehawk City | Firehawk | brick, sidewalk and asphalt textures, the street lamp, hydrant, mailbox, newsbox, trash can and a cab; a brownstone stoop built here |

Every place uses the same lights as the porch (one shadowed spot, one point, a shadowed moon or sun and a
hemisphere), so moving the display never recompiles the pumpkins' shaders. Each has its own sky, fog,
reflection map and two moods. Display spots are dropped onto whatever is under them (the highest of five
samples across the pumpkin's footprint, so one standing across two basalt columns sits on the taller).

Place files are not part of the start-up download: `assets.js` lists them under `CN_PLACES`, and
`src/gltf.js` fetches a place's files the first time it is picked. The save remembers the place, and loose
candles remember which place they were set down in.

## Things learned the hard way

- Three.js reads a material's uniforms object once, when it compiles. A baker that swapped the
  whole object between passes kept drawing the first pass. It now only changes `.value`.
- A negative shadow bias on a light that starts inside a closed shell lets a thin line of light out
  under the base, where the skin sits on the table. The candle lights use a positive bias.
- Three copies a mesh material's `alphaMap` and `alphaTest` onto its custom shadow material. The
  invisible shadow copy of the skin had none, so its shadow was solid and no carved face was thrown by the
  candle; the lid's skin material had a colour map whose alpha is roughness, so most of the lid vanished
  from the shadow pass and the work lamp flooded the inside. Both now cast from plain invisible
  materials that carry the cut mask.
- A knife that only accepts hits on intact skin stops dead when it reaches its own cut at the end of
  a loop. Dragging tools now follow the skin surface whether or not it has been cut there.

## Files

| File | What it does |
| --- | --- |
| `src/shape.js` | Varieties, `P(u, v)`, normals, inner wall, grids |
| `src/bake.js` | GPU texture baker for skin, flesh, goop, stem, wood, siding, straw |
| `src/masks.js` | Keep, etch and goop masks, painting, undo |
| `src/contour.js` | Marching squares cut walls |
| `src/materials.js` | Physical materials with candle glow spliced in |
| `src/pumpkin.js` | One pumpkin: meshes, goop, seeds, fibres, stem, lid |
| `src/piece.js` | Plugs and lids cut free |
| `src/candle.js` | Candles, flames and the light pool |
| `src/porch.js` | The porch, the table, the sky and the lighting moods |
| `src/stencils.js` | 23 stencils, projected through the shell |
| `src/post.js` | Bloom and the film pass |
| `src/audio.js` | Synthesised sound |
| `src/save.js` | The porch kept in IndexedDB |
| `src/main.js` | Tools, camera, input, HUD, porch arrangement |
| `src/forge.js` | Prism Forge adapter, shared with Prism Light |
| `src/seven.js` | Seven: rig, treads, peeking, copying (badly), reactions, speech |
| `src/places.js` | The four borrowed places: models, skies, lights, moods, display spots |
| `src/gltf.js` | GLTFLoader with meshopt, and on-demand fetching of place files |

`python3 tools/make_manifest.py` rewrites `assets.js` and the module list in `index.html` after
adding or renaming a file. Bump `build` in `index.html` whenever a file changes.
