rig-c 0.0.0-stage → 2.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
package/docs/RIGGING.md
ADDED
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
# Authoring a hierarchy — bones, pivots and chains
|
|
2
|
+
|
|
3
|
+
**Read this when the request is a skeleton rather than a movement.** It is written
|
|
4
|
+
for an agent that has been handed loose part PNGs and has to decide how many bones
|
|
5
|
+
there are, where each one sits, what hangs off what, and which of those decisions
|
|
6
|
+
the frames can check.
|
|
7
|
+
|
|
8
|
+
[AUTHORING.md](AUTHORING.md) is the two file formats, the emission rules and the
|
|
9
|
+
failure map — read it first and keep it open; this page never restates a field it
|
|
10
|
+
documents. [MOTION.md](MOTION.md) is what goes *between* two poses once the
|
|
11
|
+
skeleton exists. This page is the part before both of them, and the part neither
|
|
12
|
+
has: **the structure itself, and which of its numbers are measurements.**
|
|
13
|
+
|
|
14
|
+
🚨 **Nothing here grades a hierarchy, and the reason is different from MOTION's.**
|
|
15
|
+
MOTION cannot grade a movement because a movement is a judgement. A hierarchy is
|
|
16
|
+
not a judgement — it is either the structure the pictures were made with or it is
|
|
17
|
+
not — and it is *still* ungraded, because **the pixels are nearly blind to it.** A
|
|
18
|
+
rig with its head off its torso passes the gate ([README](../README.md)). A rig with
|
|
19
|
+
every joint in the wrong place reproduces the setup pose *exactly* and pays for the
|
|
20
|
+
error somewhere in the movement, where it reads as an ordinary residual. The
|
|
21
|
+
instruments that see structure at all are two, they are named in **§11**, and
|
|
22
|
+
neither of them is a pass bar.
|
|
23
|
+
|
|
24
|
+
- The two spec files, field by field: **AUTHORING §1–§4**; the bone list itself is
|
|
25
|
+
**§3.2**, the constraints **§3.5**, and `invariants` — where a hierarchy claim
|
|
26
|
+
that the artifact cannot carry gets written down — is **§3.7**
|
|
27
|
+
- Named failures, and the file each one points at: **AUTHORING §5–§6**
|
|
28
|
+
- Reading a pose out of a picture with no rig yet: **AUTHORING §11** (`rigc pose`)
|
|
29
|
+
- Reading the parts of that picture `pose` refuses, *through* a rig you already
|
|
30
|
+
have: **AUTHORING §12** (`rigc chainfit`). It is the one instrument that answers
|
|
31
|
+
a question about structure from a picture, and **§12.5** is what it cannot see
|
|
32
|
+
- Fitting a whole figure's pose to a frame, and the pivot arithmetic that decides
|
|
33
|
+
whether it converges: **AUTHORING §8.1**
|
|
34
|
+
- Solving a pivot from two poses, and the conditioning that decides whether the
|
|
35
|
+
answer means anything: **MOTION §3.9**
|
|
36
|
+
- Moving a pivot inside a skeleton **somebody else authored**, and what the
|
|
37
|
+
instruments say about it: **[INGEST.md](INGEST.md) §4.1**
|
|
38
|
+
- The conventions an editor user follows without being told — one image per
|
|
39
|
+
attachment, draw order keyed rather than re-parented, gauges: **AUTHORING §10**
|
|
40
|
+
- If the figure is a **face**, the hierarchy has a closed form and
|
|
41
|
+
[FACE.md](FACE.md) §3 and §7 are it
|
|
42
|
+
- If you are the *person operating* an agent rather than the agent:
|
|
43
|
+
[PROMPTING.md](PROMPTING.md)
|
|
44
|
+
|
|
45
|
+
📎 **Where the lessons come from.** Every section below is a stumble that happened
|
|
46
|
+
more than once, ranked by how often. Most of them are already codified in the
|
|
47
|
+
sections listed above, and where a figure exists only in the record of the run that
|
|
48
|
+
found it, that run is cited — as **provenance for a reader of record**, in
|
|
49
|
+
AUTHORING §0's sense, not as an input to be followed. The figures produced *here*
|
|
50
|
+
are re-derived on the fixture in the [appendix](#appendix--the-figure-this-page-measures-on)
|
|
51
|
+
and every command on this page was re-run from it verbatim. Those citations —
|
|
52
|
+
`gallery/…`, `selftest.ts`, the run records — are **repository material and not in
|
|
53
|
+
the npm package**, so they are linked by absolute URL.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 0. The normal form
|
|
58
|
+
|
|
59
|
+
**A hierarchy is decided in an order, and three of the decisions invalidate work
|
|
60
|
+
made behind them.** That is the whole reason this page has a shape. A wrong easing
|
|
61
|
+
costs one edit; a wrong pivot costs every pose fitted under it.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
the art → what the parts are, and where each one's joint is DRAWN
|
|
65
|
+
↓
|
|
66
|
+
the tree → how many bones, what hangs off what §5 §6.5 §10
|
|
67
|
+
↓
|
|
68
|
+
the pivots → where each bone sits §1 §2
|
|
69
|
+
↓ ⚠️ moving one of these invalidates every pose fitted under it — §3
|
|
70
|
+
the reach check → can each chain get to the extremes the shot visits? §6.1
|
|
71
|
+
↓ ⚠️ failing this invalidates every pose fitted under it — §6.1
|
|
72
|
+
the gauges → which parameters the pixels cannot see at all §4 §11
|
|
73
|
+
↓ ⚠️ folding one of these AFTER keying re-writes every key
|
|
74
|
+
the keys → MOTION.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
⭐ **The three ⚠️ rows are the argument for doing this at all.** Each one is cheap
|
|
78
|
+
arithmetic before the first fit and a re-run of the whole shot after it. Two of the
|
|
79
|
+
three are stated in AUTHORING §8.1 as *"do this per chain, before its first fit,
|
|
80
|
+
because the surgery to fix it invalidates every pose already fitted"* — this page's
|
|
81
|
+
§3 and §6.1 are what that costs when it is skipped.
|
|
82
|
+
|
|
83
|
+
### 0.1 What this page will not do for you
|
|
84
|
+
|
|
85
|
+
Three fields of a bone are **not in any picture**: `parent`, `length`, and
|
|
86
|
+
`inherit`. Whatever you write for them is reasoning, and every honest run in the
|
|
87
|
+
corpus says so out loud rather than reporting them as read. §11.1 is the list.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 1. Where a bone goes: on the joint, with the art pushed out
|
|
92
|
+
|
|
93
|
+
**Put the bone at the joint and give its attachment an offset. Do not centre the
|
|
94
|
+
bone on the art.** This is the single most repeated structural decision in the
|
|
95
|
+
corpus — seven independent records state it, and one of them states it as the
|
|
96
|
+
reason a whole instrument works at all.
|
|
97
|
+
|
|
98
|
+
⭐ **The reason is not tidiness, it is observability.** Art centred on its own
|
|
99
|
+
pivot *turns in place*: the drawing rotates, its centre does not move, and the
|
|
100
|
+
silhouette of anything roughly symmetric barely changes. A search over that bone's
|
|
101
|
+
one angle moves almost nothing, so **a wrong angle costs almost nothing** and the
|
|
102
|
+
number cannot say which way to go. The `chainfit` fixture says it in one comment,
|
|
103
|
+
beside the offsets it exists to make visible:
|
|
104
|
+
|
|
105
|
+
> *"Every limb attachment is OFFSET from its bone, and that is what makes the hinge
|
|
106
|
+
> visible: art centred on its own pivot turns in place, so a search over one angle
|
|
107
|
+
> would move nothing and a wrong hinge would cost nothing either."*
|
|
108
|
+
> — [`selftest.ts`](https://github.com/firejune/rigc/blob/main/selftest.ts), the chain-fit fixture
|
|
109
|
+
|
|
110
|
+
⇒ **A correctly placed pivot also collapses the spec.** A pendulum whose bone sits
|
|
111
|
+
on its hinge needs one `rotate` track and no translate at all; the same part with
|
|
112
|
+
its bone at the drawing's centre needs a rotate *and* a translate that traces the
|
|
113
|
+
arc, and the two have to agree on every key. AUTHORING §10.3's gauge rule and this
|
|
114
|
+
one are the same rule read from two ends.
|
|
115
|
+
|
|
116
|
+
### 1.1 The offset along the bone is its own parameter, and it has its own minimum
|
|
117
|
+
|
|
118
|
+
⚠️ **"On the joint" is a direction, not a number.** Where a part's own joint sits
|
|
119
|
+
*inside its drawing* is frequently not visible — the joint is under the part above
|
|
120
|
+
it — and the offset that follows is then a parameter you sweep rather than measure.
|
|
121
|
+
It is worth sweeping: on one shot it was **the single largest fidelity lever in the
|
|
122
|
+
whole run**, and the art's own rim was 48 units away from the answer.
|
|
123
|
+
|
|
124
|
+
| Record | The parameter | What the sweep read |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| rung 8, second attempt | where a trail's blunt end sits relative to the ball it comes out of | 0 → 2.70, **−48 → 1.86**, −99 → 4.11 (mean window residual); frame 0 alone 4.96 → 1.35 |
|
|
127
|
+
| rung 8, first attempt | where a chain's first bead sits below its hang point | 0 → 2.762, **30 → 2.497**, 49 → 2.642 (window MAE), re-run at 26/28/30/32/34 |
|
|
128
|
+
| rung 1, second attempt | where a cast shadow's scale pivot sits below its own centre | residual at the fitted offset 0.020–0.078 against **0.648–1.519** at offset 0, on four shadows independently |
|
|
129
|
+
|
|
130
|
+
⭐ **The rung-1 row is the one to internalise, because the fitted offsets agreed
|
|
131
|
+
with each other.** Four shadows, four independent sweeps, and every answer landed
|
|
132
|
+
at **0.14–0.16 of that art's own height**. Agreement across four parts is a
|
|
133
|
+
measurement; one part's minimum is a fit.
|
|
134
|
+
|
|
135
|
+
### 1.2 Two ways to point a bone at its own art, and neither is checkable
|
|
136
|
+
|
|
137
|
+
A bone's local **+x** is a real direction with real consequences (§9.2), and art
|
|
138
|
+
is not always drawn along it. Two spellings, and the frames decide between them
|
|
139
|
+
never:
|
|
140
|
+
|
|
141
|
+
- **Omit `length` and let the bone point away from the art.** Nothing observable
|
|
142
|
+
depends on it unless a constraint reads the axis.
|
|
143
|
+
- **`rotation: 180` on the bone and `rotation: 180` on the attachment to cancel
|
|
144
|
+
it**, so the bone points at its own mass — at the cost of two emitted fields.
|
|
145
|
+
|
|
146
|
+
> *"Neither is checkable from the frames."* — rung 3's first attempt
|
|
147
|
+
> ([`2026-08-23-rung3-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-rung3-1/LOOP.md) §11)
|
|
148
|
+
|
|
149
|
+
⇒ Pick one, and say in your log that you picked it. AUTHORING §10.1's naming
|
|
150
|
+
convention is the same shape of decision and is worth far more (it decides five of
|
|
151
|
+
`bones`'s eight measures), so spend the log line there first.
|
|
152
|
+
|
|
153
|
+
### 1.3 🚨 An attachment on a bone you scale-key must sit at that bone's origin
|
|
154
|
+
|
|
155
|
+
**An attachment offset scales with its bone.** A muzzle flare 67 units out on a
|
|
156
|
+
bone keyed to 4× lands **268** units out, off the edge of the figure — and the
|
|
157
|
+
build is green, because nothing in the format says an offset was meant to be
|
|
158
|
+
rigid.
|
|
159
|
+
|
|
160
|
+
> *"the muzzle placeholders carried their genrig offsets, and **an attachment offset
|
|
161
|
+
> scales with its bone** — at 4× the 67-unit offset became 268 … an attachment on a
|
|
162
|
+
> bone you scale-key must sit at that bone's origin, or the offset rides the
|
|
163
|
+
> scale."*
|
|
164
|
+
> — spineboy attempt 4
|
|
165
|
+
> ([`2026-08-28-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-1/LOOP.md) §8)
|
|
166
|
+
|
|
167
|
+
⇒ This is the one exception to §1's rule, and it is not a contradiction: a bone
|
|
168
|
+
whose **scale** is the animated property is not a hinge, so there is no hinge to
|
|
169
|
+
make visible. Put the art at its origin and let a *different* bone carry the
|
|
170
|
+
placement.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 2. A pivot in the wrong place does not look like a wrong pivot
|
|
175
|
+
|
|
176
|
+
**This is the most consequential failure on the page, and it recurs in five of
|
|
177
|
+
seven from-zero attempts at the same figure plus five of the rung runs.** It is
|
|
178
|
+
codified in AUTHORING §8.1 and MOTION §3.9; what those two do not say is what it
|
|
179
|
+
*looks like* while it is happening, which is: like a search that is not converging.
|
|
180
|
+
|
|
181
|
+
### 2.1 The four signatures
|
|
182
|
+
|
|
183
|
+
None of them is an error message. Each one is a number that reads as ordinary.
|
|
184
|
+
|
|
185
|
+
| Signature | What it actually is | Record |
|
|
186
|
+
| --- | --- | --- |
|
|
187
|
+
| **A gap that letting the parts off the hierarchy closes.** `idle` fitted to 10.3 union MAE on the frame the setup pose came from and **23.9** eight frames later; a short relax off the hierarchy recovered it to **14.6** | *"A gap that a relax closes is not a pose the rig cannot reach; it is a pivot in the wrong place."* | spineboy attempt 1 ([`2026-08-23-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-1/LOOP.md) §8.2) |
|
|
188
|
+
| **A fitted centre that drifts monotonically with the fitted scale.** The per-frame fits wanted each shadow's centre 8–12 units lower whenever the shadow was small | a part scaling about a pivot below its own centre — *"a pivot you have not modelled, not motion you have measured"* | rung 1 second attempt ([`2026-08-26-rung1-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-26-rung1-1/LOOP.md) §2.3) |
|
|
189
|
+
| **A knob resting exactly on its bound, on one orientation only.** `torso.x` pinned at −35.0 against a ±35 box on the lying frames and nowhere else | the optimum is outside the box, because a pivot error `e` needs a compensation `−R(θ)·e` that grows with the local angle — *"invisible upright, saturating in the lying poses"* | spineboy attempt 5 ([`2026-08-28-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-2/LOOP.md) §3) |
|
|
190
|
+
| **An exact fit with no angular spread.** Two ankle joints solved at **rms 0.00 px** with a relative-angle spread of **0° and 2°** | a perfect fit to a system with no unique answer | spineboy, 2026-09-03 first attempt ([`2026-09-03-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-1/LOOP.md) §3.3) |
|
|
191
|
+
|
|
192
|
+
🚨 **The last one is the trap in its purest form: a zero residual reads as success
|
|
193
|
+
in every other context on this toolchain.** Here it means the estimator was handed
|
|
194
|
+
a rank-deficient system and returned one of its infinitely many answers.
|
|
195
|
+
|
|
196
|
+
### 2.2 What identifies a pivot is a change in relative angle across the joint
|
|
197
|
+
|
|
198
|
+
**Not the number of frames. Not the number of shots.** AUTHORING §8.1 states the
|
|
199
|
+
rule and MOTION §3.9 gives it a determinant; both are worth quoting because they
|
|
200
|
+
are the same fact measured by two different instruments.
|
|
201
|
+
|
|
202
|
+
From the fit side (AUTHORING §8.1):
|
|
203
|
+
|
|
204
|
+
> *"a **pivot** — the point one bone turns about relative to its parent — is
|
|
205
|
+
> identified only by frames whose **relative rotation across that joint actually
|
|
206
|
+
> differs**. So a spread can draw frames from every single shot, satisfy the
|
|
207
|
+
> paragraph above to the letter, and still be **ill-conditioned**."*
|
|
208
|
+
|
|
209
|
+
From the two-pose side (MOTION §3.9): the 2×2 solve's determinant is
|
|
210
|
+
`|det| = 4·sin²(Δ/2)` in the *change* of relative angle Δ, so a reading error in
|
|
211
|
+
the placements is amplified by about `1 / (2·sin(Δ/2))` — **0.8× at Δ = 80°, five
|
|
212
|
+
times at Δ = 11°**, and nothing reports it.
|
|
213
|
+
|
|
214
|
+
⭐ **And here is the measurement that says the two agree.** Attempt 5's
|
|
215
|
+
triangulation of one chest joint, solved from template matches over 18 frames
|
|
216
|
+
across all eight shots:
|
|
217
|
+
|
|
218
|
+
> *"⭐ upright-only rows re-solve to p = (−0.4, 68.7) with residuals 0.9–3.0 — a
|
|
219
|
+
> 21-unit-different answer that fits the upright frames just as well. The joint is
|
|
220
|
+
> ill-conditioned without the lying poses, which is how the inherited triangulation
|
|
221
|
+
> (idle zoom-read) went wrong without any frame saying so."*
|
|
222
|
+
> — [`2026-08-28-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-2/LOOP.md) §5
|
|
223
|
+
|
|
224
|
+
Two answers 21 art units apart, at residuals that do not separate them. That is
|
|
225
|
+
what *ill-conditioned* means with a picture attached.
|
|
226
|
+
|
|
227
|
+
### 2.3 The conditioning check, in three forms
|
|
228
|
+
|
|
229
|
+
**Whichever estimator you used, re-run it with the data disturbed and see how far
|
|
230
|
+
the answer moves.** All three of these are cheap and all three appear in the
|
|
231
|
+
record:
|
|
232
|
+
|
|
233
|
+
1. **Perturb the inputs.** MOTION §3.9's own prescription: re-solve from placements
|
|
234
|
+
pushed by a pixel. On one figure this was the only thing separating a usable knee
|
|
235
|
+
from an unusable one — **29.0 px of pivot movement for 1 px of placement noise**,
|
|
236
|
+
at a closure rms of 0.91 px that looked fine.
|
|
237
|
+
2. **Drop the diverse rows.** AUTHORING §8.1's: *"re-solve the joint from a subset
|
|
238
|
+
that excludes the diverse configurations and see how far the answer moves. If it
|
|
239
|
+
moves a long way at comparable residuals, the diverse frames were carrying the
|
|
240
|
+
whole identification."* ⚠️ **No run in the corpus performs this one as a
|
|
241
|
+
check** — attempt 5 performed the *unconditional* version of it by accident and
|
|
242
|
+
that is §2.2's 21-unit result, which is what a deliberate subset test is designed
|
|
243
|
+
to produce on purpose. It is prescribed, cheap and unexercised.
|
|
244
|
+
3. **Look at the spread of relative angle directly**, before believing any residual.
|
|
245
|
+
The table that made this legible ranked ten joints by that spread, and the two
|
|
246
|
+
rank-deficient rows were exactly the two with 0° and 2° of it.
|
|
247
|
+
|
|
248
|
+
📌 **The three are not interchangeable.** (1) tells you how noisy the answer is,
|
|
249
|
+
(2) tells you *which frames* the answer is made of, and (3) tells you whether the
|
|
250
|
+
question was well posed at all. (3) is free.
|
|
251
|
+
|
|
252
|
+
### 2.4 🚫 The repair that looks obvious is inert
|
|
253
|
+
|
|
254
|
+
**A structural descent that holds the fitted poses fixed cannot recover a
|
|
255
|
+
mis-triangulated pivot.** AUTHORING §8.1 states this and it is worth restating
|
|
256
|
+
here, because it is the first thing anybody tries:
|
|
257
|
+
|
|
258
|
+
> *"the per-frame poses were fitted against the wrong pivot, so they have already
|
|
259
|
+
> absorbed its error. Move the pivot with those poses held and every frame gets
|
|
260
|
+
> worse; hold the pivot and refit the poses and they re-absorb it. **The gradient at
|
|
261
|
+
> fixed poses points nowhere.**"*
|
|
262
|
+
|
|
263
|
+
⇒ And multi-start does not help either, for the reason §8.1 gives: *"the defect is
|
|
264
|
+
not a basin you failed to reach, it is a parameter the objective is no longer a
|
|
265
|
+
function of."* The repair is a **different estimator** — triangulate the joint from
|
|
266
|
+
part matches across configurations, then refit the poses — not more of the same
|
|
267
|
+
search.
|
|
268
|
+
|
|
269
|
+
### 2.5 The configurations that make a trunk joint observable
|
|
270
|
+
|
|
271
|
+
⭐ **"Different configurations" is a property of the shot list, not of the frame
|
|
272
|
+
count.** AUTHORING §8.1's own words: *"A figure standing, walking and running may
|
|
273
|
+
hold one joint at nearly the same angle throughout; a figure **lying down**, or
|
|
274
|
+
inverted, or reaching across itself, is what makes that joint observable."*
|
|
275
|
+
|
|
276
|
+
What the records add is the concrete list of what worked:
|
|
277
|
+
|
|
278
|
+
- **lying and inverted poses** conditioned the trunk joints on the character corpus
|
|
279
|
+
— *"`death` and `hit` supply the lying and inverted configurations §8.1 asks for,
|
|
280
|
+
and they are what makes any of it conditioned."*
|
|
281
|
+
- **a parent that turns all the way over** conditioned a chain's top joint on rung 4,
|
|
282
|
+
after the same fit on the wave shots alone *"converged to 5 px below the disc with
|
|
283
|
+
a residual of 0.06 px — a tidy, confident, wrong answer."* The fix was the second
|
|
284
|
+
shot, where the disc turns through 360°.
|
|
285
|
+
- ⚠️ **and it is the parent's rotation that matters, not the child's travel.** Rung
|
|
286
|
+
4's failed fit was *"a circle fit to an arc that spans 11.6 px in x and 0.95 px in
|
|
287
|
+
y"* — plenty of motion, almost none of it about the joint.
|
|
288
|
+
|
|
289
|
+
### 2.6 When the frames do not decide: a prior with a measured width
|
|
290
|
+
|
|
291
|
+
**Say so, and say how wide.** On a figure whose limbs are eight or nine frame
|
|
292
|
+
pixels across, one run swept 21 structural knobs against a 14-frame spread and
|
|
293
|
+
found that **every one of them measured a basin at the sweep's own cap** — ±4.5 to
|
|
294
|
+
±9 units, or ±1 to ±2 frame pixels:
|
|
295
|
+
|
|
296
|
+
> *"On this figure the joint offsets are **weakly identified at the scale a frame
|
|
297
|
+
> can see**, and the honest reading is that they are a prior with a measured width
|
|
298
|
+
> rather than a measurement."*
|
|
299
|
+
> — [`2026-09-03-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-2/LOOP.md) §3.5
|
|
300
|
+
|
|
301
|
+
⇒ That sentence is the deliverable when the pixels are silent. It is not a hedge:
|
|
302
|
+
a width is a number, and the next attempt can tell whether its own change is inside
|
|
303
|
+
it. AUTHORING §8.1 asks for the same thing in one line — *"if the shot list has
|
|
304
|
+
only one configuration, say in the log that the pivot is a prior."*
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## 3. Moving a pivot, and the row that gets forgotten
|
|
309
|
+
|
|
310
|
+
[INGEST.md](INGEST.md) §4.1 is the recipe: which objects change when a bone's
|
|
311
|
+
origin moves, and the arithmetic invariant that says the compensation is right. It
|
|
312
|
+
is the recipe this section builds on, and it says which row is the dangerous one:
|
|
313
|
+
|
|
314
|
+
> *"⚠️ **The child-bone row is the one that gets forgotten**, and it fails quietly:
|
|
315
|
+
> the re-pivoted bone's own art lands correctly and everything hanging off it is
|
|
316
|
+
> displaced by exactly the vector you moved."*
|
|
317
|
+
|
|
318
|
+
⚠️ **And INGEST's worked example has no children** — *"this bone has no children"*
|
|
319
|
+
— so the row it warns about is the one row it does not demonstrate. This section
|
|
320
|
+
demonstrates it.
|
|
321
|
+
|
|
322
|
+
### 3.1 The edit, on a bone with a child
|
|
323
|
+
|
|
324
|
+
The fixture's `arm` bone sits at the top of its own plate, and the ask is INGEST
|
|
325
|
+
§4.1's: *swing from the middle, not the end.* The plate is 30 long, so the pivot
|
|
326
|
+
moves **+15 along the bone's own +y**, and `arm.rotation` is 0, so the parent-space
|
|
327
|
+
and bone-space vectors are the same one. Three objects change:
|
|
328
|
+
|
|
329
|
+
```diff
|
|
330
|
+
- { "name": "arm", "parent": "trunk", "x": 9, "y": 36 },
|
|
331
|
+
+ { "name": "arm", "parent": "trunk", "x": 9, "y": 51 }, ← the bone: to the new pivot
|
|
332
|
+
- { "name": "hand", "parent": "arm", "x": 0, "y": -26 },
|
|
333
|
+
+ { "name": "hand", "parent": "arm", "x": 0, "y": -41 }, ← the CHILD: same vector, opposite sign
|
|
334
|
+
|
|
335
|
+
- "arm": { "arm": { "image": "arm.png", "y": -15 } },
|
|
336
|
+
+ "arm": { "arm": { "image": "arm.png", "y": -30 } }, ← the attachment: same vector, opposite sign
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Nothing in the motion spec changes.** That is the entire point of the edit:
|
|
340
|
+
`rotate` keys are angles about the origin, and the origin is what moved.
|
|
341
|
+
|
|
342
|
+
The arithmetic invariant first, before rendering anything — the world position of
|
|
343
|
+
each affected object at the setup pose, `bone(x, y) + R(bone.rotation)·att(x, y)`
|
|
344
|
+
composed down the chain:
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
arm bone arm att arm art centre hand bone
|
|
348
|
+
original (109, 76) (0, −15) (109, 61) (109, 50)
|
|
349
|
+
re-pivot (109, 91) (0, −30) (109, 61) (109, 50)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
⇒ Both unmoved. If either moves at the setup pose, the compensation is wrong and
|
|
353
|
+
no amount of looking at frames will say which of the two numbers to blame.
|
|
354
|
+
|
|
355
|
+
### 3.2 🚨 Worked: the wrong edit wins on every aggregate
|
|
356
|
+
|
|
357
|
+
Two variants: `mid` compensates the child, `mid-nochild` forgets it. Both are
|
|
358
|
+
checked against frames rendered from the original — so both *should* differ, because
|
|
359
|
+
the movement genuinely changed. What separates them is where.
|
|
360
|
+
|
|
361
|
+
⚠️ **Pin `--viewport` from the reference's own sidecar**, INGEST §4.1's warning: a
|
|
362
|
+
re-pivot changes the skeleton's world extent, `frames.json`'s box is then refused on
|
|
363
|
+
`coordinates`, and the framing is fitted instead — which moves every figure below.
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
rigc render --candidate out --animation swing --fps 12 --out ref
|
|
367
|
+
# .. swing 8 frame(s), 0.583s + contact.png -> ref/swing
|
|
368
|
+
# → ref/frames.json's viewport: 20.4013, 31.8317, 119.1974, 153.8671
|
|
369
|
+
|
|
370
|
+
rigc check --candidate out --frames ref --viewport 20.4013,31.8317,119.1974,153.8671 --all-frames
|
|
371
|
+
rigc check --candidate mid --frames ref --viewport 20.4013,31.8317,119.1974,153.8671 --all-frames
|
|
372
|
+
rigc check --candidate mid-nochild --frames ref --viewport 20.4013,31.8317,119.1974,153.8671 --all-frames
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The control first, because a comparison of two builds needs one:
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
── out ── the original against its own frames
|
|
379
|
+
MAE mean 0.00 worst 0.00 at f0000
|
|
380
|
+
f0000 0.00 f0001 0.00 f0002 0.00 f0003 0.00
|
|
381
|
+
f0004 0.00 f0005 0.00 f0006 0.00 f0007 0.00
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Then the two edits, per frame:
|
|
385
|
+
|
|
386
|
+
| frame | `mid` — child compensated | `mid-nochild` — child forgotten |
|
|
387
|
+
| --- | ---: | ---: |
|
|
388
|
+
| **f0000** | **0.00** | **3.94** |
|
|
389
|
+
| f0001 | 2.19 | 3.22 |
|
|
390
|
+
| f0002 | 3.96 | 1.21 |
|
|
391
|
+
| f0003 | 13.37 | 9.17 |
|
|
392
|
+
| f0004 | 16.70 | 12.26 |
|
|
393
|
+
| f0005 | 17.43 | 12.70 |
|
|
394
|
+
| f0006 | 17.60 | 12.89 |
|
|
395
|
+
| f0007 | 17.66 | 12.96 |
|
|
396
|
+
| **mean (union)** | 11.11 | **8.54** |
|
|
397
|
+
| **mean over the reference's own pixels** | 12.30 | **8.98** |
|
|
398
|
+
| **worst** | 17.66 | **12.96** |
|
|
399
|
+
|
|
400
|
+
🚨 **The wrong edit is better on every aggregate the report prints, and it is
|
|
401
|
+
distinguished by exactly one row.** `mid`'s **f0000 = 0.00** is the pivot's own
|
|
402
|
+
signature — INGEST §4.1's *"invisible in the pose, and everything in the
|
|
403
|
+
movement"*, climbing monotonically from there. `mid-nochild`'s f0000 is **3.94**,
|
|
404
|
+
and that non-zero at the setup pose is the entire evidence that a child was left
|
|
405
|
+
behind. Every mean, and the worst frame, point the other way.
|
|
406
|
+
|
|
407
|
+
⇒ **This is AUTHORING §9.2's own instruction arriving as data: read the per-frame
|
|
408
|
+
column before the MAE.** The aggregates are smaller for the wrong rig because it
|
|
409
|
+
moves the arm's art less far overall; being *closer on average* and *wrong at the
|
|
410
|
+
setup pose* are not two readings of one quality.
|
|
411
|
+
|
|
412
|
+
📌 **And a second instrument names the same defect differently.** `chainfit` on the
|
|
413
|
+
two rigs, against a render of the setup pose (§8's recipe, same anchor):
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
out CHAIN hand.png residual=0.0227 visible= 32% hinge 0.33°
|
|
417
|
+
mid-nochild REFUSE hand.png residual=0.9505 visible= 8% hinge 0.00°
|
|
418
|
+
occluded: only 8.3% of it survives the parts drawn over it
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The hand's own bone is 15 units high, so it sits up inside the trunk and its
|
|
422
|
+
visible share collapses — and **the hinge search cannot undo it**, because
|
|
423
|
+
AUTHORING §12.5's *"the hinge is searched; the pivot is not."* One rotation about a
|
|
424
|
+
wrong centre is not a translation, so the fit reports 0.00° and a residual of 0.95
|
|
425
|
+
rather than quietly absorbing the error. That is the failure mode being loud for
|
|
426
|
+
once, and it is loud because the instrument reads *through the structure* instead
|
|
427
|
+
of around it.
|
|
428
|
+
|
|
429
|
+
### 3.3 Sequence, because the surgery invalidates work
|
|
430
|
+
|
|
431
|
+
⚠️ **Do it before the per-frame fitting budget is spent.** AUTHORING §8.1's
|
|
432
|
+
sequencing rule, and the two records that paid it:
|
|
433
|
+
|
|
434
|
+
- attempt 4's two arm surgeries were done at builds 5 and 7 of 8, and the run's own
|
|
435
|
+
note is *"measure the chain's reach against the extremes the shot visits **before**
|
|
436
|
+
fitting it"*.
|
|
437
|
+
- attempt 5's repair was **six numbers in three objects** — the neck bone, the head
|
|
438
|
+
bone's compensation and the neck attachment's compensation — *"setup render
|
|
439
|
+
invariant by construction"*, followed by refits of every channel hung off that
|
|
440
|
+
joint, with the hip and both legs **frozen** so the figures that were already good
|
|
441
|
+
could not move.
|
|
442
|
+
|
|
443
|
+
⭐ **Freeze what is already measured.** That is not thrift, it is correctness: *"chains
|
|
444
|
+
share parents, so a search free to move a converged limb's ancestors will walk it back
|
|
445
|
+
off the floor to buy a fraction of a point somewhere else"* (AUTHORING §8.1). One run
|
|
446
|
+
measured the cost of not doing it — unfreezing the trunk for *one* local polish moved
|
|
447
|
+
its best frame from **58.2 → 67.5** on its own objective, because the cheapest
|
|
448
|
+
available improvement was to drag the trunk off the place the placements had measured.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 4. A bone that carries no art is a gauge
|
|
453
|
+
|
|
454
|
+
**Turn it by δ, turn every child back by δ, and not one pixel changes.** So a
|
|
455
|
+
coordinate descent will walk it, and the pose stays right while the numbers become
|
|
456
|
+
unusable. AUTHORING §10.3 names the shape; four records on one figure are the arc
|
|
457
|
+
of what to do about it.
|
|
458
|
+
|
|
459
|
+
The failure, watched happening:
|
|
460
|
+
|
|
461
|
+
> *"`hip` carries no attachment and everything that moves hangs under it … this run
|
|
462
|
+
> watched `walk/f3` reach `hip +181.3° torso −184.3° front-thigh −150.6°
|
|
463
|
+
> rear-thigh −199.8°`, whose net world rotations are −3°, +31° and −19°: the picture
|
|
464
|
+
> was right and the numbers were unusable. A rotate series that swings 180° between
|
|
465
|
+
> two keys does not interpolate, it spins the figure through a whole turn."*
|
|
466
|
+
> — spineboy attempt 1 ([`2026-08-23-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-1/LOOP.md) §8.4)
|
|
467
|
+
|
|
468
|
+
### 4.1 ⚠️ The exact fold has a precondition, and it is easy to miss
|
|
469
|
+
|
|
470
|
+
**An exact rotation gauge needs the children to sit *at the parent's origin*.** If
|
|
471
|
+
they sit off it, turning the parent moves their origins and the render, so the fold
|
|
472
|
+
AUTHORING §10.3 prescribes is not a null operation — it costs pixels:
|
|
473
|
+
|
|
474
|
+
> *"the hip's three children sit 9–13 units off it, so turning the hip moves their
|
|
475
|
+
> origins and the render. Folding cost MAE on every frame it touched (`idle` mean
|
|
476
|
+
> 23.0 with the fold against 19.9 without, same search)."*
|
|
477
|
+
> — spineboy attempt 2 ([`2026-08-23-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-2/LOOP.md) §5)
|
|
478
|
+
|
|
479
|
+
⇒ **Test the precondition before folding.** Children at the origin ⇒ exact gauge,
|
|
480
|
+
fold it (the L1 optimum is the *median* of the children's deltas, which is one line).
|
|
481
|
+
Children fanned out at different offsets ⇒ a **soft** degeneracy, which wants a
|
|
482
|
+
penalty rather than a fold — that run used 2e-5 per squared degree, invisible at
|
|
483
|
+
animator-sized angles and decisive against a 180°-against-180° pair.
|
|
484
|
+
|
|
485
|
+
### 4.2 Four answers, in the order they were tried
|
|
486
|
+
|
|
487
|
+
| Answer | What it costs | Record |
|
|
488
|
+
| --- | --- | --- |
|
|
489
|
+
| **fold the gauge out exactly** after every frame | MAE, if the precondition fails; the same run also paid 30.7 → 33.8 on `walk` when the exact fold landed the descent in a different minimum — *"the fit got slightly worse and the animation got usable"* | attempt 1 |
|
|
490
|
+
| **penalise it softly** | nothing measurable at ordinary angles | attempt 2 |
|
|
491
|
+
| **delete the bone**, making the trunk the body root so every keyed bone carries art | the conventional pelvis, and it is measured — `bones` fell from 0.924 on the 18-bone rigs to **0.702** | 2026-09-03 attempt 1 |
|
|
492
|
+
| **never key `root`**, leaving it at the world origin as the floor | nothing; the trunk carries the position | 2026-09-03 attempt 2 |
|
|
493
|
+
|
|
494
|
+
⭐ **Read that arc rather than picking a row.** *Manage → penalise → delete → never
|
|
495
|
+
key it* is four runs converging on removing the freedom instead of policing it —
|
|
496
|
+
and the last two paid for it in a name-matched measure against a reference that
|
|
497
|
+
does have a pelvis. Whichever you choose, choose it before you key, because folding
|
|
498
|
+
a gauge after keying rewrites every key hung off it.
|
|
499
|
+
|
|
500
|
+
### 4.3 The terminal link is a gauge too, and it is the quiet case
|
|
501
|
+
|
|
502
|
+
**A part that is rigid to its parent in the pixels has an unobservable rotation.**
|
|
503
|
+
Two records, same reading, on the last link of a chain:
|
|
504
|
+
|
|
505
|
+
- rung 4's `chain-end` is *"a rotationally near-symmetric ring centred on its own
|
|
506
|
+
joint, and |ring − bead₄| holds at 18.1–18.6 px (σ 0.15) across every frame of both
|
|
507
|
+
short shots — so the ring is rigid to `chain-4` in the pixels, and its bone's
|
|
508
|
+
rotation is an unobservable gauge. It is **not keyed**."*
|
|
509
|
+
- ⚖️ And the honest half of the same paragraph: *"If the reference keys it, this
|
|
510
|
+
candidate is one timeline short, and that is the honest trade."*
|
|
511
|
+
|
|
512
|
+
⇒ Note that this is the §1 rule's converse: the ring is centred on its own joint,
|
|
513
|
+
which is *why* its rotation is invisible. A part you cannot see turning is a part
|
|
514
|
+
whose bone you should not be keying.
|
|
515
|
+
|
|
516
|
+
### 4.4 ✅ The artless parent that is not a mistake
|
|
517
|
+
|
|
518
|
+
**A bone with no art whose job is the component every child shares is the
|
|
519
|
+
legitimate case, and it appears four times.** The difference from §4's gauge is
|
|
520
|
+
that it is *keyed for a reason the arithmetic states*, not left free for a solver:
|
|
521
|
+
|
|
522
|
+
| Bone | Carries | Its children then key |
|
|
523
|
+
| --- | --- | --- |
|
|
524
|
+
| `faceshift` ([`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait/)) | `−R·sin t`, the shift every feature shares in a yaw | only their residual — FACE §3 |
|
|
525
|
+
| `comet` (rung 6, rung 8) | the whole travel of a ball and its trail | the squash, and the trail's bend |
|
|
526
|
+
| `body` (rung 7, second attempt) | **translation only** — *"it holds no attachment, so keying its rotation as well would be a gauge"* | their own rotations |
|
|
527
|
+
| `ground` ([`gallery/walk`](https://github.com/firejune/rigc/tree/main/gallery/walk/)) | the contact line | the foot targets — §9.1 |
|
|
528
|
+
|
|
529
|
+
⭐ **FACE §3's argument for it is an auditing one before it is a rigging one**: a
|
|
530
|
+
residual is 1–6 units where a total is 30–40, *"nobody can eyeball an error in the
|
|
531
|
+
second, and everybody can eyeball one in the first."* The hierarchy is what makes
|
|
532
|
+
the small number the one in the file.
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## 5. Siblings, not a chain
|
|
537
|
+
|
|
538
|
+
**What must take only a *part* of another bone's motion cannot hang under it.** Six
|
|
539
|
+
records, and the failure mode is the same every time: the rig is defensible, the
|
|
540
|
+
gate is green, the spec looks right, and the picture is wrong in a way that is
|
|
541
|
+
invisible in the numbers.
|
|
542
|
+
|
|
543
|
+
### 5.1 Scale propagates, so a scaling bone must be a sibling
|
|
544
|
+
|
|
545
|
+
> *"🚨 **The breath scales the chest plate without scaling the head.** `torso` and
|
|
546
|
+
> `chest` are **siblings** under `bust`, not a chain: `torso` carries the plate and
|
|
547
|
+
> takes the `scale` key (1 → 1.005, 1.014), `chest` carries the neck-and-head chain
|
|
548
|
+
> and takes a `translatey` (0 → 3.4). Parent the head chain to the bone that scales
|
|
549
|
+
> and the whole face inflates 1.4% every breath — visible, and invisible in the
|
|
550
|
+
> spec."*
|
|
551
|
+
> — [`gallery/portrait/README.md`](https://github.com/firejune/rigc/blob/main/gallery/portrait/README.md)
|
|
552
|
+
|
|
553
|
+
The same decision, on a ball and its trail:
|
|
554
|
+
|
|
555
|
+
> *"`ball` is a **sibling** of `tail0` under `comet`, not its parent, so squashing
|
|
556
|
+
> the ball does not squash the trail."*
|
|
557
|
+
> — rung 6 ([`2026-08-23-rung6-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-rung6-1/README.md))
|
|
558
|
+
|
|
559
|
+
⇒ **The construction is always the same three bones**: an artless parent that
|
|
560
|
+
carries what both share (§4.4), and two children — one that takes the scale and one
|
|
561
|
+
that takes the chain.
|
|
562
|
+
|
|
563
|
+
### 5.2 A part that takes a *fraction* of another's motion cannot be its ancestor
|
|
564
|
+
|
|
565
|
+
> *"the neck plate has to take a *fraction* of the head's shift, so it cannot be an
|
|
566
|
+
> ancestor of the head. `neckbase` is the shared parent; `neck` carries the plate and
|
|
567
|
+
> `headroll` carries the head"*
|
|
568
|
+
> — [`gallery/portrait/README.md`](https://github.com/firejune/rigc/blob/main/gallery/portrait/README.md), the part table
|
|
569
|
+
|
|
570
|
+
⭐ **And the pivot goes one link up, which is the same edit for a second reason.**
|
|
571
|
+
The same rig keys `headroll` and never `head`: *"a head rotates about the top of the
|
|
572
|
+
neck, not about the middle of its own face."* A renderer policy assertion
|
|
573
|
+
(`A15_IDLE_NO_MESH_BONE_KEYS`, AUTHORING §5) forced that answer independently — two
|
|
574
|
+
arguments, one bone. ⚠️ **It is a renderer rule, so like §10.3's `A25` it only fires
|
|
575
|
+
under `--profile spine-html`**; under `--profile spine` it reports `PROF` and the
|
|
576
|
+
structural argument is the only one you get. [FACE.md](FACE.md) §3 is the same bone
|
|
577
|
+
from the mesh's side.
|
|
578
|
+
|
|
579
|
+
📌 **What the extra link buys, for free:** *"a rotation about the neck pivot carries
|
|
580
|
+
its descendants on a circle for free."* A yaw expressed as `translatex` draws a
|
|
581
|
+
straight line; 1.6° of roll on a bone at the top of the neck bends every feature's
|
|
582
|
+
path into an arc, with no per-part curve anywhere (MOTION §3.5).
|
|
583
|
+
|
|
584
|
+
### 5.3 Inheritance splits into shape and position, and you can cancel one
|
|
585
|
+
|
|
586
|
+
**A child inherits its parent's scale on both, and sometimes you want only one of
|
|
587
|
+
them.** `gallery/portrait`'s iris is the worked case: it is a child of the socket,
|
|
588
|
+
so a socket at `scalex 0.89` makes a circular iris an ellipse — which reads as a
|
|
589
|
+
squashed drawing rather than a turned head.
|
|
590
|
+
|
|
591
|
+
- the **shape** is cancelled with a counter-scale of `1/scaleX` on the child;
|
|
592
|
+
- the **position** is left inherited, because *"a bone's scale moves its children's
|
|
593
|
+
local translation, so the highlight at (−11, +11) slides inward with the surface
|
|
594
|
+
it reflects off."*
|
|
595
|
+
|
|
596
|
+
⭐ And the counter-scale belongs to the **socket**, not the part: two parts at
|
|
597
|
+
different local `x` under one socket take the *same* number, which is why it stays a
|
|
598
|
+
plain shared value rather than a per-member model (AUTHORING §4.5.1's last note).
|
|
599
|
+
Measured effect: without it the turn stops reading at about 18°, with it about 26°.
|
|
600
|
+
|
|
601
|
+
### 5.4 The parent chain is a coordinate space, and a model across two is refused
|
|
602
|
+
|
|
603
|
+
**This is the rule stated from the compiler's side, and it is the strongest form of
|
|
604
|
+
it.** AUTHORING §4.5.1's `derive` reads each member's coordinate from its parent's
|
|
605
|
+
origin, so:
|
|
606
|
+
|
|
607
|
+
> *"members under different parents have coordinates measured from different origins
|
|
608
|
+
> and the model would average them. Refused by name, naming the parents. In the
|
|
609
|
+
> worked example that is what splits `features` (under `faceshift`) from `hair`
|
|
610
|
+
> (under `head`) — and that split is the shared-shift split itself, so **the refusal
|
|
611
|
+
> falls exactly where the geometry already wanted a seam.**"*
|
|
612
|
+
|
|
613
|
+
⇒ Read that last clause as a design test. If a construct you want is refused for
|
|
614
|
+
spanning two parents, the parents are usually right and the construct is one track
|
|
615
|
+
too wide — split it. A correct hierarchy is the thing that makes the model
|
|
616
|
+
expressible; the refusal is how you find out you have one.
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## 6. The chain: how far it reaches, how many links, and what it folds to
|
|
621
|
+
|
|
622
|
+
### 6.1 🚨 The reach check is arithmetic, and it runs before the first fit
|
|
623
|
+
|
|
624
|
+
**Take the chain's total reach from your rig; take the longest excursion the frames
|
|
625
|
+
show its end travelling; compare.** AUTHORING §8.1 states it, and the reason it is
|
|
626
|
+
first in this section is that failing it is invisible:
|
|
627
|
+
|
|
628
|
+
> *"If a chain's segment lengths are short … then every frame where the chain is
|
|
629
|
+
> folded fits beautifully, and the fitter *silently absorbs* the deficit on every
|
|
630
|
+
> other frame by rotating the parts it does have. **Nothing reports a failure.**"*
|
|
631
|
+
|
|
632
|
+
The record that named it:
|
|
633
|
+
|
|
634
|
+
> *"idle's folded arm had been misread as short bones — shoulder→elbow 42,
|
|
635
|
+
> elbow→wrist 41 units, total reach ≈ 98 with the fist offset, while `walk`'s own
|
|
636
|
+
> fist pendulum spans 184 units from the shoulder and `death`'s raised hand ~220.
|
|
637
|
+
> Segments reset to the art's proportions (75 + 58), pivot moves compensated so the
|
|
638
|
+
> setup render is unchanged. walk 0.176 → 0.150, run 0.232 → 0.201, aim 0.212 →
|
|
639
|
+
> 0.186 after refit."*
|
|
640
|
+
> — spineboy attempt 4
|
|
641
|
+
> ([`2026-08-28-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-1/LOOP.md) §6)
|
|
642
|
+
|
|
643
|
+
⭐ **The tie-break is on the frames' side.** *"the shot's own extremes are a
|
|
644
|
+
measurement, while segment lengths taken off a folded pose are an estimate — so when
|
|
645
|
+
they disagree, suspect the estimate"* (AUTHORING §8.1). The same run's second
|
|
646
|
+
surgery makes the point about *where* the joint is, too: in the lying pose the
|
|
647
|
+
shoulder joint sat at row 344, under the body, while the wave's arm base reads
|
|
648
|
+
~(92, 315) — triangulated through the posed torso it belongs at the **spine top, not
|
|
649
|
+
at the visible shoulder pad**, and *"the wave becomes reachable."*
|
|
650
|
+
|
|
651
|
+
### 6.2 ⚠️ It bites in both directions, and §8.1 is written for one of them
|
|
652
|
+
|
|
653
|
+
**A chain that is too *long* fails the same way and looks completely different: the
|
|
654
|
+
fits converge, every residual is ordinary, and the figure splays.**
|
|
655
|
+
|
|
656
|
+
> *"Here it was the other way up — the chain was **too long**. Read off the art's own
|
|
657
|
+
> ends, hip→knee→ankle measured **250 units**; the frames put the stance's pelvis
|
|
658
|
+
> **215 units** above the floor and the boot's ankle-to-sole at **54**, so a straight
|
|
659
|
+
> leg overshoots the floor by about 90 units and the fitter has to bend it sideways
|
|
660
|
+
> to land … Re-seeding the leg joints inset from the art's ends — thigh 86 units,
|
|
661
|
+
> shin 94 — put the ankle at world y **53.5** against the boot's own 54."*
|
|
662
|
+
> — [`2026-09-03-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-1/LOOP.md) §4.4
|
|
663
|
+
|
|
664
|
+
⭐ **Why the art misled it, and this generalises:** the art's outer ends are not the
|
|
665
|
+
joints. A rounded cap's centroid sits at the plate's *tip*, while the joint two
|
|
666
|
+
plates share sits inside the overlap their two rounded ends make — so every
|
|
667
|
+
cap-to-cap distance over-reads by about the cap's own radius. Two separate runs
|
|
668
|
+
filed the same guide note about §8.1 being one-directional, which is itself the
|
|
669
|
+
evidence that this recurs.
|
|
670
|
+
|
|
671
|
+
📏 **And there is a cheap detector for the too-long case**: `check`'s `in units`
|
|
672
|
+
line, on one set, with a static candidate. One run read
|
|
673
|
+
`candidate 352.8 x 727.9 reference 460.7 x 648.7` and that names the excess
|
|
674
|
+
directly, while doubling as the confirmation that the shot was measured in the
|
|
675
|
+
frames' own units.
|
|
676
|
+
|
|
677
|
+
### 6.3 ⚖️ A hypothesis raised by arithmetic is worth testing, not acting on
|
|
678
|
+
|
|
679
|
+
**The same run then refused its own repair, and the refusal is the more useful
|
|
680
|
+
result.** Its leg chain assembled from the art's caps reached 308 units where the
|
|
681
|
+
frames put the standing leg at about 233 — a 79-unit excess with a plausible cause
|
|
682
|
+
(the cap-radius argument above) and an obvious fix.
|
|
683
|
+
|
|
684
|
+
> *"⇒ REFUSED. Shortening every link by 10% costs 0.70 on the spread mean, an 11%
|
|
685
|
+
> rise, and the direction is unambiguous … The cap-derived link lengths are the
|
|
686
|
+
> better rig, and the 79 units are a real knee bend in the reference's own stance
|
|
687
|
+
> rather than an error in this rig. The surgery §8.1 warns about … was therefore not
|
|
688
|
+
> performed, and the per-frame fitting budget was not spent twice."*
|
|
689
|
+
> — [`2026-09-03-spineboy-2/evidence/limb-reach.txt`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-2/evidence/limb-reach.txt)
|
|
690
|
+
|
|
691
|
+
⇒ **The reach check tells you a chain and a shot disagree. It does not tell you
|
|
692
|
+
which one is wrong.** A bent limb and a long limb produce the same excess. One
|
|
693
|
+
build settles it, and it is cheaper than the surgery.
|
|
694
|
+
|
|
695
|
+
### 6.4 Bound the chain — and bound it where the value is written
|
|
696
|
+
|
|
697
|
+
**An unbounded chain is a degeneracy sink: it will fold 180° to absorb an ambiguity
|
|
698
|
+
somewhere else in the rig, and the fit will report an answer nobody can key.**
|
|
699
|
+
|
|
700
|
+
Rung 4's saucer is an ellipse, so `prot` and `prot + 180` differ only in which side
|
|
701
|
+
a 3 px orange band sits on — and:
|
|
702
|
+
|
|
703
|
+
| the chain | mean residual | what it answered |
|
|
704
|
+
| --- | ---: | --- |
|
|
705
|
+
| unbounded | 0.318 | rotations of **−740°, −833°, +879°** on frames 3–24 |
|
|
706
|
+
| bounded (no fold back on itself) | 0.194 fresh, 0.137 after a geometry pass | — |
|
|
707
|
+
| bounded + a measured seed | **0.114** | the fast passage came in |
|
|
708
|
+
|
|
709
|
+
The same degeneracy, independently, one rung over: *"the fitter was landing in
|
|
710
|
+
folded configurations (a chain joint at 162°, the ball shrunk to 0.69 to cover a gap
|
|
711
|
+
its own fold had opened)"*, fixed by *"bound the chain: no joint past the first may
|
|
712
|
+
turn more than 80°."* ⚠️ Note the second half of that sentence — **the fold dragged
|
|
713
|
+
a sibling's scale with it**, so the symptom appeared on a part that was not in the
|
|
714
|
+
chain.
|
|
715
|
+
|
|
716
|
+
⚠️ **And a bound has to be enforced where the value is written.** Two records say
|
|
717
|
+
it in the same words: *"A bound enforced on the transition is not a bound on the
|
|
718
|
+
state"* and *"a constraint that is not enforced where the value is written is not a
|
|
719
|
+
constraint."* A limit on each search *step* lets the walk arrive anywhere, one
|
|
720
|
+
accepted step at a time.
|
|
721
|
+
|
|
722
|
+
### 6.5 How many links: one bone is an affine
|
|
723
|
+
|
|
724
|
+
⭐ **The sharpest method in the corpus, and it needs no build.** A Spine bone's
|
|
725
|
+
local transform *is* a general affine, so *"is this part one bone?"* has an exact
|
|
726
|
+
form: **is each frame's silhouette an affine image of the part's own drawing?**
|
|
727
|
+
|
|
728
|
+
> *"⇒ **the sack is not one bone.** The frames read nearly twice as far from affine
|
|
729
|
+
> as a deliberate 20 %-of-width bend, and fourteen times the floor — and the
|
|
730
|
+
> estimator does **not** mistake a stretch for a deformation, which is the control
|
|
731
|
+
> that matters, because a stretch is what one bone *can* do."*
|
|
732
|
+
>
|
|
733
|
+
> *"`tools/warp-order.ts` then sized it, by fitting polynomial warps of rising order:
|
|
734
|
+
> order 1 (= one bone) mean 0.1531, order 2 (= a 3×3 lattice) **0.0915**, order 3
|
|
735
|
+
> 0.0713 … ⇒ a second-order warp recovers about **40 %** of the gap, and that is the
|
|
736
|
+
> freedom the mesh was built with — a four-bone chain, weights blended by height
|
|
737
|
+
> only."*
|
|
738
|
+
> — rung 7, first attempt
|
|
739
|
+
> ([`2026-08-28-rung7-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-rung7-1/LOOP.md) §5)
|
|
740
|
+
|
|
741
|
+
⇒ Two controls are what make that quotable and they are worth copying: the art
|
|
742
|
+
through a **known affine** read 0.0044 → 0.0007 (the floor), and the art plus a
|
|
743
|
+
**deliberate 26 px bend** read 0.0367 → 0.0246 (a positive control). A distance-from-
|
|
744
|
+
affine with no floor beside it is a number, not a measurement.
|
|
745
|
+
|
|
746
|
+
📌 **The frames cannot tell a mesh from a non-uniform scale in general — but they
|
|
747
|
+
can tell both from one affine**, and that is what this measures. Which is also the
|
|
748
|
+
honest bound on the method.
|
|
749
|
+
|
|
750
|
+
### 6.6 When the pixels refuse to choose
|
|
751
|
+
|
|
752
|
+
**Say so, and choose on something else.** Two records, opposite instruments, same
|
|
753
|
+
conclusion:
|
|
754
|
+
|
|
755
|
+
- rung 6 built 5, 6 and 8 segments and fitted all three: total residual **1.598 /
|
|
756
|
+
1.600 / 1.588** — *"within 0.8 %, so the frames do not choose. Six was chosen on
|
|
757
|
+
what a comet-tail rig has to do next rather than on pixels that are silent."*
|
|
758
|
+
- rung 8's second attempt read 3 bones 2.148, 4 bones 1.777, **5 bones 1.651**, 6
|
|
759
|
+
bones 1.919 — and refused to over-read its own minimum: *"the 6-bone figure is not
|
|
760
|
+
evidence that 6 is worse structurally, only that the fit has more places to get
|
|
761
|
+
stuck."*
|
|
762
|
+
|
|
763
|
+
🚨 **That second caveat is general.** A sweep over bone count measures
|
|
764
|
+
*representational capacity confounded with search difficulty*, and those move in
|
|
765
|
+
opposite directions. A minimum in the middle is what a confound looks like.
|
|
766
|
+
|
|
767
|
+
### 6.7 Chain length is the motion budget
|
|
768
|
+
|
|
769
|
+
**A limb's excursion is bounded by its chain, not by taste**, and going past it does
|
|
770
|
+
not fail — it degenerates:
|
|
771
|
+
|
|
772
|
+
> *"The first candidate lifted the foot 56 units on a 130-unit leg: the chain has to
|
|
773
|
+
> fold to a 70-unit span, and a two-bone solve does that by throwing the knee
|
|
774
|
+
> sideways. 28 units — a fifth of the leg — is the version that reads as a step."*
|
|
775
|
+
> — [`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md)
|
|
776
|
+
|
|
777
|
+
The same arithmetic on the other side of the chain, from
|
|
778
|
+
[`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex/): rotating a rigid panel about a seam opens a wedge
|
|
779
|
+
of `halfHeight × tan(angle)` — about 22 px at 20° on that cloth — so *"the bend
|
|
780
|
+
angles in `motion.json` are chosen against that overlap, not the other way round."*
|
|
781
|
+
|
|
782
|
+
⇒ **Both are the same rule: the structure sets the range, and the keys are chosen
|
|
783
|
+
inside it.** Deciding the keys first and the structure after is how a solve gets
|
|
784
|
+
asked for a pose outside its reachable set (§6.1).
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## 7. A local key is not a world key
|
|
789
|
+
|
|
790
|
+
**Every number in a rig spec below `root` is in its parent's space, and a parent's
|
|
791
|
+
motion multiplies through every descendant.** Six records, and every one of them is
|
|
792
|
+
a case where a spec that read correctly produced a figure somewhere else.
|
|
793
|
+
|
|
794
|
+
### 7.1 A translate key is an offset from the setup position
|
|
795
|
+
|
|
796
|
+
⚠️ **Two different quantities, and a fitter drives the other one.** Spine's
|
|
797
|
+
`TranslateTimeline.apply` is `pose.x = setup.x + x`, so a key is an *offset*; a
|
|
798
|
+
fitter working in `bone.pose.x` is driving the **absolute local position**. The two
|
|
799
|
+
differ by exactly the bone's own setup `x`/`y`:
|
|
800
|
+
|
|
801
|
+
> *"the first spec moved the figure by the setup offset a second time. Rotation and
|
|
802
|
+
> scale needed no correction: their setups are 0 and 1."*
|
|
803
|
+
> — rung 7, second attempt
|
|
804
|
+
> ([`2026-08-28-rung7-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-rung7-2/LOOP.md) §5)
|
|
805
|
+
|
|
806
|
+
The same confusion one attempt earlier, from the other end: *"The first fitter used
|
|
807
|
+
0 as every knob's neutral value. `bone.pose.x` is the bone's whole local
|
|
808
|
+
translation, not an offset from setup, so that put every bone at its parent's
|
|
809
|
+
origin — the sack 16.5 units sideways and 64.5 up before the search started."*
|
|
810
|
+
|
|
811
|
+
⇒ **The rule that catches both, and it costs one command:** *"a fitted run should
|
|
812
|
+
diff its compiled animation against its own pose series before it reads a single
|
|
813
|
+
measure. The gate cannot see it, `check` sees it only as a framing catastrophe, and
|
|
814
|
+
the diff names it in one line."*
|
|
815
|
+
|
|
816
|
+
### 7.2 Composition is not addition
|
|
817
|
+
|
|
818
|
+
⚠️ **A bone under a rotated parent is not at the parent's position plus its own
|
|
819
|
+
numbers, and a leg chain is exactly that case.** That sentence is a comment in
|
|
820
|
+
[`gallery/stage.ts`](https://github.com/firejune/rigc/blob/main/gallery/stage.ts), which exists because a drawing script
|
|
821
|
+
needed setup-pose world positions and *"writing them a second time in the drawing
|
|
822
|
+
script is the defect that produces a shadow under nobody's feet, and it is
|
|
823
|
+
invisible in both files — each is internally consistent."*
|
|
824
|
+
|
|
825
|
+
⇒ **The rig spec is the one author of the geometry.** Anything else that needs a
|
|
826
|
+
world position walks the parent chain and applies each parent's rotation and scale
|
|
827
|
+
the way the runtime does. Two files that each state a position are two files that
|
|
828
|
+
will disagree, silently, on the day one of them changes.
|
|
829
|
+
|
|
830
|
+
The same fact as a whole-part misplacement: rung 7's mesh variant *"bound each
|
|
831
|
+
vertex against a chain whose y values were written as if the mesh's local frame were
|
|
832
|
+
centred on the sheet, when the frame is `panel`'s own — one segment out. It drew the
|
|
833
|
+
**same ink in the wrong place**: rest pose 51.73 against the region variant's
|
|
834
|
+
11.14"* — and until that was fixed, a comparison between two *mechanisms* was
|
|
835
|
+
measuring a coordinate-space error.
|
|
836
|
+
|
|
837
|
+
### 7.3 Worked: a leaf's displacement is the sum along its chain
|
|
838
|
+
|
|
839
|
+
**Three bones, each keyed to arrive from its own scatter offset, and the leaf
|
|
840
|
+
arrives from the sum of all three.** The fixture's `together` animation keys
|
|
841
|
+
`trunk (−40, 60)`, `arm (30, 40)` and `hand (25, 30)` down to zero over 0.6 s.
|
|
842
|
+
`bonedist` reads each bone's world origin against the same rig holding still:
|
|
843
|
+
|
|
844
|
+
```bash
|
|
845
|
+
cat > vs-home.json <<'EOF'
|
|
846
|
+
{ "spec": "rigc-bonedist/1",
|
|
847
|
+
"bones": { "trunk": "trunk", "arm": "arm", "hand": "hand" },
|
|
848
|
+
"animations": { "together": "home", "leaves": "home" } }
|
|
849
|
+
EOF
|
|
850
|
+
|
|
851
|
+
rigc bonedist --candidate out --reference out --bones vs-home.json --fps 30 --all-bones
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
```
|
|
855
|
+
candidate size 132.880 (root `root` -> `arm` in the setup pose)
|
|
856
|
+
|
|
857
|
+
together vs home 19 frame(s), 0.600s vs 0.600s
|
|
858
|
+
position mean 0.269930 worst 0.984820 (bone `hand`, frame 0)
|
|
859
|
+
bone position (mean/worst)
|
|
860
|
+
hand 0.349196/0.984820
|
|
861
|
+
arm 0.268173/0.756314
|
|
862
|
+
trunk 0.192422/0.542679
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
⚠️ `position` is in **skeleton sizes** — each bone's world origin relative to its
|
|
866
|
+
own skeleton's root, divided by the greatest root-to-bone distance in that
|
|
867
|
+
skeleton's setup pose. The report prints that convention and the other three
|
|
868
|
+
verbatim above the figures, and its header names the size it used, so multiply
|
|
869
|
+
back:
|
|
870
|
+
|
|
871
|
+
| bone | its own key | the sum along its chain | `worst × 132.880` |
|
|
872
|
+
| --- | ---: | ---: | ---: |
|
|
873
|
+
| `trunk` | (−40, 60) → 72.11 | (−40, 60) → **72.11** | **72.11** |
|
|
874
|
+
| `arm` | (30, 40) → 50.00 | (−10, 100) → **100.50** | **100.50** |
|
|
875
|
+
| `hand` | (25, 30) → **39.05** | (15, 130) → **130.86** | **130.86** |
|
|
876
|
+
|
|
877
|
+
🚨 **The hand was keyed to come in from 39 units and comes in from 131 — 3.35× its
|
|
878
|
+
own number — and every figure agrees with the arithmetic to two decimals.** Scatter
|
|
879
|
+
a figure by reading world positions off a picture and writing them into local keys,
|
|
880
|
+
and each leaf is displaced by its whole ancestry. On a deeper rig the leaf starts
|
|
881
|
+
off-canvas.
|
|
882
|
+
|
|
883
|
+
⇒ **The fix is a conversion, not a smaller number.** Decide the world displacement
|
|
884
|
+
you want, then subtract what the ancestors already contribute at that instant. Which
|
|
885
|
+
is FACE §3's shared-shift split (§4.4) arriving from the other direction: `carried`
|
|
886
|
+
exists precisely because *"the depth whose shift a parent bone already applies"* has
|
|
887
|
+
to come out of the child's own key.
|
|
888
|
+
|
|
889
|
+
### 7.4 What that leaves undecided, and who decides it
|
|
890
|
+
|
|
891
|
+
**The compounding is measurable. The ordering is not.** Both animations in the
|
|
892
|
+
fixture start and end in the same two poses, so the *extent* is identical — worst
|
|
893
|
+
`0.984820` on `hand` at frame 0 for both — and they differ only in the path:
|
|
894
|
+
|
|
895
|
+
```
|
|
896
|
+
together vs home position mean 0.269930 worst 0.984820 (bone `hand`, frame 0)
|
|
897
|
+
leaves vs home position mean 0.489455 worst 0.984820 (bone `hand`, frame 0)
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
`leaves` moves one link at a time — hand 0→0.2 s, arm 0.2→0.4, trunk 0.4→0.6 — so
|
|
901
|
+
each part's world displacement equals its own key while it is playing, and the
|
|
902
|
+
parent then carries a finished sub-assembly. `together` moves all three at once, so
|
|
903
|
+
every leaf's world velocity is the sum of its ancestors'.
|
|
904
|
+
|
|
905
|
+
🚫 **Which of those reads better is not a question this toolchain answers**, and no
|
|
906
|
+
number above is evidence for either. It is MOTION §0's rule about movement, applying
|
|
907
|
+
to a movement that happens to be structural: *"the one thing that judges a movement
|
|
908
|
+
is a person's eye, through `rigc vote`."*
|
|
909
|
+
|
|
910
|
+
⚠️ **Provenance note, stated because the honest version is short:** the prescription
|
|
911
|
+
*assemble leaves first* has **no record in the repository, and this page does not
|
|
912
|
+
present one.** What is re-derived here is the mechanism it describes — the compounding in §7.3 — and the
|
|
913
|
+
measured fact that ordering changes the path and not the extent. The ordering claim
|
|
914
|
+
itself is one person's eye, once.
|
|
915
|
+
|
|
916
|
+
---
|
|
917
|
+
|
|
918
|
+
## 8. Duplicate art needs distinct pivots
|
|
919
|
+
|
|
920
|
+
**One drawing used twice is irreducibly ambiguous from pixels alone, and the
|
|
921
|
+
hierarchy is the thing that resolves it.** Two arms, two shins, two wings: the same
|
|
922
|
+
plate, two places. `rigc pose` is *right* to report both, and cannot do better.
|
|
923
|
+
|
|
924
|
+
### 8.1 Worked: `pose` reports both, `chainfit` reports one each
|
|
925
|
+
|
|
926
|
+
The fixture's two wings carry one `wing.png` at mirrored pivots (`x −14` at `+40°`,
|
|
927
|
+
`x +14` at `−40°`). Render the setup pose and read it back with no rig:
|
|
928
|
+
|
|
929
|
+
```bash
|
|
930
|
+
rigc render --candidate out --animation home --fps 2 --max 154 --out pic
|
|
931
|
+
rigc pose --images parts --frame pic/home@2fps/f0000.png --scale 0.85,1.2 --out pose0.json
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
```
|
|
935
|
+
PLACE trunk.png x= 80.0 y= 122.0 rot= 0.0° scale=0.998 residual=0.0050 unexplained= 0%
|
|
936
|
+
AMBIG wing.png x= 102.2 y= 109.7 rot= 40.3° scale=0.931 residual=0.0183 unexplained= 3%
|
|
937
|
+
alt 2: x= 57.2 y= 109.7 rot= -40.3° scale=0.931 residual=0.0186 unexplained= 4%
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
⭐ **Two placements, mirrored, 0.0003 apart in residual.** That is not a weak
|
|
941
|
+
reading — both are excellent, and `unexplained` is 3 % and 4 %. There is no
|
|
942
|
+
threshold that picks one, because there is nothing wrong with either.
|
|
943
|
+
|
|
944
|
+
Now read the same frame *through* the rig. `trunk` anchors (it is the one part
|
|
945
|
+
`pose` placed unambiguously inside `chainfit`'s criterion), and every wing hangs off
|
|
946
|
+
its own pivot one link out:
|
|
947
|
+
|
|
948
|
+
```bash
|
|
949
|
+
rigc chainfit --candidate out --images parts --frame pic/home@2fps/f0000.png \
|
|
950
|
+
--anchor pose0.json --out cf0.json
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
```
|
|
954
|
+
CHAIN hand.png x= 89.0 y= 141.9 rot= -0.3° scale=0.998 residual=0.0227 visible= 32%
|
|
955
|
+
bone hand · depth 2 from trunk · hinge 0.33° (local 0.33° Spine) · 46 px scored
|
|
956
|
+
REFUSE arm.png x= 88.9 y= 125.0 rot= 0.0° scale=0.998 residual=0.0263 visible= 17%
|
|
957
|
+
occluded: arm.png: only 17.0% of it survives the parts drawn over it
|
|
958
|
+
ANCHOR trunk.png x= 80.0 y= 122.0 rot= 0.0° scale=0.998 residual=0.0045 visible=100%
|
|
959
|
+
CHAIN wing.png x= 57.6 y= 110.1 rot= -40.6° scale=0.998 residual=0.0483 visible=100%
|
|
960
|
+
bone wing_l · depth 1 from trunk · hinge 0.61° (local 40.61° Spine) · 240 px scored
|
|
961
|
+
CHAIN wing.png x= 102.1 y= 109.9 rot= 38.8° scale=0.998 residual=0.0386 visible=100%
|
|
962
|
+
bone wing_r · depth 1 from trunk · hinge 1.20° (local -38.80° Spine) · 240 px scored
|
|
963
|
+
|
|
964
|
+
.. 4 of 5 part(s) read; 3 of them the anchor pass refused and the chain bought.
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
⭐ **Two rows for one plate, one per bone, neither ambiguous.** The ambiguity did
|
|
968
|
+
not get resolved by a better objective; it stopped existing, because a child whose
|
|
969
|
+
parent is placed has **one degree of freedom about a pivot the rig declares**
|
|
970
|
+
instead of four (AUTHORING §12): two pivots, two arcs, one answer each.
|
|
971
|
+
|
|
972
|
+
📌 **Read the hinges as the honesty check they are.** The frame *is* the setup pose,
|
|
973
|
+
so the truth is 0°, and the search returned 0.33°, 0.61° and 1.20°. That is the
|
|
974
|
+
instrument's own noise at this size, not a finding, and it is why AUTHORING §12
|
|
975
|
+
calls every threshold in the report a reporting threshold.
|
|
976
|
+
|
|
977
|
+
### 8.2 ⬇️ And the walk only goes outward
|
|
978
|
+
|
|
979
|
+
⚠️ **A placed parent determines its children; a placed child says nothing about its
|
|
980
|
+
parent.** This is the strongest single statement about assembly order in the
|
|
981
|
+
toolchain, and it comes from the instrument:
|
|
982
|
+
|
|
983
|
+
> *"An anchored part fixes its own bone completely — four numbers read off the
|
|
984
|
+
> picture for the four a similarity has — and every descendant then follows from the
|
|
985
|
+
> rig. A bone ABOVE an anchor does not: recovering it would need to know what the
|
|
986
|
+
> link between them did, which is precisely the unknown the anchor does not carry."*
|
|
987
|
+
> — [`src/chainfit.ts`](../src/chainfit.ts)
|
|
988
|
+
|
|
989
|
+
⇒ **So the direction of readability is a hierarchy design input**, and one run
|
|
990
|
+
designed around it:
|
|
991
|
+
|
|
992
|
+
> *"**`torso` is the trunk and everything hangs off it**, rather than a pelvis with
|
|
993
|
+
> the chest and the legs as siblings. Chosen for identifiability, not anatomy …
|
|
994
|
+
> Under a pelvis the legs sit above the only reliable anchor and every leg comes back
|
|
995
|
+
> `no-anchor` on every frame — measured, not predicted."*
|
|
996
|
+
> — [`2026-09-03-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-2/README.md)
|
|
997
|
+
|
|
998
|
+
That is the only place in the corpus where a tree was chosen for what could be
|
|
999
|
+
measured through it rather than for anatomy or for the art, and it is worth knowing
|
|
1000
|
+
the option exists.
|
|
1001
|
+
|
|
1002
|
+
### 8.3 In an authored rig, the same duplication is a per-side flag
|
|
1003
|
+
|
|
1004
|
+
**Two legs made from one drawing are *translated* copies, not mirrored ones, so one
|
|
1005
|
+
constraint value bends both knees the same way** — which reads as a leg on
|
|
1006
|
+
backwards:
|
|
1007
|
+
|
|
1008
|
+
| | `bendPositive` | thigh at x | knee at x | knee sits |
|
|
1009
|
+
| --- | --- | --- | --- | --- |
|
|
1010
|
+
| `leg_f_ik` | `false` | 382 | 397.8 | 15.8 outward |
|
|
1011
|
+
| `leg_b_ik` | `true` | 322 | 306.2 | 15.8 outward |
|
|
1012
|
+
|
|
1013
|
+
⚠️ **And it is invisible where you would look for it.** *"Two of the four
|
|
1014
|
+
`bendPositive` combinations were indistinguishable at 4× zoom on the frame where the
|
|
1015
|
+
difference is largest. `shin_f.worldX` separated them immediately. Look at frames
|
|
1016
|
+
for whether it reads; read bone positions for what it is doing."*
|
|
1017
|
+
([`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md))
|
|
1018
|
+
|
|
1019
|
+
### 8.4 ⚠️ And `pose` can be confident about the wrong twin
|
|
1020
|
+
|
|
1021
|
+
**The residual does not know which of two identical parts it placed.** One run's
|
|
1022
|
+
`rear-thigh` read residual **0.0751** on a frame — *better* than `front-thigh`'s
|
|
1023
|
+
0.0840 — while sitting on the near leg. AUTHORING §8.1's rule for it is a
|
|
1024
|
+
calibration, not a threshold: settle the assignment on the frames where the two
|
|
1025
|
+
parts are *unambiguous* (far apart, or only one drawn), read the separation there
|
|
1026
|
+
where it is a real gap, and then **pin it for the run** so no per-frame search
|
|
1027
|
+
reopens it.
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
## 9. Constraints are structure
|
|
1032
|
+
|
|
1033
|
+
**A constraint is part of the hierarchy, not decoration on it — and it is the part
|
|
1034
|
+
with no pixel signature at all.** That combination is why this section is both a
|
|
1035
|
+
list of structural rules and a list of refusals.
|
|
1036
|
+
|
|
1037
|
+
### 9.1 🚨 The target's parentage *is* the rig
|
|
1038
|
+
|
|
1039
|
+
> *"In a walk the hip **bobs** and the planted foot **does not**, so the target has
|
|
1040
|
+
> to be somewhere the hip's own motion cannot reach it. `ground` is a child of `root`
|
|
1041
|
+
> at the contact line; `foot_f` and `foot_b` are children of `ground`. Parent a foot
|
|
1042
|
+
> target to `hip` and the hip carries its own feet up with it, **the solver reports
|
|
1043
|
+
> success, and the figure hovers.**"*
|
|
1044
|
+
> — [`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md)
|
|
1045
|
+
|
|
1046
|
+
Measured on that build, over the stance where `foot_f` is planted: the hip travels
|
|
1047
|
+
**7.9 units up and 2.9 sideways** while the foot reads `382.0, 68.0` on every
|
|
1048
|
+
sampled time, to the decimal.
|
|
1049
|
+
|
|
1050
|
+
⇒ **The general rule: an IK target must not be a descendant of the chain it
|
|
1051
|
+
drives.** The failure is silent and self-consistent — the constraint is satisfied on
|
|
1052
|
+
every frame, and the thing it is satisfied *relative to* is moving.
|
|
1053
|
+
|
|
1054
|
+
### 9.2 The solver reads the chain off the hierarchy
|
|
1055
|
+
|
|
1056
|
+
**`IkConstraint.apply2` measures a two-bone chain as `l1 = child.x` and
|
|
1057
|
+
`l2 = child.length`**, and solves so that the point `l2` along the child's **+x**
|
|
1058
|
+
reaches the target. So:
|
|
1059
|
+
|
|
1060
|
+
- each bone's local +x has to run down the limb (`thigh` at `rotation: -90`, `shin`
|
|
1061
|
+
at the thigh's own `x: 56`);
|
|
1062
|
+
- *"A chain whose bones point some other way solves correctly and draws nowhere near
|
|
1063
|
+
the target"*;
|
|
1064
|
+
- and the plates then need `"rotation": 90` to cancel the bone, plus an `x` offset to
|
|
1065
|
+
slide their centre down it.
|
|
1066
|
+
|
|
1067
|
+
⭐ **This is where the art and the hierarchy stop being two decisions.** Both plates
|
|
1068
|
+
have to be **drawn from their own joint**, and the joint's coordinates *inside each
|
|
1069
|
+
drawing* are what every `length` and every attachment offset is measured from —
|
|
1070
|
+
[`gallery/rigby.ts`](https://github.com/firejune/rigc/blob/main/gallery/rigby.ts) names all four points for one leg, and
|
|
1071
|
+
[`gallery/walk/rig.json`](https://github.com/firejune/rigc/blob/main/gallery/walk/rig.json) states them.
|
|
1072
|
+
|
|
1073
|
+
### 9.3 A constraint carries the whole subtree, and that is the point
|
|
1074
|
+
|
|
1075
|
+
> *"`cart` is the constrained bone; `wheel_b`/`wheel_f` and `hip` are its children,
|
|
1076
|
+
> so the constraint carries the wheels and the whole figure with it and the tangent
|
|
1077
|
+
> rotation tilts all three."*
|
|
1078
|
+
> — [`gallery/ride/README.md`](https://github.com/firejune/rigc/blob/main/gallery/ride/README.md)
|
|
1079
|
+
|
|
1080
|
+
📌 Two placement conveniences from the same paragraph, both of the form *put the
|
|
1081
|
+
bone where the numbers become trivial*: `root` is at the world origin, *"which is why
|
|
1082
|
+
the `track` slot hangs off it: a path attachment's vertices are in its slot bone's
|
|
1083
|
+
space"*, so the rig's numbers are world coordinates; and a stage-sized plate's bone
|
|
1084
|
+
sits at the stage's centre, *"because a stage-sized part centred on the stage's centre
|
|
1085
|
+
needs no offset — one less number that can be wrong."*
|
|
1086
|
+
|
|
1087
|
+
### 9.4 A constraint aimed at nothing loads clean and moves nothing
|
|
1088
|
+
|
|
1089
|
+
**`PathConstraint.update` opens with `if (!(attachment instanceof PathAttachment)) return`**,
|
|
1090
|
+
so a path constraint pointed at a slot that never shows a path loads perfectly,
|
|
1091
|
+
reports every mix it was given, and does nothing. AUTHORING §3.5.1 has that and two
|
|
1092
|
+
siblings: a physics constraint that names no component parses cleanly and is inert,
|
|
1093
|
+
and a mode string like `"PERCENT"` resolves to `undefined` and runs *some other
|
|
1094
|
+
mode*. rigc refuses all three, and `A36`/`A23` refuse them again on the artifact.
|
|
1095
|
+
|
|
1096
|
+
⇒ Read the pattern rather than the three cases: **a constraint's silence is its
|
|
1097
|
+
default failure mode**, because it drives bones rather than drawing anything.
|
|
1098
|
+
|
|
1099
|
+
### 9.5 ⚠️ A motion-side default can silently revert a rig-side structure
|
|
1100
|
+
|
|
1101
|
+
> *"Four builds differing only in the rig's two `bendPositive` values produced **one
|
|
1102
|
+
> pose** — `shin_f` world x = 366.2 in all four. `SkeletonJson` reads the bend
|
|
1103
|
+
> direction per *timeline key* as well as per constraint, with the same default of
|
|
1104
|
+
> `true`, so any IK timeline that did not restate it overwrote the constraint's value
|
|
1105
|
+
> for the whole animation, with the field still in the file and the gate green either
|
|
1106
|
+
> way."*
|
|
1107
|
+
> — [`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md)
|
|
1108
|
+
> (rigc stamps the rig's value onto every emitted ik key)
|
|
1109
|
+
|
|
1110
|
+
⇒ **A structural fact stated in the rig can be overwritten by a per-key default in
|
|
1111
|
+
the motion.** It took four builds and one bone position to see it; nothing else
|
|
1112
|
+
could have.
|
|
1113
|
+
|
|
1114
|
+
### 9.6 🚫 And a constraint has no pixel signature, which is why the corpus refuses to guess
|
|
1115
|
+
|
|
1116
|
+
**A bone driven by a constraint and the same bone keyed directly render identical
|
|
1117
|
+
pixels.** Every from-zero run in the corpus therefore authored **no constraints at
|
|
1118
|
+
all**, unanimously, and paid for it in a measure:
|
|
1119
|
+
|
|
1120
|
+
> *"authoring one would be a guess dressed as a reading, and the run has nothing to
|
|
1121
|
+
> point at if asked why there are four rather than one … that measure is the price of
|
|
1122
|
+
> the rule, and it is worth paying rather than winning by guessing."*
|
|
1123
|
+
> — rung 8, first attempt
|
|
1124
|
+
> ([`2026-08-23-rung8-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-rung8-1/LOOP.md) §13)
|
|
1125
|
+
|
|
1126
|
+
⭐ **The cleanest demonstration that the rule costs something real and is still the
|
|
1127
|
+
right rule** is in that same run, which authored two rigs against two references:
|
|
1128
|
+
*"`pendulum` has none, `ball` has four. §13's decision not to guess scored **1.000**
|
|
1129
|
+
on one and **0.000** on the other, from the same reasoning."*
|
|
1130
|
+
|
|
1131
|
+
⚠️ **And guessing one is not merely unscored, it is actively destructive**: *"a
|
|
1132
|
+
physics constraint is simulated at load, so it would have moved the hand-fitted poses
|
|
1133
|
+
`check` was measuring, and the run would have lost its only view of whether the
|
|
1134
|
+
animation is right."* AUTHORING §12.5 states the same hazard for a fit — with any
|
|
1135
|
+
constraint in the candidate, *"a fitted `localRotationDeg` is still a placement but
|
|
1136
|
+
not necessarily a value you can key and reproduce."*
|
|
1137
|
+
|
|
1138
|
+
⇒ **So: author a constraint when it is doing structural work you can state**
|
|
1139
|
+
(§9.1–§9.3 are three of those), and not to match a feature list. On the largest
|
|
1140
|
+
reference in the corpus that trade is stark — one run's `bench` line reads
|
|
1141
|
+
`constraints=0/24` against `bones=8/31`, which is a reference whose hierarchy is
|
|
1142
|
+
mostly constraints and a frames-only candidate that recovers none of it.
|
|
1143
|
+
|
|
1144
|
+
---
|
|
1145
|
+
|
|
1146
|
+
## 10. Declaration order, draw order and parentage are three different things
|
|
1147
|
+
|
|
1148
|
+
**Three separate orderings, all expressed in the same two arrays, and runs confuse
|
|
1149
|
+
them.** Two attempts at the same figure had to write the disclaimer out —
|
|
1150
|
+
*"`slots.order` 12/21 and `bones.order` 9/18 are **declaration order**, not draw
|
|
1151
|
+
order being wrong"* (attempt 2), and *"`bones.order` 9/18 and `slots.order` 14/20
|
|
1152
|
+
are the same fact twice"* (attempt 1). If a low order measure sends you looking at
|
|
1153
|
+
depth, you are debugging the wrong array.
|
|
1154
|
+
|
|
1155
|
+
### 10.1 A forward reference is a second root
|
|
1156
|
+
|
|
1157
|
+
`parent` resolves **by name against bones already declared**, exactly as the parser
|
|
1158
|
+
does (AUTHORING §3.2). ⚠️ **This is not a rigc restriction and it does not fail
|
|
1159
|
+
loudly**: *"in the loaded skeleton it would simply be a second root."* Declare
|
|
1160
|
+
parents before children — which is also the convention an editor produces, so it is
|
|
1161
|
+
free.
|
|
1162
|
+
|
|
1163
|
+
### 10.2 ⛔ Never express an overlap by re-parenting
|
|
1164
|
+
|
|
1165
|
+
**Depth is the slots array, and a change of depth is a `drawOrder` key.** AUTHORING
|
|
1166
|
+
§10.2's rule, and §10.2's own reason slots exist at all — *"slots decouple bones from
|
|
1167
|
+
the draw order"* — which is what lets one part sit in front of its own parent:
|
|
1168
|
+
|
|
1169
|
+
> *"**The gun's slot is lifted out of its bone's chain.** It is drawn in front of the
|
|
1170
|
+
> torso and behind the front leg, which is §10.2's own reason slots exist and the only
|
|
1171
|
+
> arrangement that satisfies both things the frames measure: the gun reads unoccluded
|
|
1172
|
+
> in `idle`, and the near leg covers it in `walk`."*
|
|
1173
|
+
> — spineboy attempt 2
|
|
1174
|
+
> ([`2026-08-23-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-2/README.md))
|
|
1175
|
+
|
|
1176
|
+
⚖️ **But parentage is a real question with a real answer, and the evidence for it is
|
|
1177
|
+
smoothness.** Rung 4 modelled a chain's top as a *sibling* of the disc it hangs
|
|
1178
|
+
under — translating with it, not rotating:
|
|
1179
|
+
|
|
1180
|
+
> *"That forces chain1's rotation to sweep a full −360° through the flip.
|
|
1181
|
+
> Re-parenting chain1 *under* the platform bone turns the same measurements into a
|
|
1182
|
+
> smooth ±40° curve — and the smoothness is the evidence, because an animator's curve
|
|
1183
|
+
> is the smooth one."*
|
|
1184
|
+
|
|
1185
|
+
⇒ The two rules do not conflict. **Re-parent to fix what a part is carried by; key
|
|
1186
|
+
draw order to fix what a part is drawn over.** Using either for the other's job is
|
|
1187
|
+
the divergence AUTHORING §10 exists to stop.
|
|
1188
|
+
|
|
1189
|
+
### 10.3 🔒 The one machine guard on parentage, and why it exists
|
|
1190
|
+
|
|
1191
|
+
**`A25_DETACHED_BONE_PARENTAGE` is the only assertion in the suite that reads the
|
|
1192
|
+
tree's shape against a stated intent.** Some bones are detached on purpose — an
|
|
1193
|
+
emitter must not ride the part that released it — and:
|
|
1194
|
+
|
|
1195
|
+
> *"the wrong parentage still loads and still animates — it just lies. That is
|
|
1196
|
+
> exactly the class of invariant that belongs in a machine guard rather than in
|
|
1197
|
+
> prose."*
|
|
1198
|
+
> — [`src/validate.ts`](../src/validate.ts)
|
|
1199
|
+
|
|
1200
|
+
Declare the pair in `invariants.detached` (AUTHORING §3.7) and it fires:
|
|
1201
|
+
|
|
1202
|
+
```bash
|
|
1203
|
+
rigc build --rig detached.rig.json --motion stack.motion.json --images parts \
|
|
1204
|
+
--out det --profile spine-html
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
```
|
|
1208
|
+
FAIL A25_DETACHED_BONE_PARENTAGE: "hand" is a descendant of "arm"; it must not be dragged by that bone's motion
|
|
1209
|
+
rigc: 1 assertion(s) failed — nothing written
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
⚠️ **It is an archetype rule, so `--profile spine` reports `PROF` and not a pass**
|
|
1213
|
+
(AUTHORING §5.2, §7 step 3), and an absent `detached` field reports **SKIP** rather
|
|
1214
|
+
than a pass. Both of those are the honest readings and neither is a green light.
|
|
1215
|
+
|
|
1216
|
+
### 10.4 🚨 The renumbering landmine: mesh weights by index
|
|
1217
|
+
|
|
1218
|
+
**Inserting one bone can rebind every vertex of every mesh below it, with a green
|
|
1219
|
+
gate and an unmoved `diff`.** Spine's own weight encoding stores a `boneIndex` — a
|
|
1220
|
+
position in the *emitted* bone array, a list the rig spec never writes and cannot
|
|
1221
|
+
see:
|
|
1222
|
+
|
|
1223
|
+
> *"Put one bone ahead of the meshes and every vertex rebinds: the file still loads,
|
|
1224
|
+
> every index is still in range, every vertex's weights still sum to 1, and `A04`,
|
|
1225
|
+
> `A20` and `diff` are all quiet, because an index has no name to be wrong.
|
|
1226
|
+
> (Measured, on the rung 6 transcription: union MAE 3.30 → 15.09, worst mesh-slot
|
|
1227
|
+
> drift 0.09 px → 9.8 px, with a green gate throughout.)"*
|
|
1228
|
+
> — AUTHORING §3.4
|
|
1229
|
+
|
|
1230
|
+
⇒ rigc's `weights` form binds **by name**, like every other reference in a rig spec,
|
|
1231
|
+
and the index form needs `"boneIndexing": "raw"` said out loud — *"an opt-in, because
|
|
1232
|
+
what is being opted into is the silence."* Use the named form and bone insertion is
|
|
1233
|
+
free.
|
|
1234
|
+
|
|
1235
|
+
---
|
|
1236
|
+
|
|
1237
|
+
## 11. What nothing measures
|
|
1238
|
+
|
|
1239
|
+
### 11.1 Three fields no render carries
|
|
1240
|
+
|
|
1241
|
+
**`parent`, `length` and `inherit` are not in any picture.** Five records say so and
|
|
1242
|
+
each declares its choice as reasoning:
|
|
1243
|
+
|
|
1244
|
+
| Field | What the honest runs did |
|
|
1245
|
+
| --- | --- |
|
|
1246
|
+
| `parent` | stated the tree §10.1's naming convention implies, and said the internal hierarchy *"is not measurable at this scale and is not claimed to be right"* where the figure was ~90 px of ink over eleven parts |
|
|
1247
|
+
| `length` | ⭐ *"`bones.length_present` is **a coin flip taken deliberately**: the eleven character bones state a `length` and `root`, `course` and `ball` do not, on the reading that an editor-drawn skeleton has lengths and a bone created by dragging an image in does not. Nothing in the frames can check it."* Two other runs read `length_present 1/3` and `1/5` and said the same — *"a rendered frame carries no trace of a bone's `length` or of its inheritance mode, so a run authored from pictures cannot recover them at all"* |
|
|
1248
|
+
| `inherit` | left off everywhere, and reported as such |
|
|
1249
|
+
|
|
1250
|
+
⇒ Write them, and write one line saying they are reasoning. AUTHORING §9.3 is the
|
|
1251
|
+
general form of this and it is the section to read next.
|
|
1252
|
+
|
|
1253
|
+
### 11.2 The instruments, and which loop each one belongs in
|
|
1254
|
+
|
|
1255
|
+
| Instrument | What it sees of a hierarchy | Which loop |
|
|
1256
|
+
| --- | --- | --- |
|
|
1257
|
+
| `build` / `validate` | that the tree parses, that every name resolves, and `A25` if you declared a forbidden pair | every build |
|
|
1258
|
+
| `check` | the *consequences* of the structure, in pixels — and **only where the structure moves art.** A pivot is invisible at the pose and everything in the movement (§3.2) | inside the authoring loop |
|
|
1259
|
+
| `chainfit` | ⭐ **the only reading of structure against a picture.** `pivotDisagreementPx` is *"the one direct measurement of your rig against the picture"*; `bone.carriedBones` names the links whose hinge could not be fitted and whose setup rotation was carried through | inside the loop, once a candidate exists |
|
|
1260
|
+
| `explain` | ⭐ **the only reader of the tree that needs no reference at all** — every bone with its resolved `parent=`, in one table, off the compiled rig. It sees what the spec *says* the hierarchy is, never what it looks like, and writes nothing | before the first build, and whenever a rig compiles and still looks wrong |
|
|
1261
|
+
| `diff` | that two files *say* the same thing. It read **1.000 on all 49 measures** across a 236.5-unit pivot move (INGEST §4.1) | finish line |
|
|
1262
|
+
| `bonedist` | per-frame, per-bone world-transform distance against **another skeleton** — so it reads the reference and is subject to the honesty rule | finish line only |
|
|
1263
|
+
| `bench`'s `depth_histogram` / `degree_sequence` | ⭐ the **name-agnostic** read of a tree: as many bones at each depth, as many with each child count. Nine records use this pair as the honest statement about a hierarchy, precisely because it survives two people naming the same parts differently | finish line only |
|
|
1264
|
+
|
|
1265
|
+
⚠️ **`pivotDisagreementPx` needs two independently read parts on one chain.** It is
|
|
1266
|
+
reported for anchored bones only — it is the distance between the chain's own
|
|
1267
|
+
prediction of a bone's pivot and where the anchor pass put it — so a rig with one
|
|
1268
|
+
anchor (the fixture in §8) produces none. One run got it on exactly one joint of
|
|
1269
|
+
sixteen bones: **median 2.00 px, max 5.17** over 126 frames on the head bone, *"the
|
|
1270
|
+
same order as the sweep's own basin there."*
|
|
1271
|
+
|
|
1272
|
+
### 11.3 Below a scale there is no hierarchy to measure
|
|
1273
|
+
|
|
1274
|
+
**Two records draw the line explicitly**, and it is worth knowing where it is before
|
|
1275
|
+
promising a tree:
|
|
1276
|
+
|
|
1277
|
+
- eleven parts over about **90 px of ink** *"cannot decide a bone tree; what was
|
|
1278
|
+
fitted is each part's placement and spin, and the tree is the one §10.1's naming
|
|
1279
|
+
rule implies."*
|
|
1280
|
+
- at **8 × 9 frame pixels** per limb, one run declined a separate neck bone because
|
|
1281
|
+
*"a separate neck bone is a second rotation between the torso and the head, and at
|
|
1282
|
+
8 × 9 frame pixels the frames cannot separate them"* — §10.1 asks for one slot per
|
|
1283
|
+
image, and *"it does not ask for one bone per slot."*
|
|
1284
|
+
|
|
1285
|
+
⇒ ⭐ **A bone the frames cannot separate from its parent is a bone you are choosing,
|
|
1286
|
+
not measuring.** Choose it on what the rig has to do next (§6.6), and say which it
|
|
1287
|
+
was.
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
## 12. Non-goals — stated, so nobody proposes them as gaps
|
|
1292
|
+
|
|
1293
|
+
🔭 **A hierarchy grade.** There is no measure on this page and none is coming from
|
|
1294
|
+
it. §11.2 is the complete list of what the instruments see, two of them read a
|
|
1295
|
+
reference skeleton and are finish-line only, and the two that run inside the loop
|
|
1296
|
+
report distances rather than verdicts.
|
|
1297
|
+
|
|
1298
|
+
🔭 **A pivot solver in the CLI.** Both estimators are documented arithmetic — MOTION
|
|
1299
|
+
§3.9's 2×2 for two poses, AUTHORING §8.1's fixed point in two parts' own coordinates
|
|
1300
|
+
for N frames — and the thing that decides whether either answer means anything is a
|
|
1301
|
+
**conditioning check on data the tool does not have** (§2.3). A command that returned
|
|
1302
|
+
a pivot and no conditioning would be the confident wrong answer §2.1 is a catalogue
|
|
1303
|
+
of.
|
|
1304
|
+
|
|
1305
|
+
🔭 **A tree generator from parts.** Naming, depth and parentage are all decided by
|
|
1306
|
+
things outside the PNGs: what the art is named (AUTHORING §10.1), what has to move
|
|
1307
|
+
independently, and what can be read through the structure afterwards (§8.2). A
|
|
1308
|
+
generated tree would have to guess all three and would report the guess as a
|
|
1309
|
+
derivation.
|
|
1310
|
+
|
|
1311
|
+
🔭 **Automatic child compensation on a pivot move.** §3's edit is four rows and
|
|
1312
|
+
INGEST §4.1 states all four; the reason it is not a command is that the *decision*
|
|
1313
|
+
being made — which children move with the origin and which were wrong before —
|
|
1314
|
+
belongs to whoever knows why the pivot moved. An automatic version would silently
|
|
1315
|
+
propagate a mistake through a subtree, which is the failure mode of §10.4.
|
|
1316
|
+
|
|
1317
|
+
🔭 **A constraint inferred from frames.** §9.6: the pixels are identical either way.
|
|
1318
|
+
Anything here would be a guess with machinery attached.
|
|
1319
|
+
|
|
1320
|
+
---
|
|
1321
|
+
|
|
1322
|
+
## Appendix — the figure this page measures on
|
|
1323
|
+
|
|
1324
|
+
**Four plates, six bones, four animations.** Everything in §3, §7.3, §8.1 and §10.3
|
|
1325
|
+
runs on it, and it is built from the bytes below so the sections run end to end.
|
|
1326
|
+
|
|
1327
|
+
```bash
|
|
1328
|
+
mkdir -p stack/parts && cd stack
|
|
1329
|
+
bun -e '
|
|
1330
|
+
const files = {
|
|
1331
|
+
"parts/trunk.png": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAwCAYAAAALiLqjAAAATUlEQVR42u3SIRUAIAwA0WVBkwOxRwhikGStoNKIMMwQvBOnvznZqp6ZAIRAG+aZAQAAAAAAvAFK7Z7ZBwAXAQAAAAAAXAHTlmcGEHYAa2smZ4bNX2AAAAAASUVORK5CYII=",
|
|
1332
|
+
"parts/arm.png": "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAeCAYAAAAVdY8wAAAALUlEQVR42mPQ05D7TwxmoL7CMwvs/hODRxUOWoXTChT+E4OJVzga4KMKyVYIALj674flJv6nAAAAAElFTkSuQmCC",
|
|
1333
|
+
"parts/hand.png": "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAIUlEQVR42mOQkxD7j4yfVMXgxQyDUAMhBcNBA3qgDEINAJw3WmDDMo/XAAAAAElFTkSuQmCC",
|
|
1334
|
+
"parts/wing.png": "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAYCAYAAADDLGwtAAAAK0lEQVR42mP48sbtPzGYgfoKE3ZV/ScGjyqkkkKtBq//xGDiFY4G+CBVCAChAm6LWQ8FxAAAAABJRU5ErkJggg=="
|
|
1335
|
+
};
|
|
1336
|
+
for (const [p, b] of Object.entries(files)) await Bun.write(p, Buffer.from(b, "base64"));
|
|
1337
|
+
'
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
`trunk` is 24×48 with a red cap band and a dark seam, `arm` 10×30, `hand` 12×12,
|
|
1341
|
+
`wing` 10×24 with a yellow tip. ⚠️ **None of them is one flat colour, on purpose** —
|
|
1342
|
+
a self-similar plate makes `pose`'s scale window read at its own floor (MOTION §2.2),
|
|
1343
|
+
which is noise this page does not need.
|
|
1344
|
+
|
|
1345
|
+
### The rig
|
|
1346
|
+
|
|
1347
|
+
⭐ **Read the four §1 decisions in it before the sections use them**: every bone sits
|
|
1348
|
+
at a joint, every attachment is offset off that joint, both wings carry one image
|
|
1349
|
+
under two placeholder names of `wing` (AUTHORING §10.1 — the attachment name *is* the
|
|
1350
|
+
image name), and `root` is never keyed.
|
|
1351
|
+
|
|
1352
|
+
```bash
|
|
1353
|
+
cat > stack.rig.json <<'EOF'
|
|
1354
|
+
{ "spec": "rigc-rig/1", "name": "stack", "images": "parts",
|
|
1355
|
+
"skeleton": { "x": 0, "y": 0, "width": 200, "height": 200 },
|
|
1356
|
+
"bones": [
|
|
1357
|
+
{ "name": "root" },
|
|
1358
|
+
{ "name": "trunk", "parent": "root", "x": 100, "y": 40 },
|
|
1359
|
+
{ "name": "arm", "parent": "trunk", "x": 9, "y": 36 },
|
|
1360
|
+
{ "name": "hand", "parent": "arm", "x": 0, "y": -26 },
|
|
1361
|
+
{ "name": "wing_l", "parent": "trunk", "x": -14, "y": 26, "rotation": 40 },
|
|
1362
|
+
{ "name": "wing_r", "parent": "trunk", "x": 14, "y": 26, "rotation": -40 }
|
|
1363
|
+
],
|
|
1364
|
+
"slots": [
|
|
1365
|
+
{ "name": "hand", "bone": "hand", "attachment": "hand" },
|
|
1366
|
+
{ "name": "arm", "bone": "arm", "attachment": "arm" },
|
|
1367
|
+
{ "name": "trunk", "bone": "trunk", "attachment": "trunk" },
|
|
1368
|
+
{ "name": "wing_l", "bone": "wing_l", "attachment": "wing" },
|
|
1369
|
+
{ "name": "wing_r", "bone": "wing_r", "attachment": "wing" }
|
|
1370
|
+
],
|
|
1371
|
+
"skins": { "default": {
|
|
1372
|
+
"trunk": { "trunk": { "image": "trunk.png", "y": 24 } },
|
|
1373
|
+
"arm": { "arm": { "image": "arm.png", "y": -15 } },
|
|
1374
|
+
"hand": { "hand": { "image": "hand.png", "y": -6 } },
|
|
1375
|
+
"wing_l": { "wing": { "image": "wing.png", "y": 13 } },
|
|
1376
|
+
"wing_r": { "wing": { "image": "wing.png", "y": 13 } }
|
|
1377
|
+
} }
|
|
1378
|
+
}
|
|
1379
|
+
EOF
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
📌 **The draw order is load-bearing for §8.** `trunk` is drawn after `hand` and
|
|
1383
|
+
`arm`, so the arm straddles its edge and comes back `occluded` at 17 % visible —
|
|
1384
|
+
which is the part `pose` refuses and the chain buys. The wings are drawn in front of
|
|
1385
|
+
it, so their duplication is a *duplication* rather than an occlusion.
|
|
1386
|
+
|
|
1387
|
+
### The motion
|
|
1388
|
+
|
|
1389
|
+
Four animations: `home` holds the setup pose (the reference `bonedist` measures
|
|
1390
|
+
against, and the picture §8 reads), `swing` turns the arm 60° (the movement §3
|
|
1391
|
+
measures a pivot through), and `together` / `leaves` are §7's two orderings.
|
|
1392
|
+
|
|
1393
|
+
```bash
|
|
1394
|
+
cat > stack.motion.json <<'EOF'
|
|
1395
|
+
{ "spec": "rigc-motion/1", "archetype": "stack", "cut": "stack",
|
|
1396
|
+
"easings": { "land": [0.33, 0, 0.15, 1] },
|
|
1397
|
+
"animations": {
|
|
1398
|
+
"home": { "duration": 0.6, "tracks": [
|
|
1399
|
+
{ "bone": "arm", "property": "rotate", "keys": [ { "t": 0, "v": [0] }, { "t": 0.6, "v": [0] } ] } ] },
|
|
1400
|
+
"swing": { "duration": 0.6, "tracks": [
|
|
1401
|
+
{ "bone": "arm", "property": "rotate", "keys": [ { "t": 0, "v": [0], "ease": "land" }, { "t": 0.6, "v": [-60] } ] } ] },
|
|
1402
|
+
"together": { "duration": 0.6, "tracks": [
|
|
1403
|
+
{ "bone": "trunk", "property": "translate", "keys": [ { "t": 0, "v": [-40, 60], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] },
|
|
1404
|
+
{ "bone": "arm", "property": "translate", "keys": [ { "t": 0, "v": [30, 40], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] },
|
|
1405
|
+
{ "bone": "hand", "property": "translate", "keys": [ { "t": 0, "v": [25, 30], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] } ] },
|
|
1406
|
+
"leaves": { "duration": 0.6, "tracks": [
|
|
1407
|
+
{ "bone": "hand", "property": "translate", "keys": [ { "t": 0, "v": [25, 30], "ease": "land" }, { "t": 0.2, "v": [0, 0] }, { "t": 0.6, "v": [0, 0] } ] },
|
|
1408
|
+
{ "bone": "arm", "property": "translate", "keys": [ { "t": 0, "v": [30, 40] }, { "t": 0.2, "v": [30, 40], "ease": "land" }, { "t": 0.4, "v": [0, 0] }, { "t": 0.6, "v": [0, 0] } ] },
|
|
1409
|
+
{ "bone": "trunk", "property": "translate", "keys": [ { "t": 0, "v": [-40, 60] }, { "t": 0.4, "v": [-40, 60], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] } ] }
|
|
1410
|
+
} }
|
|
1411
|
+
EOF
|
|
1412
|
+
|
|
1413
|
+
rigc build --rig stack.rig.json --motion stack.motion.json --images parts --out out
|
|
1414
|
+
# .. pages=4 regions=4 bones=6 slots=5 animations=4 version=4.3.13 … profile=spine
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
✅ It is green under **both** profiles, so `--profile spine-html` is available for
|
|
1418
|
+
§10.3 without any unrelated failure in the report.
|
|
1419
|
+
|
|
1420
|
+
### The three variants the sections build
|
|
1421
|
+
|
|
1422
|
+
```bash
|
|
1423
|
+
# §3 — the pivot moved +15 along the arm's own axis, children compensated
|
|
1424
|
+
sed -e 's/"x": 9, "y": 36/"x": 9, "y": 51/' \
|
|
1425
|
+
-e 's/"x": 0, "y": -26/"x": 0, "y": -41/' \
|
|
1426
|
+
-e 's/"arm.png", "y": -15/"arm.png", "y": -30/' stack.rig.json > mid.rig.json
|
|
1427
|
+
|
|
1428
|
+
# §3 — the same move with the CHILD row forgotten
|
|
1429
|
+
sed -e 's/"x": 9, "y": 36/"x": 9, "y": 51/' \
|
|
1430
|
+
-e 's/"arm.png", "y": -15/"arm.png", "y": -30/' stack.rig.json > mid-nochild.rig.json
|
|
1431
|
+
|
|
1432
|
+
rigc build --rig mid.rig.json --motion stack.motion.json --images parts --out mid
|
|
1433
|
+
rigc build --rig mid-nochild.rig.json --motion stack.motion.json --images parts --out mid-nochild
|
|
1434
|
+
|
|
1435
|
+
# §10.3 — the same rig with a forbidden parentage declared
|
|
1436
|
+
# add to stack.rig.json: "invariants": { "detached": [
|
|
1437
|
+
# { "bone": "hand", "notUnder": "arm",
|
|
1438
|
+
# "why": "what the hand releases stays where it was released; parented to the
|
|
1439
|
+
# swinging arm it would be dragged along with every swing" } ] }
|
|
1440
|
+
# → detached.rig.json
|
|
1441
|
+
```
|