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/FACE.md
ADDED
|
@@ -0,0 +1,1948 @@
|
|
|
1
|
+
# Authoring a face — a turn, a gaze and a blink on plain Spine data
|
|
2
|
+
|
|
3
|
+
**Read this when the request is a head rather than a body.** It is written for an
|
|
4
|
+
agent that has been handed a drawn face — or has to draw one — and asked for the
|
|
5
|
+
moves a portrait makes: it breathes, it blinks, its eyes move, and it **turns a
|
|
6
|
+
few degrees off axis**. The last one is the reason this page exists. It is the
|
|
7
|
+
move a second format is usually bought for, and it is authorable here, out of an
|
|
8
|
+
ordinary `deform` timeline plus per-part parallax, at a cost this page prices
|
|
9
|
+
before you spend it.
|
|
10
|
+
|
|
11
|
+
[AUTHORING.md](AUTHORING.md) is the format — read it first and keep it open; this
|
|
12
|
+
page never restates a field it documents, and
|
|
13
|
+
[AUTHORING §4.11](AUTHORING.md) is the `deform` timeline field by field.
|
|
14
|
+
[MOTION.md](MOTION.md) is the recipe for
|
|
15
|
+
*movement* — timing, easing, anticipation, follow-through, the offset table — and
|
|
16
|
+
everything in it applies to a face unchanged. This page is the part neither has:
|
|
17
|
+
**a face's own geometry**, and what a projection costs when the spec can only
|
|
18
|
+
hold its results.
|
|
19
|
+
|
|
20
|
+
- The `deform` timeline, field by field, and the six things rigc refuses in it:
|
|
21
|
+
**AUTHORING §4.11**
|
|
22
|
+
- Named failures, and the file each one points at: **AUTHORING §5–§6** — the
|
|
23
|
+
refusals this page names are read there, and §4.2 quotes the one that points
|
|
24
|
+
back at this page
|
|
25
|
+
- Timing, easing, arcs, anticipation, follow-through, the per-bone offset table:
|
|
26
|
+
**MOTION §3** — a blink and a gaze are ordinary MOTION.md work
|
|
27
|
+
- Candidate spreading and the ballot: **MOTION §4–§5**. Nothing on this page
|
|
28
|
+
replaces a person's eye
|
|
29
|
+
- A skeleton somebody else authored, and moving a pivot inside it:
|
|
30
|
+
[INGEST.md](INGEST.md)
|
|
31
|
+
- The hierarchy underneath a face, as a general rule rather than this closed form:
|
|
32
|
+
[RIGGING.md](RIGGING.md) — §5 is why §3's `faceshift` is a bone at all and why a
|
|
33
|
+
breath's `chest` is a **sibling** of the plate it must not scale, and §4.4 is the
|
|
34
|
+
artless-parent pattern the shared-shift split is one instance of
|
|
35
|
+
- The worked example every number below comes from:
|
|
36
|
+
[`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait),
|
|
37
|
+
and its measurement half,
|
|
38
|
+
[`FINDINGS.md`](https://github.com/firejune/rigc/tree/main/gallery/portrait/FINDINGS.md)
|
|
39
|
+
- **The same closed forms on the other axis**:
|
|
40
|
+
[`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) is a
|
|
41
|
+
worked `pitch`, and this page stays written for a **yaw**. It re-derives §4.2's
|
|
42
|
+
fold angle on uneven *rows* and brackets it against `A39` at 33°/34°, and it
|
|
43
|
+
measures §5's foreshortening at **0.863–1.176** against the yaw's 0.892–1.064
|
|
44
|
+
below — a wider span at the same 12°, because a face is taller than it is deep.
|
|
45
|
+
Read it after this page, not instead of it
|
|
46
|
+
- **The turn driven by a value instead of played as a time** — the same keys, on
|
|
47
|
+
an axis: §8's *The turn as a value rather than a time* derives the range,
|
|
48
|
+
**AUTHORING §3.5.2** is the `slider` constraint that does it, and
|
|
49
|
+
[`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) is
|
|
50
|
+
the worked case. Read it when the angle has to follow something outside the
|
|
51
|
+
animation — a pointer, a gaze target, a game value
|
|
52
|
+
|
|
53
|
+
🚨 **A `deform` key's winding is gated; how far it moved the geometry is not, and
|
|
54
|
+
that half is the one you have to author around.**
|
|
55
|
+
`A39_DEFORM_KEEPS_TRIANGLE_WINDING` refuses a key that turns the mesh inside out —
|
|
56
|
+
by name, by key and by triangle — and the build writes nothing. What no assertion
|
|
57
|
+
has an opinion about is **magnitude**: a band that stretches where the projection
|
|
58
|
+
says it should compress keeps every triangle's winding, so it gates green, beside
|
|
59
|
+
the same `MESH` coverage line as the good build, because coverage reports the
|
|
60
|
+
*setup* pose. §9.2 is that pair as three builds of one rig — the good one, one
|
|
61
|
+
wrong and green, one refused. For the half still ungated, `explain`'s `DEFORM`
|
|
62
|
+
block prints each key's area and stretch ratios per triangle with no reference
|
|
63
|
+
render (§9.2, AUTHORING §4.11.2), and §9.3 is the differential audit, the three
|
|
64
|
+
things it cannot do, and the procedure that survives them.
|
|
65
|
+
|
|
66
|
+
📐 **Where the numbers on this page come from.** Every figure marked **derived**
|
|
67
|
+
is re-computed from the closed form in §1 and reproduces to the digits printed.
|
|
68
|
+
Every figure marked **measured** was read off the shipped artifact by the
|
|
69
|
+
`gallery/portrait` record and carries its scope in the line. Nothing here is a
|
|
70
|
+
prediction, and nothing here is a pass bar.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 0. The normal form
|
|
75
|
+
|
|
76
|
+
**A face request normalises to a depth list plus MOTION.md.** That is the whole
|
|
77
|
+
internal shape:
|
|
78
|
+
|
|
79
|
+
| What arrived | What it becomes |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| a face that **blinks**, **breathes**, **looks around** | ordinary MOTION.md tracks. A lid, a chest, an iris. Nothing on this page is needed |
|
|
82
|
+
| a face that **turns** | a list of `(x, z)` — every part's position across the screen and its **depth** — plus one line of arithmetic evaluated at each of them |
|
|
83
|
+
| a face that turns **far** (a three-quarter view) | ⛔ a different rig, and §8 says where the line is. Not a format problem: a parts-and-labour problem |
|
|
84
|
+
| a face that turns **by however much something outside it says** — a pointer, a gaze target, a game value | the same list and the same arithmetic, reached by a **value** instead of by a playhead: the turn animation becomes a `slider`'s lookup table (§8's last subsection, AUTHORING §3.5.2). What changes is not the geometry, it is the range — which stops being a choice and becomes a measurement |
|
|
85
|
+
|
|
86
|
+
⭐ **The turn is the only part of a face that is not already MOTION.md's job**,
|
|
87
|
+
and it is 90% of this page. A blink is a translating plate (§6); a gaze is
|
|
88
|
+
MOTION §3.7's offset table applied to an eye (§7). If the request does not
|
|
89
|
+
contain a turn, read §6 and §7 and stop.
|
|
90
|
+
|
|
91
|
+
The one thing that is genuinely new: a turn is **not a pose you can key**. It is
|
|
92
|
+
a projection, so its values are not chosen, they are *evaluated* — and the
|
|
93
|
+
authoring is transcription rather than judgement. Which means the failure mode is
|
|
94
|
+
not "it reads badly", it is **"one number is wrong and nothing can see it"**.
|
|
95
|
+
That is what §3 and §9 are for.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 1. The one line, and that it **is** one line
|
|
100
|
+
|
|
101
|
+
Treat the face as painted on a cylinder standing on the skull's vertical axis. A
|
|
102
|
+
yaw of `t` about that axis carries a point at `(x, z)` — `x` across the screen,
|
|
103
|
+
`z` toward the viewer — to `x·cos t − z·sin t`, so the shift a part takes is
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
dx = x·(cos t − 1) − z·sin t
|
|
107
|
+
\____________/ \______/
|
|
108
|
+
the head depth times sin t:
|
|
109
|
+
narrowing to the WHOLE of the move
|
|
110
|
+
cos t of its
|
|
111
|
+
width (2.2% at 12°)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**That is the entire model.** Everything else on this page is that expression
|
|
115
|
+
evaluated somewhere, and the two terms are worth reading separately:
|
|
116
|
+
|
|
117
|
+
- **`−z·sin t` is proportional to depth.** A part further forward travels
|
|
118
|
+
further. That *is* parallax, and it is why a fringe moves more than a face and
|
|
119
|
+
why hair behind the axis moves the **other way** (§2).
|
|
120
|
+
- **`x·(cos t − 1)` pulls both edges inward by the same amount** — the head
|
|
121
|
+
narrowing. It is second order and it is small: at 12° it is 2.2%, which is 3.5
|
|
122
|
+
units at the silhouette against a 35-unit centre shift.
|
|
123
|
+
|
|
124
|
+
At the 12° the worked example ships (**derived**):
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
t = 0.209440 rad cos t − 1 = −0.021852 sin t = 0.207912
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
⇒ **A reader who has this line derives every number in a turn and never reaches
|
|
131
|
+
for a hand-tuned table.** That matters more than it sounds: a hand-tuned face
|
|
132
|
+
table has no property you can check, and a derived one has exactly one — it
|
|
133
|
+
agrees with the line, or it does not.
|
|
134
|
+
|
|
135
|
+
### 1.1 ⭐ And now the spec holds the line, not its results
|
|
136
|
+
|
|
137
|
+
**A `deform` key states this expression** (AUTHORING §4.11.1). The worked
|
|
138
|
+
example's held 12° yaw is four lines of which two are the angle:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{ "t": 0.62, "transform": { "kind": "yaw", "radius": 170, "degrees": 12 }, "ease": "swell" },
|
|
142
|
+
{ "t": 1.5, "transform": { "kind": "yaw", "radius": 170, "degrees": 12 }, "ease": "settle" }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
⇒ **`radius` is `R` off the depth table (§2) and `degrees` is the angle.** The
|
|
146
|
+
compiler evaluates `dx = x·(cos t − 1) − z·sin t` at every vertex with
|
|
147
|
+
`z = √(R² − x²)`, writes the millimetre-level results into the artifact, and
|
|
148
|
+
`explain` prints both the model and the offsets it produced. What the key
|
|
149
|
+
states is a measurement of the shape the drawing implies, and nothing else.
|
|
150
|
+
|
|
151
|
+
Three things follow, and they are the reasons the construct exists rather than
|
|
152
|
+
side effects:
|
|
153
|
+
|
|
154
|
+
- **A second angle is a second number.** Each angle of §8's cliff sweep — 8, 12,
|
|
155
|
+
16, 20, 24, 28 and 32 degrees — is `"degrees": <n>` and a rebuild.
|
|
156
|
+
- **A small yaw is one key.** The anticipation MOTION §3.6 asks for and the
|
|
157
|
+
head-follow §7 prices are each one key with a smaller `degrees`.
|
|
158
|
+
- **The audit is two parameters.** A transcription can only be checked against the
|
|
159
|
+
line by hand, which is §9.3's gap; a stated model is checked by reading two
|
|
160
|
+
parameters. What is still unmeasured is the *consequence* — whether that angle
|
|
161
|
+
folds the mesh — and §4.2 and A39 are that half.
|
|
162
|
+
|
|
163
|
+
⚠️ **What it does not do.** It evaluates; it never chooses. The radius, the
|
|
164
|
+
angle, the depth table and whether 12° reads are all yours, and a wrong `radius`
|
|
165
|
+
produces 160 consistent wrong numbers at once — see §4's warning, which the
|
|
166
|
+
construct makes cheaper to get wrong rather than harder.
|
|
167
|
+
|
|
168
|
+
📌 **Author `t` in degrees, in the spec.** Radians appear nowhere: the key holds
|
|
169
|
+
the angle and the compiler holds the conversion.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 2. Depth is the parameter you are actually authoring
|
|
174
|
+
|
|
175
|
+
🚨 **A part list for a face is not a list of drawings. It is a list of
|
|
176
|
+
`(x, z)`.** The bones carry `x` (`eye_l` at `−62`, `nose` at `0`) and the rig
|
|
177
|
+
carries the mesh columns' `x`; the `z` is in no drawing.
|
|
178
|
+
|
|
179
|
+
⭐ **Every depth goes in the file, and each half has its own construct.** A `yaw`
|
|
180
|
+
deform key states its `radius` (§1.1), which is the `R` of the cylinder that plate
|
|
181
|
+
is painted on — so the head's 170 and the fringe's 196 are in `motion.json`. A
|
|
182
|
+
**bone** track's key can state a `derive` model over a group, whose `depth` is one
|
|
183
|
+
number **per member** — so the feature depths, the hair depths and the sign flip
|
|
184
|
+
below are in the file too (§3, AUTHORING §4.5.1). Neither is spelled `"z"`: the
|
|
185
|
+
field is `radius` on a mesh key and `depth` on a bone track.
|
|
186
|
+
|
|
187
|
+
⇒ **Write the depth table down somewhere a reader will find it.** The specs hold
|
|
188
|
+
the depths the turn *uses*; a project's own reasoning about them — why the fringe
|
|
189
|
+
stands off 26 and not 15 — belongs beside the rig, and in the worked example that
|
|
190
|
+
is a table in its README.
|
|
191
|
+
|
|
192
|
+
⚠️ **A depth is a decision.** The table below is the thing to argue about before
|
|
193
|
+
authoring anything, and the point of writing it into the spec is that a reader of
|
|
194
|
+
the numbers can see the table that produced them:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
grep -c '"depth"' gallery/portrait/motion.json # 8, one per derive key
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**The depths in the worked example**, and what each one is doing (**derived**
|
|
201
|
+
column: `dx` at 12°):
|
|
202
|
+
|
|
203
|
+
| Part | `x` | `z` | `dx` | What the depth is |
|
|
204
|
+
| --- | --- | --- | --- | --- |
|
|
205
|
+
| face centre (skull surface) | 0 | **170** | −35.345 | `R`, the cylinder radius. Every other number is relative to this |
|
|
206
|
+
| fringe centre | 0 | **196** | −40.751 | 26 **in front** of the skull. The one number in the rig that exists only to make parallax |
|
|
207
|
+
| `nose` | 0 | **192** | −39.919 | protrudes 22. The only feature in front of the surface |
|
|
208
|
+
| `eye_l` / `eye_r` | ∓62 | 150 | −29.832 / −32.542 | 20 **below** the surface — a socket is a hollow |
|
|
209
|
+
| `brow_l` / `brow_r` | ∓62 | 158 | −31.495 / −34.205 | 12 below. A brow sits proud of its socket |
|
|
210
|
+
| `mouth` | 0 | 166 | −34.513 | 4 below. Almost on the surface |
|
|
211
|
+
| `hairmass` (back hair) | 0 | **−55** | **+11.435** | its centroid is 55 **behind** the axis |
|
|
212
|
+
| `lock_l` / `lock_r` | ∓150 | 100 | −17.513 / −24.069 | off-axis and shallow, so they travel about half as far as the centre |
|
|
213
|
+
| `ahoge` (cowlick) | −23 | 20 | −3.656 | almost on the axis, so it barely moves |
|
|
214
|
+
|
|
215
|
+
⭐ **The sign flip is the strongest single depth cue you can buy.** `hairmass` at
|
|
216
|
+
`z = −55` makes `−z·sin t` positive: as the face swings left, the back of the
|
|
217
|
+
head swings **right**. It is one track, one number, and it is what separates a
|
|
218
|
+
turn from a slide more than any mesh work does.
|
|
219
|
+
|
|
220
|
+
⚠️ **Get the depths right and the parallax is free; get one wrong and nothing
|
|
221
|
+
complains.** A depth is not measurable from the art — it is a decision about a
|
|
222
|
+
shape the drawing only implies. Two readings that help:
|
|
223
|
+
|
|
224
|
+
1. **Depth is what the drawing overlaps for.** A part drawn *over* another is
|
|
225
|
+
usually in front of it, and the slot order already records that (AUTHORING R4).
|
|
226
|
+
Depth is the same ordering with a magnitude attached, so **derive the sign from
|
|
227
|
+
the draw order and argue only about the size.**
|
|
228
|
+
2. **The stand-off is the number to state out loud.** The fringe's 26 is not a
|
|
229
|
+
measurement of anything; it is how much parallax the shot wanted. At 12° it
|
|
230
|
+
buys `26·sin 12° = 5.406` units of extra travel — **derived**, and the record
|
|
231
|
+
measured 5.406 on the artifact. If the fringe does not read as separate,
|
|
232
|
+
this is the one number to move, and moving it moves nothing else.
|
|
233
|
+
|
|
234
|
+
### 2.1 One depth per part, and the limit of that
|
|
235
|
+
|
|
236
|
+
Everything above states **one depth per part** — a stand-off the whole plate
|
|
237
|
+
shares. That is the right resolution for a part list: the fringe is 26 in front
|
|
238
|
+
of the skull, and arguing about which *pixel* of the fringe is 26 would be
|
|
239
|
+
arguing past what the drawing says.
|
|
240
|
+
|
|
241
|
+
It stops being the right resolution the moment a part's own surface is the
|
|
242
|
+
subject. §4 meshes the face plate into columns precisely because the plate is not
|
|
243
|
+
flat, and §4.2 then finds that refining those columns makes the fold **worse**,
|
|
244
|
+
not better — the columns are sampling a cylinder that was never the shape of a
|
|
245
|
+
face. A cylinder is one number pretending to be a surface, and past a certain
|
|
246
|
+
density the pretence is what fails.
|
|
247
|
+
|
|
248
|
+
⭐ **Two things arrive together, and both are generated rather than written.**
|
|
249
|
+
[AUTHORING §3.4](AUTHORING.md#grid--a-lattice-over-the-part-window)'s `grid`
|
|
250
|
+
generator builds the lattice from the column positions alone, and reproduces this
|
|
251
|
+
example's hand-numbered 25 vertex pairs, 32 triangles and hull walk exactly. That
|
|
252
|
+
is the prerequisite: a turn needs interior vertices to move, and
|
|
253
|
+
§4.3's contour has none.
|
|
254
|
+
|
|
255
|
+
⭐ **And a depth map is the number above becoming a surface.** A greyscale sheet in the
|
|
256
|
+
part's own pixel grid gives every mesh vertex its own `z`, sampled where the
|
|
257
|
+
vertex actually is, and `yaw`/`pitch` project off it with the same closed form as
|
|
258
|
+
§1 — only the source of `z` changes. What it buys is stated where it is authored:
|
|
259
|
+
[AUTHORING §3.4](AUTHORING.md#depth--give-every-vertex-its-own-z-instead-of-one-cylinder-radius),
|
|
260
|
+
with the sampling order, the refusals and the one that matters most (a sheet cut
|
|
261
|
+
to the art covers **none** of a contour mesh's vertices, because they all sit on
|
|
262
|
+
the silhouette and outside it).
|
|
263
|
+
|
|
264
|
+
⭐ **And the mesh really is evaluating the pixels' model — measured, not
|
|
265
|
+
asserted.** A consumer that renders the same sheet in a shader displaces every
|
|
266
|
+
PIXEL by its own depth; a mesh displaces vertices and interpolates across
|
|
267
|
+
triangles. On a dome (a ramp would prove nothing — linear interpolation is exact
|
|
268
|
+
on a linear field) at 18°, the two evaluations converge as the lattice refines:
|
|
269
|
+
|
|
270
|
+
**No run reproduces this:** the 2026-09-05 density study's figures, printed by `bench/studies/2026-09-05-density/tools/densprobe.ts` and kept in its `evidence/`; no command this page states re-takes them
|
|
271
|
+
|
|
272
|
+
| lattice | 3×3 | 5×5 | 9×9 | 17×17 | 33×33 |
|
|
273
|
+
| --- | --- | --- | --- | --- | --- |
|
|
274
|
+
| mean disagreement, px | 6.4141 | 1.8452 | 0.5887 | 0.2276 | **0.0926** |
|
|
275
|
+
| the same lattice reading a **cylinder** instead | 3.8972 | 3.1628 | 3.2558 | 3.3429 | **3.3777** |
|
|
276
|
+
|
|
277
|
+
🚨 **Read the second row before the first.** A mesh evaluating the wrong surface
|
|
278
|
+
does not converge — it settles at ~3.4px however dense it gets — and at 3×3 it
|
|
279
|
+
reads *better* than the right one. So one measurement at one density cannot tell
|
|
280
|
+
the two models apart, and would have picked the wrong one. The claim is the
|
|
281
|
+
convergence, never a single number. (The worst
|
|
282
|
+
case falls more slowly than the mean and is expected to, because the dome's rim
|
|
283
|
+
has an unbounded depth gradient.)
|
|
284
|
+
|
|
285
|
+
⚠️ **This is not the same quantity as §4.2's fold angle, which refining makes
|
|
286
|
+
WORSE.** Both are true: a finer lattice buys fidelity to the model and costs the
|
|
287
|
+
angle at which a column pair inverts. This measures the first; `A39` refuses the
|
|
288
|
+
second.
|
|
289
|
+
|
|
290
|
+
⚠️ **It does not make depth measurable.** The map is relative — 8 bits of level
|
|
291
|
+
say nothing about world units — so `zScale` is authored exactly as the fringe's
|
|
292
|
+
26 was, and the two warnings above survive intact: get it right and the parallax
|
|
293
|
+
is free, get it wrong and nothing complains. What the map removes is the
|
|
294
|
+
*resolution* limit, not the judgement.
|
|
295
|
+
|
|
296
|
+
### 2.2 The angle belongs to the map, not to the mesh
|
|
297
|
+
|
|
298
|
+
§2.1 leaves an open question, and it is measured here. §4.2 finds that
|
|
299
|
+
refining the lattice makes the fold **worse**, and read that as the cylinder's
|
|
300
|
+
error surfacing — so per-vertex depth ought to have bought the angle back. ⛔ **It
|
|
301
|
+
does not, and it never could have.**
|
|
302
|
+
|
|
303
|
+
Two neighbouring vertices swap places when the turn tips one past the other,
|
|
304
|
+
which is `tan t ≥ Δu/Δz` — so the largest turn a part supports is
|
|
305
|
+
|
|
306
|
+
tan t_max = 1 / max |dz/du|
|
|
307
|
+
|
|
308
|
+
**the reciprocal of the steepest slope anywhere in its depth map**, and there is
|
|
309
|
+
no mesh in that formula at all. Refining the lattice does not change the angle;
|
|
310
|
+
it changes which slopes the lattice is close enough to *find*. A map with a
|
|
311
|
+
vertical edge has an infinite slope there, so a fine enough mesh folds at any
|
|
312
|
+
angle you name.
|
|
313
|
+
|
|
314
|
+
⭐ **Which is exactly what a dome is.** `z = Z√(1 − r²)` is vertical at its rim,
|
|
315
|
+
and an even lattice over it folds at `tan t = √h·√(R/2)/Z` for spacing
|
|
316
|
+
`h = W/(side−1)` — a **√h** that goes to zero. Measured against that closed form
|
|
317
|
+
over a 1,300× range of vertex counts, agreeing to ≤ 1.2°:
|
|
318
|
+
|
|
319
|
+
**No run reproduces this:** the same 2026-09-05 density study, its `evidence/` and its harness; the closed-form rows are arithmetic and the measured rows are that run's
|
|
320
|
+
|
|
321
|
+
| lattice | 5×5 | 17×17 | 33×33 | 65×65 | 129×129 | 181×181 |
|
|
322
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
323
|
+
| **dome**, largest turn admitted | 62° | 41° | 31° | 23° | 17° | **14°** |
|
|
324
|
+
| the closed form above | 59.0° | 39.8° | 30.5° | 22.6° | 16.4° | **14.0°** |
|
|
325
|
+
| **raised cosine**, slope bounded | 73° | 65° | 64° | 64° | 64° | **63°** |
|
|
326
|
+
| its closed form, `atan(2R/Zπ)` | 64.8° | 64.8° | 64.8° | 64.8° | 64.8° | **64.8°** |
|
|
327
|
+
|
|
328
|
+
⇒ **Author the map so its slope is bounded, and the angle stops depending on the
|
|
329
|
+
mesh.** A raised cosine — flat at the centre, flat again at the rim — holds
|
|
330
|
+
63–64° from 289 vertices to 32,761. The dome loses three quarters of its angle
|
|
331
|
+
over the same refinement. Both are "correct" depth; only one of them is a
|
|
332
|
+
*surface a turn can be built on*, and the difference is entirely in the input.
|
|
333
|
+
|
|
334
|
+
🚨 **So a map traced straight off a rendered normal or a photogrammetry pass is
|
|
335
|
+
the dome case, not the cosine case.** Where the part curves away to its
|
|
336
|
+
silhouette is where every such map goes vertical. Flattening it there — letting
|
|
337
|
+
z reach its floor *before* the outline rather than at it — is the edit that buys
|
|
338
|
+
the angle, and it is an edit to the sheet. rigc will not do it for you: the
|
|
339
|
+
compiler never invents a value that is not in the spec, and a depth map is a
|
|
340
|
+
measurement.
|
|
341
|
+
|
|
342
|
+
⚠️ **This does not touch §2.1's convergence.** A finer lattice still evaluates
|
|
343
|
+
the map's own surface more faithfully; it also finds steeper slopes in it. Those
|
|
344
|
+
are two different quantities and both are true — §2.1 measures the first, `A39`
|
|
345
|
+
refuses the second. What is new here is that the second is a fact about the
|
|
346
|
+
sheet, and can be fixed there.
|
|
347
|
+
|
|
348
|
+
⭐ **And you do not have to find the ceiling by building into it.** `build`
|
|
349
|
+
and `explain` print it for any mesh with a depth map — per axis, per direction,
|
|
350
|
+
naming the triangle that goes first — from `tan t = A₀/A_axis` on the mesh's own
|
|
351
|
+
geometry ([AUTHORING §3.4](AUTHORING.md)):
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
|
|
355
|
+
1st pct yaw +31.55° x1.004 of 1004 / -32.10° x1.003 of 1044 pitch +32.10° x1.003 of 1044 / -31.55° x1.004 of 1004
|
|
356
|
+
first to fold: yaw + at 31.41°, triangle 960 [113,112,593], the sheet steps 12.52 level(s) across it, which is 0.049 of the range this mesh sampled
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Read it as a fact about **the sheet**. If the number is too small, the fix is in
|
|
360
|
+
the map — flatten it where the part curves away — and not in the lattice.
|
|
361
|
+
|
|
362
|
+
#### ⚠️ That rule presumes the map is continuous
|
|
363
|
+
|
|
364
|
+
"Flatten it where the part curves away" is an edit to a **surface**, and it
|
|
365
|
+
assumes there is one under the whole mesh. A depth sheet estimated from a
|
|
366
|
+
picture of cut-out art is not a surface: it is piecewise, with a **cliff at every
|
|
367
|
+
occlusion boundary** — figure against background at the silhouette, and one part
|
|
368
|
+
of the figure over another wherever they overlap. There is nothing to flatten
|
|
369
|
+
across a cliff, because the two sides are not two ends of a slope. They are two
|
|
370
|
+
different things at two different depths, and a 2.5D turn does not model
|
|
371
|
+
occlusion at all.
|
|
372
|
+
|
|
373
|
+
The ceiling reads the cliff, correctly, and the number it reports is real:
|
|
374
|
+
walked one degree at a time through the survey `A39` refuses from, a reported
|
|
375
|
+
1.936° admits +1° and reverses 8 triangles at +2°. **The rig genuinely folds at
|
|
376
|
+
two degrees.** What is wrong is not the instrument and not the mesh — it is that
|
|
377
|
+
the question "how far can this turn" has no answer for an input with a
|
|
378
|
+
discontinuity in it, and the ceiling proves it by halving with every doubling of
|
|
379
|
+
the lattice: measured tangent ratios 2.02 / 1.95 / 2.02, `tan t ∝ h` exactly,
|
|
380
|
+
with no limit to converge to.
|
|
381
|
+
|
|
382
|
+
🚨 **And the two figures beside the ceiling both call it healthy.** The 1st
|
|
383
|
+
percentile reads 1.02–2.17, which is a band reaching the limit together — and it
|
|
384
|
+
*is* a band, because an outline is long. The depth step reads 148–252 levels of
|
|
385
|
+
255, far above the quantisation floor — and the sheet really did say that much,
|
|
386
|
+
in one step. Divided by the range, the same number says the opposite: the step
|
|
387
|
+
share pins at **0.92–0.99** where rigc's own gallery reads 0.112 and 0.468.
|
|
388
|
+
|
|
389
|
+
⇒ Two ways out, and **neither of them is flattening**:
|
|
390
|
+
|
|
391
|
+
- **Mesh only what is continuous.** One face, one lock, one sleeve — a region
|
|
392
|
+
the sheet describes without a jump in it — rather than a lattice over a whole
|
|
393
|
+
figure. Whether a mask that tight gives a usable angle is not yet measured;
|
|
394
|
+
the mask has to be painted rather than thresholded, for the reason `soft`
|
|
395
|
+
is painted (§3.4's `soft` block).
|
|
396
|
+
- **State a sheet that was authored rather than estimated.** §2.2's raised
|
|
397
|
+
cosine holds 63–64° from 289 vertices to 32,761 because somebody drew its
|
|
398
|
+
slope. That is the input this whole section is about.
|
|
399
|
+
|
|
400
|
+
⛔ rigc will not decide that your sheet is the wrong kind of thing. It has every
|
|
401
|
+
authority to say what it measured, and the step share is that.
|
|
402
|
+
|
|
403
|
+
📐 Method, harness and the full ladders live in the repository rather than in
|
|
404
|
+
this package, as
|
|
405
|
+
[`bench/studies/2026-09-05-density`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-density) —
|
|
406
|
+
the same study also measures why `contour` is the wrong generator for a turn: it
|
|
407
|
+
saturates at 868 vertices however fine the tolerance, its vertices all sit on the
|
|
408
|
+
silhouette so it samples **2 %** of the depth range, and its ear-clipped interior
|
|
409
|
+
holds triangles three orders of magnitude apart in area, the smallest of which
|
|
410
|
+
reverse under a fraction of a pixel.
|
|
411
|
+
|
|
412
|
+
**No run reproduces this:** the ladder in the paragraph below is the 2026-09-05 noise study's, taken with `bench/studies/2026-09-05-noise/tools/noiseprobe.ts`, one evidence file per experiment; no command this page states re-takes it
|
|
413
|
+
|
|
414
|
+
🚨 **The sheet's grain is a slope too, and the ceiling reads it.** Because
|
|
415
|
+
`max|dz/du|` is a maximum over sampled gradients, there is no averaging anywhere
|
|
416
|
+
in it. On a sheet whose true ceiling is 64.77°, measured at 4,225 vertices:
|
|
417
|
+
**±1 level** of noise reports 61.37°, **±8 levels** reports 45.34°, and **one
|
|
418
|
+
stray pixel out of 160,000** reports 6.08° — and `A39` refuses at each of those
|
|
419
|
+
angles, so the rig is as damaged as the report says. Refining the lattice makes
|
|
420
|
+
every one of them worse. Two hard bounds follow, both independent of the form:
|
|
421
|
+
a sheet can never report above `atan(255·h / zScale)` for cell size `h`, and
|
|
422
|
+
below one texel per cell the answer saturates at the steepest adjacent-texel
|
|
423
|
+
step. ⇒ **Author the sheet so it changes by at least ~8 levels across one mesh
|
|
424
|
+
cell**, and spend its whole 0–255 range on the part — a map using an eighth of
|
|
425
|
+
the range describes the same surface and reports 5.8° less of it. Method and
|
|
426
|
+
ladders:
|
|
427
|
+
[`bench/studies/2026-09-05-noise`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-noise).
|
|
428
|
+
|
|
429
|
+
⭐ **And you do not have to guess which of the three you are looking at.** The
|
|
430
|
+
lines under the ceiling say it: the **1st percentile over
|
|
431
|
+
the ceiling** is near 1 when a band of the mesh reaches the limit together and
|
|
432
|
+
near 10 when one triangle does, which is a texel — 1.003 against 10.652 for the
|
|
433
|
+
two sheets above. The **depth step across the triangle that folds first**, in
|
|
434
|
+
levels, is the second: below about 3 the ceiling is quantisation, and at 1 it is
|
|
435
|
+
`atan(255·h / zScale)` and carries nothing about the form at all. The **same step
|
|
436
|
+
over the range the mesh sampled** is the third, and it is the one that separates
|
|
437
|
+
a steep surface from a cliff — a form's halves with every doubling of the lattice
|
|
438
|
+
while its angle settles, a discontinuity's does not move while its angle halves.
|
|
439
|
+
All three are reports and none of them moves the ceiling — ⛔ rigc will not filter
|
|
440
|
+
a depth map, because a smoothed measurement would describe a surface the deform
|
|
441
|
+
key is not built from and `A39` would go on refusing at the raw angle.
|
|
442
|
+
[AUTHORING §3.4](AUTHORING.md) has the reading table.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## 3. One shared shift, then residuals
|
|
447
|
+
|
|
448
|
+
⛔ **Do not key each feature's whole `dx`.** Put a bone at the face plate's own
|
|
449
|
+
origin, key the part every feature shares onto that one bone, and let each
|
|
450
|
+
feature key only what is left:
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
faceshift.translatex = −R·sin t ← −35.345 at 12°, R = 170
|
|
454
|
+
|
|
455
|
+
residual(x, z) = dx(x, z) − (−R·sin t)
|
|
456
|
+
= x·(cos t − 1) + (R − z)·sin t
|
|
457
|
+
\_______/
|
|
458
|
+
the part's depth BELOW
|
|
459
|
+
the skull surface
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
⭐ **So a feature's own track is nothing but its depth below the surface, times
|
|
463
|
+
`sin t`.** That is the sentence this page exists to produce.
|
|
464
|
+
|
|
465
|
+
**The features of the worked example** (**derived**, and every value matches the
|
|
466
|
+
shipped `motion.json` to the last digit printed there):
|
|
467
|
+
|
|
468
|
+
| Bone | `x` | `z` | `dx` | residual | `scalex` |
|
|
469
|
+
| --- | --- | --- | --- | --- | --- |
|
|
470
|
+
| `eye_l` (far) | −62 | 150 | −29.832 | **5.513** | 0.8922 |
|
|
471
|
+
| `eye_r` (near) | 62 | 150 | −32.542 | **2.803** | 1.0641 |
|
|
472
|
+
| `brow_l` | −62 | 158 | −31.495 | 3.850 | 0.8966 |
|
|
473
|
+
| `brow_r` | 62 | 158 | −34.205 | 1.140 | 1.0597 |
|
|
474
|
+
| `nose` | 0 | 192 | −39.919 | **−4.574** | 0.9781 |
|
|
475
|
+
| `mouth` | 0 | 166 | −34.513 | 0.832 | 0.9781 |
|
|
476
|
+
|
|
477
|
+
🚨 **The reason to do it this way is that it makes a wrong number visible.** A
|
|
478
|
+
residual is **1–6 units**; a total is **30–40**. Nobody can eyeball an error in
|
|
479
|
+
the second, and everybody can eyeball one in the first — a residual with the
|
|
480
|
+
wrong sign, or one an order of magnitude off its neighbours, is obvious in a
|
|
481
|
+
column of six. ⇒ **The shared-shift split is an auditing decision before it is a
|
|
482
|
+
rigging one**, and given §9 that is the whole argument for it.
|
|
483
|
+
|
|
484
|
+
### 3.1 ⭐ And now the spec holds this split, not its results
|
|
485
|
+
|
|
486
|
+
The pattern above is a
|
|
487
|
+
named construct rather than a page of advice: **one group track whose key states
|
|
488
|
+
the model, with a depth per member** (AUTHORING §4.5.1). The worked example's six
|
|
489
|
+
residuals and six scale factors are two tracks:
|
|
490
|
+
|
|
491
|
+
```json
|
|
492
|
+
{ "group": "features", "property": "translatex", "keys": [
|
|
493
|
+
{ "t": 0, "v": [0], "ease": "rise" },
|
|
494
|
+
{ "t": 0.62, "derive": { "kind": "yaw", "degrees": 12, "carried": 170,
|
|
495
|
+
"depth": { "eye_l": 150, "eye_r": 150, "brow_l": 158,
|
|
496
|
+
"brow_r": 158, "nose": 192, "mouth": 166 } },
|
|
497
|
+
"ease": "swell" },
|
|
498
|
+
{ "t": 2.2, "v": [0] } ] }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`carried` **is** the shared shift, stated: it is the depth whose `−R·sin t` the
|
|
502
|
+
`faceshift` bone already applies, so what each member keys is exactly the residual
|
|
503
|
+
this section derives. Drop it — write `carried: 0` or leave it out — and the same
|
|
504
|
+
kind emits the **full** `dx` instead, which is what the parts hanging off `head`
|
|
505
|
+
rather than off `faceshift` need. ⇒ **The split is the one parameter, and the seam falls where the parent chain already
|
|
506
|
+
put it** (AUTHORING §4.5.1 refuses a model over members under different parents,
|
|
507
|
+
for exactly that reason).
|
|
508
|
+
|
|
509
|
+
⭐ **`explain` then prints the column of six, which is what the argument above
|
|
510
|
+
asked for.** The `MEMBER` block (AUTHORING §4.5.2) is a row per member — the
|
|
511
|
+
emitted value, the `x` it read off the rig, and the depth the spec stated — so the
|
|
512
|
+
nose diagnostic below is a line you read rather than arithmetic you redo:
|
|
513
|
+
|
|
514
|
+
**No run reproduces this:** abridged — the five derivation lines the run prints between this record's head and its rows are cut; AUTHORING §4.5.2 quotes them
|
|
515
|
+
```
|
|
516
|
+
MEMBER turn group "features".translatex t=0.620000 6 member(s) derive yaw degrees=12 carried=170 -> the displacement
|
|
517
|
+
eye_l 5.513083 <- -62 at depth 150
|
|
518
|
+
eye_r 2.803385 <- 62 at depth 150
|
|
519
|
+
brow_l 3.849789 <- -62 at depth 158
|
|
520
|
+
brow_r 1.140092 <- 62 at depth 158
|
|
521
|
+
nose -4.574057 <- 0 at depth 192
|
|
522
|
+
mouth 0.831647 <- 0 at depth 166
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
⭐ **The nose is the diagnostic.** It is the only **negative** residual on the
|
|
526
|
+
face, because it is the only feature in front of the surface: it protrudes 22, and
|
|
527
|
+
`22·sin 12° = 4.57` is exactly how much further left it goes than the cheek it
|
|
528
|
+
sits on (**derived**: −4.574). ⇒ **If the nose's residual is not negative, the
|
|
529
|
+
depths are wrong.** It is the cheapest check on this page and it is arithmetic,
|
|
530
|
+
not a render.
|
|
531
|
+
|
|
532
|
+
📌 **`faceshift` has to be its own bone, and not the head bone.** The head bone
|
|
533
|
+
is the head mesh's own slot bone, and the mesh deform already carries that
|
|
534
|
+
plate's motion; a translate there would move the plate **twice**. Make
|
|
535
|
+
`faceshift` a child of `head` that carries only the features — the worked
|
|
536
|
+
example's chain is `headroll → head → faceshift → {eyes, brows, nose, mouth}`,
|
|
537
|
+
which `rigc explain` prints as a parent column (§9.2).
|
|
538
|
+
|
|
539
|
+
⭐ **The same reasoning puts the head's pivot one link above the mesh.** A head
|
|
540
|
+
rotates about the top of the neck, not about the middle of its own face, so the
|
|
541
|
+
roll belongs on a `headroll` bone above `head`. There is a rule that enforces it
|
|
542
|
+
under `--profile spine-html`: `A15_IDLE_NO_MESH_BONE_KEYS` refuses an `idle` that
|
|
543
|
+
keys a bone driving a mesh — its own slot bone or its control bone — because that
|
|
544
|
+
renderer must never idle-skip a mesh. Keying the pivot one link up satisfies it,
|
|
545
|
+
and **the rig that satisfies the assertion is the better rig anyway.**
|
|
546
|
+
|
|
547
|
+
⚠️ **A15 assumes the meshes are mostly static, and that assumption is the rule's
|
|
548
|
+
whole premise.** The `spine-html` renderer skips redrawing a mesh nothing moved, so a
|
|
549
|
+
face whose `idle` breathes through one pivot keeps every mesh under it cheap only if
|
|
550
|
+
nothing keys the meshes' own bones. The rule reads bone **names**: keying `headroll`
|
|
551
|
+
passes it, and `head`'s mesh still moves, because a child's world transform is
|
|
552
|
+
composed from its parent's — so pivoting is right here for the anatomical reason
|
|
553
|
+
above, not because it stops a redraw. A **painting rig** is the genre where the
|
|
554
|
+
premise is false by design: one illustration in layers, most of them weighted
|
|
555
|
+
meshes, with an `idle` whose job is to move them. Pivoting every keyed bone one link
|
|
556
|
+
up there satisfies the wording and not the purpose (issue #855: 42 extra bones, the
|
|
557
|
+
same pose, the meshes still moving). That rig **declares** instead —
|
|
558
|
+
`invariants.idleDrivesMeshes: { "why": … }` ([AUTHORING](AUTHORING.md) §3.7) — and A15
|
|
559
|
+
reports the bones, meshes and vertices the `idle` moves as a SKIP rather than a
|
|
560
|
+
refusal per bone.
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## 4. The mesh — where the columns go is the whole decision
|
|
565
|
+
|
|
566
|
+
Two meshes and **40 vertices** carried a head turn in the worked example, which
|
|
567
|
+
cuts against the expectation that a face mesh needs hundreds. The reason is
|
|
568
|
+
structural: **a yaw moves nothing vertically**, so the rows are along for the
|
|
569
|
+
ride and only the column count buys anything.
|
|
570
|
+
|
|
571
|
+
```
|
|
572
|
+
head 340 × 380 plate, R = 170 hair_bang 372 × 168 plate, R = 196
|
|
573
|
+
5 columns × 5 rows = 25 vertices 5 columns × 3 rows = 15 vertices
|
|
574
|
+
32 triangles 16 triangles
|
|
575
|
+
columns x = −162, −120, 0, 120, 162 columns x = −170, −120, 0, 120, 170
|
|
576
|
+
rows y = 180, 90, 0, −90, −180 rows y = 80, 0, −80
|
|
577
|
+
z = √(R² − x²) z = √(R² − x²)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Each column's offset is `dx` at its own `(x, z)`, and **every row gets the same
|
|
581
|
+
value**, so the run the compiler writes is one row of five repeated down the grid
|
|
582
|
+
with a `0` for every `y`:
|
|
583
|
+
|
|
584
|
+
```
|
|
585
|
+
-7.175, 0, -22.414, 0, -35.345, 0, -27.658, 0, -14.255, 0, <- ×5 rows
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
⭐ **The spec states the model and the compiler writes that** (§1.1): the key is
|
|
589
|
+
`{ "kind": "yaw", "radius": 170, "degrees": 12 }`, and the 50 numbers above are
|
|
590
|
+
what `explain` prints and what lands in the artifact. The offsets are still worth
|
|
591
|
+
reading, because the *shape* of that row is §4.1's whole argument.
|
|
592
|
+
|
|
593
|
+
⚠️ **`R` is the radius of the cylinder a part is painted on, and it is not always
|
|
594
|
+
the plate's half-width.** For the head plate the two coincide (`340/2 = 170`). For
|
|
595
|
+
the fringe they do **not**: its plate is 372 wide (half-width 186) but its `R` is
|
|
596
|
+
**196**, because 196 is where the fringe *sits* — 26 in front of the skull. ⇒
|
|
597
|
+
Read `R` off the depth table, never off the PNG.
|
|
598
|
+
|
|
599
|
+
### 4.1 Uneven columns, and it costs nothing
|
|
600
|
+
|
|
601
|
+
🚨 **The columns are not evenly spaced, and that is the trick.** `−162, −120, 0,
|
|
602
|
+
120, 162` puts them **dense near the silhouette and sparse in the middle**, which
|
|
603
|
+
is the sampling a cosine needs: the centre of the face travels `R·sin t` and the
|
|
604
|
+
edges barely travel at all, so all the *variation* is at the edges. Five evenly
|
|
605
|
+
spaced columns spend their resolution where nothing happens.
|
|
606
|
+
|
|
607
|
+
What the grid then does to the drawing is a **non-uniform horizontal
|
|
608
|
+
redistribution** (**derived** at 12°, widest row):
|
|
609
|
+
|
|
610
|
+
| Band, from the far edge | Rest width | At 12° | Ratio |
|
|
611
|
+
| --- | --- | --- | --- |
|
|
612
|
+
| −162 → −120 | 42.0 | 26.76 | **0.637** |
|
|
613
|
+
| −120 → 0 | 120.0 | 107.07 | 0.892 |
|
|
614
|
+
| 0 → 120 | 120.0 | 127.69 | 1.064 |
|
|
615
|
+
| 120 → 162 | 42.0 | 55.40 | **1.319** |
|
|
616
|
+
|
|
617
|
+
The far side compresses to 64%, the near side stretches to 132%, and the ink
|
|
618
|
+
inside each band compresses and stretches with it. **That gradient is what makes
|
|
619
|
+
it read as a turn instead of a slide.** The whole-head narrowing, by contrast, is
|
|
620
|
+
the cheap part: ink edge to ink edge, `309 · cos 12° = 302.2` (**derived**; the
|
|
621
|
+
record measured 309.0 → 302.2 on the artifact).
|
|
622
|
+
|
|
623
|
+
⭐ **It is one decision, made once, in the setup geometry, and every deform key
|
|
624
|
+
after it is better for free.** There is no cost side to this trade.
|
|
625
|
+
|
|
626
|
+
### 4.2 The silhouette is a tangent, not a mark — and refining makes it worse
|
|
627
|
+
|
|
628
|
+
This is the most misleading thing about a face plate, and the counter-intuitive
|
|
629
|
+
result of the whole experiment.
|
|
630
|
+
|
|
631
|
+
A column at `x` sits at depth `z = √(R² − x²)`. Project two adjacent columns and
|
|
632
|
+
ask when they **swap order** — when the mesh turns inside out:
|
|
633
|
+
|
|
634
|
+
```
|
|
635
|
+
x₁·cos t − z₁·sin t = x₂·cos t − z₂·sin t
|
|
636
|
+
|
|
637
|
+
Δx x₁ − x₂
|
|
638
|
+
⇒ tan θ_fold = ── = ───────── over ADJACENT columns
|
|
639
|
+
Δz z₁ − z₂
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
and a grid folds at the **minimum** of that over its pairs. As the gap closes,
|
|
643
|
+
`Δx/Δz → z/|x|`, so the limit for a column is `atan(z / |x|)` — a property of the
|
|
644
|
+
**continuous surface**, not of the mesh.
|
|
645
|
+
|
|
646
|
+
**Re-derived from that formula** (and each agrees with a bisection search on the
|
|
647
|
+
shipped column table to 0.01°):
|
|
648
|
+
|
|
649
|
+
| Columns | Folds at | The pair that folds |
|
|
650
|
+
| --- | --- | --- |
|
|
651
|
+
| 5 — `±162, ±120, 0` (shipped) | **31.37°** | `−162, −120` |
|
|
652
|
+
| 7 — `±145` added | **24.56°** | `−162, −145` |
|
|
653
|
+
| 7 — `±155` added | **20.95°** | `−162, −155` |
|
|
654
|
+
| 9 — `±155, ±145` added | **20.95°** | `−162, −155` |
|
|
655
|
+
| 13 — uniform, every 27 units | 27.54° | `−162, −135` |
|
|
656
|
+
| continuous limit at `x = −162` | **17.65°** | — |
|
|
657
|
+
|
|
658
|
+
🚨 **A denser face mesh is not a safer face mesh.** Refining near the silhouette
|
|
659
|
+
drives `Δx/Δz` toward the tangent limit *from above*, so every column you add out
|
|
660
|
+
there **lowers** the angle at which the mesh inverts. A coarse grid survives past
|
|
661
|
+
17.65° only because it does not *sample* there — it crushes instead of folding.
|
|
662
|
+
|
|
663
|
+
⚠️ **And the fold angle is set by the outermost gap, not by the column count.**
|
|
664
|
+
The 5-, 7- and 9-column rows above make that concrete: the 9-column grid folds at
|
|
665
|
+
**exactly** the same 20.95° as the 7-column one, because both contain the pair
|
|
666
|
+
`(−162, −155)` and the `±145` column changes nothing. ⇒ Counting vertices tells
|
|
667
|
+
you nothing about this failure; **look at the outermost two columns.**
|
|
668
|
+
|
|
669
|
+
⭐ **The rule, and it is an identity rather than a rule of thumb.** Put the
|
|
670
|
+
outermost column at
|
|
671
|
+
|
|
672
|
+
```
|
|
673
|
+
|x|outer = R · cos θmax ⇒ its tangent limit is EXACTLY θmax
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
because `z = R·sin θ` there and `atan(z/|x|) = θ`. So you do not tune this: you
|
|
677
|
+
**pick the ceiling first and the column position falls out.** For `R = 170`
|
|
678
|
+
(**derived**):
|
|
679
|
+
|
|
680
|
+
| θmax you want | Outermost column at |
|
|
681
|
+
| --- | --- |
|
|
682
|
+
| 12° | 166.3 |
|
|
683
|
+
| 16° | 163.4 |
|
|
684
|
+
| 20° | 159.7 |
|
|
685
|
+
| 26° | 152.8 |
|
|
686
|
+
|
|
687
|
+
The shipped grid's 162 gives 17.65°, comfortably above the 12° it ships and just
|
|
688
|
+
above the 16° §8 calls the instrument's ceiling. ⇒ **Then let the last band be a
|
|
689
|
+
single wide one** — that is the direction that buys safety, and it is the
|
|
690
|
+
opposite of refining.
|
|
691
|
+
|
|
692
|
+
🔁 **And this is the section read from the other end: the refusal you get for
|
|
693
|
+
picking the ceiling wrong sends you back here by name.** A build whose key
|
|
694
|
+
evaluates past the fold angle above is refused by
|
|
695
|
+
`A39_DEFORM_KEEPS_TRIANGLE_WINDING` — AUTHORING §5–§6 is the failure map and its
|
|
696
|
+
`A39` row reads this message field by field — and the message carries the
|
|
697
|
+
triangles, the signed areas, the fix and this section:
|
|
698
|
+
|
|
699
|
+
**No run reproduces this:** abridged and re-wrapped — the three further triangles it names and its `and 4 more` are cut at the ellipsis, and the run prints the whole refusal on one line; §9.2's build (b) is what prints it
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head key 1
|
|
703
|
+
(t=0.6200000047683716s): 8 of 32 triangle(s) reverse winding — triangle 0
|
|
704
|
+
[0,15,16] 1890.000 -> -544.548px²; … The mesh has turned inside out there
|
|
705
|
+
and draws its texture backwards. Fix the key's offsets in the motion
|
|
706
|
+
spec's deform timeline (a projection past its fold angle is the usual
|
|
707
|
+
cause — docs/FACE.md §4.2 has the closed form), or, if this slot folds on
|
|
708
|
+
purpose, declare it in the rig spec as invariants.deformMayFold:
|
|
709
|
+
[{ "slot": "head", "why": … }]
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
⇒ The table above is what *"a projection past its fold angle"* means, and the
|
|
713
|
+
outermost-column identity is how you stop meeting the message at all.
|
|
714
|
+
|
|
715
|
+
🎭 **The fourth way out, and the one a Live2D-style face actually takes: stop
|
|
716
|
+
drawing the part.** The ceiling only binds while the part is on screen, so a turn
|
|
717
|
+
that has to go past it fades the far cheek or ear out — `rgba` to alpha 0 — or
|
|
718
|
+
swaps the attachment away, and another part takes over. `A39` measures that: a
|
|
719
|
+
deform key whose slot draws **no pixels at that key's own time** is passed over by
|
|
720
|
+
name rather than refused, with the reason on the stats line and beside the key's
|
|
721
|
+
own figures in the `DEFORM` block. Two things it is not — the
|
|
722
|
+
bar is alpha **exactly 0**, so a part faded halfway is still refused with the
|
|
723
|
+
alpha in the message; and it is per key and per time, so the same slot folding at
|
|
724
|
+
full alpha anywhere else is refused. ⛔ It is also not
|
|
725
|
+
`invariants.deformMayFold`: that field turns the check off for the slot at every
|
|
726
|
+
angle, including the ones where the part is fully visible.
|
|
727
|
+
|
|
728
|
+
🚨 **Fade out *up to* the angle you cannot take, never *at* it — and the gate
|
|
729
|
+
keeps that rule rather than asking you to.** The runtime interpolates between
|
|
730
|
+
keys, so an alpha-0 key landing exactly on the folding key leaves the frames just
|
|
731
|
+
before it drawn and nearly folded (§9.2 measures it: 8 reversed triangles at
|
|
732
|
+
alpha 0.20). `A39` scans the spans between consecutive keys as well and refuses
|
|
733
|
+
one by name, at a time solved for in closed form and then posed and measured
|
|
734
|
+
(AUTHORING §4.11.3). ⇒ The
|
|
735
|
+
practical shape is checkable: **key the fade to 0 at or before
|
|
736
|
+
the last angle that gates green, and let the fold happen after it.**
|
|
737
|
+
|
|
738
|
+
📘 **The identity above, used forwards on a `pitch`.**
|
|
739
|
+
[`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod)
|
|
740
|
+
(repository material) picks its ceiling first and solves the *rows* out of it —
|
|
741
|
+
`|y|outer = R·cos θmax` with θmax chosen at 21° giving 140.037 — and then
|
|
742
|
+
brackets the fold this section predicts against `A39` itself: **33° gates green
|
|
743
|
+
and 34° does not**, with the refusal naming the row pair the closed form names.
|
|
744
|
+
That is this table checked from the other end, on the other axis.
|
|
745
|
+
|
|
746
|
+
### 4.3 The perimeter comes first, and `hull` is read off the triangles
|
|
747
|
+
|
|
748
|
+
⚠️ **A grid's perimeter is 16 of its 25 vertices, and in row-major order they are
|
|
749
|
+
interleaved with the interior.** Spine's `hull` is the first `hull` vertices of
|
|
750
|
+
the list, in order — the editor draws the outline by joining them in sequence — so
|
|
751
|
+
a row-major grid cannot declare one. And an undeclared hull is not neutral: the
|
|
752
|
+
editor's import repairs a `hull: 0` by making
|
|
753
|
+
**every** vertex a hull vertex in list order, which is a self-intersecting outline
|
|
754
|
+
on the mesh the person refining the draft sees. So the list is written the way the editor writes one: **the perimeter
|
|
755
|
+
first, walked around — top row, right column, bottom row, left column — then the
|
|
756
|
+
interior, row-major.** rigc derives `hull` from the triangles (AUTHORING §3.4) and
|
|
757
|
+
refuses any other order with the walk to renumber along, so getting this wrong
|
|
758
|
+
costs one loop rather than a silent file.
|
|
759
|
+
|
|
760
|
+
**What it costs the reader.** `explain` prints one offset per vertex in list
|
|
761
|
+
order, so the run does not read as one row of five values repeated down the
|
|
762
|
+
grid: entries 0–15 walk the perimeter and 16–24 are the interior. The column
|
|
763
|
+
table is still what to check it against — a vertex's offset depends on its column
|
|
764
|
+
alone — and the right column's five entries (4–8) carrying one value is the
|
|
765
|
+
quickest check that the walk and the table agree. `gallery/portrait`'s README
|
|
766
|
+
shows the listing beside the order.
|
|
767
|
+
|
|
768
|
+
⚠️ **A large `MESH` overshoot on a face is not a defect.** A rectangular grid
|
|
769
|
+
over an oval face has transparent corners, and the line says so:
|
|
770
|
+
|
|
771
|
+
```
|
|
772
|
+
MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**`covers 100.00%` is what matters** — nothing of the drawing is outside the
|
|
776
|
+
triangles. The 95.90px is the corner. A mesh that hugged the silhouette instead
|
|
777
|
+
would have to be a `contour`, which cannot be a grid and has **no interior
|
|
778
|
+
vertices to redistribute** — so it cannot carry a turn at all.
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## 5. What foreshortens, and what does not
|
|
783
|
+
|
|
784
|
+
A feature is a rigid drawing sitting on a curved surface, so it also has to
|
|
785
|
+
**narrow** as its patch of surface turns away. That is a bone `scalex`:
|
|
786
|
+
|
|
787
|
+
```
|
|
788
|
+
scaleX = cos(α − t) / cos α where α = atan2(x, z)
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
The far eye narrows to 89%, the near eye widens to 106% (**derived**: 0.8922 and
|
|
792
|
+
1.0641). Parts on the axis get `cos t = 0.9781`.
|
|
793
|
+
|
|
794
|
+
📘 **How much this is worth depends on the axis, and there is a worked case for
|
|
795
|
+
the other one.** The same closed form on the `pitch` of
|
|
796
|
+
[`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod)
|
|
797
|
+
(repository material) spans **0.863 … 1.176** across its five features, against
|
|
798
|
+
the **0.892 … 1.064** above — a wider span at the *identical* angle, because
|
|
799
|
+
`α = atan2(coordinate, depth)` grows with the coordinate and a face is taller
|
|
800
|
+
than it is deep. ⇒ **The foreshortening buys more on a nod than on a turn**, and
|
|
801
|
+
that is arithmetic rather than a judgement about the art.
|
|
802
|
+
|
|
803
|
+
📌 **The on-axis pair needs no `groups` entry.** `scalex` is the
|
|
804
|
+
foreshortening projection of the same `derive` kind §3.1 uses (AUTHORING §4.5.1),
|
|
805
|
+
so all six features are one track and the on-axis pair's shared value **falls out
|
|
806
|
+
of the arithmetic** — `α = atan2(0, z)` is 0, so `cos(α − t)/cos α` is `cos t`.
|
|
807
|
+
A coincidence between two members is not something an author has to notice and
|
|
808
|
+
spend a group on.
|
|
809
|
+
|
|
810
|
+
### 🚨 The iris does not foreshorten, and this is the finding
|
|
811
|
+
|
|
812
|
+
`iris` and `spark` are children of `eye`, so they inherit the socket's `scalex` —
|
|
813
|
+
and **a circular iris under `scalex 0.89` is an ellipse.** That reads as *a
|
|
814
|
+
drawing squashed sideways*, not as a head turned, and it is the first thing to
|
|
815
|
+
break as the angle grows.
|
|
816
|
+
|
|
817
|
+
It is also wrong on the physics. If the character keeps looking at the camera
|
|
818
|
+
through the turn, her eyeball counter-rotates by the same angle the head yawed,
|
|
819
|
+
so the iris stays square-on and stays **circular**. ⇒ **The socket foreshortens;
|
|
820
|
+
the pupil does not.**
|
|
821
|
+
|
|
822
|
+
The fix is one reciprocal per side, on two groups (**derived**):
|
|
823
|
+
|
|
824
|
+
```
|
|
825
|
+
look_l: [iris_l, spark_l] scalex = 1 / 0.8922 = 1.1208
|
|
826
|
+
look_r: [iris_r, spark_r] scalex = 1 / 1.0641 = 0.9398
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
⭐ **These two stay a shared value on a group, and they are the case where the
|
|
830
|
+
per-member construct is the wrong tool** — stated here because "there is a
|
|
831
|
+
model" is exactly the reasoning that would spoil them. A counter-scale belongs to
|
|
832
|
+
the **socket**, not to the part: `spark_l` sits at local `x = −11` and takes the
|
|
833
|
+
same `1.1208` as `iris_l` at `0`, because what is being cancelled is the socket's
|
|
834
|
+
foreshortening and not the highlight's own. ⇒ **The value is precisely NOT a
|
|
835
|
+
function of the member's position**, so a `groups` entry with one number is the
|
|
836
|
+
true statement and a `derive` over `iris_l`/`spark_l` would be a fiction that
|
|
837
|
+
happened to use the `derive` field.
|
|
838
|
+
|
|
839
|
+
⭐ **Their positions still ride the socket, and that is the part that makes it
|
|
840
|
+
correct rather than a hack.** A bone's scale moves its children's local
|
|
841
|
+
translation, so the highlight at local `(−11, +11)` under the far socket's 0.8922
|
|
842
|
+
lands at `−9.81` — it slides 1.19 units inward, toward the surface it reflects
|
|
843
|
+
off (**derived**). Only the *shape* is held.
|
|
844
|
+
|
|
845
|
+
**No run reproduces this:** a person's judgement over seven renders, kept in the `gallery/portrait` record (`FINDINGS.md`, *The measured sweep*); no instrument in this repository grades a turn, so nothing re-takes it and nothing can
|
|
846
|
+
|
|
847
|
+
📊 **Measured effect on the cliff, by the worked example's own sweep — a
|
|
848
|
+
looked-at judgement over seven renders, not a computed figure:** without the
|
|
849
|
+
counter-scale the turn stops reading at about **18°**; with it, about **26°**.
|
|
850
|
+
Eight degrees of usable range for two tracks, and the record reports nothing else
|
|
851
|
+
in the experiment came close to that ratio.
|
|
852
|
+
|
|
853
|
+
⚠️ **A `clipping` attachment would lift the iris's travel ceiling and you cannot
|
|
854
|
+
have one.** Spine has them (AUTHORING §3.4) but `A11_NO_CLIPPING_ATTACHMENTS`
|
|
855
|
+
refuses one under `--profile spine-html`, so a rig carrying one builds on only one
|
|
856
|
+
of the two profiles. The ceiling is then geometric: in the worked example the
|
|
857
|
+
iris's ink ring has radius 28.5 against a socket opening 36 half-wide, so
|
|
858
|
+
`36 − 28.5 = 7.5` units is as far as it can travel before it crosses its own lash.
|
|
859
|
+
⇒ **Compute that ceiling from the art before keying a gaze**; the first draft of
|
|
860
|
+
the worked example keyed 9 and had to come back to 7.
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## 6. A blink is a continuous channel, not a swap
|
|
865
|
+
|
|
866
|
+
**Two ways to blink, and both are right somewhere:**
|
|
867
|
+
|
|
868
|
+
| | An `attachment` swap | A translating lid plate |
|
|
869
|
+
| --- | --- | --- |
|
|
870
|
+
| what it is | two drawings, `eyes` and `eyes_shut`, stepped | one plate, one `translatey`, 65 units down its own bone |
|
|
871
|
+
| costs | 1 extra PNG | 2 PNGs, 2 bones, 2 slots |
|
|
872
|
+
| where it is right | a mascot, a stylised blink, anything whose shut eye is a **different drawing** rather than a covered one | a portrait |
|
|
873
|
+
|
|
874
|
+
⭐ **Choose the swap for a mascot and the channel for a face**, and the reasons
|
|
875
|
+
are all timing:
|
|
876
|
+
|
|
877
|
+
- **A swap has no shape.** A real blink is fast shut and slow open. The worked
|
|
878
|
+
example is `0.07 s` down and `0.16 s` open — a **1 : 2.3** asymmetry, easing
|
|
879
|
+
into the close and out of the open. A stepped timeline has one frame of
|
|
880
|
+
transition and no curve to put an asymmetry in.
|
|
881
|
+
- **A swap has no partial.** A half-blink, a sleepy lid, a lid that rides the
|
|
882
|
+
gaze — every one of them is a *fraction of the same channel*, and none of them
|
|
883
|
+
is a third drawing.
|
|
884
|
+
- **A swap has to dodge the frame grid.** A stepped key exactly on a sample time
|
|
885
|
+
can be missed by a player accumulating `1/fps`, which is why
|
|
886
|
+
[`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash)'s
|
|
887
|
+
blink key sits at `0.399999` (AUTHORING §4.5). **A continuous channel does not
|
|
888
|
+
care where the samples land.**
|
|
889
|
+
|
|
890
|
+
⚠️ **One constraint on the lid's own drawing, and it is an art decision the rig
|
|
891
|
+
cannot express.**
|
|
892
|
+
|
|
893
|
+
**Fade its top edge out, and size the fade in pixels.** A flat plate of skin
|
|
894
|
+
translating down a forehead has a visible edge; fading its top 30 pixels
|
|
895
|
+
removes it. ⛔ **Do not write that fade proportionally.** In the worked example
|
|
896
|
+
it was first authored as `offset 0.34` in `objectBoundingBox` units — 34% of
|
|
897
|
+
*whatever height the plate happened to be* — and when the plate grew from 88 to
|
|
898
|
+
112 to cover the eye, the faded band grew with it and stopped covering the top
|
|
899
|
+
of the shut eye. In `userSpaceOnUse` with `y2="30"` the height becomes a free
|
|
900
|
+
variable and the coverage becomes a stated one: `106 − 30 = 76` opaque pixels
|
|
901
|
+
above the lash, against a 70-unit eye.
|
|
902
|
+
|
|
903
|
+
📌 **Blink both lids on one `groups` track.** An L/R offset of one frame was
|
|
904
|
+
tried in the worked example and rejected: at 25 fps it does not read as a soft
|
|
905
|
+
blink, it reads as a **wink**. ⇒ MOTION §3.7's offset table is about a chain
|
|
906
|
+
hanging off a driver, and two lids are not that — they are one event.
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## 7. Channel allocation, before the first key
|
|
911
|
+
|
|
912
|
+
🚨 **Whoever composes with this face will layer — an idle that keeps running
|
|
913
|
+
under a triggered gaze, under a turn — and two animations keying the same bone
|
|
914
|
+
property are BLENDED, not summed.** The rig cannot decide when those play, and it
|
|
915
|
+
is the only thing that can decide whether they collide when they do. So the
|
|
916
|
+
animations have to divide the rig up front. The
|
|
917
|
+
worked example's table, which is the shape of thing to write before authoring
|
|
918
|
+
anything:
|
|
919
|
+
|
|
920
|
+
| Channel | `idle` | `gaze` | `turn` |
|
|
921
|
+
| --- | --- | --- | --- |
|
|
922
|
+
| `torso` scale, `chest` translatey | ✔ | | |
|
|
923
|
+
| `lids` translatey | ✔ | | |
|
|
924
|
+
| `brows` translatey | ✔ | ✔ | |
|
|
925
|
+
| `irises` / `sparks` translate | | ✔ | |
|
|
926
|
+
| `head` + `hair_bang` mesh deform | | | ✔ |
|
|
927
|
+
| `faceshift`, feature translatex / scalex | | | ✔ |
|
|
928
|
+
| `hairmass` translatex | | | ✔ |
|
|
929
|
+
| `lock_l` / `lock_r` / `ahoge` | rotate | rotate | translatex |
|
|
930
|
+
| `headroll` rotate | ✔ | ✔ | ✔ |
|
|
931
|
+
| `neck` | rotate | | translatex |
|
|
932
|
+
|
|
933
|
+
Three collisions survive there: `headroll` rotate in all three, `brows`
|
|
934
|
+
translatey in two, and the locks' rotate in two. ⚠️ **On plain Spine the fixes
|
|
935
|
+
are ordinary — `MixBlend.add` on the layered track, or splitting a bone into a
|
|
936
|
+
stack (`headroll_idle` under `headroll_layer`) — but both are runtime or rig
|
|
937
|
+
decisions the motion spec cannot express, so nothing warns an author that two of
|
|
938
|
+
their animations will fight.**
|
|
939
|
+
|
|
940
|
+
🚨 **A `slider` is a third way for two animations to meet on one property, and it
|
|
941
|
+
is an overwrite rather than a blend — so allocate it in this table too.** An
|
|
942
|
+
animation a slider applies (AUTHORING §3.5.2) is never on a track: the constraint
|
|
943
|
+
applies it every frame, at whatever time its own bone currently points at. At
|
|
944
|
+
`mix: 1` with `additive` left at its default that apply **writes the property
|
|
945
|
+
outright**, which erases both any earlier slider on the same property and the
|
|
946
|
+
**playing** animation on the bones its animation keys — including at its own
|
|
947
|
+
neutral, where it looks switched off. ⇒ Every face axis that shares a target
|
|
948
|
+
declares `"additive": true`, and unlike the two collisions above this one **is**
|
|
949
|
+
gated: `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` names the bone, the property,
|
|
950
|
+
every slider keying it in array order and which one wins today. ⛔ The one case
|
|
951
|
+
`additive` cannot rescue is a **slot colour, an attachment swap, a draw order or
|
|
952
|
+
a sequence**: those timelines ignore the flag entirely, so a fade — §8's way of
|
|
953
|
+
taking a part off the screen before its own ceiling — belongs *inside* the single
|
|
954
|
+
animation one slider applies, never in a second slider beside it.
|
|
955
|
+
|
|
956
|
+
⛔ **And the cost is real, so name it rather than discovering it by shipping.** In
|
|
957
|
+
the worked example `idle` keys **nothing** on the iris, on purpose, even though a
|
|
958
|
+
completely still eye reads as a mannequin. The iris is `gaze`'s channel; an
|
|
959
|
+
`idle` drift plus a `gaze` on a second track would be two animations holding two
|
|
960
|
+
opinions about where she is looking. **That is a loss, it was chosen, and it is
|
|
961
|
+
written down.**
|
|
962
|
+
|
|
963
|
+
⇒ **The gaze itself is then pure MOTION §3.7** — a chain of offsets, and the only
|
|
964
|
+
face-specific part is which bone leads. The worked example's ordering
|
|
965
|
+
(**measured** off its own key times):
|
|
966
|
+
|
|
967
|
+
| Track | Extreme at | What it is |
|
|
968
|
+
| --- | --- | --- |
|
|
969
|
+
| `irises` translate | **0.22 s** | the eyes lead. `(7, −3)` |
|
|
970
|
+
| `sparks` translate | 0.22 s | `(2.8, −1.2)` — **40% of the iris distance**, arriving at the *same time*. A specular highlight is fixed to the light, not to the eyeball, so it lags in **distance** and not in time |
|
|
971
|
+
| `brows` translatey | 0.34 s | +1.8, which turns a flick of the eyes into interest |
|
|
972
|
+
| `headroll` translatex + rotate | **0.40 s** | the head follows **+12% of the duration after the eyes**. A rigid 3.4-unit slide and a −1.2° roll |
|
|
973
|
+
| `lock_l` / `lock_r` / `ahoge` rotate | 0.72 / 0.76 / 0.82 s | **+21% / +24% / +28% after the head**, each with one overshoot crossing of opposite sign |
|
|
974
|
+
|
|
975
|
+
🩹 **The head's follow there is a rigid slide plus a roll, not a small yaw.** A
|
|
976
|
+
head following a gaze really does yaw a few degrees, and that is one `transform`
|
|
977
|
+
key with a smaller `degrees` (§1.1). ⇒ What there is to weigh is §7's
|
|
978
|
+
table — a yaw on the `gaze` channel is a second animation keying the head mesh's
|
|
979
|
+
deform, which `turn` already owns, and that collision is the thing the format
|
|
980
|
+
cannot express. Same trade in the turn's anticipation: MOTION §3.6 asks
|
|
981
|
+
for a counter-move, and the worked example's is a **−0.9° counter-roll on the
|
|
982
|
+
neck bone rather than a counter-yaw**, which would be one more `transform` key.
|
|
983
|
+
|
|
984
|
+
⭐ **A roll channel is worth having for a second reason: it is where a turn's arc
|
|
985
|
+
comes from.** A yaw shift is a straight horizontal line and `translatex` draws
|
|
986
|
+
exactly that (MOTION §3.5). A 1.6° roll on a bone at the top of the neck bends
|
|
987
|
+
every feature's path into an arc, because a rotation carries its descendants on a
|
|
988
|
+
circle for free.
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## 8. The three cliffs, and picking a construction from the turn you need
|
|
993
|
+
|
|
994
|
+
Three separate failures at three different angles. ⇒ **Read this before the art
|
|
995
|
+
is drawn**, because two of the three are answered by *parts*, and parts are the
|
|
996
|
+
expensive thing to change.
|
|
997
|
+
|
|
998
|
+
**The sweep** — the worked example's `turn` re-derived at seven angles, built,
|
|
999
|
+
rendered and looked at. The geometry columns are **derived**; the last column is
|
|
1000
|
+
the record's looked-at judgement:
|
|
1001
|
+
|
|
1002
|
+
**No run reproduces this:** the geometry columns are §1's line evaluated by hand and the last column is a person's, from the sweep in the `gallery/portrait` record; each of the seven builds is `"degrees": <n>` and a rebuild, but no command this page states takes them
|
|
1003
|
+
|
|
1004
|
+
| yaw | centre shift | far band 42 → | ratio | 9-unit ink → | reads as a turn? |
|
|
1005
|
+
| --- | --- | --- | --- | --- | --- |
|
|
1006
|
+
| 8° | 23.7 | 32.0 | 0.762 | 6.9 | yes, gently |
|
|
1007
|
+
| **12° (shipped)** | **35.3** | **26.8** | **0.637** | **5.7** | **yes** |
|
|
1008
|
+
| 16° | 46.9 | 21.4 | 0.509 | 4.6 | yes |
|
|
1009
|
+
| 20° | 58.1 | 15.9 | 0.379 | 3.4 | marginal |
|
|
1010
|
+
| 24° | 69.1 | 10.4 | 0.247 | 2.2 | no — the eyes have stopped being eyes |
|
|
1011
|
+
| 28° | 79.8 | 4.7 | 0.113 | 1.0 | no — the far outline is gone |
|
|
1012
|
+
| 32° | 90.1 | −0.9 | −0.021 | — | **the mesh has folded** |
|
|
1013
|
+
|
|
1014
|
+
**Failure 1 — the iris goes elliptical, at ~18°, and it is fixable.** §5. Two
|
|
1015
|
+
reciprocal `scalex` tracks buy 8° of range. Do this one always; it is the best
|
|
1016
|
+
ratio in the experiment.
|
|
1017
|
+
|
|
1018
|
+
**Failure 2 — a `scalex` cannot rotate an almond, at ~26°, and it is not
|
|
1019
|
+
fixable.** The eye *socket* goes next. A real eye at 26° does not narrow
|
|
1020
|
+
uniformly: its far corner disappears behind the nose bridge, its lash line
|
|
1021
|
+
rotates, its lid wraps. `scalex` does exactly one of those things, so the far eye
|
|
1022
|
+
becomes *a thin version of a front-facing eye* and the near eye's lash stretches
|
|
1023
|
+
into a wide flat slab (**derived** socket scales, from §1's line at the sweep's
|
|
1024
|
+
own angles and re-derivable from it alone: 0.892/1.064 at 12°, 0.745/1.082
|
|
1025
|
+
at 24°, 0.629/1.067 at 32° — note the near side barely moves past 20°, which is
|
|
1026
|
+
why the stretch stops looking like foreshortening). ⇒ **Past roughly 26° the eyes
|
|
1027
|
+
need their own deform meshes** — socket, lash and lid as a 3–4 column grid each —
|
|
1028
|
+
and that is where the vertex count stops being 40. The fringe tips (a rigid plate
|
|
1029
|
+
that should be splaying) and the neck go in the same band for the same reason.
|
|
1030
|
+
|
|
1031
|
+
**Failure 3 — the mesh folds, and refining it makes this worse.** §4.2, and it is
|
|
1032
|
+
the one to design around rather than discover.
|
|
1033
|
+
|
|
1034
|
+
🩹 **The neck is the honest fudge, and label yours the same way.** A neck twists:
|
|
1035
|
+
its top follows the head almost entirely and its base hardly at all. A single
|
|
1036
|
+
rigid plate can only take an average — the worked example takes **28%** of the
|
|
1037
|
+
head's shift (`−10` against `−35.345`), which is **the one number in its turn that
|
|
1038
|
+
is not derived**, chosen as the value at which the chin stopped hanging off the
|
|
1039
|
+
throat at 12°. A turn that had to read at 20° would need the neck to be its own
|
|
1040
|
+
mesh with its own column table.
|
|
1041
|
+
|
|
1042
|
+
### The verdict on angle
|
|
1043
|
+
|
|
1044
|
+
⭐ **A 5-column grid with bone-scaled features is a 0–16° instrument, comfortable
|
|
1045
|
+
at 12°.** For a standing portrait that is enough: an idle turn, a glance away, a
|
|
1046
|
+
lean into frame. **A 30–45° three-quarter turn is a different rig** — per-eye
|
|
1047
|
+
meshes, a meshed neck, probably a second art layer for the far cheek — and it is
|
|
1048
|
+
not a format problem, it is a parts-and-labour problem.
|
|
1049
|
+
|
|
1050
|
+
📊 **What one held yaw actually costs**, from the shipped `motion.json`
|
|
1051
|
+
(**derived** by counting it):
|
|
1052
|
+
|
|
1053
|
+
| Animation | Duration | Tracks | Track keys | Deform entries | Deform keys | Hand-written deform floats |
|
|
1054
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
1055
|
+
| `idle` | 3.2 s | 9 | 37 | 0 | 0 | 0 |
|
|
1056
|
+
| `gaze` | 1.5 s | 8 | 32 | 0 | 0 | 0 |
|
|
1057
|
+
| **`turn`** | 2.2 s | **8** | **33** | 2 | 8 | **0** |
|
|
1058
|
+
|
|
1059
|
+
⇒ **`idle` and `gaze` cost what an ordinary MOTION.md shot costs, and the turn
|
|
1060
|
+
has no transcription in it.** The deform is four `transform` keys (§1.1) and the
|
|
1061
|
+
compiler writes the run: **five distinct values** per head key (one per column,
|
|
1062
|
+
repeated down five rows), with **25 of its 50 slots structurally `0`** because a
|
|
1063
|
+
yaw has no vertical component. The **tracks** column is 8 because the features are
|
|
1064
|
+
two group tracks whose keys state a model (§3, AUTHORING §4.5.1) and the four hair
|
|
1065
|
+
parts are one.
|
|
1066
|
+
|
|
1067
|
+
🚨 **And here is the number that matters more than either count: what the
|
|
1068
|
+
hand-written figures ARE.** The turn states **34 depths** — of which **11 are
|
|
1069
|
+
distinct**, the rest being the same table repeated on the second held key and on
|
|
1070
|
+
the `scalex` track. ⇒ The point is **auditability**: a residual of `1.14` is a
|
|
1071
|
+
number a reader can only take on trust, and `brow_r at depth 158` is a claim they
|
|
1072
|
+
can argue with. §3 is why that is the whole point, and §2 is why the depths had
|
|
1073
|
+
nowhere else to live.
|
|
1074
|
+
|
|
1075
|
+
⚠️ **A plain `groups` entry still buys almost nothing on a face, and that is
|
|
1076
|
+
structural rather than an oversight.** Keying several bones **identically** is
|
|
1077
|
+
right for a wheel pair, and **every part needing a different number is what
|
|
1078
|
+
parallax means.** The on-axis pair's shared `cos t` falls out of the same closed
|
|
1079
|
+
form as everybody else's value, so it needs no group, while `look_l`/`look_r` stay
|
|
1080
|
+
one — because those two really are one shared number (§5).
|
|
1081
|
+
|
|
1082
|
+
### The turn as a value rather than a time
|
|
1083
|
+
|
|
1084
|
+
Everything above prices a turn as an animation somebody plays. The same geometry
|
|
1085
|
+
also runs on an **axis**: a `slider` constraint reads a driving bone, maps that
|
|
1086
|
+
bone's rotation to a time inside the turn animation, and applies the animation
|
|
1087
|
+
there (AUTHORING §3.5.2). Nothing in §1–§5 changes — the keys are still §1's line
|
|
1088
|
+
evaluated at each angle — but what selects among them is a **value** rather than a
|
|
1089
|
+
playhead, so the face follows a number somebody else is holding: a pointer, a gaze
|
|
1090
|
+
target, a game state.
|
|
1091
|
+
|
|
1092
|
+
⭐ **The object offers a dial and does not decide when it turns.** That is the
|
|
1093
|
+
whole of the claim and it is deliberately not a larger one: what the rig
|
|
1094
|
+
guarantees is the axis — its range, its arithmetic, and that every angle on it is
|
|
1095
|
+
sound — and what moves the dial belongs to whoever is using the face. The
|
|
1096
|
+
paragraphs below are the part that is ours.
|
|
1097
|
+
|
|
1098
|
+
#### The range stops being a choice and becomes a measurement
|
|
1099
|
+
|
|
1100
|
+
This is the half an author has no other way to get right. §4.2's fold angle is not
|
|
1101
|
+
a rule of thumb once the angle is a dial position: it is the **top of the dial**,
|
|
1102
|
+
because past it a triangle turns inside out and `A39` refuses the build by name.
|
|
1103
|
+
`build` prints that angle for every depth mesh it compiles (AUTHORING §3.4), so
|
|
1104
|
+
the whole mapping falls out of one reading:
|
|
1105
|
+
|
|
1106
|
+
```
|
|
1107
|
+
ceiling the largest turn this depth mesh admits — printed by `build`, not guessed
|
|
1108
|
+
range the largest whole degree strictly INSIDE the ceiling
|
|
1109
|
+
from -range max +range local true (AUTHORING §3.5.2's circle)
|
|
1110
|
+
scale seconds per degree — the one number here you choose
|
|
1111
|
+
duration 2 x range x scale
|
|
1112
|
+
time to + (degrees - from) x scale the slider's own mapping
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
📐 **Worked, on [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look)**,
|
|
1116
|
+
whose face is a 21 × 9 `grid` over one depth sheet:
|
|
1117
|
+
|
|
1118
|
+
```bash
|
|
1119
|
+
bun cli.ts build --rig gallery/look/rig.json \
|
|
1120
|
+
--motion gallery/look/motion.json \
|
|
1121
|
+
--out gallery/look/build
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
```
|
|
1125
|
+
MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
|
|
1126
|
+
depth "face_depth.png" bf156ea0cfc970a3 near=white zScale=194 z=[0, 194]
|
|
1127
|
+
80 of 189 vertices sample a texel the part image does not draw — their z is the sheet's reading of somewhere the part is not
|
|
1128
|
+
turn ceiling yaw +19.32° / -19.32° pitch +22.92° / -26.94°
|
|
1129
|
+
1st pct yaw +19.32° x1.000 of 80 / -19.32° x1.000 of 80 pitch +22.92° x1.000 of 102 / -26.94° x1.000 of 130
|
|
1130
|
+
first to fold: yaw + at 19.32°, triangle 174 [119,138,139], the sheet steps 28.50 level(s) across it, which is 0.112 of the range this mesh sampled
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
⇒ the ceiling is **±19.32°**, so the range is **19** — `floor(19.32)`, and
|
|
1134
|
+
*strictly inside* is the whole of the rule. At the **0.05 s per degree** that rig
|
|
1135
|
+
chooses, `turn` runs `2 × 19 × 0.05` = **1.9 s** and the map is
|
|
1136
|
+
`time = 0 + (degrees + 19) × 0.05`, which is the constraint as its rig spec
|
|
1137
|
+
declares it: `"from": -19, "to": 0, "scale": 0.05, "max": 19, "local": true,
|
|
1138
|
+
"additive": true`. ⭐ **The only two numbers there that anybody chose are `scale`
|
|
1139
|
+
and `to`** — and `to: 0` says nothing more than *the bottom of the range is the
|
|
1140
|
+
animation's first frame*. `from`, `max` and the duration are all the ceiling.
|
|
1141
|
+
|
|
1142
|
+
🔸 **And `scale` is chosen for the endpoint.** rigc emits every number as its
|
|
1143
|
+
float32's shortest name, so a `scale` the float cannot hold moves the top of the
|
|
1144
|
+
dial: `1/60` ships as `0.016666668`, and a 60° turn then applies at 1.00000008 s
|
|
1145
|
+
rather than 1 s — the last frame under `loop: false`, the *first* under
|
|
1146
|
+
`loop: true` (AUTHORING §3.5.2). `0.05` is its own float's name, so the file states
|
|
1147
|
+
exactly `0.05`, which is the only reason the example can put its endpoint exactly
|
|
1148
|
+
on the duration. Pick a `scale` that is not, and land the endpoint inside the
|
|
1149
|
+
duration instead.
|
|
1150
|
+
|
|
1151
|
+
⚠️ **The ceiling is per mesh, and the face's is not the smallest one on the
|
|
1152
|
+
face.** The same run prints one for every depth mesh, and in this example each
|
|
1153
|
+
sidelock reads `yaw +17.04° / -45.80°`: it folds at **17.04°** on one side, which
|
|
1154
|
+
is *inside* the ±19° the face itself admits, and not until 45.80° on the other.
|
|
1155
|
+
The asymmetry is the sheet's, not a coincidence — each of those sheets is
|
|
1156
|
+
steepest at the edge where the strand curves away, and a yaw folds a pair of
|
|
1157
|
+
vertices only in the direction their depth is rising.
|
|
1158
|
+
|
|
1159
|
+
⇒ **A part whose ceiling is lower than the range has to be gone before the turn
|
|
1160
|
+
reaches it.** That is AUTHORING §3.4's third way to live with a ceiling: fade the
|
|
1161
|
+
slot to alpha 0 **inside the animation the slider applies**, landing the alpha-0
|
|
1162
|
+
key *before* the key that folds rather than on it. §7's paragraph on sliders is
|
|
1163
|
+
why that fade cannot be a second slider.
|
|
1164
|
+
|
|
1165
|
+
⭐ **A lookup table wants linear keys, and that is not a style note.** The slider
|
|
1166
|
+
makes the pose a function of the dial, so an easing curve between two keys makes
|
|
1167
|
+
it a **non-linear** function of a number the consumer may be holding perfectly
|
|
1168
|
+
still — the face would drift and settle while the value sits where it was put.
|
|
1169
|
+
Anticipation and follow-through fail for the same reason, not a weaker one
|
|
1170
|
+
(MOTION §3.6, §3.7): both are functions of time, and there is no time on this
|
|
1171
|
+
axis. [MOTION §0.1](MOTION.md) is that split written out, and it is also where the
|
|
1172
|
+
shaping *does* belong — in whatever animation moves the dial.
|
|
1173
|
+
|
|
1174
|
+
⚠️ **Two axes on one face need `"additive": true` on both.** A `pitch` dial
|
|
1175
|
+
beside the `yaw` is the ordinary case and it is the one the format's default
|
|
1176
|
+
breaks the moment the two share a target — in the worked example both `turn` and
|
|
1177
|
+
`tilt` key `headroll`. §7's paragraph on sliders is the mechanism and
|
|
1178
|
+
`A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` is the refusal.
|
|
1179
|
+
|
|
1180
|
+
✅ **And the space between the two dials is measured rather than inferred.**
|
|
1181
|
+
Two additive sliders posed
|
|
1182
|
+
at a grid of *both* values — and over a rotation and a translation, so the claim
|
|
1183
|
+
is not one property's — are the closed-form sum at every cell of it, to float64:
|
|
1184
|
+
each slider maps its own reading to a time by §3.5.2's rule, its animation is read
|
|
1185
|
+
there, and the two contributions add. ⭐ That is a property of the **interior**
|
|
1186
|
+
and not of the corners, which is the whole reason it needed a grid: with an `ease` on one of the two
|
|
1187
|
+
animations, every corner of the space still reads as correct while most of the
|
|
1188
|
+
inside has moved. ⇒ **a
|
|
1189
|
+
lookup table wants linear keys for a second reason** — not only that the face
|
|
1190
|
+
would drift while the value sat still, but that a curve is invisible to any check
|
|
1191
|
+
that reads an axis at its ends.
|
|
1192
|
+
|
|
1193
|
+
⚠️ **And `mix` below 1 is not what it looks like when the later slider is not
|
|
1194
|
+
additive.** An additive slider scales its whole contribution by its own `mix`, so
|
|
1195
|
+
two of them at any pair of mixes are still the sum — which is why `A40` skipping
|
|
1196
|
+
below full authority is right: what happens there is a weighting, not the erasure
|
|
1197
|
+
it refuses. A **non-additive** slider at `mix` α applies
|
|
1198
|
+
`current + (value + setup − current) × α`, a lerp *from the pose it found*, so the
|
|
1199
|
+
earlier slider is not erased — it is attenuated by `1 − α`. A pitch dial turned
|
|
1200
|
+
halfway down takes that share of the yaw with it, on every frame, with the gate
|
|
1201
|
+
green. ⇒ write `"additive": true` on **every** slider that shares a target and
|
|
1202
|
+
not only on the later one, which is what the paragraph above already asks for and
|
|
1203
|
+
this is the second reason for.
|
|
1204
|
+
|
|
1205
|
+
🚨 **A second dial on a slot colour, an attachment swap or a draw order is not a
|
|
1206
|
+
second dial at all.** Those timelines ignore `additive`, so the flag is not the
|
|
1207
|
+
repair and writing it on both changes nothing: the slider **later in the
|
|
1208
|
+
`constraints` array** owns that property outright and the earlier one contributes
|
|
1209
|
+
nothing, at every position of its dial. An **ik constraint's mix** behaves the
|
|
1210
|
+
same way. What does compose is the rest of what a face keys — a bone transform, a
|
|
1211
|
+
mesh deform, a transform constraint's mix, a physics `wind`, a path constraint's
|
|
1212
|
+
`mix`, and another slider's own `mix` or `time` — and those are the same sum as
|
|
1213
|
+
the two axes above, over each target's own setup value. ⇒ if a blink fades a slot
|
|
1214
|
+
and the yaw dial also fades it, one of the two has to stop: key that property
|
|
1215
|
+
from **one** slider, or move both edits into the animation a single slider
|
|
1216
|
+
applies. `A40` refuses the rest by name, and AUTHORING §3.5.2 is the mechanism.
|
|
1217
|
+
|
|
1218
|
+
✅ **And which of the two a timeline is, the gate poses rather than asks.**
|
|
1219
|
+
`A40` applies the shared timeline twice with `add` set and reads whether the second
|
|
1220
|
+
application accumulated, rather than reading `Timeline.additive`, the runtime's own
|
|
1221
|
+
declaration, which two classes state falsely about themselves — a path
|
|
1222
|
+
constraint's `mix` and a slider's `time`, both in the composing list above. Two
|
|
1223
|
+
dials whose animations both fire **events** are not refused either: a slider fires
|
|
1224
|
+
no event at all — it applies its animation with `firedEvents` null.
|
|
1225
|
+
|
|
1226
|
+
⭐ **Three dials are the same sum as two — as long as every one of them is
|
|
1227
|
+
additive.** The case worth knowing is a non-additive dial in the *middle* of
|
|
1228
|
+
three, because it is neither of the two failures you would expect: it erases every
|
|
1229
|
+
dial **before** it and is then added to by every dial **after** it, so the face is
|
|
1230
|
+
neither the sum nor the last dial alone, and no reading of a two-dial rig has that
|
|
1231
|
+
shape.
|
|
1232
|
+
|
|
1233
|
+
⚠️ **`"loop": true` puts the animation's FIRST frame at the top of the dial.** A
|
|
1234
|
+
looping slider wraps its time as a positive modulo rather than holding the last
|
|
1235
|
+
frame, so the axis is a sawtooth: the two ends of the range are the same pose and
|
|
1236
|
+
every position past the top repeats the range from its bottom. That is right for a
|
|
1237
|
+
parameter that genuinely cycles — a wheel, a breath — and wrong for a yaw, where
|
|
1238
|
+
the range's top has to *stay* at the extreme of the turn. Leave `loop` off for a
|
|
1239
|
+
face axis; the default is the one you want.
|
|
1240
|
+
|
|
1241
|
+
🔸 **And a `local: false` dial never reads back the number you set.** The world
|
|
1242
|
+
reader goes through the bone's matrix, where the reference runtime's float32 π
|
|
1243
|
+
leaves a fraction of a degree behind, and it has a period: a bone one whole turn
|
|
1244
|
+
from its position reads *identically*, so the dial cannot tell the two apart. The
|
|
1245
|
+
composition is unchanged — two world dials add exactly as two local ones do — but
|
|
1246
|
+
`local: true` is what makes the number on the dial the number the rig reads, which
|
|
1247
|
+
is the same repair §3.5.2's circle already asks for. Every one of the six
|
|
1248
|
+
`property` readings composes by that one arithmetic under `local: true`, and so
|
|
1249
|
+
does every one of them read through the world — the composition is the same sum
|
|
1250
|
+
on both sides of the flag, and what the flag changes is what the dial can say.
|
|
1251
|
+
|
|
1252
|
+
🚨 **A `local: false` scale axis folds at zero: the negative half is the positive
|
|
1253
|
+
half again.** A world scale reading is a square root, so a dial at −2 and a dial
|
|
1254
|
+
at +2 are not two positions — they read the same, select the same frame and pose
|
|
1255
|
+
the same face. Nothing at compile or at runtime says so, and a squash axis
|
|
1256
|
+
authored through negative scale therefore gets the mirror of the dial you wrote,
|
|
1257
|
+
symmetric about the point where it should have passed through. A world `shearY`
|
|
1258
|
+
axis folds the same way at a whole turn, and its seam is not at a fixed value —
|
|
1259
|
+
it moves with wherever the bone is pointing. ⇒ for a scale or a shear axis, write
|
|
1260
|
+
`local: true`; for a scale axis you cannot, keep the whole range on one side of
|
|
1261
|
+
zero, because the reader has no other half to give you.
|
|
1262
|
+
|
|
1263
|
+
⭐ **A dial can drive another dial's authority, and that composes as a product.**
|
|
1264
|
+
A slider's own `mix` is a keyable property, so one axis can scale another axis'
|
|
1265
|
+
whole contribution — a "strength" dial over an expression, which is the one shape
|
|
1266
|
+
on this page that multiplies rather than adds. ⚠️ **It only works downward through
|
|
1267
|
+
the `constraints` array.** A slider reads its own authority when its turn comes
|
|
1268
|
+
and the array is the update order, so a dial that keys the `mix` of a slider
|
|
1269
|
+
*earlier* than itself writes a number that slider has already read past: the
|
|
1270
|
+
driven axis is dead at every position, every frame. ✅ **The gate names that
|
|
1271
|
+
pair** — `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` refuses it with both
|
|
1272
|
+
sliders, the property and both array indices, and says which way to move them.
|
|
1273
|
+
⇒ put the driving slider **first**, which is what the refusal tells you to do.
|
|
1274
|
+
|
|
1275
|
+
🚨 **And a dial cannot turn itself on.** `Slider.update` reads its own `mix` as
|
|
1276
|
+
the alpha it applies the animation with, before that animation runs, so a slider
|
|
1277
|
+
whose *own* animation keys its `mix` writes after the only read of it — the same
|
|
1278
|
+
refusal with the two array indices equal. The shape to watch for is an axis
|
|
1279
|
+
muted at setup that means to raise itself: it never applies anything at all,
|
|
1280
|
+
because `update` returns on `mix` 0 before reaching the key that would raise it,
|
|
1281
|
+
and `A37` reports green because it asks whether *an* animation keys the mix and
|
|
1282
|
+
never which one.
|
|
1283
|
+
|
|
1284
|
+
🚨 **And it is not only another dial: a face axis that drives a jiggle or a
|
|
1285
|
+
path is the same rule.** `physics.<name>.wind`, `path.<name>.position`, an ik or
|
|
1286
|
+
transform mix — every property a dial can key belongs to a constraint that reads
|
|
1287
|
+
it when its own turn comes, and that turn is its place in `constraints`. A
|
|
1288
|
+
"wind strength" dial declared after the physics constraint it drives poses the
|
|
1289
|
+
number in that constraint's pose and moves nothing at all: [measured] the bone a
|
|
1290
|
+
physics constraint drives travels `0.000e+0` across the dial with the constraint
|
|
1291
|
+
declared first and `4.256e+2` with it declared last, and a path constraint's
|
|
1292
|
+
rider `0.000e+0` against `1.620e+2`. ✅ `A42` names those pairs
|
|
1293
|
+
too, with the constraint's kind and both array indices. ⇒ **every constraint a
|
|
1294
|
+
dial drives goes after that dial in `constraints`** — which, for a face, means
|
|
1295
|
+
the dials come first and the jiggles, paths and aim constraints they scale come
|
|
1296
|
+
after. 🔸 One key is outside the rule because no order repairs it: a `physics`
|
|
1297
|
+
`reset` from a dial fires on a crossed frame time and a slider applies its
|
|
1298
|
+
animation at a single instant, so it never fires at all — the gate says that in
|
|
1299
|
+
its SKIP rather than asking you to move anything.
|
|
1300
|
+
|
|
1301
|
+
🔸 **The bone-less slider is the same story one field over.** A slider with no
|
|
1302
|
+
`bone` takes its time from `slider.<name>.time`, which any animation can key — and
|
|
1303
|
+
two dials keying it *add*, so a time-driven axis composes like everything else
|
|
1304
|
+
here, and the gate agrees. The same array rule applies, for the
|
|
1305
|
+
same reason — and `A42` refuses it there too, naming `time` instead of `mix`.
|
|
1306
|
+
⚠️ Two things the bone
|
|
1307
|
+
form does and this one does not: there is no `Math.max(0, time)` and no wrap, so a
|
|
1308
|
+
driven time below zero does not pose the first frame — it leaves the pose exactly
|
|
1309
|
+
as it found it, which is a different picture whenever the animation's first frame
|
|
1310
|
+
is not the rest pose.
|
|
1311
|
+
|
|
1312
|
+
⚠️ **Two `skinRequired` sliders are three states, not two.** Under a skin that
|
|
1313
|
+
lists one of them the face is that dial alone; under a skin that lists the other
|
|
1314
|
+
it is the other alone; and under a skin that lists **neither** — the default skin
|
|
1315
|
+
is usually one — every dial is dead and the face holds its rest pose with both
|
|
1316
|
+
dials turned to their extremes. That last state is indistinguishable, from the
|
|
1317
|
+
outside, from a rig whose sliders do not work, and the gate is right to be silent
|
|
1318
|
+
about it because the pair genuinely never meets.
|
|
1319
|
+
|
|
1320
|
+
⭐ **Four dials are the same sum as three.** Nothing new arrives with the fourth
|
|
1321
|
+
axis: the arithmetic, the flag, and the non-additive-in-the-middle case all read
|
|
1322
|
+
exactly as they do above. Write `"additive": true` on all of them.
|
|
1323
|
+
|
|
1324
|
+
✅ **The editor half, measured.** The round trip was taken with
|
|
1325
|
+
`tools/editor_roundtrip.ts` on a licensed editor (data version 4.3.26) against a 4.3.13 build of
|
|
1326
|
+
[`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) — this
|
|
1327
|
+
subsection's worked case, not the `gallery/portrait` this page names at the top,
|
|
1328
|
+
which declares no constraints at all and so can carry no slider. What it found:
|
|
1329
|
+
|
|
1330
|
+
- **Both sliders come back, and the parameter axis survives.** `additive`,
|
|
1331
|
+
`local`, `bone`, `property`, `from`, `max` and `scale` are identical field for
|
|
1332
|
+
field, and the two keep their places in the `constraints` array. `mix: 1` and
|
|
1333
|
+
`to: 0` are dropped, and those are the format's own defaults (`SkeletonJson`
|
|
1334
|
+
reads `mix` as 1 and `to` as 0 when absent) — an elision, not a loss.
|
|
1335
|
+
- ⚠️ **The animation each slider *names* comes back because rigc emits
|
|
1336
|
+
animations in the editor's own order** — natural and case-insensitive. The
|
|
1337
|
+
editor re-sorts the `animations` object and a slider's animation is an ordinal
|
|
1338
|
+
in the format, so out of that order `yaw -> "turn"` returns as
|
|
1339
|
+
`yaw -> "sweep"` — the first animation of the sorted list. In order, on the
|
|
1340
|
+
same rig through the same editor, `yaw -> "turn"` comes back and the
|
|
1341
|
+
re-rendered mean absolute error is 0.3035 / 0.0769 / 0.0588
|
|
1342
|
+
(`sweep` / `tilt` / `turn`) against 10.4655 / 8.4961 / 8.7140 out of order,
|
|
1343
|
+
worst drift 3.947 px against 16.535 px.
|
|
1344
|
+
- 🚨 **The physics constraint on the cowlick comes back driving nothing.**
|
|
1345
|
+
`rotate: 1` is absent from the export, and an absent `rotate` parses as **0**
|
|
1346
|
+
(`SkeletonJson`), so the returned file states *drives nothing* rather than
|
|
1347
|
+
omitting a default — which is why `A23_PHYSICS_CONSTRAINT_EFFECTIVE` refuses it
|
|
1348
|
+
by name. Independent of the ordering.
|
|
1349
|
+
|
|
1350
|
+
✅ **Why, measured** — and corrected. It is not elision and not a defect in
|
|
1351
|
+
one field. Issue #540 measured three rigs and twelve constraints, predictions
|
|
1352
|
+
written before the round trip: a lone `y` came back, `x` and `y` together came
|
|
1353
|
+
back, and a lone `rotate`, a lone `scaleX` and a lone `shearX` each came back as
|
|
1354
|
+
**no components at all**, with every constraint's fixed-point `strength`
|
|
1355
|
+
returning exactly. It read that as *"the editor's physics model holds `x` and
|
|
1356
|
+
`y` and nothing else"*, and that reading was wrong.
|
|
1357
|
+
|
|
1358
|
+
🔁 **Corrected 2026-10-06 (issue #1196), on Spine 4.3.23 and 4.3.26:** what
|
|
1359
|
+
decides the loss is the **bone's length**, not the component. The editor's own
|
|
1360
|
+
example export `sack-pro` keeps 18 of 18 `rotate` constraints through the same
|
|
1361
|
+
import and export, and deleting the `length` of one of its bones loses `rotate`
|
|
1362
|
+
on exactly that one constraint. `look` with a 40-unit `ahoge_whip` keeps
|
|
1363
|
+
`rotate`, `scaleX` and `shearX` each; at 0.01 and 1 `rotate` is kept too; with no
|
|
1364
|
+
length all three are lost, on both versions. `look`'s `ahoge_whip` was emitted
|
|
1365
|
+
with no length; #540's rigs were not at hand to read, but every row it reported
|
|
1366
|
+
is reproduced here at length 0. ⇒
|
|
1367
|
+
**A rotation-driven jiggle survives the editor on a bone that has a length**,
|
|
1368
|
+
and `A23_PHYSICS_CONSTRAINT_EFFECTIVE` refuses one on a bone that has none
|
|
1369
|
+
(issue #1195).
|
|
1370
|
+
|
|
1371
|
+
✅ **And the locus is measured: the loss is at EXPORT** — against Spine
|
|
1372
|
+
4.3.26 and without decoding the project format. `gallery/look`'s build and three variants of its
|
|
1373
|
+
`skeleton.json` were each imported with the documented CLI, the project files
|
|
1374
|
+
inflated, and compared byte by byte. **Two imports of the same file differ
|
|
1375
|
+
only at bytes 13–28** — a timestamp or a hash — so that is the noise floor,
|
|
1376
|
+
and reading the bytes works because everything below byte 1914 is stable
|
|
1377
|
+
across imports. Against that floor the **only** stable difference between the
|
|
1378
|
+
`rotate`-driving file and the `rotate`-less one is the float32 at byte 248:
|
|
1379
|
+
`3f 80 00 00` = **1.0** where the source said `rotate: 1`, `7f c0 00 00` =
|
|
1380
|
+
**NaN** — the editor's own unset — where it did not, while an `x: 1` variant
|
|
1381
|
+
leaves 248 at NaN and moves a slot 66 bytes on, which is the component that
|
|
1382
|
+
survives. ⇒ The importer read `rotate` and kept it as a **non-default** value;
|
|
1383
|
+
the exporter wrote nothing for it. It is the editor's **writer**, not its
|
|
1384
|
+
reader, and not rigc's emitter.
|
|
1385
|
+
|
|
1386
|
+
The importer stores the value on a zero-length bone too, at 4.3.23 as well as
|
|
1387
|
+
4.3.26 (issue #1196: `rotate: 0.234375` lands as `3e 70 00 00` in the project,
|
|
1388
|
+
and the JSON and binary exports both omit it — the binary export of a project
|
|
1389
|
+
holding `rotate: 1` is byte-identical to one holding NaN).
|
|
1390
|
+
|
|
1391
|
+
🗑️ **A41, the rule that gated the editor round trip, and its declaration
|
|
1392
|
+
`invariants.editorRoundTrip` were retired in issue #1196.** They gated the
|
|
1393
|
+
components rather than the bone, so they refused rotation physics the editor
|
|
1394
|
+
keeps, and once A23 refuses those components on a zero-length bone a rig that
|
|
1395
|
+
passes it has nothing the editor's export drops — the declaration could no
|
|
1396
|
+
longer move a verdict.
|
|
1397
|
+
- 🔸 Unexplained, and **unreproduced by anything in this tree**: `diff` reports
|
|
1398
|
+
`animations.curve_kinds` moved on **196 of 200** keys in every round trip taken,
|
|
1399
|
+
the clean one included. Visually small once the ordering is fixed — but it is
|
|
1400
|
+
98% of the keys, and *small* is not *explained*. ⚠️ **[measured] by
|
|
1401
|
+
`tools/editor_roundtrip.ts` on the trips this subsection records, and no
|
|
1402
|
+
command this page states re-takes it** — so read the figure as those runs' and
|
|
1403
|
+
not as a property the gate holds.
|
|
1404
|
+
|
|
1405
|
+
✅ **Two more things are measured, and both came back safe:**
|
|
1406
|
+
|
|
1407
|
+
- **More than one event is safe.** The editor re-keys `events` the way it re-keys
|
|
1408
|
+
`animations` — `zebra, mike, alpha` came back `alpha, mike, zebra` — but every
|
|
1409
|
+
firing resolved **by name**, `0.3 -> mike` and `0.6 -> alpha`, payloads intact. The ordinal shape does
|
|
1410
|
+
*not* bite here, and rigc emits events in the order you declare them.
|
|
1411
|
+
- **More than one skin imports.** A four-skin rig imports in **both build
|
|
1412
|
+
modes** — default and `--copy-images` — **exit 0, project written**. What binds
|
|
1413
|
+
is a `CompileError` when the default skin shares a placeholder with a named
|
|
1414
|
+
one. ⇒ Skins are not the reason to stay at one, and every figure on this page
|
|
1415
|
+
was taken on a rig carrying exactly one (AUTHORING §10.1).
|
|
1416
|
+
|
|
1417
|
+
Every figure above was read back through `spine-core`.
|
|
1418
|
+
|
|
1419
|
+
---
|
|
1420
|
+
|
|
1421
|
+
## 9. Looking at it, and the audit gap
|
|
1422
|
+
|
|
1423
|
+
### 9.1 The looking protocol is three scales, not one
|
|
1424
|
+
|
|
1425
|
+
| Scale | What it is for |
|
|
1426
|
+
| --- | --- |
|
|
1427
|
+
| **the contact sheet** (~0.45×) | whether the **motion** reads. Spacing is a comparison *across* frames, so the grid is the only place to see it |
|
|
1428
|
+
| **1:1** | whether the **drawing arrived** |
|
|
1429
|
+
| **3–4× on the eyes** | because that is where a reader will look, and a face has no other equivalent |
|
|
1430
|
+
|
|
1431
|
+
📊 **Four of the five art defects in the worked example were invisible at contact
|
|
1432
|
+
sheet scale** and all four were found at 1:1 or better: the dark seam, the lid's fade letting a
|
|
1433
|
+
shut eye's lash show through as a grey smudge, the iris crossing its own lash at
|
|
1434
|
+
the gaze extreme, and a forehead highlight turning the lid's soft edge into a
|
|
1435
|
+
tonal step.
|
|
1436
|
+
|
|
1437
|
+
⚠️ **A portrait's plates are *supposed* to overlap invisibly**, and that is
|
|
1438
|
+
where a renderer's edge defect shows: a part that carries an ink outline at its
|
|
1439
|
+
edge hides one, because a dark rim on a dark line cannot be seen. ⇒ **Scene work exercises a renderer where game-part work does
|
|
1440
|
+
not.** Expect to find renderer defects on your first face, and check a suspicious
|
|
1441
|
+
edge against a **region build** before reading a single vertex — which is the
|
|
1442
|
+
next item.
|
|
1443
|
+
|
|
1444
|
+
⭐ **When a mesh is the new thing in a shot, build the region version and diff it
|
|
1445
|
+
before suspecting the mesh.** The worked example's first seam suspect was the
|
|
1446
|
+
grid: faint vertical lines down the forehead, at what looked like column
|
|
1447
|
+
positions. Building the same rig with both meshes replaced by plain regions and
|
|
1448
|
+
diffing the rest frame settled it in one command — worst channel difference 1,
|
|
1449
|
+
zero pixels differing — so the mesh was innocent and the lines were art
|
|
1450
|
+
compositing. **Two minutes, and it halves the search space.**
|
|
1451
|
+
|
|
1452
|
+
📌 **Draw the face before you rig it.** The worked example took **seven art
|
|
1453
|
+
passes before a `rig.json` existed** — brows twice, hair silhouette, fringe
|
|
1454
|
+
depth, neck length, garment mass, choker, proportions. That order is not
|
|
1455
|
+
fastidiousness: **the head plate's half-width *is* `R`, and `R` is in every one of
|
|
1456
|
+
the thirty derived numbers.** Rigging first means re-deriving all of them every
|
|
1457
|
+
time the plate changes width.
|
|
1458
|
+
|
|
1459
|
+
⚠️ **And expression lives in ink weight before it lives in shape.** The worked
|
|
1460
|
+
example's brows read as a scowl for two iterations and the brows were not the
|
|
1461
|
+
problem — two eyes whose upper lash is heaviest at the **inner** corner read as
|
|
1462
|
+
angry whatever the brow above them does. Worth knowing before spending a pass on
|
|
1463
|
+
the wrong part.
|
|
1464
|
+
|
|
1465
|
+
### 9.2 🚨 The half nothing measures — three builds, one of them refused
|
|
1466
|
+
|
|
1467
|
+
**The setup geometry is measured; one half of the deformed geometry is not.**
|
|
1468
|
+
Here is that claim as three builds of the same rig, each command runnable verbatim from a
|
|
1469
|
+
clean checkout. Start from the good one:
|
|
1470
|
+
|
|
1471
|
+
```bash
|
|
1472
|
+
bun install # once
|
|
1473
|
+
|
|
1474
|
+
bun cli.ts build --rig gallery/portrait/rig.json \
|
|
1475
|
+
--motion gallery/portrait/motion.json \
|
|
1476
|
+
--out gallery/portrait/build --profile spine-html
|
|
1477
|
+
bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
|
|
1478
|
+
--out gallery/portrait/render
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
```
|
|
1482
|
+
MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
|
|
1483
|
+
MESH hair_bang authored 15 vertices / 16 triangles (budget 32) bones=[bang] attachments=[hair_bang] covers 100.00% of the art, reaching 55.22px past it
|
|
1484
|
+
.. validate (spine-core round trip + machine assertions, profile spine-html)
|
|
1485
|
+
.. profile spine-html — every assertion applies
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
Green — the profile line above says so in the tool's own words.
|
|
1489
|
+
|
|
1490
|
+
Now break the projection two ways. Both scripts write a variant motion spec
|
|
1491
|
+
beside the originals and touch nothing in the repository:
|
|
1492
|
+
|
|
1493
|
+
```bash
|
|
1494
|
+
# (a) INVERT ONE BAND: give the two far columns each other's shift.
|
|
1495
|
+
# The far side now STRETCHES 1.363 where it should compress to 0.637 —
|
|
1496
|
+
# the head reads as turning the other way at its own edge. This one has to
|
|
1497
|
+
# REPLACE the transform with a table: an inverted band is not the closed
|
|
1498
|
+
# form at any angle, and §1.1 refuses a `transform` beside a `vertices` run.
|
|
1499
|
+
# Each vertex takes the shift for THE COLUMN IT IS IN, looked up in the rig:
|
|
1500
|
+
# a `vertices` run is positional, so a script that assumes the list's order
|
|
1501
|
+
# rather than reading it breaks in silence the day the list is renumbered.
|
|
1502
|
+
bun -e '
|
|
1503
|
+
const r = await Bun.file("gallery/portrait/rig.json").json();
|
|
1504
|
+
const m = await Bun.file("gallery/portrait/motion.json").json();
|
|
1505
|
+
const v = r.skins.default.head.head.vertices;
|
|
1506
|
+
const d = m.animations.turn.deform.find(x => x.slot === "head");
|
|
1507
|
+
const shift = {"-162": -22.414, "-120": -7.175, "0": -35.345, "120": -27.658, "162": -14.255};
|
|
1508
|
+
const run = [];
|
|
1509
|
+
for (let i = 0; i < v.length / 2; i++) run.push(shift[v[i * 2]], 0);
|
|
1510
|
+
for (const k of d.keys) if (k.transform) {
|
|
1511
|
+
delete k.transform;
|
|
1512
|
+
k.fromVertex = 0;
|
|
1513
|
+
k.vertices = run;
|
|
1514
|
+
}
|
|
1515
|
+
await Bun.write("/tmp/swapped.motion.json", JSON.stringify(m, null, 2));
|
|
1516
|
+
'
|
|
1517
|
+
bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/swapped.motion.json \
|
|
1518
|
+
--out /tmp/swapped --profile spine-html
|
|
1519
|
+
|
|
1520
|
+
# (b) FOLD IT: evaluate the same closed form at 40°, past the 31.37° of §4.2,
|
|
1521
|
+
# so the two far columns swap order and the mesh turns inside out. Since
|
|
1522
|
+
# §1.1 the whole of it is one number.
|
|
1523
|
+
bun -e '
|
|
1524
|
+
const m = await Bun.file("gallery/portrait/motion.json").json();
|
|
1525
|
+
const d = m.animations.turn.deform.find(x => x.slot === "head");
|
|
1526
|
+
for (const k of d.keys) if (k.transform) k.transform.degrees = 40;
|
|
1527
|
+
await Bun.write("/tmp/folded.motion.json", JSON.stringify(m, null, 2));
|
|
1528
|
+
'
|
|
1529
|
+
bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/folded.motion.json \
|
|
1530
|
+
--out /tmp/folded --profile spine-html
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
**What comes back from both:**
|
|
1534
|
+
|
|
1535
|
+
| | good | (a) one band inverted | (b) mesh folded |
|
|
1536
|
+
| --- | --- | --- | --- |
|
|
1537
|
+
| `--profile spine-html`, **without `A39`** | green | **green, and the same counts** | **green, and the same counts** |
|
|
1538
|
+
| `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | PASS | **PASS** | **PASS** |
|
|
1539
|
+
| the `MESH` coverage line | 100.00%, 95.90px past | **byte-identical** | **byte-identical** |
|
|
1540
|
+
| `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | PASS | PASS | **FAIL, both keys, 8 of 32 triangles** |
|
|
1541
|
+
| `--profile spine-html`, **with `A39`** | green | green | **refused, and nothing written** |
|
|
1542
|
+
| the `DEFORM` block, `head` key 1 `area` | x0.637174 … x1.319122 | **x0.765250 … x1.362834** | **x−0.288121 … x1.820211** |
|
|
1543
|
+
| … and its `winding` | 32 of 32 kept | 32 of 32 kept | **24 of 32 kept** |
|
|
1544
|
+
|
|
1545
|
+
🚨 **All three were green, and the coverage line is the same string in all three,
|
|
1546
|
+
because it reports the SETUP pose.** The `--profile spine-html` row with `A39` is
|
|
1547
|
+
the only verdict that separates them, and the two rows above it measure only the
|
|
1548
|
+
setup geometry: `A35` is silent about build (b). `A35` checks that a deform run *fits* its
|
|
1549
|
+
attachment — an honest and useful check, and orthogonal to whether the numbers in
|
|
1550
|
+
it mean anything.
|
|
1551
|
+
|
|
1552
|
+
⭐ **The two `DEFORM` rows are the ones that separate (a) from the good build**,
|
|
1553
|
+
and nothing else in the toolchain does that without a reference render. `A39` is
|
|
1554
|
+
right to pass (a) — no triangle reverses — so the whole of the difference is a
|
|
1555
|
+
pair of ratios, and the way to read them is to ask the tool rather than to copy
|
|
1556
|
+
them down:
|
|
1557
|
+
|
|
1558
|
+
```bash
|
|
1559
|
+
bun cli.ts explain --rig gallery/portrait/rig.json \
|
|
1560
|
+
--motion /tmp/swapped.motion.json --out /tmp/explain-swapped
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
```
|
|
1564
|
+
DEFORM turn default/head/head key 1 t=0.620000 authored table
|
|
1565
|
+
frame played on a track
|
|
1566
|
+
moved 25 of 25 vertices, worst 35.3450px at v10
|
|
1567
|
+
area min x0.765250 tri 19 max x1.362834 tri 1 (32 triangles, 0 with no area at the cleared pose, band 0.146694px²)
|
|
1568
|
+
stretch max x1.362834 tri 1 min x0.765250 tri 26
|
|
1569
|
+
winding 32 of 32 kept, 0 collapsed
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
🚨 **Read it band by band, because the extremes have swapped ends and comparing
|
|
1573
|
+
worst to worst hides that.** The block names the triangle beside every ratio, and
|
|
1574
|
+
§4.1's table says what each band should be. `tri 1` spans **−162 → −120**, the
|
|
1575
|
+
band §4.1 puts at **0.637** and the good build's own block reports as
|
|
1576
|
+
`x0.637174 tri 17` — the same band, and here it comes back at **x1.362834**. That
|
|
1577
|
+
is this section's prose, *"stretches 1.363 where it should compress to 0.637"*, as
|
|
1578
|
+
a figure the tool produces, and it is the far edge turning the wrong way. `tri 19`
|
|
1579
|
+
spans **−120 → 0**, tabled at 0.892 and measured at **x0.765250**: the swapped
|
|
1580
|
+
column is the boundary between those two bands, so the compression the far band
|
|
1581
|
+
gave up lands next door. ⚠️ Read as min-against-min and max-against-max instead,
|
|
1582
|
+
the very same four figures say 1.319 → 1.363 and 0.637 → 0.765 — two comparisons
|
|
1583
|
+
across *different* bands, both of them mild, neither of them what happened. Only
|
|
1584
|
+
the two bands the swapped columns bound move at all: the 0 → 120 and 120 → 162
|
|
1585
|
+
bands still read **1.064** and **1.319**, exactly as §4.1 tables them. `authored table` on the key line is the other half of the diagnosis:
|
|
1586
|
+
the model is gone, so nothing is left to check the ratios against but the ratios.
|
|
1587
|
+
|
|
1588
|
+
`rigc explain` is the instrument that prints every timeline's actual values, and
|
|
1589
|
+
on a deform that is the model and every offset it produced (AUTHORING §4.11.1)
|
|
1590
|
+
and the `DEFORM` block's geometry (AUTHORING §4.11.2) —
|
|
1591
|
+
|
|
1592
|
+
```bash
|
|
1593
|
+
bun cli.ts explain --rig gallery/portrait/rig.json \
|
|
1594
|
+
--motion gallery/portrait/motion.json --out /tmp/explain
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
```
|
|
1598
|
+
DEFORM turn default/head/head key 1 t=0.620000 transform yaw radius=170 degrees=12
|
|
1599
|
+
frame played on a track
|
|
1600
|
+
moved 25 of 25 vertices, worst 35.3450px at v2
|
|
1601
|
+
area min x0.637174 tri 17 max x1.319122 tri 31 (32 triangles, 0 with no area at the cleared pose, band 0.146694px²)
|
|
1602
|
+
stretch max x1.319121 tri 22 min x0.637175 tri 8
|
|
1603
|
+
winding 32 of 32 kept, 0 collapsed
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
⇒ **`0.637` is a figure the tool prints.** It is a *report* and not a bar —
|
|
1607
|
+
`explain` takes no `--profile` and gates nothing, because a deliberate
|
|
1608
|
+
3× stretch is a real thing to author, and the one deformed-geometry fault with no
|
|
1609
|
+
legitimate counter-example is the fold, which is `A39`'s.
|
|
1610
|
+
|
|
1611
|
+
**`A39_DEFORM_KEEPS_TRIANGLE_WINDING` closes the half of that gap the fold
|
|
1612
|
+
lives in** — `--profile spine-html`. Build (b) above is refused
|
|
1613
|
+
by name, on both its keys, with the triangles listed:
|
|
1614
|
+
|
|
1615
|
+
```
|
|
1616
|
+
FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head key 1
|
|
1617
|
+
(t=0.6200000047683716s): 8 of 32 triangle(s) reverse winding — triangle 0
|
|
1618
|
+
[0,15,16] 1890.000 -> -544.548px²; …
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
and builds (a) and the good one both still PASS it, because **an inverted band is
|
|
1622
|
+
not a fold**: its winding survives. The angle A39 first fires at agrees with
|
|
1623
|
+
§4.2's `tan θ = Δx/Δz` to **0.0001°**, so the formula above is checkable by
|
|
1624
|
+
running the gate instead of by rendering seven variants.
|
|
1625
|
+
|
|
1626
|
+
**And one thing it does not refuse.** Build (b) is refused
|
|
1627
|
+
because the head is *drawn* while it folds. Fade that slot to alpha exactly 0 over
|
|
1628
|
+
the same keys — §4.2's fourth way out, and what a face past its ceiling actually
|
|
1629
|
+
does — and the same build is green, with the key still measured and the reason
|
|
1630
|
+
printed rather than passed over in silence:
|
|
1631
|
+
|
|
1632
|
+
```
|
|
1633
|
+
DEFORM turn default/head/head key 1 t=0.500000 transform yaw radius=170 degrees=40
|
|
1634
|
+
skipped A39 reads no winding off this key: the slot's alpha is exactly 0 at this
|
|
1635
|
+
time (slot 0.0000 x attachment 1.0000), so this key draws no pixels — a
|
|
1636
|
+
triangle that draws no pixels cannot draw them backwards
|
|
1637
|
+
winding 24 of 32 kept, 0 collapsed <- a fold, and nothing gates it: this key draws no pixels
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
`A39`'s message says the mesh "draws its texture backwards there", and that
|
|
1641
|
+
sentence is false when the slot draws nothing. The bar is **alpha exactly
|
|
1642
|
+
0**; at 0.5 build (b) is refused, with the alpha in the message. The
|
|
1643
|
+
same fold at full alpha in another animation is refused, because the
|
|
1644
|
+
measurement is of one key at one time.
|
|
1645
|
+
|
|
1646
|
+
🚨 **And a key is not the whole of it: the geometry between two keys is
|
|
1647
|
+
interpolated.** Put the alpha-0 key exactly on
|
|
1648
|
+
the 40° key and 8 triangles are already reversed at `t=0.4`, where the slot is
|
|
1649
|
+
still drawing at **alpha 0.20**:
|
|
1650
|
+
|
|
1651
|
+
| t | slot alpha | reversed triangles |
|
|
1652
|
+
| --- | ---: | ---: |
|
|
1653
|
+
| 0.30 | 0.40 | 0 |
|
|
1654
|
+
| 0.40 | **0.20** | **8** |
|
|
1655
|
+
| 0.45 | 0.10 | 8 |
|
|
1656
|
+
| 0.49 | 0.02 | 8 |
|
|
1657
|
+
| 0.50 | 0.00 | 8 |
|
|
1658
|
+
|
|
1659
|
+
📌 The table and the refusal below are on the **turn probe** — this head's own
|
|
1660
|
+
five columns and 32 triangles on a one-second timeline, which is why the times
|
|
1661
|
+
are not build (b)'s.
|
|
1662
|
+
|
|
1663
|
+
**Every interval between two consecutive deform keys is scanned.** A deform interpolated between two
|
|
1664
|
+
keys travels a **straight line through offset space**, so a triangle's signed
|
|
1665
|
+
area is a *quadratic in the interpolation fraction* — the fold is a root of it,
|
|
1666
|
+
solved for rather than searched, with no sample spacing anybody would have to
|
|
1667
|
+
defend. What that arithmetic names is then posed and measured by the same code
|
|
1668
|
+
that measures a key, **alpha read at that same instant**, so the fade a correct
|
|
1669
|
+
rig relies on is not refused and the frames it does not cover are. The sentence names
|
|
1670
|
+
the two keys the fold lies between rather than one key index, the time it solved for
|
|
1671
|
+
and how far along the segment that is, the reversed triangles with their signed areas,
|
|
1672
|
+
`NO KEY LANDS THERE` in those words, and the alpha read at that same instant — so what
|
|
1673
|
+
it refuses is legible as a frame rather than as a key. AUTHORING §4.11.3 reads it field
|
|
1674
|
+
by field and [`src/validate.ts`](../src/validate.ts) builds it; no spec this repository
|
|
1675
|
+
ships produces one.
|
|
1676
|
+
|
|
1677
|
+
⇒ The rule — *fade out over the run up to the angle you cannot
|
|
1678
|
+
take, so that every key past the ceiling is one that draws nothing* — is a
|
|
1679
|
+
**measurement**: land the alpha-0 key
|
|
1680
|
+
on the fold and the build is refused, with the frame it is refused for.
|
|
1681
|
+
|
|
1682
|
+
⚠️ **What that scan cannot see**: the closed form holds the BONES still across the span. On an
|
|
1683
|
+
unweighted attachment that is exact — one matrix multiplies every vertex and its
|
|
1684
|
+
determinant cancels out of the sign comparison — but on a weighted mesh whose
|
|
1685
|
+
bones move across the span it is an approximation, and a prediction no
|
|
1686
|
+
measurement reproduced is reported (`deformSpansUnconfirmed`) rather than
|
|
1687
|
+
refused. A fold caused by the bones alone is not this rule's subject at all, and
|
|
1688
|
+
`check` against a trusted render (§9.3) is what sees it.
|
|
1689
|
+
|
|
1690
|
+
⚠️ **One thing it deliberately does not do.** It is an **archetype** rule, so a
|
|
1691
|
+
`--profile spine` build reads `PROF` — the premise "a fold has no legitimate
|
|
1692
|
+
counter-example" is false: an official
|
|
1693
|
+
`spineboy-pro` export reverses one of `hoverboard-board`'s 101 triangles, and a
|
|
1694
|
+
`validity` rule would have told its author to change correct data. ⇒ `explain`'s
|
|
1695
|
+
`DEFORM` block is the surface with **no profile at all**, so the winding count is
|
|
1696
|
+
readable on a `--profile spine` build the gate will not mention it to.
|
|
1697
|
+
|
|
1698
|
+
`check` against a trusted render, below, stays the deeper instrument for the
|
|
1699
|
+
reasons §9.3 gives, and these two are the cheap always-on layer above it.
|
|
1700
|
+
|
|
1701
|
+
### 9.3 The audit that works today, and exactly what it cannot do
|
|
1702
|
+
|
|
1703
|
+
`rigc check` renders a candidate onto reference frames' own pixel grid and
|
|
1704
|
+
compares (INGEST §1.4). Point it at a render of a build you already trust and it
|
|
1705
|
+
**does** see a wrong deform:
|
|
1706
|
+
|
|
1707
|
+
```bash
|
|
1708
|
+
bun cli.ts check --candidate gallery/portrait/build --frames gallery/portrait/render/turn@25fps
|
|
1709
|
+
bun cli.ts check --candidate /tmp/swapped --frames gallery/portrait/render/turn@25fps
|
|
1710
|
+
bun cli.ts check --candidate /tmp/folded --frames gallery/portrait/render/turn@25fps
|
|
1711
|
+
```
|
|
1712
|
+
|
|
1713
|
+
⚠️ **The third of those needs a build §9.2's own command will not write.** Build (b)
|
|
1714
|
+
under `--profile spine-html` is refused and nothing
|
|
1715
|
+
lands in `/tmp/folded` — which is the row above it in §9.2's table, working. Take
|
|
1716
|
+
that candidate from the **default** profile, where `A39` reads `PROF` and the
|
|
1717
|
+
artifact is written — a profile selects which assertions apply and not what is
|
|
1718
|
+
emitted, and on this rig the two profiles write a `skeleton.json` and a
|
|
1719
|
+
`skeleton.atlas` that are identical byte for byte.
|
|
1720
|
+
|
|
1721
|
+
**No run reproduces this:** the three commands above read a build directory and two `/tmp` paths this repository does not track, so no gate reaches them; re-taken by hand against a render of the good build
|
|
1722
|
+
|
|
1723
|
+
| Candidate | MAE mean | worst | at |
|
|
1724
|
+
| --- | --- | --- | --- |
|
|
1725
|
+
| the build the frames came from | **0.00** | 0.00 | — |
|
|
1726
|
+
| (a) one band inverted | **0.20** | 0.38 | **f0016** |
|
|
1727
|
+
| (b) mesh folded | **2.07** | 3.66 | **f0016** |
|
|
1728
|
+
|
|
1729
|
+
⭐ **Both defects land on `f0016`, which is the frame the turn arrives on** — the
|
|
1730
|
+
`worst at` column points straight at the moment, which is what makes this worth
|
|
1731
|
+
running at all.
|
|
1732
|
+
|
|
1733
|
+
🚨 **And now the three limits, because this is the instrument you will be tempted
|
|
1734
|
+
to call an audit:**
|
|
1735
|
+
|
|
1736
|
+
1. **It is differential.** It measures a candidate against **a render of another
|
|
1737
|
+
build**, so it catches a *regression* and cannot validate a *first authoring*.
|
|
1738
|
+
There is no reference for a face nobody has drawn yet.
|
|
1739
|
+
2. **A wrong projection is a whisper in the aggregate.** Inverting a whole band —
|
|
1740
|
+
the far edge stretching to **1.363** where it should compress to **0.637**, so
|
|
1741
|
+
the head's own edge turns the wrong way — moves the mean MAE by **0.20 of
|
|
1742
|
+
255**. Nothing about that number says *"the winding"*; you have to already
|
|
1743
|
+
suspect it.
|
|
1744
|
+
3. **The `slot drift` column cannot see it at all.** It was `1.2 px "lid_r"` in
|
|
1745
|
+
**all three** runs above, unchanged, because drift is attributed per **slot**
|
|
1746
|
+
and a folded head mesh is entirely inside one slot.
|
|
1747
|
+
|
|
1748
|
+
📌 **`explain`'s `DEFORM` block (§9.2, AUTHORING §4.11.2) takes half of limits 1
|
|
1749
|
+
and 2 away, and none of limit 3.** It is reference-free, so it says something
|
|
1750
|
+
about a first authoring; and it is *per key and per triangle*, so the band
|
|
1751
|
+
inversion above reads as `x1.362834` on `tri 1` where the same band is `x0.637174`
|
|
1752
|
+
in the model, rather than as 0.20 of 255 in an aggregate. What it still cannot say is whether **12° was the angle the shot
|
|
1753
|
+
wanted** — that needs the picture, which is why the procedure below survives the
|
|
1754
|
+
block as it survived `A39`.
|
|
1755
|
+
|
|
1756
|
+
📌 **`explain`'s `MEMBER` block (§3.1, AUTHORING §4.5.2) does the same for the
|
|
1757
|
+
bone half, and it takes the **nose test** off the procedure below.** §3 makes the
|
|
1758
|
+
nose the diagnostic — *if the nose's residual is not negative, the depths are
|
|
1759
|
+
wrong* — and the block prints the six residuals in a column with the depth that
|
|
1760
|
+
produced each one, so the check is reading one sign. ⚠️ It still cannot say
|
|
1761
|
+
whether the **depth** was right: `nose at depth 192` evaluates as consistently
|
|
1762
|
+
wrong as it does right, and no reference frame separates a plausible depth table
|
|
1763
|
+
from the intended one.
|
|
1764
|
+
|
|
1765
|
+
⇒ **So the honest procedure — thinner for `A39` and the two report blocks, and
|
|
1766
|
+
still a procedure, because none of the three limits above is one they
|
|
1767
|
+
lift:** state the model on the key rather than deriving a table (§1.1 for the
|
|
1768
|
+
mesh, §3.1 for the bones), so what a reviewer reads is a radius, an angle and a
|
|
1769
|
+
depth per part; read the **nose's sign** off the `MEMBER` block and check the
|
|
1770
|
+
**fold angle** (§4.2's formula) arithmetically before you build — the second is
|
|
1771
|
+
still yours, and a stated model evaluates a wrong radius as consistently as a
|
|
1772
|
+
right one — then render, **look at three scales**, and keep a render of the last
|
|
1773
|
+
build you trusted so `check` has something to be differential against.
|
|
1774
|
+
|
|
1775
|
+
📌 **What §1.1 and §9.2's block make readable, and what neither does.**
|
|
1776
|
+
`explain` prints the model beside the offsets it produced, so *what a key claims* is readable; the `DEFORM` block prints what the
|
|
1777
|
+
key **did** — the area and stretch extremes, the displacement and the winding —
|
|
1778
|
+
so *what the claim came to* is readable too, per key and with no reference
|
|
1779
|
+
(`A39` catches the fold inside it). *Whether the claim is right* is not: **nothing
|
|
1780
|
+
measures whether 12° was the angle the shot wanted**, and nothing above is a
|
|
1781
|
+
substitute for looking at three scales.
|
|
1782
|
+
|
|
1783
|
+
---
|
|
1784
|
+
|
|
1785
|
+
## 10. The worked example
|
|
1786
|
+
|
|
1787
|
+
[`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait)
|
|
1788
|
+
is this page on real art — **22 parts, 27 bones, 22 slots, 2 meshes, 40
|
|
1789
|
+
vertices**, three animations, and a README that derives every number rather than
|
|
1790
|
+
listing it. Its
|
|
1791
|
+
[`FINDINGS.md`](https://github.com/firejune/rigc/tree/main/gallery/portrait/FINDINGS.md)
|
|
1792
|
+
is the measurement half: what it cost, the seven-angle sweep, the five tool gaps
|
|
1793
|
+
it filed.
|
|
1794
|
+
|
|
1795
|
+
📘 **The `pitch` has its own worked case**, and this page deliberately stays a
|
|
1796
|
+
yaw: [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) is
|
|
1797
|
+
the same two closed forms on the other axis, plus a travelling `wave` (AUTHORING
|
|
1798
|
+
§4.11.1). §4.2 and §5 above point at the two places its figures are worth
|
|
1799
|
+
reading beside these — the fold angle bracketed against `A39`, and a
|
|
1800
|
+
foreshortening span that is wider at the same angle.
|
|
1801
|
+
|
|
1802
|
+
```bash
|
|
1803
|
+
bun install # once
|
|
1804
|
+
|
|
1805
|
+
bun cli.ts build --rig gallery/portrait/rig.json \
|
|
1806
|
+
--motion gallery/portrait/motion.json \
|
|
1807
|
+
--out gallery/portrait/build
|
|
1808
|
+
bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
|
|
1809
|
+
--out gallery/portrait/render
|
|
1810
|
+
bun cli.ts preview --candidate gallery/portrait/build \
|
|
1811
|
+
--out gallery/portrait/preview.html
|
|
1812
|
+
|
|
1813
|
+
# do the cycles close on the poses they opened with?
|
|
1814
|
+
bun gallery/loop_seam.ts gallery/portrait/render/idle@25fps
|
|
1815
|
+
bun gallery/loop_seam.ts gallery/portrait/render/gaze@25fps
|
|
1816
|
+
bun gallery/loop_seam.ts gallery/portrait/render/turn@25fps
|
|
1817
|
+
```
|
|
1818
|
+
|
|
1819
|
+
`build`, `render` and `preview` are not committed; the specs and the 22 part PNGs
|
|
1820
|
+
are, and those commands regenerate the rest. **The frames to look at:**
|
|
1821
|
+
|
|
1822
|
+
| Frame | What it is |
|
|
1823
|
+
| --- | --- |
|
|
1824
|
+
| `render/turn@25fps/f0016.png` | the yaw arrives. **Look at this one at 1:1** — a contact sheet cannot show you whether the face turned or merely slid |
|
|
1825
|
+
| `render/turn@25fps/f0000.png` | rest, for the comparison. The pair is the whole example |
|
|
1826
|
+
| `render/idle@25fps/f0028.png` | the blink, shut |
|
|
1827
|
+
| `render/gaze@25fps/f0015.png` | the gaze, held |
|
|
1828
|
+
|
|
1829
|
+
**What those commands print** — re-run verbatim for this page, from a checkout
|
|
1830
|
+
with no `build/`, `render/` or `preview.html` in that directory. The `build`
|
|
1831
|
+
says it in its own words, and these are the four lines this page's claims about
|
|
1832
|
+
it come off:
|
|
1833
|
+
|
|
1834
|
+
```
|
|
1835
|
+
MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
|
|
1836
|
+
MESH hair_bang authored 15 vertices / 16 triangles (budget 32) bones=[bang] attachments=[hair_bang] covers 100.00% of the art, reaching 55.22px past it
|
|
1837
|
+
.. validate (spine-core round trip + machine assertions, profile spine)
|
|
1838
|
+
.. profile spine — 8 renderer-policy and 8 archetype assertion(s) do not apply
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
`spine` is the default, so that is the `build` above with no `--profile` on it —
|
|
1842
|
+
the report names what the profile leaves out rather than this page counting it.
|
|
1843
|
+
§9.2 runs the same rig under `--profile spine-html` and quotes what comes back
|
|
1844
|
+
there; `A13_MESH_BUDGET` and `A15_IDLE_NO_MESH_BONE_KEYS` are among the rules
|
|
1845
|
+
the default profile excludes.
|
|
1846
|
+
|
|
1847
|
+
**No run reproduces this:** the three rows below come from commands that read the build directory this repository does not track, so no gate reaches them; re-taken by hand from a clean `gallery/portrait`
|
|
1848
|
+
|
|
1849
|
+
| Command | What came back |
|
|
1850
|
+
| --- | --- |
|
|
1851
|
+
| `render --fps 25 --max 640` | **81 + 39 + 56 frames**, 478×640, three contact sheets |
|
|
1852
|
+
| `loop_seam.ts` ×3 | **0 / 255**, **0 of 305 920 pixels** differing, for all three |
|
|
1853
|
+
| `preview` | one **414.5 KiB** HTML file, 22 pages embedded as data URIs — the figure the tool prints |
|
|
1854
|
+
|
|
1855
|
+
📊 **Figures this page took from the record rather than re-deriving**, because
|
|
1856
|
+
they need the artifact's own pixels: the blink's occlusion (hiding the whole eye
|
|
1857
|
+
assembly at the shut hold changes **0 of 305 920** pixels; positive control at
|
|
1858
|
+
rest moves **8 183**), the per-edge narrowing displacements, the `spine-core`
|
|
1859
|
+
agreement of every posed column and scale with §1's line to **under 0.001 px**,
|
|
1860
|
+
and the Web Player interop pass (**0 console errors, 0 page exceptions**).
|
|
1861
|
+
`setup: { "slot": null }`, the obvious way to hide one slot, is a named
|
|
1862
|
+
`rigc compile error` that gives the spelling. Measured on a copy of this
|
|
1863
|
+
example's `motion.json` carrying one such entry, with the path the run echoes
|
|
1864
|
+
shortened and the line wrapped:
|
|
1865
|
+
|
|
1866
|
+
```
|
|
1867
|
+
rigc compile error: motion.json: `setup."eye_l"` is null; a setup entry is an
|
|
1868
|
+
object of `{ attachment?: string | null, color?: [r, g, b, a] }` — to show
|
|
1869
|
+
nothing there write `"eye_l": { "attachment": null }`, and to show an attachment
|
|
1870
|
+
write `"eye_l": { "attachment": "<name>" }`
|
|
1871
|
+
```
|
|
1872
|
+
|
|
1873
|
+
⚠️ **And the second refusal says where that edit goes.** Writing the spelling that message names on a slot this rig
|
|
1874
|
+
gives an attachment to is refused in turn — `slot "eye_l" has a setup attachment
|
|
1875
|
+
in the rig spec AND in the motion spec; the setup pose has one author` — so on
|
|
1876
|
+
the worked example hiding one slot is an edit to the **rig** spec rather
|
|
1877
|
+
than a line in the motion spec.
|
|
1878
|
+
|
|
1879
|
+
⭐ **Vela is a second cast member and that was deliberate**, against the gallery's
|
|
1880
|
+
own rule that its examples share one drawing. A 2.5D turn reads off four things: a
|
|
1881
|
+
brow that frames an eye, an **iris and a highlight as separate parts**, hair in
|
|
1882
|
+
**layers** that can lag the skull, and a cheek-to-jaw silhouette with a landmark
|
|
1883
|
+
in it to foreshorten. The gallery's mascot has a muzzle, and a muzzle points
|
|
1884
|
+
wherever the head points — so a mascot's turn is a bone rotation and nothing
|
|
1885
|
+
else, which is precisely the move this page is not about. ⇒ **If the art you were
|
|
1886
|
+
handed has no landmark to foreshorten, a turn will not read no matter how the
|
|
1887
|
+
mesh is built**, and that is worth saying to the user before you build it.
|
|
1888
|
+
|
|
1889
|
+
---
|
|
1890
|
+
|
|
1891
|
+
## 11. Non-goals — stated, so nobody proposes them as gaps
|
|
1892
|
+
|
|
1893
|
+
🚫 **No command generates a turn, and neither model construct is one.**
|
|
1894
|
+
§1 is one line of arithmetic; a `rigc yaw --degrees 12` would be guessing at
|
|
1895
|
+
every depth in §2 on the user's behalf, and depth is the parameter the *author*
|
|
1896
|
+
is choosing. Both constructs are the other thing — **a way to say the model in
|
|
1897
|
+
the spec** (§1.1 for the mesh, §3.1 for the bones) — so the radius, the angle and
|
|
1898
|
+
**every depth** arrive from the author and the compiler only evaluates. Neither one generates an in-between
|
|
1899
|
+
either: a model is evaluated at one key, and sweeping an angle is editing one
|
|
1900
|
+
number per key. What the toolchain owes is that the file is checkable,
|
|
1901
|
+
that you can look, and that a person can choose.
|
|
1902
|
+
|
|
1903
|
+
🚫 **No pass bar for a face, and nothing here to hang one on.** MOTION.md's
|
|
1904
|
+
banner applies unchanged: `build` says a file is valid, `render` and `preview`
|
|
1905
|
+
let you look, `vote` lets a person choose. The angles in §8 are where a
|
|
1906
|
+
construction **stopped reading for one viewer looking at one drawing**, not
|
|
1907
|
+
thresholds.
|
|
1908
|
+
|
|
1909
|
+
🚫 **No claim that 12° is the right angle for any request.** It is the angle the
|
|
1910
|
+
worked example ships, chosen to sit comfortably inside a 5-column grid's
|
|
1911
|
+
17.65° tangent limit. §4.2 is how to pick your own, and picking it **first** is
|
|
1912
|
+
the entire point of that section.
|
|
1913
|
+
|
|
1914
|
+
⚠️ **Not a Live2D comparison, and not a recommendation between formats.** What
|
|
1915
|
+
the worked example measured is that a portrait turn is authorable on plain Spine
|
|
1916
|
+
4.3 at draft quality — nothing outside the format, no plugin, no runtime patch —
|
|
1917
|
+
and that the **split is authoring cost rather than runtime capability**. Neither
|
|
1918
|
+
the deform table nor the track table is transcribed (§1.1, §3.1), so the
|
|
1919
|
+
remaining cost is not on the keyboard but on the **parts**: per-eye meshes, a meshed neck, a
|
|
1920
|
+
second art layer for the far cheek (§8). Whether to pay *that* is a project's
|
|
1921
|
+
decision and this page does not make it.
|
|
1922
|
+
|
|
1923
|
+
🚫 **No Live2D file is read or written, and none ever will be — a boundary
|
|
1924
|
+
rather than an unbuilt feature, and it runs in both directions.** rigc's inputs
|
|
1925
|
+
are a rig spec and a motion spec; its outputs are Spine 4.3 skeleton data and an
|
|
1926
|
+
atlas. There is no importer, no exporter and no converter for `.moc3`, `.cmo3`,
|
|
1927
|
+
`.model3.json` or anything else in that family, and nothing in this repository
|
|
1928
|
+
claims compatibility with that format in either direction.
|
|
1929
|
+
⭐ **What is in scope is an authoring idea, stated on its own terms rather than
|
|
1930
|
+
as anybody's feature: that a face angle can be a value rather than a time.**
|
|
1931
|
+
§8's *The turn as a value rather than a time* is that idea on Spine's own
|
|
1932
|
+
`slider` constraint, and every mechanism under it is Spine's — the arithmetic,
|
|
1933
|
+
the flags, the readers and the failure modes are all in AUTHORING §3.5.2 and all
|
|
1934
|
+
measured against `spine-core`. ⚠️ Nothing on this page is a statement about how
|
|
1935
|
+
any other tool works inside, and nothing above implies one: what this repository
|
|
1936
|
+
has measured is its own format.
|
|
1937
|
+
|
|
1938
|
+
🚫 **No per-eye mesh recipe.** §8 says the eyes need their own deform meshes past
|
|
1939
|
+
about 26°, and nobody has built that here. The column-placement arithmetic in
|
|
1940
|
+
§4.2 applies to any grid over any curved patch, so the tangent limit is the part
|
|
1941
|
+
that carries over; **what a lash line and a wrapping lid need is unmeasured, and
|
|
1942
|
+
this page does not guess.**
|
|
1943
|
+
|
|
1944
|
+
🚫 **No expression system, no visemes, no phoneme mapping.** A face that *acts* is
|
|
1945
|
+
a different document and a different measurement. Everything here is one head
|
|
1946
|
+
turning, blinking and looking — and the one general lesson that might carry into
|
|
1947
|
+
that work is §7's: **allocate the channels before the first key, because Spine
|
|
1948
|
+
blends and does not add.**
|