The loop on the rig-c organisation's front page, with everything that made it:
one generated painting, its two See-through layer sets, the config an agent wrote for
@rig-c/parts, and three short scripts. One command rebuilds the loop
from them with the published packages.
It is here as a worked case, not as a template: a character that is not standing, a prop that is part of her skeleton, a performance longer than an idle, and a record of what review caught on the way.
To change the film rather than rebuild it, read WORKING.md: where each kind of change goes, how to check one without being able to watch it, and the rules this film was corrected into.
git clone https://github.com/rig-c/trailer && cd trailer
pip install -r requirements.txt # numpy, pillow
./build.sh # installs @rig-c/parts 1.12.0 and rig-c 2.47.1, then runs the five stepsNeeds bun ≥ 1.2 (the tools run on it), npm, and python3. No GPU and no ComfyUI: the
painting, the sky, the palm and the See-through layers are inputs, already in inputs/.
The whole build took 12 min 29 s on an Apple M4 that was doing other work (measured once, this cut).
The result is out/scene/trailer.webp (960×540, 240 frames, 12 s at 20 fps), with the 1920×1080 frames
beside it and a contact sheet. Run from a clean directory with the published packages, on the machine the trailer was made on, it reproduced the published loop byte for byte — all 240 frames and the WebP (python 3.13.1, pillow 12.2.0, numpy 2.4.4, bun 1.4.2). Another machine's encoders and caption font can move bytes without moving the picture.
| An input, not rebuilt | inputs/painting.png, inputs/layers/ (See-through), inputs/sky.png, inputs/palm.png — generated on a ComfyUI box; generation.json, sky.json and palm.json record how, and tools/trim_loops.py the one edit made to the painting and its back-hair layer afterwards |
| Written by the author (an AI agent) | inputs/config.json, keypoints.json, requirements.json, and film/ |
Rebuilt by build.sh |
the parts, the rig, the compiled skeleton, every rendered frame, the composite |
1. Parts, rig, check — rig-parts build. The painting and the two See-through runs are assembled into
23 rig-ready parts, meshed and weighted to the bones config.json declares (1,420 triangles in ten meshes; a
lattice at each part's own spacing had 3,081 — see The meshes), given a four-second idle, and
checked. The check passes nine bars of nine, two of them through a declaration:
BREATH_VISIBLE: PASS — declared_not_planted shoes; …
TIP_OVER_ROOT: PASS — declared_rises handwear_r; …
check: PASS; 9 of 9 bar(s) measured, 0 skipped
rig-parts check assumes a figure that stands. inputs/requirements.json says what the scene knows and the
tool cannot: she sits, so her shoes are not planted, and her right forearm rises from the elbow. A relaxed
bar prints the declaration first, so it never reads as a passed one.
2. The film rig — film/palm.py, film/rig.py. Plain rig-spec and motion-spec data added to what
step 1 wrote: the swing, a physics constraint on every chain, the breath, the palm, and one animation.
The sections below say what each is for.
3. Compile and gate — rigc build, twice. Nothing is written unless the gate is green:
53 assertions: 20 measured (20 passed, 0 failed), 17 skipped, 16 not in profile "spine"
bones=83 slots=29 animations=2 regionAttachments=18 meshAttachments=11 physicsConstraints=24
The second build is the same rig with every slot tinted black; step 5 needs it.
4. Pose — rigc render --geometry. 321 frames of the rig, and geometry.json: every bone's world
transform and every mesh vertex after skinning, per frame.
5. The film layer — film/compose.py. Sky, lights, the mesh-and-bone reveal, captions.
| Beat | Seconds | What moves it |
|---|---|---|
| Sway | whole loop | swing rotates ±2.5° about a pivot far above the frame; seat and the whole character hang under it |
| Breath | whole loop, 3 s | the chest rises 14 units and widens; the shoulders follow 0.12 s late and the head 0.2 s late; hair and the soft chest take it through their springs |
| Tilt and smile | 1.5 – 4.1 | head rotation; the eyes group's scaley, brows, mouth |
| Kicking legs | 4.3 – 6.4 | leg1 rotation, with a spring on both leg bones |
| The reveal | 4.1 – 6.6 | not the rig: the film layer drawing the render's own geometry |
| Hop | 6.7 – 7.8 | seat jumps 44 units; everything on a spring arrives late |
| Wave | 9.6 – 11.7 | the forearm from the elbow, an open palm swapped in, the hand trailing at the wrist |
The swing is two bones of her skeleton (root > swing > seat > hip), and its bar and ropes are three
attachments on seat. So the sway is ordinary bone motion, and it reaches her hair the way any motion of a
parent does: through the physics constraints on the chains. Nothing synchronises a character to a prop.
24 physics constraints. rig-parts keys its idle on a <bone>_ctl parent of every chain link, which
leaves the link itself free for a constraint; rig.py gives one to each link of the twin tails, bangs,
tassel, skirt, sleeve and legs, tapering in strength toward the tip, and one to the chest.
The soft chest is the recipe from the parts guide: a bone under chest, a circular region of the
topwear mesh handed to it in config.json, and a spring on its x and y. It moves 4 – 6 units through the
sway and about 20 on the hop.
Depth by parallax. Three bones get a keyed twin above them (leg0_px, leg1_px, chest_jiggle_px),
keyed in step with the swing: what is nearer the viewer — knees, feet, the chest's front — travels a little
further than what is behind it, and the torso leans 1.5° against the sway. It is bone motion bending
meshes, not a depth model.
The breath replaces the idle's own (2.6 units on the swing's 4 s, which read as part of the swing).
Three seconds a breath, four to the loop, so it never falls in step with the sway. The shoulders and the
head each ride a twin bone above the one rig-parts keys, and follow by lagging rather than by a motion
of their own.
The meshes. Six parts are on auto — a traced outline reduced under declared bounds — and four on
contour, a traced outline with a coarse grid inside: the skirt and the waving sleeve as
rig-parts mesh-draft drafted them, the hairpin at the same settings, and the long hair. Each part is on
the mode that came out smaller when both were built. Where the film bends a chain a few degrees auto is
far the smaller (the back hair, 28 triangles against a lattice's 120). Where it bends one 24° or 33°, a
reduction held to those bends kept two to three times what the draft needs — the skirt 276 against 132,
the sleeve 148 against 49 — and the film is the same to the eye. Each auto chain's bend in this film is
declared in config.json (auto.stimulus, measured with tools/bends.py), because a mesh is otherwise
reduced against the idle, and the idle bends a chain one or two degrees. No triangle folds over anywhere
in the film (rigc render --folds names no mesh): the long hair and the hairpin, which folded three and
two in the last cut, declare their weight cut (influences). The per-part counts and the declared bends
are tabled in WORKING.md. The reveal draws each mesh's outline over its inner edges, so the silhouettes
read.
The loop closes with a lead-in. Springs make an animation's first frame differ from its last. The
animation is 16 s: the loop's last four seconds, then the loop. By the loop's first frame the springs are
where they will be at its last, and step 5 keeps only the loop. tools/loop.py reads it: 0.000 units
between loop time 0 and one loop later, on every bone and vertex.
That was not true of this film until rig-c 2.45.2. Through seven cuts the springs were up to 26 px apart
at the seam and jumped together on three frames inside the loop, and the page said the loop closed. See
the last row of the table below.
The author of a rig cannot see it; an agent reads a handful of frames. Every row below was green at the gate and wrong in the picture, and was found by a person watching the loop.
| Seen | Cause | Fix here | Where it went |
|---|---|---|---|
| "The eyes do not match the tilt of the face" | The head is painted 23.7° off vertical and the five face bones were written with the page's axes, so a blink's scaley squashed the eyes along the page |
first by hand in rig.py; since @rig-c/parts 1.7.0 the rig stage stands the five bones on the line through the eyes and declares it, rig-c 2.40.0's gate holds the build to it (A51), and rig.py only checks that it was done |
parts#239, core#1351 — both shipped |
| "The chest does not move — a paper doll" | The torso was one mesh on two rigid bones | the soft region and its spring; the parallax | — |
| "The hand is too small: an open hand goes by the size of the face" | The palm was sized from the painted, half-hidden hand | wrist to fingertip 58 → 78 units, about half the face's width | — |
| "Her wrist breaks when she waves" | Two things: the palm's forearm pointed the way the painted wrist did, 14° off the sleeve; and the whole palm turned ±15° at the cuff, against the arm | the forearm follows the sleeve's axis; the palm is two pieces, and only the hand turns, at the wrist, ±7°, a quarter beat behind the arm | — |
| "Hair lying over the seat will be hard to swing" | In the first painting the twin tails draped over the swing | a new painting, hair hanging behind the seat line (generation.json) |
— |
| The swing's end joints read as eyes | Glossy balls with a dark centre | flat open rings | — |
| "The curled loops beside her hips read as hair lying on the ground; she is in the air, so they float" | The painting drew a ring of hair on the outside of each twin tail at hip height | both rings erased from the painting and the See-through layer (tools/trim_loops.py: the tail's edge measured on the rows above and below each ring, everything outside it removed) |
— |
| "I cannot feel her breathing" | The idle's breath was 2.6 units, and at 6.5 (2.5 % of the torso, right for a standing figure in close-up) it was still lost: she is small in the frame and swinging | 14 units, its own 3 s period, shoulders and head following late | the drafted breath now scales with the torso (parts#252, #270) |
| The loop did not close on its springs, and springs jumped on three frames | Not the film: the renderer advanced physics in steps of 1/60 s under frames of 1/20 s, and three such steps are not exactly a twentieth in floating point, so what a spring showed flipped between two states (the official runtime does the same on the same calls) | nothing here; rebuilt on the fixed renderer | rigc render got a physics clock of its own (core#1380, rig-c 2.45.2). Found by an agent handed this repository and nothing else, checking a claim this page made |
| "The meshes have no outline" | The reveal drew every edge alike, and the largest mesh was still a lattice whose blocks covered the rest | the long hair on a traced outline; outlines drawn over inner edges | — |
Four more were found by the tools refusing, or by the author reading their reports, and became issues:
long hair behind the back dropped from the assembly plan
(parts#233), a cut piece given its base's chains a second time
(parts#230), the check's standing-figure bars
(parts#231), and a tip on a single bone accepted and ignored
(in #239). All of them have shipped; the first three in @rig-c/parts 1.6.0, which is why step 1 is one green command here.
One was found by the author comparing two builds frame by frame: with every mesh reduced on the idle, the
build and every check were green and the skirt's hem kinked in the film. The film bends its chains ten to
twenty times further than the idle does. That became the rule in WORKING.md, the declared bends in the
config, and two issues — what a mesh should be reduced against (parts#265)
and a weight rule that folds triangles under a large bend (parts#284).
Both have shipped, and this cut uses them: the figure's own spacing and rig-parts mesh-draft for the
first, meshes.<part>.influences for the second. The hairpin's tassel swings 95° from rest, and at that
bend its mesh folded before anything was reduced — the tool now says so in one sentence
(parts#280) rather than leaving it to be found, and with the
weight cut declared it no longer folds.
Rebuilding this repository on each new release is itself a check. Moving it to 1.7.0 found one more: a face bone that takes its own tip from the config gets no declared axis, and nothing says so (parts#258) — this config had done exactly that as the 1.6.0 workaround. Moving it to 1.10.0 and 1.11.0 found the count in The meshes: the author's own reduction of the three most-bent parts was two to three times heavier than what the new draft command wrote, and nothing had said so because nothing had built both. rig-c 2.46.0's fold reading named triangles nobody had seen at two curl tips and on the hairpin's outline; that it named them without saying where they were became core#1386, and 2.47.0 prints the box and frames a picture to it.
rig-c draws a build for an agent to read — bones, names, a coordinate grid, what the rig declares, any mesh's
weights and every mesh's density against the figure, over the art — with a legend beside the picture that maps pixels back to rig coordinates:
node_modules/.bin/rigc inspect --candidate out/build --show art,bones,names,declared --dim --zoom bone:head:120 --out head.pngMost of the findings in the table above were first seen in pictures like that one, drawn by throwaway scripts before the command existed.
rig-c makes an object that can be directed; composing a take is the consumer's. This repository is a
consumer. So:
- The rig: every motion in the table above, compiled by
rigc buildand posed byrigc render. - Staging: the sky, the lights, the shadow, the captions, and the choice of when she does what.
- The reveal is in between and is honest about it: the lines are drawn by
compose.py, at the positionsgeometry.jsongives for that frame. The caption's counts — bones in the skeleton, meshes drawn — are read from the same file.
The frames are posed by rigc render, not played by @rig-c/play: the
trailer is a video. The compiled model in out/build/ is what @rig-c/play loads.
build.sh the five steps
film.sh steps 2-5 alone, for a change to film/
WORKING.md how to change the film and check the change
inputs/painting.png the generated painting, 1664×2432; generation.json and pose_seated.json record it, tools/trim_loops.py the edit after
inputs/layers/ the two See-through runs (full body, head crop) and what each was fed
inputs/config.json the rig-parts config: plan, bones, meshes, regions, motion
inputs/keypoints.json the joints, read off the painting
inputs/requirements.json what the scene declares to the check
inputs/sky.png the painted sky; sky.json records it
inputs/palm.png the generated open hand; palm.json records it
film/palm.py the palm, cut out, recoloured and split at the wrist
film/rig.py the film rig
film/compose.py the film layer
tools/ the readings WORKING.md describes: frames.py (two builds, frame by frame),
moved.py (which bones a change moved), overlay.py (two frames, red and cyan),
loop.py (does the loop close; does anything pop), bends.py (how far the film
turns each bone from its setup pose); trim_loops.py is the input edit, not a reading
trailer.webp what build.sh makes
MIT, covering the scripts, the generated images and the trailer — see LICENSE. NOTICE.md names the
checkpoint the images were generated with, See-through, and the tools the build runs.
