M2 parametric generation
M2 turns patterns into instances: "generate a crew of six", "build the bridge set". It
does this deterministically and offline (Tier 1), with results good enough to swap into
the M1 scene. Read with ARCHITECTURE §4–§6 and ADRs 0002, 0005 and 0007, and the schemas
character-pattern, set-pattern, recipe, prop, prop-pattern.
What ships
| Piece | Where |
|---|---|
| CharacterPattern sampler (rules, archetypes, conditional rules, costume, palette, persona, voice, names) | core/src/generate/character/ |
| Palette solver (OKLab/OKLCh, style color rules, contrast) | core/src/generate/palette.ts |
| Ensemble selection (silhouette, palette and height distance, unique hair) | core/src/generate/ensemble.ts |
| SetPattern sampler (room shell, zones, placements, dressing, walkways, entrances, marks, setups, lighting) | core/src/generate/set/ |
Recipe builder (kit pieces and props from recipe.schema.json nodes) |
engine/addons/reelwright/runtime/stage/recipe/ |
silhouettes job task (character masks for ensemble scoring) |
engine/addons/reelwright/runtime/character/ |
| StyleSpec coverage (every field does something) and a second demo style | engine/addons/reelwright/runtime/style/, examples/shows/marigold/styles/ |
reelwright generate characters and reelwright generate set |
core/src/cli/ |
Determinism rules for samplers
- Seeds. A request has a
seed. Candidate k usesinstanceSeed = deriveSeed(requestSeed, "<patternId>/candidate/<k>"). Every attribute draws from its own stream,Pcg32(deriveSeed(instanceSeed, "<pointer or step name>")). So adding a rule, changingcount, or re-sampling one attribute never reshuffles the others (ADR-0002). - Order. fixed →
params→ archetype → archetype params →rules(in order; a matchingwhenre-draws itsthenrules) → costume → palette → persona → voice → name. - Archetypes. Candidate k of a draw takes its archetype from a low-discrepancy walk
over the archetype weights:
u = (u₀ + k/φ) mod 1, withu₀fromderiveSeed(requestSeed, "<patternId>/archetypes"). So a handful of candidates covers the archetypes in proportion. Independent picks once gave an 11-candidate draw nine rookies, and no crew of three could reach its height spread. Other draws (replace, a single draw) pick from stream"archetype". - Locks and variations. Given an existing instance,
varyre-draws only the listed pointers and keeps everything else byte-identical, including every locked subtree (meta.locks,status: locked). A request that would change a locked pointer is an error, not a silent skip. - Output. Every instance validates against the schema and the semantic validator, and
records
meta.derivation(pattern id, pattern hash, seed, sampler name and version, archetype) withstatus: draft. Sampler versions follow ADR-0007: any output change is a new sampler version.
Character sampling details
- Ranges.
biasuniform, center, low or high (the core'srange), then quantized tostep, then clamped to the instance schema's bounds for that pointer. Integers round. - Choices. Weighted, from the value's stream. A choice value must be a legal enum member of the target field; the semantic validator already checks patterns for this.
- Costume. Each layer rule is included with its
probability. It picks family, cut, fit, sleeves, legs and pattern, plus details (k-of-n without replacement, k fromdetails.pick). Colors copy the rule's slot references; wear is drawn from its range.- An archetype's
costumerules (1.2) take the place of the pattern's layers they replace, in place: the one named inreplaces, else the one with the same id. A rule that replaces nothing is drawn after the pattern's layers. So a captain wears a captain's coat where the rest of the crew may wear a jacket: the crew pattern's captain replacesjacketwithcoat(familycostume.captain_coat, cutcoat, colored from the captain's own@coatpalette slot). - (implementation) A replacing rule draws from its own streams
(
costume/<its id>/<field>), and nothing else in the draw reads the layers, so opting in moves only that archetype's costume and the palette slots it adds (solved after the pattern's). A pattern without archetype costume draws exactly what it drew before; the sampler version stays 1.2.0, since no existing input draws differently. An archetype's layer may use that archetype's own palette slots, and no other archetype's.
- An archetype's
- Palette solver.
- An archetype's
paletterules (1.1) replace the pattern's rules for the same slots, in place; its other slots are solved after the pattern's. - Slots are solved in order:
pick: weighted;ramp: OKLab interpolation at t drawn withbias;derive(in OKLCh):- complementary: hue + 180°;
- analogous left or right: ∓30°;
- triadic: +120°;
- darker or lighter: L ∓ 0.3·amount;
- muted or saturated: chroma × (1 ∓ 0.5·amount);
harmony(1.1): one of several derive rules, picked by weight from the slot's stream, so secondary colors vary across a cast and still relate to the primary.
- Then apply the StyleSpec color rules: saturation and value ranges (HSV) clamp each
slot, except
skinandhair, which keep natural ranges. - Then
minSlotContrastbetween adjacent-part pairs (skin↔costume_primary, costume_primary↔costume_secondary, hair↔skin). Nudge the lighter slot's L up or the darker one's down in 0.01 steps until the WCAG luminance ratio holds, with at most 40 steps. A move may not break a pair the moved slot already meets. After every pair has had its turn, each pair still below the ratio gets a warning.
- An archetype's
- Persona.
traitCountis drawn from its range, then traits are drawn weighted without replacement, the archetype's traits first, with intensity from each trait's range.- The role comes from the archetype, else
roleHints. (implementation) An archetype's role that offers a choice ("captain or first mate") takes the one the backstory names first ("Captain of the Marigold"), whole; it stays as written when the story names none. - Two speech patterns are drawn.
- The pattern cannot know backstory, wants or fears; they are left out (optional fields). The showrunner fills them in M3.
- Voice.
- pitch, pace and energy come from their ranges, and timbre from its choice.
- The local voice binding is the entry in
localVoicesnearest in (pitch, timbre, age hint), using a small catalog of Kokoro voice descriptors incore/src/adapters/voices.ts. - Speed = pace.
- (implementation, sampler 1.3.0) How the character is described leans its dials: the
archetype's name and description, the story's description of them (a persona's
backstory), their traits and speech. Calm, laconic or deadpan draws pace and energy from
the low half of the pattern's range; eager, excitable or lively from the high half;
gruff or gravelly draws pitch from the low half (
voiceLean). A description that says both ways, or neither, leans no way. The ninth watch-it's captain, "calm to the point of absurdity", had come out quick and lively. - (sampler 1.6.0,
voices-by-age.md) A person's pitch, pace and energy are not drawn from the pattern's ranges. They come from where people of their age and sex speak, leaned by their words, and Kokoro's nearest voice is moved to that pitch. Their timbre is the voice's own, unless the words give one.
- Names. A deterministic Tier 1 name generator draws from syllable tables per
theme.language.naming(apeopleconvention if present, else a neutral table). It gives a given name and a family name, plusshortName= the given name in caps. It avoids names already in the show. Ids arechar.<given>plus a numeric suffix on collision. The showrunner may rename in M3.
Ensemble selection
"A crew of six":
Sample
max(3 × count, count + 8)candidates, capped byensemble.maxAttempts.Ask the engine for silhouettes in one job. The
silhouettestask renders each candidate's front view in the neutral pose as a 64-px-tall alpha mask, normalized to height and centered on the feet. Each is a PNG plus its sha256.Descriptors:
- silhouette (a mask);
- dominant color (
costume_primary, OKLab); - height (cm);
- hair style.
Greedy farthest-point selection. Start from candidate 0. Repeatedly add the candidate that maximizes its minimum distance to the chosen set while meeting every constraint:
1 − IoU ≥ minSilhouetteDistance;- OKLab ΔE ≥
minPaletteDistance; - hair unique if
uniqueHair.
Ties break by lowest candidate index. The last pick also counts a missed
minHeightSpreadCmas a violation, so the fill completes the spread when it can. Afterwards the spread must hold over the chosen set, or selection retries with the next start candidate.If constraints can't be met within
maxAttempts, return the best set found with warnings naming the violated constraints. Never loop forever.
Silhouettes are cached by (instance hash, builder major). The core's tests use a fake engine that rasterizes a metrics-based capsule, so no Godot is needed. The real path is proven by the CLI recipe.
Set sampling details
Follow set-pattern.schema.json, in order:
- Room size from its ranges, in meters.
- The shell (floor, walls, ceiling, openings) as elements built from kit pieces.
- Zones mapped to meters.
- Placements in order. Each resolves its piece, params and materials, and its position from its zone and relations to earlier placements (against a wall, facing, beside, centered). Collisions are checked against the footprints of earlier placements, and a placement retries up to N draws before a warning.
- Dressing by density, avoiding walkways.
- Walkways as clearance polygons.
- Entrances. (implementation) A wall gets an opening for an entrance's door when its piece
has a door param; one without is warned of (
generate.wall_no_opening, andpattern.wall_no_openingwhen the pattern is proposed), unless the door piece stands out in the room, deeper than any door cut in a wall (0.5 m): a ladder down from a ceiling hatch. The forty-first watch-it's cargo hold warned of its ladder hatch. (implementation) A pattern may also have openings (openings, SetPattern 1.1): ways on with no door and no wall, each at the middle of a zone, with the side people come on from (a stage's wing). An opening becomes one of the set's entrances, drawn after the placements, on what stands there, so someone coming on stands on the stage's top; a placed piece in its way fails the draw (generate.entrance_failed), and a pattern whose opening names no zone of its own, or that says it is 1.0, is refused when proposed (pattern.opening_zone,pattern.version). The theatre stage had no way on at all, so "Grace walks on from the wings" staged nothing: it has one at each wing now, behind a black curtain upstage of the arch, and doors for the house. - Marks from mark rules. Each mark must be reachable along walkways and not inside a
footprint. (implementation) One that cannot stand where its rule puts it is left out
with
generate.mark_failed, which says what is in the way (a wall, or the piece whose footprint it stands in) and, for a piece, how far to move it out: the twenty-fifth watch-it's showrunner read "is blocked" as its mark under the table being dropped. (implementation) A walkway to a seat leaves through the seat's footprint only when the mark stands inside it, and only a little past its nearest edge (1.5 times the way to it, and 0.15 m more): exempting the whole footprint of a seat's piece, the twenty-fifth watch-it's galley ran a walkway from the hatch across the table to a bench seat behind it. (implementation) A mark to lie on at a piece's anchor stands at the anchor's height (heightM): a bunk's upper berth, not the floor under it. The twenty-ninth watch-it's showrunner set both berths by hand, and the set drifted from its pattern. (implementation) A piece people walk on (walkOn: a ramp, a dune) holds marks and lets walkways across; it is in nobody's way. - Setups: solve the position, look-at and lens from each rule, then check the target is unobstructed by casting rays against footprints in the floor plane.
- Lighting presets.
- The wild wall (below), last.
Wild walls
A three-wall set leaves its front open so cameras can stand there. A deliberate reverse, a camera inside looking out, then sees the engine's flat shell instead of a wall that belongs to the room. Sitcom stages solve this with a wild wall that is flown in only for those shots.
The smallest design that does it: when the room is an interior with openSide: "front", the
sampler adds wall_front with wild: true, using the back wall's piece and colors (else the
first standing wall's) at the front's length and the ceiling height, facing into the room,
with none of the back wall's other params (no porthole, no door). There is no pattern field:
openSide: "none" (a real front wall) and exteriors get none. It is drawn last, so no
placement, dressing, mark, walkway or setup step sees it, and cameras outside still come in
off the open edge exactly as before. The director's set context leaves it out, since nobody
in the room plays to it. A hand-made set marks its own front wall wild (the Marigold
bridge does). Set sampler 1.2.0.
An establishing setup frames every mark where the action is (not those just inside a door)
by its whole face: 0.15 m either side of a head at the mark's posture's height must be in
the picture, as grammar.face_cut reads a face the edge cuts, else the set warns
generate.setup_framing. The nineteenth watch-it's cargo hold counted the fuse box mark's
head as framed at the very edge of its wide, and every scene there opened on the Chief's
face cut in half.
A piece that is only light (madeOfLight: every material a haze or a shaft, such as sun rays
through water or a water surface's sheen) takes no room. The sampler keeps it in the room and
its zone and from nothing else: no piece, dressing, mark, walkway or camera is kept from it, its
footprint takes nothing off a zone's floor for dressing, and the compiler's walks and sightlines
pass through it. The coral reef's water surface over the whole floor had left its sand no room
for sea grass, and the kelp forest had no room for sun rays among its rocks and kelp.
The SetInstance embeds the kit piece recipes it uses (the new optional pieces field;
roadmap task #11). Jobs stay self-contained, and a locked set keeps its geometry even if
the pattern's kit changes.
(implementation) One set type can have several patterns, each a look of its own. The demo
show's bridges come from two: pattern.set.marigold_ship, the Marigold's cramped, patched-up
bridge (round porthole, helm stool, engineering console, pipes, the kettle on its shelf), and
pattern.set.patrol_cutter, the bridge of an official ship such as the Licensing Board's
patrol cutter, with a kit of its own: a wide viewport across the back wall, a command
lectern before it where the officer of the watch stands facing the room, duty consoles under
wall screens along both side walls, the Board's banners and gold seal, a polished deck with
a runner, a sliding door, cool white light and no clutter. The thirty-ninth watch-it's
Boxwood came out of the Marigold's pattern as her bridge, kettle and all; the writers' room
now picks the pattern for a new set (see m5-writers-room.md, the missing-asset pass).
Engine recipe builder
- Recipe nodes (box, rounded_box, cylinder, ring, extrude, lathe, panel, decal, array,
subtract) evaluate their expression-valued parameters with Godot's
Expression, using only the grammar documented inrecipe.schema.json: numbers, the piece's params,+ - * /, parentheses, andmin,max,clamp,abs,floor,round. Anything else is a build error. - Geometry is built with SurfaceTool and CSG only where
subtractneeds it (then baked to an ArrayMesh). The same material slots and set ink are used as for primitives. prim.*stays as is;kit.*resolves from the set'spieces.- Builder major stays 1, unless an existing primitive's output changes (ADR-0007).
Style coverage and a second style
- Every StyleSpec field must visibly do something, or be documented as reserved, with a
test per group:
- line: variation, taper, boil, innerLines;
- shading: bands, rim, highlight;
- texture: paper, grain, halftone, atmosphere, backgrounds;
- post: vignette, bloom, chromatic aberration, weave, crt;
- motion dials;
- camera handheld;
- transitions: wipe, iris.
- A second demo style,
style.bright_modern: thin tapered lines, soft shading, no grain, saturated flat backgrounds, on ones. It proves restyling end to end: switchingshow.styleand re-rendering the M1 scene gives a clearly different look with identical story data.
Character polish (toon.v1)
What the sampler draws must also read on screen. Follow-ups from the watch-its and the M2 polish review:
- Coats. A
coatis drawn as a coat, not a jacket: a shell over the torso and a skirt to its length, split between the legs so a walk reads. Its details are drawn where the schema has them:buttons,collar,rank_stripes,pockets,patches.- (implementation) Length comes from the layer's
legs(short: mid-thigh; unset or none: just below the knee; cropped: mid-calf; long: the ankle), andfitsets the flare as well as the width. Below the belly the coat hangs straight down from the widest of the body above it. - (implementation) The skirt is two halves, one per leg (
coat_l,coat_r). Each hangs from the hips where it leaves the shell and swings with its thigh further down (RwPart.hang: the hips' weight gives way to the thigh's over the upper half). A half's swing is held back on a long or full coat, so its hem never dips to the floor. What faces the camera hangs over that leg; what faces away (the lining, a shade darker) hangs behind both legs, so they show through an open front or a stride's split. - (implementation) Buttoned, a coat closes double-breasted below a V at the neck (two
columns of three); unbuttoned, it hangs open down the front like a jacket. Lapels are
always drawn.
rank_stripesis an epaulette (it still reads on a small figure) and three cuff stripes on each sleeve of the topmost garment that has it.buttonsalso draws a placket on a base garment and a column on a jacket.
- (implementation) Length comes from the layer's
- Afro. An afro must read as a round, lumpy cloud, not a box.
- (implementation) It is a ring of eleven rounded lumps (their sizes from the design
seed) round a ball set high on the cranium. Seen from the front it is cut above the jaw
with the cut curving up to the sides; turned away the cut sinks to the nape, and the
back hairline dips in a U. The cut's corners are rounded off (shrink, then grow back).
A test holds every view to no corner sharper than 45° below its middle and at least
three lumps 4% proud round its top; the old ball turned 66° corners with one 3% lump.
curlykeeps its ball.
- (implementation) It is a ring of eleven rounded lumps (their sizes from the design
seed) round a ball set high on the cranium. Seen from the front it is cut above the jaw
with the cut curving up to the sides; turned away the cut sinks to the nape, and the
back hairline dips in a U. The cut's corners are rounded off (shrink, then grow back).
A test holds every view to no corner sharper than 45° below its middle and at least
three lumps 4% proud round its top; the old ball turned 66° corners with one 3% lump.
- Stride. A gait's feet must plant whatever the legs' length.
- (implementation) The compiler's cadence moves the body two of the gait's steps a cycle
(
GAIT_SPEED_MPStimesGAIT_CYCLE_SEC). Legs too short for that step already crouched (up to a fifth of the leg) and then took a shorter step, and the planted foot slid. Now the solver also takes the short step nominal/short times as often, by scaling the unwrapped cycle count the sampler passes (gait_phase), so the planted foot keeps pace with the floor: the Chief walks and runs 28% quicker and strides 66% quicker, Otto strides 19% quicker. The compiler is unchanged: the cadence fix lives where the legs are known.
- (implementation) The compiler's cadence moves the body two of the gait's steps a cycle
(
- Turning cost. A turning card cost about twice a still one.
- (implementation) Measured on Otto, a drawing costs about 9 ms still (the head is built every drawing) and 25 ms while the body turns: 7.5 ms to build the body view, 5 ms to swap its nodes and skin weights, 3.4 ms to rebuild the hands for it. The view was rounded to 1° at any size. It is now rounded to the coarsest of 1, 2, 3, 4, 5, 6, 9, 10, 12 or 15° (divisors of 180, so front, profile and back stay exact) that moves no body point more than 1 px between views, from the body's reach (its farthest bone end plus the thickest limb, or a coat's hem). A slow turn on a small figure then builds a third to a half of the views; a large figure, a fast turn (a new view every drawing anyway) and a still card are as before. Reusing a nearby cached view instead was rejected: what is in the cache depends on what was drawn before, so a frame would no longer be a function of its number alone.
- Rim light at small sizes. A rim light must not read as a halo on a small figure.
- (implementation) Lines thin with the figure's drawn height down to a floor (35% of the style's weight at 168 px and below). The rim had the same floor, so it grew against a shrinking figure. It now has none: below 480 px tall it narrows in proportion, and below 168 px it also fades, gone by 72 px (15% of 480): on a figure that small any bright edge reads as a halo. The fade follows the figure's drawn height, like the line weights; fading by the rim's own width in pixels dimmed every rim at 640×360 and moved the Bright Modern swatch. A render test holds a 100 px figure's rim share under its 400 px share (it was 1.8 times it).
CLI
reelwright generate characters <show> --pattern <id> --count N [--seed S] [--archetype <id>…]
[--out <dir> | --into-show] [--replace char.a,char.b] [--vary <char> --pointers /a,/b]
reelwright generate set <show> --pattern <id> [--seed S] [--set-type <id>] [--id set.<name>]
[--name <text>] [--out <dir> | --into-show]
Without --into-show, output goes to .reelwright/generated/<request hash>/ as drafts, with
model sheets rendered next to them. --replace keeps the replaced characters' ids, persona,
voice and locked subtrees, which is how the M1 scene is re-rendered with generated looks.
Watch it (M2 done when)
reelwright generate characters examples/shows/marigold --pattern pattern.character.marigold_crew --count 6 --seed 7gives six drafts that validate, with distinct silhouettes, model sheets, and a six-person line-up image.reelwright generate set examples/shows/marigold --pattern pattern.set.marigold_ship --seed 3gives a bridge variant that validates, with set views from every setup.- The Lighthouse cold open re-renders with a generated cast (
--replace) and a generated bridge, with no manual edits to the episode. - The same episode renders in
style.bright_modernby switchingshow.style.