spine-rigc 0.12.0 β 0.13.0
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/README.md +1 -0
- package/docs/AUTHORING.md +14 -2
- package/docs/FACE.md +942 -0
- package/docs/MOTION.md +10 -0
- package/package.json +2 -1
- package/src/compile.ts +42 -0
- package/src/pose.ts +14 -6
- package/src/render.ts +115 -19
package/README.md
CHANGED
|
@@ -448,6 +448,7 @@ the art. Its shape is under
|
|
|
448
448
|
| --- | --- |
|
|
449
449
|
| π **[docs/AUTHORING.md](docs/AUTHORING.md)** | **the format guide, and the one to read before writing a spec.** Both input files field by field with a complete minimal example each, every field with its Spine meaning, the rules that decide what is emitted, the build β read the report β fix β repeat loop, the map from every named failure to the file that has to change, and the features rigc refuses by name so you do not spend a loop discovering them. It travels **inside the npm package**, at `node_modules/spine-rigc/docs/AUTHORING.md` |
|
|
450
450
|
| ποΈ **[docs/MOTION.md](docs/MOTION.md)** | **the key-pose recipe.** How to get two poses, what a pair of poses does and does not fix, the in-betweening rules and where each comes from, and how to spread candidates so a ballot informs. Ships in the package too |
|
|
451
|
+
| π **[docs/FACE.md](docs/FACE.md)** | **authoring a face.** A blink, a gaze and a 2.5D head turn on plain Spine data: the one line of yaw arithmetic every number in a turn comes from, depth as the parameter you are actually authoring, where to put a grid's columns and the angle at which any grid folds, what foreshortens and what does not, channel allocation before the first key, and the three cliffs with their angles. Also the deform audit gap, demonstrated β a folded mesh gates green β and the differential check that works today |
|
|
451
452
|
| π₯ **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
|
|
452
453
|
| π€ **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** β the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
|
|
453
454
|
| π¬ **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
|
package/docs/AUTHORING.md
CHANGED
|
@@ -1681,8 +1681,20 @@ where the remedy is a line you own. `A35` does **not** refuse it: it is pointed
|
|
|
1681
1681
|
other people's files, and a rule stricter than the runtime tells its reader to go
|
|
1682
1682
|
and break correct data.
|
|
1683
1683
|
|
|
1684
|
-
πΌοΈ **Worked
|
|
1685
|
-
|
|
1684
|
+
πΌοΈ **Worked examples, and they use a deform for two different things.**
|
|
1685
|
+
[`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) β a 9-vertex ball squashed about its contact point,
|
|
1686
|
+
with the two affine transforms its keys were derived from.
|
|
1687
|
+
[`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait) β a 2.5D head turn, where the keys are the
|
|
1688
|
+
**projection of a yaw** rather than a squash: two grid meshes whose columns are
|
|
1689
|
+
placed to sample a cosine, every offset one line of arithmetic, and a measured
|
|
1690
|
+
account of the angle past which the mesh folds.
|
|
1691
|
+
|
|
1692
|
+
π **[FACE.md](FACE.md) is the recipe for that second case**, and it is where the
|
|
1693
|
+
grid questions this section leaves to its reader are answered: where to put the
|
|
1694
|
+
columns, why `hull: 0` is the honest declaration on a grid, the closed form for
|
|
1695
|
+
the angle at which any column pair folds β and the fact that a folded key passes
|
|
1696
|
+
`A35` and every other assertion, so the arithmetic has to be checked before the
|
|
1697
|
+
build.
|
|
1686
1698
|
|
|
1687
1699
|
---
|
|
1688
1700
|
|
package/docs/FACE.md
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
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
|
+
- Timing, easing, arcs, anticipation, follow-through, the per-bone offset table:
|
|
23
|
+
**MOTION Β§3** β a blink and a gaze are ordinary MOTION.md work
|
|
24
|
+
- Candidate spreading and the ballot: **MOTION Β§4βΒ§5**. Nothing on this page
|
|
25
|
+
replaces a person's eye
|
|
26
|
+
- A skeleton somebody else authored, and moving a pivot inside it:
|
|
27
|
+
[INGEST.md](INGEST.md)
|
|
28
|
+
- The worked example every number below comes from:
|
|
29
|
+
[`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait),
|
|
30
|
+
and its measurement half,
|
|
31
|
+
[`FINDINGS.md`](https://github.com/firejune/rigc/tree/main/gallery/portrait/FINDINGS.md)
|
|
32
|
+
|
|
33
|
+
π¨ **Nothing in this toolchain measures what a `deform` key does, and that is the
|
|
34
|
+
one gap you have to author around.** The setup geometry is measured and printed β
|
|
35
|
+
coverage, overshoot, hole. The *deformed* geometry is not. A key that turns a
|
|
36
|
+
mesh inside out gates green β **26 PASS on `--profile spine-html`, the same as
|
|
37
|
+
the good build, and the same coverage line reporting the setup pose at
|
|
38
|
+
100.00%.** Β§9.2 demonstrates that with three builds and Β§9.3 gives you the
|
|
39
|
+
differential audit that works today;
|
|
40
|
+
[issue #296](https://github.com/firejune/rigc/issues/296) is the instrument that
|
|
41
|
+
would close it. Reference it, do not wait for it.
|
|
42
|
+
|
|
43
|
+
π **Where the numbers on this page come from.** Every figure marked **derived**
|
|
44
|
+
is re-computed from the closed form in Β§1 and reproduces to the digits printed.
|
|
45
|
+
Every figure marked **measured** was read off the shipped artifact by the
|
|
46
|
+
`gallery/portrait` record and carries its scope in the line. Nothing here is a
|
|
47
|
+
prediction, and nothing here is a pass bar.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 0. The normal form
|
|
52
|
+
|
|
53
|
+
**A face request normalises to a depth list plus MOTION.md.** That is the whole
|
|
54
|
+
internal shape:
|
|
55
|
+
|
|
56
|
+
| What arrived | What it becomes |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| a face that **blinks**, **breathes**, **looks around** | ordinary MOTION.md tracks. A lid, a chest, an iris. Nothing on this page is needed |
|
|
59
|
+
| 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 |
|
|
60
|
+
| 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 |
|
|
61
|
+
|
|
62
|
+
β **The turn is the only part of a face that is not already MOTION.md's job**,
|
|
63
|
+
and it is 90% of this page. A blink is a translating plate (Β§6); a gaze is
|
|
64
|
+
MOTION Β§3.7's offset table applied to an eye (Β§7). If the request does not
|
|
65
|
+
contain a turn, read Β§6 and Β§7 and stop.
|
|
66
|
+
|
|
67
|
+
The one thing that is genuinely new: a turn is **not a pose you can key**. It is
|
|
68
|
+
a projection, so its values are not chosen, they are *evaluated* β and the
|
|
69
|
+
authoring is transcription rather than judgement. Which means the failure mode is
|
|
70
|
+
not "it reads badly", it is **"one number is wrong and nothing can see it"**.
|
|
71
|
+
That is what Β§3 and Β§9 are for.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 1. The one line, and that it **is** one line
|
|
76
|
+
|
|
77
|
+
Treat the face as painted on a cylinder standing on the skull's vertical axis. A
|
|
78
|
+
yaw of `t` about that axis carries a point at `(x, z)` β `x` across the screen,
|
|
79
|
+
`z` toward the viewer β to `xΒ·cos t β zΒ·sin t`, so the shift a part takes is
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
dx = xΒ·(cos t β 1) β zΒ·sin t
|
|
83
|
+
\____________/ \______/
|
|
84
|
+
the head depth times sin t:
|
|
85
|
+
narrowing to the WHOLE of the move
|
|
86
|
+
cos t of its
|
|
87
|
+
width (2.2% at 12Β°)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**That is the entire model.** Everything else on this page is that expression
|
|
91
|
+
evaluated somewhere, and the two terms are worth reading separately:
|
|
92
|
+
|
|
93
|
+
- **`βzΒ·sin t` is proportional to depth.** A part further forward travels
|
|
94
|
+
further. That *is* parallax, and it is why a fringe moves more than a face and
|
|
95
|
+
why hair behind the axis moves the **other way** (Β§2).
|
|
96
|
+
- **`xΒ·(cos t β 1)` pulls both edges inward by the same amount** β the head
|
|
97
|
+
narrowing. It is second order and it is small: at 12Β° it is 2.2%, which is 3.5
|
|
98
|
+
units at the silhouette against a 35-unit centre shift.
|
|
99
|
+
|
|
100
|
+
At the 12Β° the worked example ships (**derived**):
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
t = 0.209440 rad cos t β 1 = β0.021852 sin t = 0.207912
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
β **A reader who has this line derives every number in a turn and never reaches
|
|
107
|
+
for a hand-tuned table.** That matters more than it sounds: a hand-tuned face
|
|
108
|
+
table has no property you can check, and a derived one has exactly one β it
|
|
109
|
+
agrees with the line, or it does not.
|
|
110
|
+
|
|
111
|
+
π **Author `t` in degrees in your notes and radians nowhere.** The spec holds
|
|
112
|
+
neither; it holds the millimetre-level results. Write the angle in a comment
|
|
113
|
+
beside the part list, because the angle is the only input a later reader can use
|
|
114
|
+
to re-derive the file, and Β§9 is what happens when they cannot.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 2. Depth is the parameter you are actually authoring
|
|
119
|
+
|
|
120
|
+
π¨ **A part list for a face is not a list of drawings. It is a list of
|
|
121
|
+
`(x, z)`.** And here is the sharp edge:
|
|
122
|
+
|
|
123
|
+
> **`x` is in the file. `z` is not.** Neither spec has a depth field, and on the
|
|
124
|
+
> worked example
|
|
125
|
+
>
|
|
126
|
+
> ```bash
|
|
127
|
+
> grep -c '"z"' gallery/portrait/rig.json gallery/portrait/motion.json # 0 and 0
|
|
128
|
+
> ```
|
|
129
|
+
>
|
|
130
|
+
> The bones carry `x` (`eye_l` at `β62`, `nose` at `0`), the rig carries the mesh
|
|
131
|
+
> columns' `x`, and every `z` that produced every number lives **only in the
|
|
132
|
+
> README beside them**.
|
|
133
|
+
|
|
134
|
+
β **Write the depth table down somewhere a reader will find it, because the
|
|
135
|
+
toolchain will not keep it for you.** In the worked example that is a table in
|
|
136
|
+
the example's own README. In a project, put it beside the rig. This is
|
|
137
|
+
[#294](https://github.com/firejune/rigc/issues/294) seen from the authoring side:
|
|
138
|
+
the spec can hold a projection's results but cannot state the projection.
|
|
139
|
+
|
|
140
|
+
**The depths in the worked example**, and what each one is doing (**derived**
|
|
141
|
+
column: `dx` at 12Β°):
|
|
142
|
+
|
|
143
|
+
| Part | `x` | `z` | `dx` | What the depth is |
|
|
144
|
+
| --- | --- | --- | --- | --- |
|
|
145
|
+
| face centre (skull surface) | 0 | **170** | β35.345 | `R`, the cylinder radius. Every other number is relative to this |
|
|
146
|
+
| fringe centre | 0 | **196** | β40.751 | 26 **in front** of the skull. The one number in the rig that exists only to make parallax |
|
|
147
|
+
| `nose` | 0 | **192** | β39.919 | protrudes 22. The only feature in front of the surface |
|
|
148
|
+
| `eye_l` / `eye_r` | β62 | 150 | β29.832 / β32.542 | 20 **below** the surface β a socket is a hollow |
|
|
149
|
+
| `brow_l` / `brow_r` | β62 | 158 | β31.495 / β34.205 | 12 below. A brow sits proud of its socket |
|
|
150
|
+
| `mouth` | 0 | 166 | β34.513 | 4 below. Almost on the surface |
|
|
151
|
+
| `hairmass` (back hair) | 0 | **β55** | **+11.435** | its centroid is 55 **behind** the axis |
|
|
152
|
+
| `lock_l` / `lock_r` | β150 | 100 | β17.513 / β24.069 | off-axis and shallow, so they travel about half as far as the centre |
|
|
153
|
+
| `ahoge` (cowlick) | β23 | 20 | β3.656 | almost on the axis, so it barely moves |
|
|
154
|
+
|
|
155
|
+
β **The sign flip is the strongest single depth cue you can buy.** `hairmass` at
|
|
156
|
+
`z = β55` makes `βzΒ·sin t` positive: as the face swings left, the back of the
|
|
157
|
+
head swings **right**. It is one track, one number, and it is what separates a
|
|
158
|
+
turn from a slide more than any mesh work does.
|
|
159
|
+
|
|
160
|
+
β οΈ **Get the depths right and the parallax is free; get one wrong and nothing
|
|
161
|
+
complains.** A depth is not measurable from the art β it is a decision about a
|
|
162
|
+
shape the drawing only implies. Two readings that help:
|
|
163
|
+
|
|
164
|
+
1. **Depth is what the drawing overlaps for.** A part drawn *over* another is
|
|
165
|
+
usually in front of it, and the slot order already records that (AUTHORING R4).
|
|
166
|
+
Depth is the same ordering with a magnitude attached, so **derive the sign from
|
|
167
|
+
the draw order and argue only about the size.**
|
|
168
|
+
2. **The stand-off is the number to state out loud.** The fringe's 26 is not a
|
|
169
|
+
measurement of anything; it is how much parallax the shot wanted. At 12Β° it
|
|
170
|
+
buys `26Β·sin 12Β° = 5.406` units of extra travel β **derived**, and the record
|
|
171
|
+
measured 5.406 on the artifact. If the fringe does not read as separate,
|
|
172
|
+
this is the one number to move, and moving it moves nothing else.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 3. One shared shift, then residuals
|
|
177
|
+
|
|
178
|
+
β **Do not key each feature's whole `dx`.** Put a bone at the face plate's own
|
|
179
|
+
origin, key the part every feature shares onto that one bone, and let each
|
|
180
|
+
feature key only what is left:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
faceshift.translatex = βRΒ·sin t β β35.345 at 12Β°, R = 170
|
|
184
|
+
|
|
185
|
+
residual(x, z) = dx(x, z) β (βRΒ·sin t)
|
|
186
|
+
= xΒ·(cos t β 1) + (R β z)Β·sin t
|
|
187
|
+
\_______/
|
|
188
|
+
the part's depth BELOW
|
|
189
|
+
the skull surface
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
β **So a feature's own track is nothing but its depth below the surface, times
|
|
193
|
+
`sin t`.** That is the sentence this page exists to produce.
|
|
194
|
+
|
|
195
|
+
**The features of the worked example** (**derived**, and every value matches the
|
|
196
|
+
shipped `motion.json` to the last digit printed there):
|
|
197
|
+
|
|
198
|
+
| Bone | `x` | `z` | `dx` | residual | `scalex` |
|
|
199
|
+
| --- | --- | --- | --- | --- | --- |
|
|
200
|
+
| `eye_l` (far) | β62 | 150 | β29.832 | **5.513** | 0.8922 |
|
|
201
|
+
| `eye_r` (near) | 62 | 150 | β32.542 | **2.803** | 1.0641 |
|
|
202
|
+
| `brow_l` | β62 | 158 | β31.495 | 3.850 | 0.8966 |
|
|
203
|
+
| `brow_r` | 62 | 158 | β34.205 | 1.140 | 1.0597 |
|
|
204
|
+
| `nose` | 0 | 192 | β39.919 | **β4.574** | 0.9781 |
|
|
205
|
+
| `mouth` | 0 | 166 | β34.513 | 0.832 | 0.9781 |
|
|
206
|
+
|
|
207
|
+
π¨ **The reason to do it this way is that it makes a wrong number visible.** A
|
|
208
|
+
residual is **1β6 units**; a total is **30β40**. Nobody can eyeball an error in
|
|
209
|
+
the second, and everybody can eyeball one in the first β a residual with the
|
|
210
|
+
wrong sign, or one an order of magnitude off its neighbours, is obvious in a
|
|
211
|
+
column of six. β **The shared-shift split is an auditing decision before it is a
|
|
212
|
+
rigging one**, and given Β§9 that is the whole argument for it.
|
|
213
|
+
|
|
214
|
+
β **The nose is the diagnostic.** It is the only **negative** residual on the
|
|
215
|
+
face, because it is the only feature in front of the surface: it protrudes 22, and
|
|
216
|
+
`22Β·sin 12Β° = 4.57` is exactly how much further left it goes than the cheek it
|
|
217
|
+
sits on (**derived**: β4.574). β **If the nose's residual is not negative, the
|
|
218
|
+
depths are wrong.** It is the cheapest check on this page and it is arithmetic,
|
|
219
|
+
not a render.
|
|
220
|
+
|
|
221
|
+
π **`faceshift` has to be its own bone, and not the head bone.** The head bone
|
|
222
|
+
is the head mesh's own slot bone, and the mesh deform already carries that
|
|
223
|
+
plate's motion; a translate there would move the plate **twice**. Make
|
|
224
|
+
`faceshift` a child of `head` that carries only the features β the worked
|
|
225
|
+
example's chain is `headroll β head β faceshift β {eyes, brows, nose, mouth}`,
|
|
226
|
+
which `rigc explain` prints as a parent column (Β§9.2).
|
|
227
|
+
|
|
228
|
+
β **The same reasoning puts the head's pivot one link above the mesh.** A head
|
|
229
|
+
rotates about the top of the neck, not about the middle of its own face, so the
|
|
230
|
+
roll belongs on a `headroll` bone above `head`. There is a rule that enforces it
|
|
231
|
+
under `--profile spine-html`: `A15_IDLE_NO_MESH_BONE_KEYS` refuses an `idle` that
|
|
232
|
+
keys a bone driving a mesh β its own slot bone or its control bone β because that
|
|
233
|
+
renderer must never idle-skip a mesh. Keying the pivot one link up satisfies it,
|
|
234
|
+
and **the rig that satisfies the assertion is the better rig anyway.**
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 4. The mesh β where the columns go is the whole decision
|
|
239
|
+
|
|
240
|
+
Two meshes and **40 vertices** carried a head turn in the worked example, which
|
|
241
|
+
cuts against the expectation that a face mesh needs hundreds. The reason is
|
|
242
|
+
structural: **a yaw moves nothing vertically**, so the rows are along for the
|
|
243
|
+
ride and only the column count buys anything.
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
head 340 Γ 380 plate, R = 170 hair_bang 372 Γ 168 plate, R = 196
|
|
247
|
+
5 columns Γ 5 rows = 25 vertices 5 columns Γ 3 rows = 15 vertices
|
|
248
|
+
32 triangles 16 triangles
|
|
249
|
+
columns x = β162, β120, 0, 120, 162 columns x = β170, β120, 0, 120, 170
|
|
250
|
+
rows y = 180, 90, 0, β90, β180 rows y = 80, 0, β80
|
|
251
|
+
z = β(RΒ² β xΒ²) z = β(RΒ² β xΒ²)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Each column's offset is `dx` at its own `(x, z)`, and **every row gets the same
|
|
255
|
+
value**, so a `deform` key is one row of five repeated down the grid with a `0`
|
|
256
|
+
for every `y`:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{ "t": 0.62, "fromVertex": 0, "vertices": [
|
|
260
|
+
-7.175, 0, -22.414, 0, -35.345, 0, -27.658, 0, -14.255, 0,
|
|
261
|
+
-7.175, 0, -22.414, 0, -35.345, 0, -27.658, 0, -14.255, 0,
|
|
262
|
+
β¦ three more identical rows β¦ ], "ease": "swell" }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
β οΈ **`R` is the radius of the cylinder a part is painted on, and it is not always
|
|
266
|
+
the plate's half-width.** For the head plate the two coincide (`340/2 = 170`). For
|
|
267
|
+
the fringe they do **not**: its plate is 372 wide (half-width 186) but its `R` is
|
|
268
|
+
**196**, because 196 is where the fringe *sits* β 26 in front of the skull. β
|
|
269
|
+
Read `R` off the depth table, never off the PNG.
|
|
270
|
+
|
|
271
|
+
### 4.1 Uneven columns, and it costs nothing
|
|
272
|
+
|
|
273
|
+
π¨ **The columns are not evenly spaced, and that is the trick.** `β162, β120, 0,
|
|
274
|
+
120, 162` puts them **dense near the silhouette and sparse in the middle**, which
|
|
275
|
+
is the sampling a cosine needs: the centre of the face travels `RΒ·sin t` and the
|
|
276
|
+
edges barely travel at all, so all the *variation* is at the edges. Five evenly
|
|
277
|
+
spaced columns spend their resolution where nothing happens.
|
|
278
|
+
|
|
279
|
+
What the grid then does to the drawing is a **non-uniform horizontal
|
|
280
|
+
redistribution** (**derived** at 12Β°, widest row):
|
|
281
|
+
|
|
282
|
+
| Band, from the far edge | Rest width | At 12Β° | Ratio |
|
|
283
|
+
| --- | --- | --- | --- |
|
|
284
|
+
| β162 β β120 | 42.0 | 26.76 | **0.637** |
|
|
285
|
+
| β120 β 0 | 120.0 | 107.07 | 0.892 |
|
|
286
|
+
| 0 β 120 | 120.0 | 127.69 | 1.064 |
|
|
287
|
+
| 120 β 162 | 42.0 | 55.40 | **1.319** |
|
|
288
|
+
|
|
289
|
+
The far side compresses to 64%, the near side stretches to 132%, and the ink
|
|
290
|
+
inside each band compresses and stretches with it. **That gradient is what makes
|
|
291
|
+
it read as a turn instead of a slide.** The whole-head narrowing, by contrast, is
|
|
292
|
+
the cheap part: ink edge to ink edge, `309 Β· cos 12Β° = 302.2` (**derived**; the
|
|
293
|
+
record measured 309.0 β 302.2 on the artifact).
|
|
294
|
+
|
|
295
|
+
β **It is one decision, made once, in the setup geometry, and every deform key
|
|
296
|
+
after it is better for free.** There is no cost side to this trade.
|
|
297
|
+
|
|
298
|
+
### 4.2 The silhouette is a tangent, not a mark β and refining makes it worse
|
|
299
|
+
|
|
300
|
+
This is the most misleading thing about a face plate, and the counter-intuitive
|
|
301
|
+
result of the whole experiment.
|
|
302
|
+
|
|
303
|
+
A column at `x` sits at depth `z = β(RΒ² β xΒ²)`. Project two adjacent columns and
|
|
304
|
+
ask when they **swap order** β when the mesh turns inside out:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
xβΒ·cos t β zβΒ·sin t = xβΒ·cos t β zβΒ·sin t
|
|
308
|
+
|
|
309
|
+
Ξx xβ β xβ
|
|
310
|
+
β tan ΞΈ_fold = ββ = βββββββββ over ADJACENT columns
|
|
311
|
+
Ξz zβ β zβ
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
and a grid folds at the **minimum** of that over its pairs. As the gap closes,
|
|
315
|
+
`Ξx/Ξz β z/|x|`, so the limit for a column is `atan(z / |x|)` β a property of the
|
|
316
|
+
**continuous surface**, not of the mesh.
|
|
317
|
+
|
|
318
|
+
**Re-derived from that formula** (and each agrees with a bisection search on the
|
|
319
|
+
shipped column table to 0.01Β°):
|
|
320
|
+
|
|
321
|
+
| Columns | Folds at | The pair that folds |
|
|
322
|
+
| --- | --- | --- |
|
|
323
|
+
| 5 β `Β±162, Β±120, 0` (shipped) | **31.37Β°** | `β162, β120` |
|
|
324
|
+
| 7 β `Β±145` added | **24.56Β°** | `β162, β145` |
|
|
325
|
+
| 7 β `Β±155` added | **20.95Β°** | `β162, β155` |
|
|
326
|
+
| 9 β `Β±155, Β±145` added | **20.95Β°** | `β162, β155` |
|
|
327
|
+
| 13 β uniform, every 27 units | 27.54Β° | `β162, β135` |
|
|
328
|
+
| continuous limit at `x = β162` | **17.65Β°** | β |
|
|
329
|
+
|
|
330
|
+
π¨ **A denser face mesh is not a safer face mesh.** Refining near the silhouette
|
|
331
|
+
drives `Ξx/Ξz` toward the tangent limit *from above*, so every column you add out
|
|
332
|
+
there **lowers** the angle at which the mesh inverts. A coarse grid survives past
|
|
333
|
+
17.65Β° only because it does not *sample* there β it crushes instead of folding.
|
|
334
|
+
|
|
335
|
+
β οΈ **And the fold angle is set by the outermost gap, not by the column count.**
|
|
336
|
+
The 5-, 7- and 9-column rows above make that concrete: the 9-column grid folds at
|
|
337
|
+
**exactly** the same 20.95Β° as the 7-column one, because both contain the pair
|
|
338
|
+
`(β162, β155)` and the `Β±145` column changes nothing. β Counting vertices tells
|
|
339
|
+
you nothing about this failure; **look at the outermost two columns.**
|
|
340
|
+
|
|
341
|
+
β **The rule, and it is an identity rather than a rule of thumb.** Put the
|
|
342
|
+
outermost column at
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
|x|outer = R Β· cos ΞΈmax β its tangent limit is EXACTLY ΞΈmax
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
because `z = RΒ·sin ΞΈ` there and `atan(z/|x|) = ΞΈ`. So you do not tune this: you
|
|
349
|
+
**pick the ceiling first and the column position falls out.** For `R = 170`
|
|
350
|
+
(**derived**):
|
|
351
|
+
|
|
352
|
+
| ΞΈmax you want | Outermost column at |
|
|
353
|
+
| --- | --- |
|
|
354
|
+
| 12Β° | 166.3 |
|
|
355
|
+
| 16Β° | 163.4 |
|
|
356
|
+
| 20Β° | 159.7 |
|
|
357
|
+
| 26Β° | 152.8 |
|
|
358
|
+
|
|
359
|
+
The shipped grid's 162 gives 17.65Β°, comfortably above the 12Β° it ships and just
|
|
360
|
+
above the 16Β° Β§8 calls the instrument's ceiling. β **Then let the last band be a
|
|
361
|
+
single wide one** β that is the direction that buys safety, and it is the
|
|
362
|
+
opposite of refining.
|
|
363
|
+
|
|
364
|
+
### 4.3 `hull` is `0`, and the vertex order is why
|
|
365
|
+
|
|
366
|
+
β οΈ **A grid's perimeter is 16 of its 25 vertices and they are not a prefix of any
|
|
367
|
+
row-major order.** Declaring a hull would mean re-ordering the vertex list
|
|
368
|
+
perimeter-first β and then **a reader could not count the deform table against
|
|
369
|
+
the grid**, which is the one property that makes 160 transcribed floats
|
|
370
|
+
auditable at all. `0` is the honest declaration. AUTHORING Β§3.4 has what a hull
|
|
371
|
+
buys and the worked example's FINDINGS.md prices what omitting it costs in an
|
|
372
|
+
editor round trip.
|
|
373
|
+
|
|
374
|
+
β οΈ **A large `MESH` overshoot on a face is not a defect.** A rectangular grid
|
|
375
|
+
over an oval face has transparent corners, and the line says so:
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head]
|
|
379
|
+
attachments=[head] covers 100.00% of the art, reaching 95.90px past it
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**`covers 100.00%` is what matters** β nothing of the drawing is outside the
|
|
383
|
+
triangles. The 95.90px is the corner. A mesh that hugged the silhouette instead
|
|
384
|
+
would have to be a `contour`, which cannot be a grid and has **no interior
|
|
385
|
+
vertices to redistribute** β so it cannot carry a turn at all.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 5. What foreshortens, and what does not
|
|
390
|
+
|
|
391
|
+
A feature is a rigid drawing sitting on a curved surface, so it also has to
|
|
392
|
+
**narrow** as its patch of surface turns away. That is a bone `scalex`:
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
scaleX = cos(Ξ± β t) / cos Ξ± where Ξ± = atan2(x, z)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The far eye narrows to 89%, the near eye widens to 106% (**derived**: 0.8922 and
|
|
399
|
+
1.0641). Parts on the axis get `cos t = 0.9781` and can therefore **share one
|
|
400
|
+
track** β the nose and mouth do, through a `groups` entry, and it is the only
|
|
401
|
+
place in the worked example's turn where two parts can (Β§7).
|
|
402
|
+
|
|
403
|
+
### π¨ The iris does not foreshorten, and this is the finding
|
|
404
|
+
|
|
405
|
+
`iris` and `spark` are children of `eye`, so they inherit the socket's `scalex` β
|
|
406
|
+
and **a circular iris under `scalex 0.89` is an ellipse.** That reads as *a
|
|
407
|
+
drawing squashed sideways*, not as a head turned, and it is the first thing to
|
|
408
|
+
break as the angle grows.
|
|
409
|
+
|
|
410
|
+
It is also wrong on the physics. If the character keeps looking at the camera
|
|
411
|
+
through the turn, her eyeball counter-rotates by the same angle the head yawed,
|
|
412
|
+
so the iris stays square-on and stays **circular**. β **The socket foreshortens;
|
|
413
|
+
the pupil does not.**
|
|
414
|
+
|
|
415
|
+
The fix is one reciprocal per side, on two groups (**derived**):
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
look_l: [iris_l, spark_l] scalex = 1 / 0.8922 = 1.1208
|
|
419
|
+
look_r: [iris_r, spark_r] scalex = 1 / 1.0641 = 0.9398
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
β **Their positions still ride the socket, and that is the part that makes it
|
|
423
|
+
correct rather than a hack.** A bone's scale moves its children's local
|
|
424
|
+
translation, so the highlight at local `(β11, +11)` under the far socket's 0.8922
|
|
425
|
+
lands at `β9.81` β it slides 1.19 units inward, toward the surface it reflects
|
|
426
|
+
off (**derived**). Only the *shape* is held.
|
|
427
|
+
|
|
428
|
+
π **Measured effect on the cliff, by the worked example's own sweep β a
|
|
429
|
+
looked-at judgement over seven renders, not a computed figure:** without the
|
|
430
|
+
counter-scale the turn stops reading at about **18Β°**; with it, about **26Β°**.
|
|
431
|
+
Eight degrees of usable range for two tracks, and the record reports nothing else
|
|
432
|
+
in the experiment came close to that ratio.
|
|
433
|
+
|
|
434
|
+
β οΈ **A `clipping` attachment would lift the iris's travel ceiling and you cannot
|
|
435
|
+
have one.** Spine has them (AUTHORING Β§3.4) but `A11_NO_CLIPPING_ATTACHMENTS`
|
|
436
|
+
refuses one under `--profile spine-html`, so a rig carrying one builds on only one
|
|
437
|
+
of the two profiles. The ceiling is then geometric: in the worked example the
|
|
438
|
+
iris's ink ring has radius 28.5 against a socket opening 36 half-wide, so
|
|
439
|
+
`36 β 28.5 = 7.5` units is as far as it can travel before it crosses its own lash.
|
|
440
|
+
β **Compute that ceiling from the art before keying a gaze**; the first draft of
|
|
441
|
+
the worked example keyed 9 and had to come back to 7.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## 6. A blink is a continuous channel, not a swap
|
|
446
|
+
|
|
447
|
+
**Two ways to blink, and both are right somewhere:**
|
|
448
|
+
|
|
449
|
+
| | An `attachment` swap | A translating lid plate |
|
|
450
|
+
| --- | --- | --- |
|
|
451
|
+
| what it is | two drawings, `eyes` and `eyes_shut`, stepped | one plate, one `translatey`, 65 units down its own bone |
|
|
452
|
+
| costs | 1 extra PNG | 2 PNGs, 2 bones, 2 slots |
|
|
453
|
+
| where it is right | a mascot, a stylised blink, anything whose shut eye is a **different drawing** rather than a covered one | a portrait |
|
|
454
|
+
|
|
455
|
+
β **Choose the swap for a mascot and the channel for a face**, and the reasons
|
|
456
|
+
are all timing:
|
|
457
|
+
|
|
458
|
+
- **A swap has no shape.** A real blink is fast shut and slow open. The worked
|
|
459
|
+
example is `0.07 s` down and `0.16 s` open β a **1 : 2.3** asymmetry, easing
|
|
460
|
+
into the close and out of the open. A stepped timeline has one frame of
|
|
461
|
+
transition and no curve to put an asymmetry in.
|
|
462
|
+
- **A swap has no partial.** A half-blink, a sleepy lid, a lid that rides the
|
|
463
|
+
gaze β every one of them is a *fraction of the same channel*, and none of them
|
|
464
|
+
is a third drawing.
|
|
465
|
+
- **A swap has to dodge the frame grid.** A stepped key exactly on a sample time
|
|
466
|
+
can be missed by a player accumulating `1/fps`, which is why
|
|
467
|
+
[`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash)'s
|
|
468
|
+
blink key sits at `0.399999` (AUTHORING Β§4.5). **A continuous channel does not
|
|
469
|
+
care where the samples land.**
|
|
470
|
+
|
|
471
|
+
β οΈ **Two constraints on the lid's own drawing, and both are art decisions the rig
|
|
472
|
+
cannot express.**
|
|
473
|
+
|
|
474
|
+
1. **Fade its top edge out, and size the fade in pixels.** A flat plate of skin
|
|
475
|
+
translating down a forehead has a visible edge; fading its top 30 pixels
|
|
476
|
+
removes it. β **Do not write that fade proportionally.** In the worked example
|
|
477
|
+
it was first authored as `offset 0.34` in `objectBoundingBox` units β 34% of
|
|
478
|
+
*whatever height the plate happened to be* β and when the plate grew from 88 to
|
|
479
|
+
112 to cover the eye, the faded band grew with it and stopped covering the top
|
|
480
|
+
of the shut eye. In `userSpaceOnUse` with `y2="30"` the height becomes a free
|
|
481
|
+
variable and the coverage becomes a stated one: `106 β 30 = 76` opaque pixels
|
|
482
|
+
above the lash, against a 70-unit eye.
|
|
483
|
+
2. ~~**Run its sides flush to the window edge.**~~ β
**No longer needed.** This
|
|
484
|
+
was a workaround for [#292](https://github.com/firejune/rigc/issues/292):
|
|
485
|
+
`rigc render`'s bilinear filter sampled straight alpha, so **every
|
|
486
|
+
atlas-region edge drew a one-pixel dark rim** over what was behind it, and
|
|
487
|
+
running the art flush to its window gave the sampler nothing dark to reach
|
|
488
|
+
into. The sampler now interpolates premultiplied and a transparent texel gets
|
|
489
|
+
no vote in the colour, so **a lid plate may have whatever margin its drawing
|
|
490
|
+
wants.** Β§9.1 keeps the record of how the rim was found, because the lesson
|
|
491
|
+
there β that scene work exercises a renderer where game-part work does not β
|
|
492
|
+
outlives this particular defect.
|
|
493
|
+
|
|
494
|
+
π **Blink both lids on one `groups` track.** An L/R offset of one frame was
|
|
495
|
+
tried in the worked example and rejected: at 25 fps it does not read as a soft
|
|
496
|
+
blink, it reads as a **wink**. β MOTION Β§3.7's offset table is about a chain
|
|
497
|
+
hanging off a driver, and two lids are not that β they are one event.
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## 7. Channel allocation, before the first key
|
|
502
|
+
|
|
503
|
+
π¨ **Scene direction wants to layer β an idle that keeps running under a
|
|
504
|
+
triggered gaze, under a turn β and two animations keying the same bone property
|
|
505
|
+
are BLENDED, not summed.** So the animations have to divide the rig up front. The
|
|
506
|
+
worked example's table, which is the shape of thing to write before authoring
|
|
507
|
+
anything:
|
|
508
|
+
|
|
509
|
+
| Channel | `idle` | `gaze` | `turn` |
|
|
510
|
+
| --- | --- | --- | --- |
|
|
511
|
+
| `torso` scale, `chest` translatey | β | | |
|
|
512
|
+
| `lids` translatey | β | | |
|
|
513
|
+
| `brows` translatey | β | β | |
|
|
514
|
+
| `irises` / `sparks` translate | | β | |
|
|
515
|
+
| `head` + `hair_bang` mesh deform | | | β |
|
|
516
|
+
| `faceshift`, feature translatex / scalex | | | β |
|
|
517
|
+
| `hairmass` translatex | | | β |
|
|
518
|
+
| `lock_l` / `lock_r` / `ahoge` | rotate | rotate | translatex |
|
|
519
|
+
| `headroll` rotate | β | β | β |
|
|
520
|
+
| `neck` | rotate | | translatex |
|
|
521
|
+
|
|
522
|
+
Three collisions survive there: `headroll` rotate in all three, `brows`
|
|
523
|
+
translatey in two, and the locks' rotate in two. β οΈ **On plain Spine the fixes
|
|
524
|
+
are ordinary β `MixBlend.add` on the layered track, or splitting a bone into a
|
|
525
|
+
stack (`headroll_idle` under `headroll_scene`) β but both are runtime or rig
|
|
526
|
+
decisions the motion spec cannot express, so nothing warns an author that two of
|
|
527
|
+
their animations will fight.**
|
|
528
|
+
|
|
529
|
+
β **And the cost is real, so name it rather than discovering it by shipping.** In
|
|
530
|
+
the worked example `idle` keys **nothing** on the iris, on purpose, even though a
|
|
531
|
+
completely still eye reads as a mannequin. The iris is `gaze`'s channel; an
|
|
532
|
+
`idle` drift plus a `gaze` on a second track would be two animations holding two
|
|
533
|
+
opinions about where she is looking. **That is a loss, it was chosen, and it is
|
|
534
|
+
written down.**
|
|
535
|
+
|
|
536
|
+
β **The gaze itself is then pure MOTION Β§3.7** β a chain of offsets, and the only
|
|
537
|
+
face-specific part is which bone leads. The worked example's ordering
|
|
538
|
+
(**measured** off its own key times):
|
|
539
|
+
|
|
540
|
+
| Track | Extreme at | What it is |
|
|
541
|
+
| --- | --- | --- |
|
|
542
|
+
| `irises` translate | **0.22 s** | the eyes lead. `(7, β3)` |
|
|
543
|
+
| `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 |
|
|
544
|
+
| `brows` translatey | 0.34 s | +1.8, which turns a flick of the eyes into interest |
|
|
545
|
+
| `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 |
|
|
546
|
+
| `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 |
|
|
547
|
+
|
|
548
|
+
π©Ή **The head's follow there is a rigid slide plus a roll, not a small yaw, and
|
|
549
|
+
that is the transcription cost showing through.** A head following a gaze really
|
|
550
|
+
does yaw a few degrees; three degrees of yaw would be a third pair of `deform`
|
|
551
|
+
keys β 80 more hand-written numbers β for a motion a viewer reads as *"her head
|
|
552
|
+
moved a little"*. It is the right call at a draft budget and the wrong one at a
|
|
553
|
+
scene-direction budget. Same trade in the turn's anticipation: MOTION Β§3.6 asks
|
|
554
|
+
for a counter-move, and the worked example's is a **β0.9Β° counter-roll on the
|
|
555
|
+
neck bone rather than a counter-yaw**, because a yaw anticipation is another
|
|
556
|
+
50-float head key plus 30 on the fringe for a tenth of a second.
|
|
557
|
+
|
|
558
|
+
β **A roll channel is worth having for a second reason: it is where a turn's arc
|
|
559
|
+
comes from.** A yaw shift is a straight horizontal line and `translatex` draws
|
|
560
|
+
exactly that (MOTION Β§3.5). A 1.6Β° roll on a bone at the top of the neck bends
|
|
561
|
+
every feature's path into an arc, because a rotation carries its descendants on a
|
|
562
|
+
circle for free.
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 8. The three cliffs, and picking a construction from the turn you need
|
|
567
|
+
|
|
568
|
+
Three separate failures at three different angles. β **Read this before the art
|
|
569
|
+
is drawn**, because two of the three are answered by *parts*, and parts are the
|
|
570
|
+
expensive thing to change.
|
|
571
|
+
|
|
572
|
+
**The sweep** β the worked example's `turn` re-derived at seven angles, built,
|
|
573
|
+
rendered and looked at. The geometry columns are **derived**; the last column is
|
|
574
|
+
the record's looked-at judgement:
|
|
575
|
+
|
|
576
|
+
| yaw | centre shift | far band 42 β | ratio | 9-unit ink β | reads as a turn? |
|
|
577
|
+
| --- | --- | --- | --- | --- | --- |
|
|
578
|
+
| 8Β° | 23.7 | 32.0 | 0.762 | 6.9 | yes, gently |
|
|
579
|
+
| **12Β° (shipped)** | **35.3** | **26.8** | **0.637** | **5.7** | **yes** |
|
|
580
|
+
| 16Β° | 46.9 | 21.4 | 0.509 | 4.6 | yes |
|
|
581
|
+
| 20Β° | 58.1 | 15.9 | 0.379 | 3.4 | marginal |
|
|
582
|
+
| 24Β° | 69.1 | 10.4 | 0.247 | 2.2 | no β the eyes have stopped being eyes |
|
|
583
|
+
| 28Β° | 79.8 | 4.7 | 0.113 | 1.0 | no β the far outline is gone |
|
|
584
|
+
| 32Β° | 90.1 | β0.9 | β0.021 | β | **the mesh has folded** |
|
|
585
|
+
|
|
586
|
+
**Failure 1 β the iris goes elliptical, at ~18Β°, and it is fixable.** Β§5. Two
|
|
587
|
+
reciprocal `scalex` tracks buy 8Β° of range. Do this one always; it is the best
|
|
588
|
+
ratio in the experiment.
|
|
589
|
+
|
|
590
|
+
**Failure 2 β a `scalex` cannot rotate an almond, at ~26Β°, and it is not
|
|
591
|
+
fixable.** The eye *socket* goes next. A real eye at 26Β° does not narrow
|
|
592
|
+
uniformly: its far corner disappears behind the nose bridge, its lash line
|
|
593
|
+
rotates, its lid wraps. `scalex` does exactly one of those things, so the far eye
|
|
594
|
+
becomes *a thin version of a front-facing eye* and the near eye's lash stretches
|
|
595
|
+
into a wide flat slab (**derived** socket scales: 0.892/1.064 at 12Β°, 0.745/1.082
|
|
596
|
+
at 24Β°, 0.629/1.067 at 32Β° β note the near side barely moves past 20Β°, which is
|
|
597
|
+
why the stretch stops looking like foreshortening). β **Past roughly 26Β° the eyes
|
|
598
|
+
need their own deform meshes** β socket, lash and lid as a 3β4 column grid each β
|
|
599
|
+
and that is where the vertex count stops being 40. The fringe tips (a rigid plate
|
|
600
|
+
that should be splaying) and the neck go in the same band for the same reason.
|
|
601
|
+
|
|
602
|
+
**Failure 3 β the mesh folds, and refining it makes this worse.** Β§4.2, and it is
|
|
603
|
+
the one to design around rather than discover.
|
|
604
|
+
|
|
605
|
+
π©Ή **The neck is the honest fudge, and label yours the same way.** A neck twists:
|
|
606
|
+
its top follows the head almost entirely and its base hardly at all. A single
|
|
607
|
+
rigid plate can only take an average β the worked example takes **28%** of the
|
|
608
|
+
head's shift (`β10` against `β35.345`), which is **the one number in its turn that
|
|
609
|
+
is not derived**, chosen as the value at which the chin stopped hanging off the
|
|
610
|
+
throat at 12Β°. A turn that had to read at 20Β° would need the neck to be its own
|
|
611
|
+
mesh with its own column table.
|
|
612
|
+
|
|
613
|
+
### The verdict on angle
|
|
614
|
+
|
|
615
|
+
β **A 5-column grid with bone-scaled features is a 0β16Β° instrument, comfortable
|
|
616
|
+
at 12Β°.** For a standing portrait that is enough: an idle turn, a glance away, a
|
|
617
|
+
lean into frame. **A 30β45Β° three-quarter turn is a different rig** β per-eye
|
|
618
|
+
meshes, a meshed neck, probably a second art layer for the far cheek β and it is
|
|
619
|
+
not a format problem, it is a parts-and-labour problem.
|
|
620
|
+
|
|
621
|
+
π **What one held yaw actually costs**, from the shipped `motion.json`
|
|
622
|
+
(**derived** by counting it):
|
|
623
|
+
|
|
624
|
+
| Animation | Duration | Tracks | Track keys | Deform entries | Deform keys | Hand-written floats |
|
|
625
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
626
|
+
| `idle` | 3.2 s | 9 | 37 | 0 | 0 | 0 |
|
|
627
|
+
| `gaze` | 1.5 s | 8 | 32 | 0 | 0 | 0 |
|
|
628
|
+
| **`turn`** | 2.2 s | **20** | **81** | 2 | 8 | **160** |
|
|
629
|
+
|
|
630
|
+
β **`idle` and `gaze` cost what an ordinary MOTION.md shot costs. The entire
|
|
631
|
+
difficulty is the turn, and it is transcription rather than complexity:** of those
|
|
632
|
+
160 floats there are **five distinct values** (one per column, repeated down five
|
|
633
|
+
rows), **25 of the 50 slots in a head key are structurally `0`** because a yaw has
|
|
634
|
+
no vertical component, and **19 of the 20 tracks carry one value each**. Fix
|
|
635
|
+
[#294](https://github.com/firejune/rigc/issues/294) and
|
|
636
|
+
[#295](https://github.com/firejune/rigc/issues/295) and a turn is a dozen lines.
|
|
637
|
+
|
|
638
|
+
β οΈ **`groups` buys almost nothing on a face, and that is structural rather than
|
|
639
|
+
an oversight.** The usual lever for cutting track count is a `groups` entry keying
|
|
640
|
+
several bones identically β but **every part needing a different number is what
|
|
641
|
+
parallax means.** Of the worked example's 20 turn tracks exactly one is a group
|
|
642
|
+
for that reason (`axis`, the nose and mouth, both on the axis and therefore both
|
|
643
|
+
`cos t`), plus the two reciprocal groups Β§5 needs. Sixteen of the rest are two
|
|
644
|
+
properties on six sibling bones with identical times, identical easings and six
|
|
645
|
+
different values: [#295](https://github.com/firejune/rigc/issues/295).
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## 9. Looking at it, and the audit gap
|
|
650
|
+
|
|
651
|
+
### 9.1 The looking protocol is three scales, not one
|
|
652
|
+
|
|
653
|
+
| Scale | What it is for |
|
|
654
|
+
| --- | --- |
|
|
655
|
+
| **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 |
|
|
656
|
+
| **1:1** | whether the **drawing arrived** |
|
|
657
|
+
| **3β4Γ on the eyes** | because that is where a reader will look, and a face has no other equivalent |
|
|
658
|
+
|
|
659
|
+
π **Four of the five art defects in the worked example were invisible at contact
|
|
660
|
+
sheet scale** and all four were found at 1:1 or better: the dark seam
|
|
661
|
+
([#292](https://github.com/firejune/rigc/issues/292)), the lid's fade letting a
|
|
662
|
+
shut eye's lash show through as a grey smudge, the iris crossing its own lash at
|
|
663
|
+
the gaze extreme, and a forehead highlight turning the lid's soft edge into a
|
|
664
|
+
tonal step.
|
|
665
|
+
|
|
666
|
+
β οΈ **A portrait is the first rig whose plates are *supposed* to overlap
|
|
667
|
+
invisibly**, which is why #292 stayed latent through four gallery examples: every
|
|
668
|
+
part in them carries an ink outline at its edge, and a dark rim on a dark line
|
|
669
|
+
cannot be seen. β **Scene work exercises a renderer where game-part work does
|
|
670
|
+
not.** Expect to find renderer defects on your first face, and check a suspicious
|
|
671
|
+
edge against a **region build** before reading a single vertex β which is the
|
|
672
|
+
next item.
|
|
673
|
+
|
|
674
|
+
β **When a mesh is the new thing in a shot, build the region version and diff it
|
|
675
|
+
before suspecting the mesh.** The worked example's first seam suspect was the
|
|
676
|
+
grid: faint vertical lines down the forehead, at what looked like column
|
|
677
|
+
positions. Building the same rig with both meshes replaced by plain regions and
|
|
678
|
+
diffing the rest frame settled it in one command β worst channel difference 1,
|
|
679
|
+
zero pixels differing β so the mesh was innocent and the lines were art
|
|
680
|
+
compositing. **Two minutes, and it halves the search space.**
|
|
681
|
+
|
|
682
|
+
π **Draw the face before you rig it.** The worked example took **seven art
|
|
683
|
+
passes before a `rig.json` existed** β brows twice, hair silhouette, fringe
|
|
684
|
+
depth, neck length, garment mass, choker, proportions. That order is not
|
|
685
|
+
fastidiousness: **the head plate's half-width *is* `R`, and `R` is in every one of
|
|
686
|
+
the thirty derived numbers.** Rigging first means re-deriving all of them every
|
|
687
|
+
time the plate changes width.
|
|
688
|
+
|
|
689
|
+
β οΈ **And expression lives in ink weight before it lives in shape.** The worked
|
|
690
|
+
example's brows read as a scowl for two iterations and the brows were not the
|
|
691
|
+
problem β two eyes whose upper lash is heaviest at the **inner** corner read as
|
|
692
|
+
angry whatever the brow above them does. Worth knowing before spending a pass on
|
|
693
|
+
the wrong part.
|
|
694
|
+
|
|
695
|
+
### 9.2 π¨ What nothing measures β three builds, all green
|
|
696
|
+
|
|
697
|
+
**The setup geometry is measured; the deformed geometry is not.** Here is that
|
|
698
|
+
claim as three builds of the same rig, each command runnable verbatim from a
|
|
699
|
+
clean checkout. Start from the good one:
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
bun install # once
|
|
703
|
+
|
|
704
|
+
bun cli.ts build --rig gallery/portrait/rig.json \
|
|
705
|
+
--motion gallery/portrait/motion.json \
|
|
706
|
+
--out gallery/portrait/build --profile spine-html
|
|
707
|
+
bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
|
|
708
|
+
--out gallery/portrait/render
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head]
|
|
713
|
+
attachments=[head] covers 100.00% of the art, reaching 95.90px past it
|
|
714
|
+
β¦ 26 PASS, 13 SKIP
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
Now break the projection two ways. Both scripts write a variant motion spec
|
|
718
|
+
beside the originals and touch nothing in the repository:
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
# (a) INVERT ONE BAND: give the two far columns each other's shift.
|
|
722
|
+
# The far side now STRETCHES 1.363 where it should compress to 0.637 β
|
|
723
|
+
# the head reads as turning the other way at its own edge.
|
|
724
|
+
bun -e '
|
|
725
|
+
const m = await Bun.file("gallery/portrait/motion.json").json();
|
|
726
|
+
const d = m.animations.turn.deform.find(x => x.slot === "head");
|
|
727
|
+
const row = [-22.414, 0, -7.175, 0, -35.345, 0, -27.658, 0, -14.255, 0];
|
|
728
|
+
for (const k of d.keys) if (k.vertices) k.vertices = [...row, ...row, ...row, ...row, ...row];
|
|
729
|
+
await Bun.write("/tmp/swapped.motion.json", JSON.stringify(m, null, 2));
|
|
730
|
+
'
|
|
731
|
+
bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/swapped.motion.json \
|
|
732
|
+
--out /tmp/swapped --profile spine-html
|
|
733
|
+
|
|
734
|
+
# (b) FOLD IT: evaluate the same closed form at 40Β°, past the 31.37Β° of Β§4.2,
|
|
735
|
+
# so the two far columns swap order and the mesh turns inside out.
|
|
736
|
+
bun -e '
|
|
737
|
+
const m = await Bun.file("gallery/portrait/motion.json").json();
|
|
738
|
+
const d = m.animations.turn.deform.find(x => x.slot === "head");
|
|
739
|
+
const D = Math.PI / 180, R = 170, t = 40 * D;
|
|
740
|
+
const row = [-162, -120, 0, 120, 162].flatMap(x => {
|
|
741
|
+
const z = Math.sqrt(R * R - x * x);
|
|
742
|
+
return [+(x * (Math.cos(t) - 1) - z * Math.sin(t)).toFixed(3), 0];
|
|
743
|
+
});
|
|
744
|
+
for (const k of d.keys) if (k.vertices) k.vertices = [...row, ...row, ...row, ...row, ...row];
|
|
745
|
+
await Bun.write("/tmp/folded.motion.json", JSON.stringify(m, null, 2));
|
|
746
|
+
'
|
|
747
|
+
bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/folded.motion.json \
|
|
748
|
+
--out /tmp/folded --profile spine-html
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
**What comes back from both:**
|
|
752
|
+
|
|
753
|
+
| | good | (a) one band inverted | (b) mesh folded |
|
|
754
|
+
| --- | --- | --- | --- |
|
|
755
|
+
| `--profile spine-html` | 26 PASS / 13 SKIP | **26 PASS / 13 SKIP** | **26 PASS / 13 SKIP** |
|
|
756
|
+
| `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | PASS | **PASS** | **PASS** |
|
|
757
|
+
| the `MESH` coverage line | 100.00%, 95.90px past | **byte-identical** | **byte-identical** |
|
|
758
|
+
|
|
759
|
+
π¨ **All three are green, and the coverage line is the same string in all three,
|
|
760
|
+
because it reports the SETUP pose.** `A35` checks that a deform run *fits* its
|
|
761
|
+
attachment β an honest and useful check, and orthogonal to whether the numbers in
|
|
762
|
+
it mean anything.
|
|
763
|
+
|
|
764
|
+
`rigc explain` is the instrument that prints every other timeline's actual values,
|
|
765
|
+
and on a deform it prints the shape of the run rather than the run:
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
bun cli.ts explain --rig gallery/portrait/rig.json \
|
|
769
|
+
--motion gallery/portrait/motion.json --out /tmp/explain
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
```
|
|
773
|
+
default/head/head.deform 4 key(s)
|
|
774
|
+
t=0 back to the setup pose bezier[4]
|
|
775
|
+
t=0.62 deform[0..50] 25 pair(s) bezier[4]
|
|
776
|
+
t=1.5 deform[0..50] 25 pair(s) bezier[4]
|
|
777
|
+
t=2.2 back to the setup pose linear
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
β **`25 pair(s)` is the whole of what any instrument in this toolchain will tell
|
|
781
|
+
you about a face turn.** Compare a scalar track two lines up in the same report,
|
|
782
|
+
which prints `value=-35.345`. That asymmetry **is**
|
|
783
|
+
[#296](https://github.com/firejune/rigc/issues/296): its proposed
|
|
784
|
+
`A39_DEFORM_KEEPS_TRIANGLE_WINDING` has no legitimate counter-example and would
|
|
785
|
+
have handed the worked example Β§4.2's fold angles without seven renders.
|
|
786
|
+
|
|
787
|
+
### 9.3 The audit that works today, and exactly what it cannot do
|
|
788
|
+
|
|
789
|
+
`rigc check` renders a candidate onto reference frames' own pixel grid and
|
|
790
|
+
compares (INGEST Β§1.4). Point it at a render of a build you already trust and it
|
|
791
|
+
**does** see a wrong deform:
|
|
792
|
+
|
|
793
|
+
```bash
|
|
794
|
+
bun cli.ts check --candidate gallery/portrait/build --frames gallery/portrait/render/turn@25fps
|
|
795
|
+
bun cli.ts check --candidate /tmp/swapped --frames gallery/portrait/render/turn@25fps
|
|
796
|
+
bun cli.ts check --candidate /tmp/folded --frames gallery/portrait/render/turn@25fps
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
| Candidate | MAE mean | worst | at |
|
|
800
|
+
| --- | --- | --- | --- |
|
|
801
|
+
| the build the frames came from | **0.00** | 0.00 | β |
|
|
802
|
+
| (a) one band inverted | **0.20** | 0.38 | **f0016** |
|
|
803
|
+
| (b) mesh folded | **2.07** | 3.67 | **f0016** |
|
|
804
|
+
|
|
805
|
+
β **Both defects land on `f0016`, which is the frame the turn arrives on** β the
|
|
806
|
+
`worst at` column points straight at the moment, which is what makes this worth
|
|
807
|
+
running at all.
|
|
808
|
+
|
|
809
|
+
π¨ **And now the three limits, because this is the instrument you will be tempted
|
|
810
|
+
to call an audit:**
|
|
811
|
+
|
|
812
|
+
1. **It is differential.** It measures a candidate against **a render of another
|
|
813
|
+
build**, so it catches a *regression* and cannot validate a *first authoring*.
|
|
814
|
+
There is no reference for a face nobody has drawn yet.
|
|
815
|
+
2. **A wrong projection is a whisper in the aggregate.** Inverting a whole band β
|
|
816
|
+
the far edge stretching to **1.363** where it should compress to **0.637**, so
|
|
817
|
+
the head's own edge turns the wrong way β moves the mean MAE by **0.20 of
|
|
818
|
+
255**. Nothing about that number says *"the winding"*; you have to already
|
|
819
|
+
suspect it.
|
|
820
|
+
3. **The `slot drift` column cannot see it at all.** It was `1.2 px "lid_r"` in
|
|
821
|
+
**all three** runs above, unchanged, because drift is attributed per **slot**
|
|
822
|
+
and a folded head mesh is entirely inside one slot.
|
|
823
|
+
|
|
824
|
+
β **So the honest procedure until #296 exists:** derive every number from Β§1,
|
|
825
|
+
check the **signs** (Β§3's nose test) and the **fold angle** (Β§4.2's formula)
|
|
826
|
+
arithmetically before you build, render, **look at three scales**, and keep a
|
|
827
|
+
render of the last build you trusted so `check` has something to be differential
|
|
828
|
+
against.
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## 10. The worked example
|
|
833
|
+
|
|
834
|
+
[`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait)
|
|
835
|
+
is this page on real art β **22 parts, 27 bones, 22 slots, 2 meshes, 40
|
|
836
|
+
vertices**, three animations, and a README that derives every number rather than
|
|
837
|
+
listing it. Its
|
|
838
|
+
[`FINDINGS.md`](https://github.com/firejune/rigc/tree/main/gallery/portrait/FINDINGS.md)
|
|
839
|
+
is the measurement half: what it cost, the seven-angle sweep, the five tool gaps
|
|
840
|
+
it filed.
|
|
841
|
+
|
|
842
|
+
```bash
|
|
843
|
+
bun install # once
|
|
844
|
+
|
|
845
|
+
bun cli.ts build --rig gallery/portrait/rig.json \
|
|
846
|
+
--motion gallery/portrait/motion.json \
|
|
847
|
+
--out gallery/portrait/build
|
|
848
|
+
bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
|
|
849
|
+
--out gallery/portrait/render
|
|
850
|
+
bun cli.ts preview --candidate gallery/portrait/build \
|
|
851
|
+
--out gallery/portrait/preview.html
|
|
852
|
+
|
|
853
|
+
# do the cycles close on the poses they opened with?
|
|
854
|
+
bun gallery/loop_seam.ts gallery/portrait/render/idle@25fps
|
|
855
|
+
bun gallery/loop_seam.ts gallery/portrait/render/gaze@25fps
|
|
856
|
+
bun gallery/loop_seam.ts gallery/portrait/render/turn@25fps
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
`build`, `render` and `preview` are not committed; the specs and the 22 part PNGs
|
|
860
|
+
are, and those commands regenerate the rest. **The frames to look at:**
|
|
861
|
+
|
|
862
|
+
| Frame | What it is |
|
|
863
|
+
| --- | --- |
|
|
864
|
+
| `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 |
|
|
865
|
+
| `render/turn@25fps/f0000.png` | rest, for the comparison. The pair is the whole example |
|
|
866
|
+
| `render/idle@25fps/f0028.png` | the blink, shut |
|
|
867
|
+
| `render/gaze@25fps/f0015.png` | the gaze, held |
|
|
868
|
+
|
|
869
|
+
**What those commands print** β re-run verbatim for this page, from a checkout
|
|
870
|
+
with no `build/`, `render/` or `preview.html` in that directory:
|
|
871
|
+
|
|
872
|
+
| Command | What came back |
|
|
873
|
+
| --- | --- |
|
|
874
|
+
| `build --profile spine` | green β **18 PASS, 7 SKIP**, 14 excluded by profile |
|
|
875
|
+
| `build --profile spine-html` | green β **26 PASS, 13 SKIP**, including `A13_MESH_BUDGET` and `A15_IDLE_NO_MESH_BONE_KEYS` |
|
|
876
|
+
| both `MESH` lines | `head` **100.00%** covered, reaching 95.90px past the art; `hair_bang` 100.00%, 55.22px |
|
|
877
|
+
| `render --fps 25 --max 640` | **81 + 39 + 56 frames**, 478Γ640, three contact sheets |
|
|
878
|
+
| `loop_seam.ts` Γ3 | **0 / 255**, **0 of 305 920 pixels** differing, for all three |
|
|
879
|
+
| `preview` | one 415 KiB HTML file, 22 pages embedded as data URIs |
|
|
880
|
+
|
|
881
|
+
π **Figures this page took from the record rather than re-deriving**, because
|
|
882
|
+
they need the artifact's own pixels: the blink's occlusion (hiding the whole eye
|
|
883
|
+
assembly at the shut hold changes **0 of 305 920** pixels; positive control at
|
|
884
|
+
rest moves **8 183**), the per-edge narrowing displacements, the `spine-core`
|
|
885
|
+
agreement of every posed column and scale with Β§1's line to **under 0.001 px**,
|
|
886
|
+
and the Web Player interop pass (**0 console errors, 0 page exceptions**). Note
|
|
887
|
+
what the first of those cost: hiding one slot at a time needs a throwaway script,
|
|
888
|
+
because `setup: { "slot": null }` crashes with a raw `TypeError`
|
|
889
|
+
([#293](https://github.com/firejune/rigc/issues/293)).
|
|
890
|
+
|
|
891
|
+
β **Vela is a second cast member and that was deliberate**, against the gallery's
|
|
892
|
+
own rule that its examples share one drawing. A 2.5D turn reads off four things: a
|
|
893
|
+
brow that frames an eye, an **iris and a highlight as separate parts**, hair in
|
|
894
|
+
**layers** that can lag the skull, and a cheek-to-jaw silhouette with a landmark
|
|
895
|
+
in it to foreshorten. The gallery's mascot has a muzzle, and a muzzle points
|
|
896
|
+
wherever the head points β so a mascot's turn is a bone rotation and nothing
|
|
897
|
+
else, which is precisely the move this page is not about. β **If the art you were
|
|
898
|
+
handed has no landmark to foreshorten, a turn will not read no matter how the
|
|
899
|
+
mesh is built**, and that is worth saying to the user before you build it.
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## 11. Non-goals β stated, so nobody proposes them as gaps
|
|
904
|
+
|
|
905
|
+
π« **No command generates a turn, and none should until the spec can state a
|
|
906
|
+
projection.** Β§1 is one line of arithmetic; a `rigc yaw --degrees 12` would be
|
|
907
|
+
guessing at every depth in Β§2 on the user's behalf, and depth is the parameter
|
|
908
|
+
the *author* is choosing. What the toolchain owes is that the file is checkable,
|
|
909
|
+
that you can look, and that a person can choose. **The generator that would
|
|
910
|
+
help is [#294](https://github.com/firejune/rigc/issues/294) β a way to *say* the
|
|
911
|
+
model in the spec β not a command that invents one.**
|
|
912
|
+
|
|
913
|
+
π« **No pass bar for a face, and nothing here to hang one on.** MOTION.md's
|
|
914
|
+
banner applies unchanged: `build` says a file is valid, `render` and `preview`
|
|
915
|
+
let you look, `vote` lets a person choose. The angles in Β§8 are where a
|
|
916
|
+
construction **stopped reading for one viewer looking at one drawing**, not
|
|
917
|
+
thresholds.
|
|
918
|
+
|
|
919
|
+
π« **No claim that 12Β° is the right angle for any request.** It is the angle the
|
|
920
|
+
worked example ships, chosen to sit comfortably inside a 5-column grid's
|
|
921
|
+
17.65Β° tangent limit. Β§4.2 is how to pick your own, and picking it **first** is
|
|
922
|
+
the entire point of that section.
|
|
923
|
+
|
|
924
|
+
β οΈ **Not a Live2D comparison, and not a recommendation between formats.** What
|
|
925
|
+
the worked example measured is that a portrait turn is authorable on plain Spine
|
|
926
|
+
4.3 at draft quality β nothing outside the format, no plugin, no runtime patch β
|
|
927
|
+
and that the **split is authoring cost rather than runtime capability**. The cost
|
|
928
|
+
is [#294](https://github.com/firejune/rigc/issues/294) and
|
|
929
|
+
[#295](https://github.com/firejune/rigc/issues/295). Whether to pay it is a
|
|
930
|
+
project's decision and this page does not make it.
|
|
931
|
+
|
|
932
|
+
π« **No per-eye mesh recipe.** Β§8 says the eyes need their own deform meshes past
|
|
933
|
+
about 26Β°, and nobody has built that here. The column-placement arithmetic in
|
|
934
|
+
Β§4.2 applies to any grid over any curved patch, so the tangent limit is the part
|
|
935
|
+
that carries over; **what a lash line and a wrapping lid need is unmeasured, and
|
|
936
|
+
this page does not guess.**
|
|
937
|
+
|
|
938
|
+
π« **No expression system, no visemes, no phoneme mapping.** A face that *acts* is
|
|
939
|
+
a different document and a different measurement. Everything here is one head
|
|
940
|
+
turning, blinking and looking β and the one general lesson that might carry into
|
|
941
|
+
that work is Β§7's: **allocate the channels before the first key, because Spine
|
|
942
|
+
blends and does not add.**
|
package/docs/MOTION.md
CHANGED
|
@@ -25,6 +25,10 @@ toolchain and this page does not invent one.
|
|
|
25
25
|
- The parts of that picture `pose` refuses because something is drawn over them,
|
|
26
26
|
once a first candidate exists: **AUTHORING Β§12** (`rigc chainfit`). It reports the
|
|
27
27
|
`rotate` key value each answer implies, which is the form this recipe wants them in
|
|
28
|
+
- If the movement is a **face** β a blink, a gaze, or a head turning off axis β
|
|
29
|
+
everything on this page still applies, and [FACE.md](FACE.md) is the geometry it
|
|
30
|
+
does not have: a turn is a projection rather than a pose, so its values are
|
|
31
|
+
evaluated rather than chosen
|
|
28
32
|
- If what you were handed is a **compiled skeleton** rather than loose parts β reading
|
|
29
33
|
it, transcribing it into specs, re-pivoting it, extending it with an animation:
|
|
30
34
|
[INGEST.md](INGEST.md)
|
|
@@ -985,6 +989,12 @@ things a candidate differs by, and a movement that does not read when rigid will
|
|
|
985
989
|
rescued by deforming it. β Land the rigid movement, choose between rigid candidates,
|
|
986
990
|
then propose deform as its own spread.
|
|
987
991
|
|
|
992
|
+
β οΈ **The one case where the deform is not an axis but the movement itself is a face
|
|
993
|
+
turning off axis**, because a rigid candidate of it does not exist β a yaw *is* the
|
|
994
|
+
redistribution. [FACE.md](FACE.md) is that case, and it also states what nothing here
|
|
995
|
+
measures: a `deform` key that folds a mesh inside out gates green on every assertion,
|
|
996
|
+
so a face's arithmetic has to be checked before the build rather than after the render.
|
|
997
|
+
|
|
988
998
|
π« **No frame rate anywhere in either spec file.** `render --fps` is a sampling rate for
|
|
989
999
|
looking; times in a motion spec are seconds (Β§3.3).
|
|
990
1000
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Rig compiler for Spine β declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"docs/PROMPTING.md",
|
|
45
45
|
"docs/MOTION.md",
|
|
46
46
|
"docs/INGEST.md",
|
|
47
|
+
"docs/FACE.md",
|
|
47
48
|
"NOTICE.md"
|
|
48
49
|
],
|
|
49
50
|
"publishConfig": {
|
package/src/compile.ts
CHANGED
|
@@ -205,6 +205,24 @@ function rgbaHex(v: number[]): string {
|
|
|
205
205
|
return v.map(channelHex).join('');
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Is this `motion.setup` entry the object shape AUTHORING Β§4.2 documents?
|
|
210
|
+
*
|
|
211
|
+
* An array is excluded on purpose: `typeof [] === 'object'` and a member read on
|
|
212
|
+
* one is silently `undefined`, which is the same false green a bare string gives.
|
|
213
|
+
*/
|
|
214
|
+
function isSetupEntry(v: unknown): boolean {
|
|
215
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** What a rejected `setup` entry actually was, for the refusal to name. */
|
|
219
|
+
function describeSetupEntry(v: unknown): string {
|
|
220
|
+
if (v === null) return 'null';
|
|
221
|
+
if (Array.isArray(v)) return 'an array';
|
|
222
|
+
if (typeof v === 'string') return `the string ${JSON.stringify(v)}`;
|
|
223
|
+
return `a ${typeof v}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
208
226
|
/**
|
|
209
227
|
* Bone timeline shapes: which JSON fields a key carries, and their defaults.
|
|
210
228
|
*
|
|
@@ -1113,6 +1131,30 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
1113
1131
|
if (!names.length) continue;
|
|
1114
1132
|
|
|
1115
1133
|
const setup = motion.setup?.[rigSlot.name];
|
|
1134
|
+
// β οΈ `motion` is a CAST, not a parse (`readJson<MotionSpec>`), so the type
|
|
1135
|
+
// says what a correct file holds and not what this one does. A `setup` entry
|
|
1136
|
+
// that is not an object gets past every `!== undefined` guard below and then
|
|
1137
|
+
// reads a member off it β and the two ways to write one are both on the
|
|
1138
|
+
// shortest path to the documented feature (issue #293):
|
|
1139
|
+
//
|
|
1140
|
+
// "lid_l": null the natural elision of `{ "attachment": null }`, by
|
|
1141
|
+
// a reader who has just learned `attachment` may be
|
|
1142
|
+
// null. Used to crash with a raw TypeError.
|
|
1143
|
+
// "lid_l": "plate" the attachment name where its wrapper belongs. Far
|
|
1144
|
+
// worse: `.attachment` on a string is `undefined`, so
|
|
1145
|
+
// it compiled GREEN and hid the slot β the exact
|
|
1146
|
+
// opposite of what was asked, stated nowhere.
|
|
1147
|
+
//
|
|
1148
|
+
// One guard for both, because the second is why this cannot be a `!== null`
|
|
1149
|
+
// check: a silent wrong is worse than a crash, and the same line makes both.
|
|
1150
|
+
if (setup !== undefined && !isSetupEntry(setup)) {
|
|
1151
|
+
throw new CompileError(
|
|
1152
|
+
`${motionPath}: \`setup."${rigSlot.name}"\` is ${describeSetupEntry(setup)}; a setup entry is an object ` +
|
|
1153
|
+
`of \`{ attachment?: string | null, color?: [r, g, b, a] }\` β to show nothing there write ` +
|
|
1154
|
+
`\`"${rigSlot.name}": { "attachment": null }\`, and to show an attachment write ` +
|
|
1155
|
+
`\`"${rigSlot.name}": { "attachment": "<name>" }\``,
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1116
1158
|
if (setup !== undefined && rigSlot.attachment !== undefined) {
|
|
1117
1159
|
throw new CompileError(
|
|
1118
1160
|
`slot "${rigSlot.name}" has a setup attachment in the rig spec AND in the motion spec; the setup pose has one author`,
|
package/src/pose.ts
CHANGED
|
@@ -54,10 +54,18 @@
|
|
|
54
54
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
55
55
|
import { basename, join, resolve } from 'node:path';
|
|
56
56
|
import { Plate, readPlate } from '../tools/plate.ts';
|
|
57
|
-
//
|
|
58
|
-
// between two pixel centres is exactly the kind of thing two
|
|
59
|
-
// drift on
|
|
60
|
-
|
|
57
|
+
// The rasteriser's sampler rather than a second one written here: how a pixel is
|
|
58
|
+
// read between two pixel centres is exactly the kind of thing two
|
|
59
|
+
// implementations drift on.
|
|
60
|
+
//
|
|
61
|
+
// β οΈ `bilinearChannels`, NOT `bilinear`. `bilinear` interpolates in
|
|
62
|
+
// premultiplied space, which is correct for an atlas and wrong for what this
|
|
63
|
+
// file samples: `materialPlate` hands it the frame's own RGB with alpha rewritten
|
|
64
|
+
// to mean "how much material is here", so the two are decoupled by construction
|
|
65
|
+
// and `errBilinear` combines them itself. Issue #292 moved `bilinear` to the
|
|
66
|
+
// premultiplied form and left this call on the channel-independent one β the
|
|
67
|
+
// objective's own semantics, and unchanged fitting numbers with it.
|
|
68
|
+
import { bilinearChannels } from './render.ts';
|
|
61
69
|
|
|
62
70
|
export class PoseError extends Error {}
|
|
63
71
|
|
|
@@ -428,7 +436,7 @@ export function readBackground(frame: Plate): PoseBackground {
|
|
|
428
436
|
* The frame as the objective reads it: the frame's own RGB, with alpha rewritten
|
|
429
437
|
* to mean **how much material is here** rather than how opaque the file is.
|
|
430
438
|
*
|
|
431
|
-
* Keeping it in a `Plate` is what lets the pyramid, `
|
|
439
|
+
* Keeping it in a `Plate` is what lets the pyramid, `bilinearChannels` and the nearest
|
|
432
440
|
* lookup all be the ones this repository already has.
|
|
433
441
|
*/
|
|
434
442
|
export function materialPlate(frame: Plate, background: PoseBackground): { plate: Plate; share: number } {
|
|
@@ -568,7 +576,7 @@ export function errNearest(level: Level, x: number, y: number, pr: number, pg: n
|
|
|
568
576
|
/** Same, sampled between pixel centres β what the refinement stages measure with. */
|
|
569
577
|
export function errBilinear(level: Level, plate: Plate, x: number, y: number, pr: number, pg: number, pb: number): number {
|
|
570
578
|
if (x < 0 || y < 0 || x >= level.width || y >= level.height) return 1;
|
|
571
|
-
const [fr, fg, fb, fa] =
|
|
579
|
+
const [fr, fg, fb, fa] = bilinearChannels(plate, x - 0.5, y - 0.5);
|
|
572
580
|
const m = fa / 255;
|
|
573
581
|
if (m <= 0) return 1;
|
|
574
582
|
const d = (Math.abs(fr - pr) + Math.abs(fg - pg) + Math.abs(fb - pb)) / 765;
|
package/src/render.ts
CHANGED
|
@@ -23,13 +23,20 @@
|
|
|
23
23
|
* real runtime would. Each triangle is then filled with barycentric UV
|
|
24
24
|
* interpolation.
|
|
25
25
|
*
|
|
26
|
-
* β **Sampling is bilinear on both paths, and the source is straight alpha
|
|
27
|
-
* One filter rather than two is not a
|
|
28
|
-
* against reference frames, and a mesh
|
|
29
|
-
* reference sampled bilinear would put a
|
|
30
|
-
* where only a rig difference belongs.
|
|
31
|
-
* region path was already bilinear and
|
|
32
|
-
*
|
|
26
|
+
* β **Sampling is bilinear on both paths, and the source is straight alpha β
|
|
27
|
+
* so the interpolation is premultiplied.** One filter rather than two is not a
|
|
28
|
+
* detail: `check` measures a candidate against reference frames, and a mesh
|
|
29
|
+
* triangle sampled nearest against a reference sampled bilinear would put a
|
|
30
|
+
* filter difference into the residual where only a rig difference belongs.
|
|
31
|
+
* Bilinear rather than nearest because the region path was already bilinear and
|
|
32
|
+
* the five committed rungs are rendered with it.
|
|
33
|
+
*
|
|
34
|
+
* Straight alpha is a property of the SOURCE, not a licence to average it
|
|
35
|
+
* channel by channel: a transparent texel's `(0, 0, 0, 0)` is the absence of a
|
|
36
|
+
* colour, and giving it a vote drew a dark rim along every region edge β over
|
|
37
|
+
* the top of whatever was behind the part, and into `check`'s residual on the
|
|
38
|
+
* candidate side. `bilinear` weights each colour by its own alpha and divides
|
|
39
|
+
* back out; see it for what that does and does not move (issue #292).
|
|
33
40
|
*
|
|
34
41
|
* β οΈ **Region rasterising is untouched by the mesh path**, deliberately. A region
|
|
35
42
|
* could be drawn as two triangles and very nearly the same pixels would come out;
|
|
@@ -1260,29 +1267,118 @@ export function blitPiece(
|
|
|
1260
1267
|
rasterisePiece(page, piece, project, dst, (px, py, r, g, b, a) => dst.blend(px, py, [r, g, b, a]));
|
|
1261
1268
|
}
|
|
1262
1269
|
|
|
1263
|
-
|
|
1270
|
+
/** The four texels one bilinear tap reads, and the fractions between them. */
|
|
1271
|
+
interface Taps {
|
|
1272
|
+
c00: RGBA;
|
|
1273
|
+
c10: RGBA;
|
|
1274
|
+
c01: RGBA;
|
|
1275
|
+
c11: RGBA;
|
|
1276
|
+
fx: number;
|
|
1277
|
+
fy: number;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* The four texels around `(x, y)`, CLAMPED at the page edge.
|
|
1282
|
+
*
|
|
1283
|
+
* β οΈ The clamp is load-bearing beyond this function: `src/atlas.ts` sizes the
|
|
1284
|
+
* gutter between packed regions against the fact that one tap reaches exactly one
|
|
1285
|
+
* texel, and `gallery/portrait`'s lid runs its art flush to its own window
|
|
1286
|
+
* because a clamped tap has no transparent neighbour to reach into. Widening the
|
|
1287
|
+
* tap is not a local change.
|
|
1288
|
+
*/
|
|
1289
|
+
function taps(page: Plate, x: number, y: number): Taps {
|
|
1264
1290
|
const x0 = Math.floor(x);
|
|
1265
1291
|
const y0 = Math.floor(y);
|
|
1266
|
-
const fx = x - x0;
|
|
1267
|
-
const fy = y - y0;
|
|
1268
1292
|
const at = (ix: number, iy: number): RGBA => {
|
|
1269
1293
|
const cx = Math.max(0, Math.min(page.width - 1, ix));
|
|
1270
1294
|
const cy = Math.max(0, Math.min(page.height - 1, iy));
|
|
1271
1295
|
return page.get(cx, cy);
|
|
1272
1296
|
};
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1297
|
+
return { c00: at(x0, y0), c10: at(x0 + 1, y0), c01: at(x0, y0 + 1), c11: at(x0 + 1, y0 + 1), fx: x - x0, fy: y - y0 };
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/** One channel of a bilinear tap: lerp along x on both rows, then between them. */
|
|
1301
|
+
function lerpTap(v00: number, v10: number, v01: number, v11: number, fx: number, fy: number): number {
|
|
1302
|
+
const top = v00 + (v10 - v00) * fx;
|
|
1303
|
+
const bottom = v01 + (v11 - v01) * fx;
|
|
1304
|
+
return top + (bottom - top) * fy;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Sample a straight-alpha page bilinearly β interpolating in PREMULTIPLIED space.
|
|
1309
|
+
*
|
|
1310
|
+
* β **Why the premultiply.** The source is straight alpha, so a transparent
|
|
1311
|
+
* texel beside the art is `(0, 0, 0, 0)`: its colour is not a colour, it is the
|
|
1312
|
+
* absence of one. Averaging R, G and B against it pulls the sample toward black
|
|
1313
|
+
* while alpha only drops part of the way, and the difference between those two
|
|
1314
|
+
* rates IS a dark rim, one pixel wide, drawn over whatever is behind the part.
|
|
1315
|
+
* Weighting each colour by its own alpha and dividing the sum back out gives the
|
|
1316
|
+
* transparent texel no vote in the colour, which is the whole of the fix: two
|
|
1317
|
+
* parts of one colour, overlapping, come out that colour. Measured before the
|
|
1318
|
+
* fix at β60/255 between two parts sharing one flat field, and β31/255 down
|
|
1319
|
+
* `gallery/portrait`'s forehead β issue #292.
|
|
1320
|
+
*
|
|
1321
|
+
* β **Why alpha is computed the old way, and why equal alpha short-circuits.**
|
|
1322
|
+
* `rasteriseQuad` and `rasteriseMesh` gate coverage on `alpha > 0.5`, so the
|
|
1323
|
+
* alpha arithmetic decides WHICH pixels are drawn β and through `frameGeometry`,
|
|
1324
|
+
* the framing box every reference frame was rendered inside. `lerpTap` on the
|
|
1325
|
+
* alpha channel is therefore the original expression, unchanged, not an
|
|
1326
|
+
* algebraically equal rearrangement: equal-but-rearranged is a last-bit
|
|
1327
|
+
* difference, and a last bit either side of 0.5 is a pixel.
|
|
1328
|
+
*
|
|
1329
|
+
* For the same reason the equal-alpha case returns early. When all four taps
|
|
1330
|
+
* carry one alpha, premultiplying by it and dividing it back out is the identity
|
|
1331
|
+
* β so the straight path is not an approximation there, it is the same number,
|
|
1332
|
+
* and taking it reproduces the five committed rungs BIT for bit rather than
|
|
1333
|
+
* merely closely. What moves is exactly the mixed-alpha tap: the edges, where the
|
|
1334
|
+
* rim was.
|
|
1335
|
+
*/
|
|
1336
|
+
export function bilinear(page: Plate, x: number, y: number): [number, number, number, number] {
|
|
1337
|
+
const { c00, c10, c01, c11, fx, fy } = taps(page, x, y);
|
|
1338
|
+
const a = lerpTap(c00[3], c10[3], c01[3], c11[3], fx, fy);
|
|
1339
|
+
if (c00[3] === c10[3] && c00[3] === c01[3] && c00[3] === c11[3]) {
|
|
1340
|
+
return [
|
|
1341
|
+
lerpTap(c00[0], c10[0], c01[0], c11[0], fx, fy),
|
|
1342
|
+
lerpTap(c00[1], c10[1], c01[1], c11[1], fx, fy),
|
|
1343
|
+
lerpTap(c00[2], c10[2], c01[2], c11[2], fx, fy),
|
|
1344
|
+
a,
|
|
1345
|
+
];
|
|
1346
|
+
}
|
|
1347
|
+
// Every tap is transparent in some proportion that sums to nothing: there is no
|
|
1348
|
+
// colour to recover and no pixel to draw (both callers gate on alpha anyway).
|
|
1349
|
+
if (a <= 0) return [0, 0, 0, 0];
|
|
1350
|
+
const out: [number, number, number, number] = [0, 0, 0, a];
|
|
1351
|
+
for (let c = 0; c < 3; c++) {
|
|
1352
|
+
const pm = lerpTap(c00[c] * c00[3], c10[c] * c10[3], c01[c] * c01[3], c11[c] * c11[3], fx, fy);
|
|
1353
|
+
// Bounded by 255 in exact arithmetic β the weighted mean of the taps' colours
|
|
1354
|
+
// cannot exceed their maximum β so the clamp absorbs float error only. It is
|
|
1355
|
+
// here rather than trusted because `Plate`'s store is a `Uint8Array`, which
|
|
1356
|
+
// WRAPS: 256 would land as a black pixel in the brightest part of the art.
|
|
1357
|
+
out[c] = Math.min(255, pm / a);
|
|
1282
1358
|
}
|
|
1283
1359
|
return out;
|
|
1284
1360
|
}
|
|
1285
1361
|
|
|
1362
|
+
/**
|
|
1363
|
+
* The same tap, each channel interpolated independently.
|
|
1364
|
+
*
|
|
1365
|
+
* β οΈ For a plate whose fourth channel is **not opacity**. `src/pose.ts`'s
|
|
1366
|
+
* `materialPlate` keeps the frame's own RGB and rewrites alpha to mean "how much
|
|
1367
|
+
* material is here", so its colour and its fourth channel are decoupled by
|
|
1368
|
+
* construction β premultiplying by it would be a colour-space correction applied
|
|
1369
|
+
* to something that is not in that colour space. `errBilinear` reads its two
|
|
1370
|
+
* answers separately and combines them itself.
|
|
1371
|
+
*
|
|
1372
|
+
* Nothing that samples an ATLAS should call this. The rasteriser's source is
|
|
1373
|
+
* straight alpha and wants `bilinear`.
|
|
1374
|
+
*/
|
|
1375
|
+
export function bilinearChannels(page: Plate, x: number, y: number): [number, number, number, number] {
|
|
1376
|
+
const { c00, c10, c01, c11, fx, fy } = taps(page, x, y);
|
|
1377
|
+
const out: [number, number, number, number] = [0, 0, 0, 0];
|
|
1378
|
+
for (let c = 0; c < 4; c++) out[c] = lerpTap(c00[c], c10[c], c01[c], c11[c], fx, fy);
|
|
1379
|
+
return out;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1286
1382
|
export function fill(plate: Plate, colour: RGBA): void {
|
|
1287
1383
|
for (let y = 0; y < plate.height; y++) for (let x = 0; x < plate.width; x++) plate.set(x, y, colour);
|
|
1288
1384
|
}
|