spine-rigc 0.2.1 → 0.3.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 +231 -6
- package/cli.ts +39 -5
- package/docs/AUTHORING.md +611 -24
- package/docs/SPEC_COVERAGE.md +16 -10
- package/package.json +5 -2
- package/src/chains.ts +170 -0
- package/src/check.ts +1013 -92
- package/src/compile.ts +325 -6
- package/src/ladder.ts +1 -1
- package/src/render.ts +22 -2
- package/src/rig.ts +169 -6
- package/src/timelines.ts +9 -5
- package/src/types.ts +80 -1
- package/src/validate.ts +192 -2
package/docs/AUTHORING.md
CHANGED
|
@@ -20,7 +20,8 @@ that can see that, and a run that skips it has verified nothing about the motion
|
|
|
20
20
|
- The motion spec and emitted shapes: [`src/types.ts`](../src/types.ts)
|
|
21
21
|
- What the format holds and rigc covers: [SPEC_COVERAGE.md](SPEC_COVERAGE.md)
|
|
22
22
|
- Reproducing a shot you were given as pictures: **§8**, and read it *before* you
|
|
23
|
-
start measuring rather than after;
|
|
23
|
+
start measuring rather than after; **§8.1** if the figure has more joints than you
|
|
24
|
+
can measure one at a time; then **§9** for the loop that closes it
|
|
24
25
|
- The conventions an editor user follows without being told — one image per
|
|
25
26
|
attachment, keying practice, curve kind, draw order — sourced from Spine's own
|
|
26
27
|
public documentation: **§10**
|
|
@@ -81,7 +82,7 @@ What the flags mean:
|
|
|
81
82
|
| `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths are written relative to it |
|
|
82
83
|
| `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory) |
|
|
83
84
|
| `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
|
|
84
|
-
| `--profile` | `spine` = the
|
|
85
|
+
| `--profile` | `spine` = the 20 validity rules · `spine-html` = all 34 (**the default**) |
|
|
85
86
|
|
|
86
87
|
Pick the profile deliberately. `spine-html` adds one renderer's policy and one
|
|
87
88
|
project's canvas budget, and those rules fire on perfectly correct Spine data
|
|
@@ -257,6 +258,14 @@ behind it writes literal `x`/`y` instead.
|
|
|
257
258
|
`spec` must be exactly `"rigc-rig/1"`. `name` must be a non-empty string. `bones`
|
|
258
259
|
must be non-empty. `slots` must be present (it may be empty).
|
|
259
260
|
|
|
261
|
+
🚫 **Every example value below is invented.** Names, coordinates, vertex lists and
|
|
262
|
+
payloads in this guide are written to illustrate a field, never copied out of a
|
|
263
|
+
reference export — an example lifted from one would be handing an authoring agent an
|
|
264
|
+
answer to the rung it is standing on, which is the rule §10.6 states and the honesty
|
|
265
|
+
rule in [LADDER.md](LADDER.md) turns on. If a snippet here matches a reference file,
|
|
266
|
+
that is a defect in this guide: report it. (It has happened — 2026-08-23, see
|
|
267
|
+
[`bench/runs/README.md`](../bench/runs/README.md), *What a run may read*.)
|
|
268
|
+
|
|
260
269
|
### 3.1 `skeleton` — the header
|
|
261
270
|
|
|
262
271
|
| Field | Spine meaning | Default |
|
|
@@ -349,9 +358,9 @@ Geometry comes in one of two fields:
|
|
|
349
358
|
|
|
350
359
|
```json
|
|
351
360
|
"weights": [
|
|
352
|
-
[{ "bone": "
|
|
353
|
-
{ "bone": "
|
|
354
|
-
[{ "bone": "
|
|
361
|
+
[{ "bone": "link_a", "x": 40, "y": 0, "weight": 0.25 },
|
|
362
|
+
{ "bone": "link_b", "x": -20, "y": 0, "weight": 0.75 }],
|
|
363
|
+
[{ "bone": "link_b", "x": -60, "y": 12, "weight": 1 }]
|
|
355
364
|
]
|
|
356
365
|
```
|
|
357
366
|
|
|
@@ -393,6 +402,59 @@ they encode a deformation model rather than a table of numbers, which is why the
|
|
|
393
402
|
are code invoked by data. A generator is for a skeleton with **no** manifest; a cut
|
|
394
403
|
that has one invokes the same builders through the manifest's `mesh` block.
|
|
395
404
|
|
|
405
|
+
**Bounding box** ([Spine: bounding boxes](http://esotericsoftware.com/spine-bounding-boxes))
|
|
406
|
+
and **clipping** ([Spine: clipping](http://esotericsoftware.com/spine-clipping))
|
|
407
|
+
attachments — a polygon, and nothing else.
|
|
408
|
+
|
|
409
|
+
**When you need one:** a *bounding box* is a shape the game hit-tests against — a
|
|
410
|
+
hurt box, a pick region, a trigger volume — that follows the skeleton and draws
|
|
411
|
+
nothing. A *clipping* attachment is a **mask**: everything drawn from the slot
|
|
412
|
+
carrying it up to and including `end` is clipped to the polygon, so a window, a
|
|
413
|
+
portal or a wipe is one attachment rather than a second set of art.
|
|
414
|
+
|
|
415
|
+
```json
|
|
416
|
+
"hitbox_a": { "hitbox_a": { "type": "boundingbox", "vertexCount": 4,
|
|
417
|
+
"vertices": [-30, -10, 30, -10, 30, 50, -30, 50] } },
|
|
418
|
+
"mask_a": { "mask_a": { "type": "clipping", "end": "box", "vertexCount": 3,
|
|
419
|
+
"vertices": [0, 0, 200, 0, 0, 160],
|
|
420
|
+
"color": "ff00ffff" } }
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Both polygons above are invented — an axis-aligned rectangle and a right triangle,
|
|
424
|
+
in round numbers, so that nothing here can be mistaken for a shape measured off a
|
|
425
|
+
reference. A real one is measured off your own art (§8) or drawn to the volume the
|
|
426
|
+
game needs.
|
|
427
|
+
|
|
428
|
+
| Field | Meaning |
|
|
429
|
+
| --- | --- |
|
|
430
|
+
| `vertexCount` | **required.** How many vertices the polygon has, stated outright — see the warning below |
|
|
431
|
+
| `vertices` / `weights` | the same two encodings a mesh's geometry uses, with the same by-name default and the same `"boneIndexing": "raw"` opt-in |
|
|
432
|
+
| `color` | `rrggbbaa`; the colour the editor draws the outline in |
|
|
433
|
+
| `end` | clipping only. The **last** slot the clip applies to, by name |
|
|
434
|
+
| `convex`, `inverse` | clipping only, 4.3, both default false |
|
|
435
|
+
|
|
436
|
+
🚨 **`vertexCount` has no parser default and rigc will not infer one.** A mesh gets
|
|
437
|
+
its count from `uvs.length`; a polygon has no uvs, and the parser reads
|
|
438
|
+
`map.vertexCount << 1` as the number of coordinates to expect. With the field
|
|
439
|
+
absent that is `undefined << 1` = **0**, so the coordinate array is decoded as a
|
|
440
|
+
*weighted* run — bone counts and weights read out of your x/y pairs — and the
|
|
441
|
+
attachment ends up holding nothing. It loads. Neither type draws a pixel, so
|
|
442
|
+
nothing downstream notices. rigc requires the count and cross-checks it against
|
|
443
|
+
whichever encoding you used; `A33_VERTEX_ATTACHMENT_GEOMETRY` checks it again on
|
|
444
|
+
the artifact.
|
|
445
|
+
|
|
446
|
+
⚠️ **A clipping `end` that names nothing is not an error to Spine.**
|
|
447
|
+
`skeletonData.findSlot` returns `null` on a miss and the parser assigns that null
|
|
448
|
+
without a word, so the clip never ends — it runs to the bottom of the draw order
|
|
449
|
+
and takes every slot below it out of the frame. rigc refuses a name the rig does
|
|
450
|
+
not declare. Omitting `end` entirely is the format's own way of saying "clip
|
|
451
|
+
everything after this one", and is left alone.
|
|
452
|
+
|
|
453
|
+
🚫 Under the default `spine-html` profile a clipping attachment is refused by
|
|
454
|
+
`A11_NO_CLIPPING_ATTACHMENTS` — that renderer skips them silently, so a mask that
|
|
455
|
+
was supposed to hide something would not. It is valid Spine and `--profile spine`
|
|
456
|
+
accepts it; the refusal is policy, not validity.
|
|
457
|
+
|
|
396
458
|
### 3.5 `constraints` — 4.3's single typed array
|
|
397
459
|
|
|
398
460
|
Spine 4.3 folds every constraint into one `constraints` array with a `type`
|
|
@@ -413,7 +475,38 @@ Field lists are in [`src/rig.ts`](../src/rig.ts); three traps worth carrying her
|
|
|
413
475
|
them parses cleanly and does nothing at all. rigc refuses it up front, and `A23`
|
|
414
476
|
catches it from the other side.
|
|
415
477
|
|
|
416
|
-
### 3.6 `
|
|
478
|
+
### 3.6 `events` — names the animation can fire
|
|
479
|
+
|
|
480
|
+
**When you need one:** something outside the skeleton has to happen on a
|
|
481
|
+
particular frame — a footstep sound, a spawn, a hit window opening. Spine
|
|
482
|
+
[events](http://esotericsoftware.com/spine-events) carry no rendering effect at
|
|
483
|
+
all; they are a named signal the game listens for, with an optional payload.
|
|
484
|
+
|
|
485
|
+
The **declaration** lives here, in the rig spec, because the name is structure.
|
|
486
|
+
The **firings** live in the motion spec (§4.8), because when they happen is time.
|
|
487
|
+
|
|
488
|
+
```json
|
|
489
|
+
"events": {
|
|
490
|
+
"cue_a": {},
|
|
491
|
+
"cue_b": { "audio": "cue_b.ogg", "volume": 0.8, "string": "line-01" }
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
An object keyed by event name — the one top-level collection in the format that
|
|
496
|
+
is not an array. Every field is optional and each is the payload a firing
|
|
497
|
+
**inherits** when it does not override it: `int` (0), `float` (0), `string`
|
|
498
|
+
(`""`), `audio` (none), `volume` and `balance`.
|
|
499
|
+
|
|
500
|
+
- **An empty object is the normal case, not a stub.** Most events carry no payload:
|
|
501
|
+
the name *is* the signal, and the declaration exists so the firings in §4.8 have
|
|
502
|
+
something to resolve against. Write `{}` and move on.
|
|
503
|
+
- ⚠️ `volume` and `balance` are read **only when `audio` is set**. Without an
|
|
504
|
+
audio path the parser drops them without a word, so rigc refuses that pairing
|
|
505
|
+
rather than emitting two numbers no runtime will read.
|
|
506
|
+
- An event that nothing fires is legitimate: a skeleton may declare the vocabulary
|
|
507
|
+
its game listens for and key only some of it in any one animation.
|
|
508
|
+
|
|
509
|
+
### 3.7 `invariants` — what the artifact cannot say about itself
|
|
417
510
|
|
|
418
511
|
Optional, and only meaningful for rigc's own formations: `meshSlots` and
|
|
419
512
|
`meshTriangles` (the two halves of the mesh budget `A13` measures against),
|
|
@@ -456,6 +549,7 @@ time puts it here.
|
|
|
456
549
|
| `note` | free text |
|
|
457
550
|
| `tracks` | the timelines |
|
|
458
551
|
| `drawOrder` | the draw-order timeline — §4.7. Not a track: it names no target |
|
|
552
|
+
| `events` | the event timeline — §4.8. Not a track, for the same reason |
|
|
459
553
|
|
|
460
554
|
`groups` (`name → [member, …]`) lets one track target several bones or slots at
|
|
461
555
|
once; `lag` shifts every key of a track, and `stagger` adds a per-member delay in
|
|
@@ -496,6 +590,19 @@ stepped.
|
|
|
496
590
|
Seconds, not frames: nothing requires a key to land on any frame grid, and a
|
|
497
591
|
reference rendered at some rate says nothing about where its keys are. Put keys
|
|
498
592
|
where the motion changes.
|
|
593
|
+
- **Key times are quantised onto a 1e-6 s grid by rounding DOWN, never to
|
|
594
|
+
nearest.** A key time is a position against the sample grid a player will step,
|
|
595
|
+
and the two directions of a half-step error are not the same size. `2/12 s` and
|
|
596
|
+
`5/30 s` are both 0.16666666…; `0.166667` is *larger* than either, so a key
|
|
597
|
+
emitted there is applied at sample **3** of a 12 fps playback and not sample 2 —
|
|
598
|
+
a whole frame late, with nothing raised. On a **stepped** timeline (an attachment
|
|
599
|
+
timeline always is) that is the wrong picture rather than a slightly wrong value:
|
|
600
|
+
the spineboy run's muzzle flare fired a frame late for exactly this until the
|
|
601
|
+
run's own frame check caught it (issue #99). Rounding down cannot do that; the
|
|
602
|
+
worst it can do is put a key a millionth of a second early, on the sample it was
|
|
603
|
+
written for. ⚠️ What this does **not** protect you from is rounding your own
|
|
604
|
+
times before you write them — write `2/12`, not `0.1667`, and let the compiler
|
|
605
|
+
do the quantising.
|
|
499
606
|
- **No key may land past the animation's `duration`.** Nothing that plays the
|
|
500
607
|
animation for the duration it declares ever reaches such a key, so it is a
|
|
501
608
|
compile error — checked on **every timeline**, not just on the latest key in the
|
|
@@ -578,6 +685,53 @@ the emitted file is the parser's requirement rather than a decision of yours, so
|
|
|
578
685
|
you state the set of moves and rigc writes them in the order the parser needs.
|
|
579
686
|
`A31_DRAW_ORDER_OFFSETS_RESOLVE` checks all four from the other side.
|
|
580
687
|
|
|
688
|
+
### 4.8 `events` — firing a declared event
|
|
689
|
+
|
|
690
|
+
The second timeline that names no target, and it sits on the animation for the
|
|
691
|
+
same reason `drawOrder` does: 4.3 writes it as `animations.<a>.events`, beside
|
|
692
|
+
`bones` and `slots`.
|
|
693
|
+
|
|
694
|
+
```json
|
|
695
|
+
"animations": {
|
|
696
|
+
"walk": {
|
|
697
|
+
"duration": 1.0666666,
|
|
698
|
+
"loop": true,
|
|
699
|
+
"events": [
|
|
700
|
+
{ "t": 0, "name": "cue_a" },
|
|
701
|
+
{ "t": 0.5333333, "name": "cue_a", "int": 2 }
|
|
702
|
+
],
|
|
703
|
+
"tracks": []
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
- `name` must be a key of the rig spec's `events` block (§3.6). A miss is a
|
|
709
|
+
compile error here; in raw JSON the parser **throws** `Event not found` in the
|
|
710
|
+
consumer's process, which is late.
|
|
711
|
+
- `int`, `float` and `string` override the declaration's payload **for this firing
|
|
712
|
+
only**. Omit them and the firing inherits the declared defaults, which is what
|
|
713
|
+
the editor writes.
|
|
714
|
+
- `volume` and `balance` are accepted only on an event that declares `audio`, for
|
|
715
|
+
the same reason as §3.6.
|
|
716
|
+
- Event keys are **instantaneous** and carry no `ease` or `curve`.
|
|
717
|
+
- Its last key counts towards the declared duration like any other (R7).
|
|
718
|
+
|
|
719
|
+
rigc refuses three things here, and only the first is loud in the parser:
|
|
720
|
+
|
|
721
|
+
| You wrote | You get |
|
|
722
|
+
| --- | --- |
|
|
723
|
+
| a name the rig spec does not declare | `event "X" is not declared in the rig spec's "events" block; declared: …` |
|
|
724
|
+
| a key time earlier than the key before it | `key times must not go backwards (at t=0.25, after t=0.5)` |
|
|
725
|
+
| `volume`/`balance` on an event with no `audio` | `volume is set but event "X" declares no "audio"` |
|
|
726
|
+
|
|
727
|
+
The ordering rule is **non-decreasing**, not strictly increasing: two different
|
|
728
|
+
events on one frame is an ordinary thing to want, and unlike a value track there
|
|
729
|
+
is no contradiction in it. What is refused is going *backwards* —
|
|
730
|
+
`readAnimation` fills frame `i` from key `i` in array order and never sorts, so a
|
|
731
|
+
decreasing time builds an `EventTimeline` whose earlier firing is simply
|
|
732
|
+
unreachable, with a perfectly clean load.
|
|
733
|
+
`A32_EVENT_KEYS_RESOLVE` checks all three from the other side.
|
|
734
|
+
|
|
581
735
|
---
|
|
582
736
|
|
|
583
737
|
## 5. Reading a failure
|
|
@@ -609,6 +763,12 @@ the frequent ones, verbatim:
|
|
|
609
763
|
| `animation "A" has two tracks on X.property; merge them into one track` | one timeline per target property |
|
|
610
764
|
| `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | §3.1 |
|
|
611
765
|
| `drawOrder at t=…: slot "X" is not one this rig emits` / `is offset twice in one key` / `puts it at N, outside the … emitted slots` | §4.7 |
|
|
766
|
+
| `events at t=…: event "X" is not declared in the rig spec's "events" block` | declare it in the rig spec (§3.6), or fix the name |
|
|
767
|
+
| `events: key times must not go backwards` | put the firings in time order (§4.8) |
|
|
768
|
+
| `events at t=…: volume is set but event "X" declares no "audio"` | drop `volume`/`balance`, or give the event an audio path |
|
|
769
|
+
| `vertexCount is undefined; a polygon needs at least 3 vertices, stated outright` | give the bounding box or clipping attachment a `vertexCount` (§3.4) |
|
|
770
|
+
| `vertexCount N wants M unweighted numbers and "vertices" holds K` | fix the count or the array; they decide the encoding between them |
|
|
771
|
+
| `end names slot "X", which this rig does not declare` | fix the clipping attachment's `end`, or add the slot |
|
|
612
772
|
| `bone "X" takes its position from …, which needs a cut manifest` | R8 — pass `--manifest`, or write literal `x`/`y` |
|
|
613
773
|
|
|
614
774
|
### 5.2 Assertions — the gate
|
|
@@ -668,6 +828,8 @@ The report prints one line per assertion:
|
|
|
668
828
|
| `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
|
|
669
829
|
| `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
|
|
670
830
|
| `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted |
|
|
831
|
+
| `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
|
|
832
|
+
| `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box or clipping polygon whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index, or a clipping `end` naming a slot the skeleton does not have (§3.4). **SKIP** when the skeleton carries neither type |
|
|
671
833
|
|
|
672
834
|
`both ◑` marks a mixed assertion: its validity half always runs and its policy
|
|
673
835
|
clauses are gated by profile.
|
|
@@ -681,9 +843,14 @@ a **`NotImplementedError` naming the field**, because the parser's own behaviour
|
|
|
681
843
|
worse: an unknown attachment `type` returns `null` and the attachment disappears,
|
|
682
844
|
and a constraint entry with an unrecognised `type` matches no case and vanishes.
|
|
683
845
|
|
|
846
|
+
Each is deferred for a stated reason, and the reason is the same one in every row:
|
|
847
|
+
**not one of these types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
|
|
848
|
+
parts 3-1 and 4-2), so none of them is on the ladder's critical path. The message
|
|
849
|
+
says so, because a deferral without its reason is a wall rather than a work item.
|
|
850
|
+
|
|
684
851
|
| You wrote | You get |
|
|
685
852
|
| --- | --- |
|
|
686
|
-
| attachment `type` of `
|
|
853
|
+
| attachment `type` of `point`, `path`, `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh, boundingbox, clipping. point, path and linkedmesh are deliberately deferred: not one of them appears anywhere in the benchmark corpus …` |
|
|
687
854
|
| constraint `type` of `path` or `slider` | `constraint type "X" … Implemented: ik, transform, physics.` |
|
|
688
855
|
| mesh `generator.kind` of `contour` | `the "contour" generator would triangulate a part's own alpha mask, and src/mesh.ts has no triangulator` |
|
|
689
856
|
|
|
@@ -774,6 +941,28 @@ where one part's **interior detail** — a marking, a highlight, anything not on
|
|
|
774
941
|
outline — lies inside the other part's area, and see which survives. Then write the
|
|
775
942
|
slots in that order (R4), because there is no other place in the file to say it.
|
|
776
943
|
|
|
944
|
+
**That reads the reference only, so it settles the edges the reference happens to
|
|
945
|
+
show an interior detail on, and no more. There is a second test, and it decides more
|
|
946
|
+
of them: render your own candidate both ways and measure the same feature on both
|
|
947
|
+
sides.** Build the pair, render each back at the frames' own scale, and compare
|
|
948
|
+
like with like. A part whose unoccluded size you can compute is a ruler — composite
|
|
949
|
+
it alone, then read how much of it survives in the reference and how much survives
|
|
950
|
+
in each build. If a part that measures 110 px on its own reads 100 px in the
|
|
951
|
+
reference, the build that also reads 100 px is covering it the way the reference
|
|
952
|
+
does and the build that reads 108 px is not, and that is the order. It costs two
|
|
953
|
+
builds and it reaches edges the frames never show a marking on: on the ladder a brief
|
|
954
|
+
has settled a single edge of a chain from interior detail and said outright that the
|
|
955
|
+
frames did not show the rest, and rendering like-for-like settled three more of them
|
|
956
|
+
— worth a measurable drop in window MAE, a convention the gate cannot see and the
|
|
957
|
+
measures can.
|
|
958
|
+
|
|
959
|
+
⚠️ **The same test knows when to stay silent, and you have to let it.** Run on
|
|
960
|
+
another shot, two orders came out **0.8 % apart over the whole shot and pointing
|
|
961
|
+
opposite ways** — a gap well inside the objective's own scatter, which
|
|
962
|
+
is correctly *no answer*. A difference that small is not a quiet vote for the
|
|
963
|
+
winner; it means the frames do not decide this edge, and you ship it on reasoning
|
|
964
|
+
and say in the log that is what you did.
|
|
965
|
+
|
|
777
966
|
And the general form of all three: **when a reading implies a key, look for a second
|
|
778
967
|
way to get the same number before you author it.** A wrong measurement costs one
|
|
779
968
|
spurious key; a wrong measurement you believed costs the shape of the whole shot.
|
|
@@ -797,16 +986,156 @@ linear, stepped or bezier and never compares two handle shapes; `check` measures
|
|
|
797
986
|
rendered result, so it can tell you a curve is *wrong* without telling you what it
|
|
798
987
|
should have been. What does not follow is that omitting `ease` abstains. It authors
|
|
799
988
|
constant speed on every span — the one shape a hand-animated reference almost never
|
|
800
|
-
has (§10.4) — and the ladder has measured both sides of that bet.
|
|
801
|
-
everything linear for exactly the reasoning above
|
|
802
|
-
its single largest structural gap.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
989
|
+
has (§10.4) — and the ladder has measured both sides of that bet. A run that keyed
|
|
990
|
+
everything linear for exactly the reasoning above scored under a **sixteenth** of its
|
|
991
|
+
`curve_kinds` measure, and that was its single largest structural gap. A second
|
|
992
|
+
attempt at another rung — same brief, same frames, same model as its own first, with
|
|
993
|
+
§10.4's rule added and nothing else — lifted `curve_kinds` by a fifth, with
|
|
994
|
+
`key_counts` rising beside it and every other section figure unchanged. ⇒ Take the
|
|
995
|
+
curve *kind* from what the motion does — starts, stops, accelerates, settles,
|
|
996
|
+
falls — rather than from how far apart the keys are; take its
|
|
806
997
|
shape from §10.4's automatic-handle advice and a small reused `easings` table; and
|
|
807
998
|
leave `check` to catch the one thing no static reading can, an easing applied the
|
|
808
999
|
wrong way round.
|
|
809
1000
|
|
|
1001
|
+
### 8.1 Getting a pose for a figure with a dozen joints
|
|
1002
|
+
|
|
1003
|
+
Everything above measures a *part*, and §9 checks a pose you already have. Between
|
|
1004
|
+
them sits the question neither answers — where do a dozen bones go on this frame —
|
|
1005
|
+
and on a character that gap is most of the run. What follows is not Spine's and no
|
|
1006
|
+
public page has an opinion about it, but it decides whether the search converges at
|
|
1007
|
+
all, so it sits here rather than being rediscovered once per figure.
|
|
1008
|
+
|
|
1009
|
+
**Fit the rendered composite, never a part on its own.** The first trap above tells
|
|
1010
|
+
you to measure each part on pixels that can only be that part. On a figure with limbs
|
|
1011
|
+
there are no such pixels: an arm crosses a torso, one leg crosses the other, a held
|
|
1012
|
+
prop is drawn over both, and **every frame is a frame where parts are touching** — the
|
|
1013
|
+
trap with no way out of it. So the objective is the whole picture. Render your
|
|
1014
|
+
candidate through the same rasteriser that drew the reference, into the frames' own
|
|
1015
|
+
viewport, and minimise the difference over the bones' local transforms; that is §8's
|
|
1016
|
+
*"look for a second way to get the same number"* applied to a whole pose at once, and
|
|
1017
|
+
it is the render loop **§9.1** sends you into. Read §9.1's warning about where a
|
|
1018
|
+
bone's local transform lives *before* you write the first sweep — a fitter that is
|
|
1019
|
+
posing nothing reports a flat number and looks like a bad objective.
|
|
1020
|
+
|
|
1021
|
+
⚠️ **Fitting one part at a time in sequence is the same mistake wearing a
|
|
1022
|
+
schedule.** A near arm solved against the composite while the far arm is still
|
|
1023
|
+
wrong is being scored on a picture the other arm is spoiling, and the minimum it
|
|
1024
|
+
walks to is not its own. The knobs come down together, coarse first — the next two
|
|
1025
|
+
rules are how.
|
|
1026
|
+
|
|
1027
|
+
**Compare at a reduced resolution first. At full resolution the objective is flat
|
|
1028
|
+
over the range a joint has to travel.** Sweep one bone alone across the width of the
|
|
1029
|
+
figure against a single frame and watch the number: at full resolution it can wander
|
|
1030
|
+
inside a few percent for the whole sweep with no slope anywhere in it, because a
|
|
1031
|
+
limb fifty pixels from where it belongs overlaps the reference no better than one a
|
|
1032
|
+
hundred pixels away — both are *no overlap*, and the difference between them is
|
|
1033
|
+
aliasing. A coordinate search sees noise, reports no improvement, and leaves the frame
|
|
1034
|
+
at a pose that shares almost nothing with the picture. ⇒ **Box-average both sides
|
|
1035
|
+
before comparing them and run the search coarse to fine.** The coarsest level places
|
|
1036
|
+
the body, the next the limbs, the last two the pixels. The same sweep at the coarse
|
|
1037
|
+
level has a slope on it, because at that block size the two figures still overlap and
|
|
1038
|
+
the number knows which way to go.
|
|
1039
|
+
|
|
1040
|
+
⚠️ **The coarsest level is for the body and nothing else.** A block big enough to
|
|
1041
|
+
give the whole figure a gradient is a block a shin is one cell of, and one cell
|
|
1042
|
+
cannot say which way a shin points. Place the root and the torso there, then decide
|
|
1043
|
+
each limb at a level whose cells are smaller than the part that level is moving.
|
|
1044
|
+
|
|
1045
|
+
**Scan each knob's whole plausible range. Do not line-search out from where it sits.**
|
|
1046
|
+
A search that steps out from the current value cannot bring an arm 60° round, because
|
|
1047
|
+
the first step overlaps the reference no better than standing still did — so it
|
|
1048
|
+
reports no improvement and stops, correctly, on the objective it was given. A figure
|
|
1049
|
+
whose legs have folded under it is that failure with a picture attached: a real local
|
|
1050
|
+
minimum, sat in for as long as you care to iterate. ⇒ For each knob, evaluate the
|
|
1051
|
+
whole range that bone can plausibly take — a few dozen samples across it, which is
|
|
1052
|
+
nothing beside the frames you are fitting — take the best, and refine only after
|
|
1053
|
+
that. The cost is linear in the number of knobs, and it is the difference between a
|
|
1054
|
+
fit that converges and a fit that reports success on a folded figure.
|
|
1055
|
+
|
|
1056
|
+
⚠️ **Some knobs only decide together.** A part hanging three rotations below a
|
|
1057
|
+
shoulder is placed by none of them alone: each single-knob scan finds its own best
|
|
1058
|
+
while the part is still nowhere near, because every value of that knob is wrong given
|
|
1059
|
+
the other two. Where a chain ends in something whose position you can actually see —
|
|
1060
|
+
a hand, a foot, a held prop — scan the two links above it as a **pair**, over the
|
|
1061
|
+
grid. That is the product of two ranges on a handful of chains, not on every bone.
|
|
1062
|
+
|
|
1063
|
+
**Re-fit the setup pose against frames drawn from every shot, not against one.** Every
|
|
1064
|
+
animation is measured from the setup pose, so an error in it is an error in all of
|
|
1065
|
+
them — and it is exactly the error one frame cannot show you. Fit an attachment's
|
|
1066
|
+
offset against a single frame and that frame's own rotations absorb whatever you got
|
|
1067
|
+
wrong: the picture comes out right, the offset is wrong, and every other shot pays
|
|
1068
|
+
for it. Across a spread it cannot hide, because a wrong offset would have to be
|
|
1069
|
+
absorbed by a *different* rotation in each frame and no one value of the offset does
|
|
1070
|
+
that. ⇒ Fit the setup pose against one clear frame to get near, then re-fit it
|
|
1071
|
+
against a handful of frames drawn from **every** animation at once, and hold it fixed
|
|
1072
|
+
while the per-frame poses are fitted. It is the spread that identifies it — a
|
|
1073
|
+
sequence of single-frame fits, one per shot, is not the same thing.
|
|
1074
|
+
|
|
1075
|
+
**Seed each frame's search from its neighbour's solution — as one start among the
|
|
1076
|
+
full-range scans, never instead of them.** Adjacent frames are adjacent poses, so the
|
|
1077
|
+
answer next door is a better first guess than the middle of any range, and it costs one
|
|
1078
|
+
extra evaluation per knob to try it. What it must not do is *replace* the scans: the
|
|
1079
|
+
neighbouring frame's pose is precisely where a line search would have started, and a
|
|
1080
|
+
limb 60° out in one frame stays 60° out in the next for the reason the rule above
|
|
1081
|
+
gives — stepping away from it overlaps no better, so the whole series inherits one
|
|
1082
|
+
frame's local minimum and looks stable while it does. ⇒ Scan the whole range, add the
|
|
1083
|
+
neighbour's value to the starts, take the best of them. Fit outward from a frame you
|
|
1084
|
+
trust in both directions rather than only forward, so a bad frame seeds its neighbour
|
|
1085
|
+
and not every frame behind it.
|
|
1086
|
+
|
|
1087
|
+
⚠️ **Then measure the adjacency drift, because a fit that lost a limb teleports.**
|
|
1088
|
+
Fitted frame by frame with nothing tying the frames together, a leg has two answers
|
|
1089
|
+
wherever the other leg is near it, and no single frame's number prefers the right one:
|
|
1090
|
+
some frames land on the wrong leg, every one of them cheap, and the series jumps back
|
|
1091
|
+
and forth between the two. That defect is invisible in any per-frame figure and loud in
|
|
1092
|
+
the relation between two — the reading §9.2's `Δpx` and `ref Δ` columns already make
|
|
1093
|
+
for the whole figure, and the chain table localises: a chain whose worst slot drift on
|
|
1094
|
+
one frame runs many times its own mean across the set did not travel that far, it was
|
|
1095
|
+
lost and refound somewhere else. **A limb that moves much further between two adjacent
|
|
1096
|
+
frames than the reference's own frame-to-frame change is a fit that lost it, not a limb
|
|
1097
|
+
that moved.** Read it per chain rather than per figure — one leg swapped for its twin
|
|
1098
|
+
is a small share of a whole-frame delta and vanishes into it.
|
|
1099
|
+
|
|
1100
|
+
**Two near-identical parts need one calibrated separator, decided once and pinned.** A
|
|
1101
|
+
front limb and a rear one are often the same drawing twice, differing by a tint or by
|
|
1102
|
+
nothing at all; a search scoring a whole composite cannot tell which of the two it just
|
|
1103
|
+
placed, because exchanging them costs almost nothing on the frames where they overlap.
|
|
1104
|
+
Left to the per-frame fit, that assignment is re-decided on every frame — the teleport
|
|
1105
|
+
above, arriving by a second route. ⇒ Settle it the way §8 settles a draw-order edge:
|
|
1106
|
+
build both hypotheses, render each at the frames' own scale, and compare like with like
|
|
1107
|
+
— but calibrate on the frames where the two parts are *unambiguous*, the ones where
|
|
1108
|
+
they are far apart or only one of them is drawn, and read the separation there, where
|
|
1109
|
+
it is a real gap rather than a rounding difference. Then **pin the assignment for the
|
|
1110
|
+
run** and let no per-frame search reopen it. ⚠️ The same test knows when to stay silent
|
|
1111
|
+
here too: two hypotheses that come out inside the objective's own scatter mean the
|
|
1112
|
+
frames do not decide this, and you ship it on reasoning and say in the log that is what
|
|
1113
|
+
you did.
|
|
1114
|
+
|
|
1115
|
+
**Spend each iteration on the worst chain, and stop re-fitting the ones already at the
|
|
1116
|
+
floor.** §9.2's chain table is this loop's work queue: it gives every limb a worst slot
|
|
1117
|
+
drift, an error per pixel, and a share of the set's error. ⇒ Take the next iteration to
|
|
1118
|
+
the worst **per-pixel** chain rather than the largest share — the share confounds
|
|
1119
|
+
*wrong* with *big*, as §9.2 says beside the table, and the chain holding most of a
|
|
1120
|
+
run's error is routinely the one that simply covers most of the figure. Then freeze the
|
|
1121
|
+
chains that have converged. Re-fitting them spends the budget the broken chain needed,
|
|
1122
|
+
and it is not merely wasteful: chains share parents, so a search free to move a
|
|
1123
|
+
converged limb's ancestors will walk it back off the floor to buy a fraction of a point
|
|
1124
|
+
somewhere else. ⚠️ **A blank where a drift should be is the loudest row in the table,
|
|
1125
|
+
not a quiet one.** The matcher refuses to name a distance past the part's own size
|
|
1126
|
+
(§9.2), so a limb far enough out reports no match rather than a large number — read
|
|
1127
|
+
that beside a high figure per pixel as the strongest signal the table has.
|
|
1128
|
+
|
|
1129
|
+
**What comes out is a pose per frame, and a pose per frame is not a key.** Two things
|
|
1130
|
+
decide what survives the reduction, and **§10.3** states both: declare one tolerance
|
|
1131
|
+
in pixels at the end of what each bone swings rather than a figure in degrees, and
|
|
1132
|
+
deal with the gauges — the directions the pixels cannot see — *before* the series
|
|
1133
|
+
becomes keys, because a fitter will have wandered along every one of them; fold the
|
|
1134
|
+
exact ones out and penalise the rest. Then close
|
|
1135
|
+
the loop with **§9**: the fit's own number says how near this pose is to this frame,
|
|
1136
|
+
and only `check` says whether the shot is the shot. **§9.3** is the list of what even
|
|
1137
|
+
that cannot see.
|
|
1138
|
+
|
|
810
1139
|
---
|
|
811
1140
|
|
|
812
1141
|
## 9. Checking against the frames — `rigc check`
|
|
@@ -816,11 +1145,13 @@ bun cli.ts check --candidate path/to/spine --frames path/to/reference/frames
|
|
|
816
1145
|
```
|
|
817
1146
|
|
|
818
1147
|
`--frames` takes either a **skeleton root** (the directory holding `frames.json`,
|
|
819
|
-
which checks every animation of that shot
|
|
820
|
-
it. Everything else is optional:
|
|
821
|
-
its skeleton, `--as <name>` when
|
|
822
|
-
|
|
823
|
-
|
|
1148
|
+
which checks every animation of that shot, framed per set — see the scope note
|
|
1149
|
+
below) or **one animation directory** inside it. Everything else is optional:
|
|
1150
|
+
`--atlas` when the candidate's atlas is not beside its skeleton, `--as <name>` when
|
|
1151
|
+
your animation is called something the frame directory is not, `--framing shared`
|
|
1152
|
+
to fit one framing across every set instead of one each, `--all-frames` to list
|
|
1153
|
+
every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
|
|
1154
|
+
per-slot report.
|
|
824
1155
|
|
|
825
1156
|
⚠️ **A frame set may be contact-sheets-only.** `check` only reads `fNNNN.png`
|
|
826
1157
|
files — a committed reference set that ships a contact sheet plus a couple of
|
|
@@ -885,10 +1216,47 @@ pixels rather than quad corners (issue #34, closed by #39; see §9.2). Pin to a
|
|
|
885
1216
|
you can name a reason for, and read the unpinned framing line first when you are not
|
|
886
1217
|
sure whether you have one.
|
|
887
1218
|
|
|
888
|
-
⚠️ **The framing is over the frames you compare
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1219
|
+
⚠️ **The framing is over the frames you compare, and `check` decides it PER SET.**
|
|
1220
|
+
Point it at a skeleton root and each animation directory under it is asked its own
|
|
1221
|
+
question first: *do this set's own drawn pixels land in the box `frames.json`
|
|
1222
|
+
records?* The sets that do are measured in that box, which is exact — it is not an
|
|
1223
|
+
estimate of where the frames were drawn, it is where they were drawn — and nothing
|
|
1224
|
+
another set does can move them. The sets that do not are measured in **one shared
|
|
1225
|
+
framing** fitted across every set, printed as the header's `shared box` line. Each
|
|
1226
|
+
set says which it got on its own `framed to` line.
|
|
1227
|
+
|
|
1228
|
+
Why the split falls there, both halves measured on an 8-shot character (147 frames):
|
|
1229
|
+
|
|
1230
|
+
- **Deciding the declared box per set is worth 15–25 MAE.** Over the union, one shot
|
|
1231
|
+
that is not in the frames' coordinates puts the pooled correction over the
|
|
1232
|
+
one-pixel threshold and the *whole root* falls back to a fit. Per set, the shots
|
|
1233
|
+
that qualify read what pinning by hand reads: `idle` **18.77** where a whole-root
|
|
1234
|
+
run read 41.59, with not one key different (issue #100).
|
|
1235
|
+
- **Fitting per set is worse, so `check` does not.** `fitFraming` registers extent,
|
|
1236
|
+
and extent is not alignment (see the ⚠️ in §9.2), so on a shot whose silhouette
|
|
1237
|
+
genuinely differs one shot's frames do not constrain the fit enough: `hit` reads
|
|
1238
|
+
**92.36** fitted on its own against 60.59 in the shared fit, and a two-frame stills
|
|
1239
|
+
set reads 101.94 against 42.98. More frames is a better-conditioned fit, so the
|
|
1240
|
+
fallback is deliberately the shared one.
|
|
1241
|
+
|
|
1242
|
+
`--framing shared` measures **every** set in the shared framing, which is what a
|
|
1243
|
+
whole-root run used to do. It answers one question and it is a good one — *does a
|
|
1244
|
+
single box serve every set?* — and it is the wrong number to read as one shot's
|
|
1245
|
+
fidelity.
|
|
1246
|
+
|
|
1247
|
+
⚠️ The two are different measurements and their absolute numbers are not comparable.
|
|
1248
|
+
The `scope` line at the top of the report says which one you got. `--viewport` pins
|
|
1249
|
+
one box for the whole run whichever scope you ask for, because a pin is a claim
|
|
1250
|
+
about your candidate's own coordinates and those do not change between shots.
|
|
1251
|
+
|
|
1252
|
+
⚠️ **`--frames <root>/<one-set>` is a third number for a set that cannot take the
|
|
1253
|
+
declared box**, and it is the least constrained of the three: the fit has only that
|
|
1254
|
+
set's frames to work from, where a root run's shared fit has every frame in the
|
|
1255
|
+
skeleton. Measured on the same character, `hit` reads 60.50 at the root and 92.41
|
|
1256
|
+
pointed at its own directory. A set that DOES take the declared box reads the same
|
|
1257
|
+
either way — that box is not fitted to anything — so pointing at one directory is
|
|
1258
|
+
exact for a shot you authored in the frames' coordinates and a rough estimate for
|
|
1259
|
+
one you did not.
|
|
892
1260
|
|
|
893
1261
|
### 9.1 Why this exists
|
|
894
1262
|
|
|
@@ -910,6 +1278,52 @@ your run ceasing to be an honest authoring run. It is a loop, in the way `build`
|
|
|
910
1278
|
a loop. `bench` and `diff` against a rung's export are not — they read the answer,
|
|
911
1279
|
and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach once.
|
|
912
1280
|
|
|
1281
|
+
🚨 **If you drive the runtime yourself, a bone's local transform lives on
|
|
1282
|
+
`bone.pose`.** A shot whose poses have to be *fitted* rather than read sends you
|
|
1283
|
+
past `check` and into your own render loop over
|
|
1284
|
+
`@esotericsoftware/spine-core` — that is §8's *"look for a second way to get the
|
|
1285
|
+
same number"* applied to a whole pose, and it is a legitimate thing to build — §8.1
|
|
1286
|
+
is how that search is set up so it converges. The
|
|
1287
|
+
first thing it hits is not a subtlety. **spine-core 4.3 keeps a bone's local
|
|
1288
|
+
transform on `bone.pose`, not on the bone**, so `bone.rotation = …` — or `.x`,
|
|
1289
|
+
`.y`, `.scaleX` — is neither an error nor a rotation: it adds a property nothing
|
|
1290
|
+
reads, and every frame renders as the setup pose. Write `bone.pose.rotation`.
|
|
1291
|
+
Rung 8 lost a loop to it at a flat 17.3; driven through `bone.pose`, the same
|
|
1292
|
+
poses measured **2.76**.
|
|
1293
|
+
|
|
1294
|
+
🚨 **That trap has a twin one level up: the *setup* transform lives on
|
|
1295
|
+
`bone.data.setupPose`, not on `bone.data`.** `BoneData` extends `PosedData`,
|
|
1296
|
+
which carries the whole setup transform on a `setupPose` object, so
|
|
1297
|
+
`bone.data.rotation` — or `.x`, `.y`, `.scaleX` — is `undefined`, and
|
|
1298
|
+
`bone.data.rotation + delta` is `NaN`. Read and write it as
|
|
1299
|
+
`bone.data.setupPose.rotation`. This one is worse to spot than the `bone.pose`
|
|
1300
|
+
trap, because nothing on the path raises: `undefined` propagates to `NaN`, and
|
|
1301
|
+
`NaN` serialises to `null`, so a fit writes `"px": null` into its own placements
|
|
1302
|
+
file, the next build reads those as zero, `validate` is green and `check` runs.
|
|
1303
|
+
⇒ **A `null` in your own placements dump is the signature of having read
|
|
1304
|
+
`bone.data` directly** — nothing in this format is ever legitimately null. On
|
|
1305
|
+
spineboy it cost the candidate MAE 13.0 → 114.6 with a green gate throughout
|
|
1306
|
+
([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
|
|
1307
|
+
§4.1). Note that the two names are not the same thing: `bone.data.setupPose` is
|
|
1308
|
+
the setup transform, while `bone.setupPose()` on a `Bone` is the method that
|
|
1309
|
+
resets `bone.pose` back to it.
|
|
1310
|
+
|
|
1311
|
+
🚨 **A region attachment's own offsets are cached.** `attachment.x`, `.y`,
|
|
1312
|
+
`.rotation`, `.scaleX/.scaleY` are inputs to a quad spine-core computes once and
|
|
1313
|
+
stores; what gets drawn is that stored quad — `computeWorldVertices` reads
|
|
1314
|
+
`getOffsets(pose)`, never the fields — so writing them is, again, neither an
|
|
1315
|
+
error nor a move. **Call `attachment.updateSequence()` after every write**, or
|
|
1316
|
+
every frame renders the quad it was loaded with. A setup fit on spineboy ran
|
|
1317
|
+
4,500 renders and reported the same number for all of them before this surfaced
|
|
1318
|
+
([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
|
|
1319
|
+
§4.2).
|
|
1320
|
+
|
|
1321
|
+
⇒ **An MAE that is identical across every pose, and that does not move for any
|
|
1322
|
+
parameter you sweep — a bone's local transform, an attachment's offsets — is one
|
|
1323
|
+
of these inert writes and not a wrong animation.** The parameter you swept was
|
|
1324
|
+
never read; a wrong rig moves the number, a write to a field nothing reads
|
|
1325
|
+
cannot.
|
|
1326
|
+
|
|
913
1327
|
### 9.2 Reading the table
|
|
914
1328
|
|
|
915
1329
|
```
|
|
@@ -922,6 +1336,7 @@ and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach on
|
|
|
922
1336
|
── heavy — candidate animation "heavy", 12 fps ──
|
|
923
1337
|
frames 65 on disk, candidate samples 65, 65 compared
|
|
924
1338
|
MAE mean 23.10 worst 43.36 at f0029 (0..255 over the union alpha; …)
|
|
1339
|
+
⤷ over the REFERENCE's own drawn pixels, mean 23.90 — the union figure compares two builds …
|
|
925
1340
|
slot drift worst 2.1 px "pendulum" at f0029
|
|
926
1341
|
per-frame 1 of 64 adjacent pair(s) change by a different amount than the reference does; worst
|
|
927
1342
|
f0018, yours moved 0 px where the reference moved 374
|
|
@@ -1012,6 +1427,20 @@ because an ad-hoc re-render check naturally computes that one, and on every set
|
|
|
1012
1427
|
measured so far it comes out ten to twenty-five times smaller and correspondingly
|
|
1013
1428
|
blunter.
|
|
1014
1429
|
|
|
1430
|
+
⚠️ **Half of that denominator is yours, so do not optimise against it.** The union
|
|
1431
|
+
is the pixels *either* side drew, and a large, mostly transparent sprite adds many
|
|
1432
|
+
cheap pixels to it — so the *mean falls* on a candidate that got worse. That is not
|
|
1433
|
+
hypothetical: spineboy-2's muzzle flare walked its own scale to 13x under a fitting
|
|
1434
|
+
loop doing exactly this, and cost every set in that run its framing (issue #119).
|
|
1435
|
+
So the line under the MAE divides the same difference by the pixels the
|
|
1436
|
+
**reference** drew, a denominator nothing you do can grow. Read the union figure to
|
|
1437
|
+
compare two builds of your own rig, where both sides cover about the same ground,
|
|
1438
|
+
and the reference-denominator figure when you are deciding whether a change made
|
|
1439
|
+
the shot better; it is not bounded by 255. A set that draws more than half again as
|
|
1440
|
+
much ink as the reference does gets `⚠️ overdraw` beside those two numbers, with
|
|
1441
|
+
both pixel counts, because at that point the first figure is cheap for a reason
|
|
1442
|
+
that has nothing to do with your keys.
|
|
1443
|
+
|
|
1015
1444
|
**`Δpx` and `ref Δ`** are the two columns that do **not** compare you against the
|
|
1016
1445
|
reference. They compare each side against **itself one frame earlier**: how many
|
|
1017
1446
|
pixels of your own frame moved since your own previous frame, and the same for the
|
|
@@ -1074,6 +1503,39 @@ summary line carries the same denominator. `N reference component(s) no slot
|
|
|
1074
1503
|
reaches` means the reference frame contains something none of your slots overlaps:
|
|
1075
1504
|
a part you have not authored, or one you have put somewhere else entirely.
|
|
1076
1505
|
|
|
1506
|
+
**The `chains` block is the same two measures on the unit you actually repair.**
|
|
1507
|
+
|
|
1508
|
+
```
|
|
1509
|
+
chains 6 from the candidate's own bone tree — the roster is at the foot of the report
|
|
1510
|
+
chain slots worst slot drift mean MAE in it share
|
|
1511
|
+
neck 5/5 3.0 px "goggles" f0004 2.3 px 30.37 24.3%
|
|
1512
|
+
rear-upper-arm 6/6 2.2 px "gun" f0000 1.4 px 46.26 34.6%
|
|
1513
|
+
front-thigh 3/3 16.3 px "front-shin" f0003 7.2 px 48.18 9.0%
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
`check` cuts **your own** bone tree at every branch point — a chain runs from a
|
|
1517
|
+
root or a fork down to the next fork; a single-bone chain that is itself a fork
|
|
1518
|
+
folds into its parent, so a `torso` that branches three ways joins the trunk rather
|
|
1519
|
+
than becoming a row of its own; and each chain is named after the first bone in it
|
|
1520
|
+
that carries a slot. On a biped that lands on the parts you would name (`neck`,
|
|
1521
|
+
`front-thigh`, the gun arm); on a serial figure with no fork it is one chain, and
|
|
1522
|
+
the slot rows under it still say which link moved. Which bones and slots went where
|
|
1523
|
+
is printed as a roster at the foot of the report, so it is never a guess. `MAE
|
|
1524
|
+
share` divides the difference over the **reference's** own drawn pixels — the
|
|
1525
|
+
denominator from the line above, which nothing you draw can grow — and splits it by
|
|
1526
|
+
giving each of those pixels to the chain whose ink is nearest, so the shares
|
|
1527
|
+
partition the set and no chain can look better by drawing more: growing its ink
|
|
1528
|
+
only pulls more of the reference's pixels, and their error, into it. Read `MAE in
|
|
1529
|
+
it` beside the share, because the share alone confounds *wrong* with *big* — a head
|
|
1530
|
+
and its features cover a lot of a figure and can carry a third of the error at a
|
|
1531
|
+
below-average figure per pixel. Reference ink further from your ink than the part's
|
|
1532
|
+
own size is left `(unattributed)` rather than charged to a neighbour, and a chain
|
|
1533
|
+
reading 0 % on `0/3` slots drawn is missing, not clean. The rollup at the foot gives
|
|
1534
|
+
each chain one line across every set — the sentence a run's README quotes instead of
|
|
1535
|
+
a per-shot list. **§8.1** is how to act on it: the next iteration goes to the worst
|
|
1536
|
+
chain by error per pixel, and a chain already at the floor is frozen rather than
|
|
1537
|
+
re-fitted.
|
|
1538
|
+
|
|
1077
1539
|
### 9.3 What it still cannot see
|
|
1078
1540
|
|
|
1079
1541
|
- **Anything a frame does not contain.** Bone `length`, the setup `inherit` mode,
|
|
@@ -1086,7 +1548,20 @@ a part you have not authored, or one you have put somewhere else entirely.
|
|
|
1086
1548
|
still cannot tell you is *how* a silhouette got its shape: a hull moved by a
|
|
1087
1549
|
bone chain and the same hull moved by deform keys render to the same pixels, and
|
|
1088
1550
|
the frames cannot separate them. Choose on what the rig has to do next, not on
|
|
1089
|
-
what the frames appear to say.
|
|
1551
|
+
what the frames appear to say. ⚠️ **But `bench` does see it.** Take a disc that
|
|
1552
|
+
squashes as it travels: a region on a bone that scales, and a grid mesh weighted
|
|
1553
|
+
1.0 to that same bone, draw the same pixels on every frame — and they are
|
|
1554
|
+
different files. One has a region and one bone; the other has a hull, a
|
|
1555
|
+
triangulation and a weight per vertex, and a rig that meshes one part usually
|
|
1556
|
+
meshes its neighbour and carries the bones to drive them. `attachments.type_counts`,
|
|
1557
|
+
`attachments.mesh_weighted`, `attachments.region_size` and the bone count that
|
|
1558
|
+
comes with the choice all move on that decision, while `animations.deform` moves
|
|
1559
|
+
for neither, because neither one deforms with keys — the whole difference is which
|
|
1560
|
+
machinery renders identical pixels. That is not an argument for guessing: the
|
|
1561
|
+
frames cannot choose, and this guide will not tell you which way any reference
|
|
1562
|
+
went. It is an argument for **writing down which way you went and why at the
|
|
1563
|
+
moment you decide it**, rather than meeting the decision again in the measures
|
|
1564
|
+
after the run is over.
|
|
1090
1565
|
- **Which of two explanations is right.** A slot 3 px low every frame and a slot
|
|
1091
1566
|
3 px low at one frame have the same drift and opposite causes. The table gives
|
|
1092
1567
|
you the frame index; §8's rule still applies — look for a second way to get the
|
|
@@ -1143,6 +1618,31 @@ attachment name"* — [Images](http://esotericsoftware.com/spine-images). ⇒ in
|
|
|
1143
1618
|
keep the placeholder equal to the PNG's basename and no `path` is written (R5). A
|
|
1144
1619
|
`path` in the emitted file means the two disagreed.
|
|
1145
1620
|
|
|
1621
|
+
🧩 **⇒ When the art is named after the parts, the art's names are the rig's names —
|
|
1622
|
+
and this is the largest lever §10 has.** *One image → one slot → one attachment,
|
|
1623
|
+
named after the image* reads as a structural rule. It is also, and mostly, a
|
|
1624
|
+
**naming** rule, and naming is what whole sections of the measures are made of:
|
|
1625
|
+
five of `bones`'s eight name-matched measures (`names`, `parent_by_name`, `order`,
|
|
1626
|
+
`length_present`, `inherit_present`) and every `slots` measure but the count are
|
|
1627
|
+
scored over the names the two sides **share** ([`src/diff.ts`](../src/diff.ts)), so
|
|
1628
|
+
a rig whose names miss reads near zero on all of them however well it is built. ⇒
|
|
1629
|
+
carry each part's own name straight through — PNG basename → slot → attachment, and
|
|
1630
|
+
the bone that moves it — instead of inventing a scheme of your own.
|
|
1631
|
+
|
|
1632
|
+
**Both directions are measured.** The one run whose art shipped a separate PNG per
|
|
1633
|
+
body part, each named for the part, applied this deliberately and posted `names`
|
|
1634
|
+
measures **an order of magnitude** above anything on the ladder before it — without
|
|
1635
|
+
either side seeing the other. Every honest run before it read near zero on the same
|
|
1636
|
+
measures. It is the largest single thing any run has got out of a convention.
|
|
1637
|
+
|
|
1638
|
+
⚠️ **And the other half, which matters just as much: when the art is *not* named
|
|
1639
|
+
after the parts, no naming strategy beats any other and the measure is noise.** On a
|
|
1640
|
+
shot whose two PNGs are called things like `square` and `pendulum`, the names carry
|
|
1641
|
+
nothing a rig could inherit, every candidate name is as good as every other, and the
|
|
1642
|
+
name measures are reporting the honesty rule's own price rather than a defect in
|
|
1643
|
+
your rig. Do not spend a loop hunting for better names there, and do not read the
|
|
1644
|
+
low figure as a miss — say in the log that the art did not carry them.
|
|
1645
|
+
|
|
1146
1646
|
📗 **Housekeeping the format fixes for you.** The default skin *"always has the name
|
|
1147
1647
|
`default`"* and *"bones are ordered so that the parent always comes before a child
|
|
1148
1648
|
bone"* — [JSON format](http://esotericsoftware.com/spine-json-format). §3.4.
|
|
@@ -1200,7 +1700,19 @@ all the time"*, and Clean Up *"deletes all unnecessary keys … keying the same
|
|
|
1200
1700
|
multiple times in a row, keying the same values as the setup pose"*, because *"often
|
|
1201
1701
|
it is convenient to set keys liberally when designing an animation, then use Clean
|
|
1202
1702
|
Up afterward"* — [Keys](http://esotericsoftware.com/spine-keys). ⇒ a shipped export
|
|
1203
|
-
is dense,
|
|
1703
|
+
is dense, and what it does not carry is a key its own neighbours already imply.
|
|
1704
|
+
|
|
1705
|
+
🧩 **⇒ A hold still needs a key at both ends, and two equal values are not a
|
|
1706
|
+
repeat.** Clean Up's *"keying the same value multiple times in a row"* is about
|
|
1707
|
+
**three or more** — a run of keys whose interior ones their neighbours imply. Two
|
|
1708
|
+
keys of equal value imply nothing: they are the only way to say *nothing moves here*
|
|
1709
|
+
on an interpolated timeline, and deleting either one ramps the value through the
|
|
1710
|
+
hold. Stillness is a thing a shot does, sometimes for a twelfth of a second and
|
|
1711
|
+
sometimes for nine, and it is authored, not omitted. ⇒ Key the start of a hold and
|
|
1712
|
+
key its end, at the same value; drop the ones in between. This is §9.2's *"held pose
|
|
1713
|
+
that is not held"* from the other side, and the same place catches it — a sloped
|
|
1714
|
+
hold shows up in `check`'s per-frame column and nowhere else, because it is cheap in
|
|
1715
|
+
every single frame and wrong only in the relation between two.
|
|
1204
1716
|
|
|
1205
1717
|
📗 **Add a key when a curve cannot carry the shape.** *"If a curve is not smooth
|
|
1206
1718
|
enough, it is easily remedied by adding another key"*, and the **Bounce** handle
|
|
@@ -1217,6 +1729,52 @@ minimal one-key-per-pose spec produces.
|
|
|
1217
1729
|
one.** Reaching for a target density is guessing. The frames are the only thing that
|
|
1218
1730
|
can say where the motion turns; §8 is how to read them.
|
|
1219
1731
|
|
|
1732
|
+
⚠️ **Two rules for a run that *fits* a pose series rather than reading it off the
|
|
1733
|
+
frames.** Neither is Spine's — no public page has an opinion about a fitter — but
|
|
1734
|
+
both decide where the keys above actually land, so they sit beside them. Both were
|
|
1735
|
+
paid for on the ladder. (**§8.1** is how the series gets fitted in the first place.)
|
|
1736
|
+
|
|
1737
|
+
**A key tolerance on a rotation is not a number of degrees.** The same angular error
|
|
1738
|
+
costs a different number of pixels at every level of a hierarchy, because everything
|
|
1739
|
+
below the bone comes with it. Measured on rung 8's four-link chain: **a quarter of a
|
|
1740
|
+
degree on the last link moves the chain's end 0.15 px, and the same quarter degree
|
|
1741
|
+
on the plate the chain hangs from moves it 0.69 px.** One figure in degrees applied
|
|
1742
|
+
per property therefore keys the far end of the chain roughly four times too loosely
|
|
1743
|
+
while over-keying the near end — it is not one tolerance at all. ⇒ Declare **one**
|
|
1744
|
+
tolerance, **in pixels at the end of what the bone swings**, and convert it per bone
|
|
1745
|
+
by that bone's lever arm. With that, the whole trade reads as one curve and you can
|
|
1746
|
+
pick a point on it deliberately: that shot measured 0.6 px → 259 keys → 1.619 window
|
|
1747
|
+
MAE, 0.3 px → 300 → 1.402, 0.15 px → 377 → 1.305.
|
|
1748
|
+
|
|
1749
|
+
**A rig's parameters are not identified by its pixels — remove the gauges before you
|
|
1750
|
+
key.** A bone that carries no attachment is an exact gauge: turn it by δ, turn its
|
|
1751
|
+
children back by δ, and **not one pixel changes**. Anything optimising against pixels
|
|
1752
|
+
is free to wander along that direction, and it does. On the spineboy run the figure's
|
|
1753
|
+
topmost body bone carries no art and every moving bone sits under it, and a
|
|
1754
|
+
coordinate descent walked it to **+181°** against its child's **−184°** — a pose
|
|
1755
|
+
whose picture is right on every frame and whose key series spins the whole figure
|
|
1756
|
+
through a full turn between two of them. The rendered result is correct and the
|
|
1757
|
+
authored rig is nonsense, and no amount of further fitting finds it, because every
|
|
1758
|
+
point on the gauge orbit has identical error. ⇒ Fold each gauge out *before* the
|
|
1759
|
+
series becomes keys — for a rotation gauge, take the median of the values along the
|
|
1760
|
+
chain and fold it back. The same shape exists wherever a transform is unobservable: a
|
|
1761
|
+
bone with no art, a slot-less parent chain, a uniform scale split across two bones.
|
|
1762
|
+
|
|
1763
|
+
⚠️ **That fold is exact only when the gauge bone's children sit at its own origin** —
|
|
1764
|
+
and a character's body bone almost never has them there, which is the very shape the
|
|
1765
|
+
paragraph above was written from. Turn the parent by δ and a child *at the origin*
|
|
1766
|
+
back by δ and the child is where it was; a child sitting 10 units off swings through
|
|
1767
|
+
an arc of that radius first, and the counter-turn only spins it on the spot. The fold
|
|
1768
|
+
moves art, so it **changes the picture**. Measured on the second spineboy run, whose
|
|
1769
|
+
`hip` carries no attachment and has three children 9–13 units off it: the fold cost
|
|
1770
|
+
**3 MAE on every `idle` frame** — mean 23.0 with it against 19.9 without, same search
|
|
1771
|
+
— and it was removed. ⇒ Read the children's offsets before you fold. At the origin,
|
|
1772
|
+
fold: it is exact and it is cheap. Off the origin the degeneracy is still there but
|
|
1773
|
+
it is **soft**, not exact, and a soft degeneracy is *regularised, not folded* — leave
|
|
1774
|
+
the values alone and add a penalty on the gauge direction to the objective instead.
|
|
1775
|
+
That run used **2e-5 per squared degree** of hip rotation: invisible at animator-sized
|
|
1776
|
+
angles, and still decisive against the +181° against −184° above.
|
|
1777
|
+
|
|
1220
1778
|
### 10.4 Curves
|
|
1221
1779
|
|
|
1222
1780
|
📗 **Linear is what a *new* key gets, and it does not survive contact with a curve.**
|
|
@@ -1250,6 +1808,35 @@ and **Ease in** (*"the value changes more slowly near the next key"*) —
|
|
|
1250
1808
|
across the file, is how an editor rig reads. Raw `curve` is R6's escape hatch — one
|
|
1251
1809
|
key needing a shape no other key has — not the normal way to write a curve.
|
|
1252
1810
|
|
|
1811
|
+
🚨 **The table is a constraint on where the keys go, not a formatting step applied
|
|
1812
|
+
afterwards — it has to exist while the keys are chosen.** A run that plans its keys
|
|
1813
|
+
by fitting each span's **own** handles, and then writes the nearest entry of a named
|
|
1814
|
+
table, has bought a key count at one tolerance and shipped it at another. Nothing in
|
|
1815
|
+
the loop can see that: the timeline count, the key count, the curve kinds and the
|
|
1816
|
+
duration are all unmoved, so `diff` does not shift and the gate stays green, while
|
|
1817
|
+
the rendered result changes by a multiple. Rung 8's first version did exactly this
|
|
1818
|
+
and went from **1.07 to 4.65 MAE** — four times its own fit's floor. ⇒ Two passes.
|
|
1819
|
+
Pass A fits freely and exists only to *discover* which shapes the shot uses; those
|
|
1820
|
+
are clustered into the table; pass B re-plans **every** timeline under the table it
|
|
1821
|
+
will actually write. Never fit free handles and substitute the nearest named shape
|
|
1822
|
+
after the fact. (The table's size then trades against the key count at a fixed
|
|
1823
|
+
tolerance — that shot ran 4 easings/368 keys, 8/314, 12/300, 16/284 — because a
|
|
1824
|
+
richer table holds more spans.) This is rung 6's clamp defect in another suit:
|
|
1825
|
+
**a constraint that is not enforced where the value is written is not a
|
|
1826
|
+
constraint.**
|
|
1827
|
+
|
|
1828
|
+
🧩 **⇒ A span with no interior sample takes the automatic handles, not linear.** Two
|
|
1829
|
+
keys on adjacent samples leave pass B nothing to fit — the samples cannot constrain
|
|
1830
|
+
that span's shape at all — and a planner with nothing to fit leaves `ease` off, which
|
|
1831
|
+
is linear (§4.5): the one shape this section says to argue for rather than default to.
|
|
1832
|
+
"No information" is not an argument for constant speed. Take instead the tangents the
|
|
1833
|
+
keys either side imply — the editor's own **automatic handles**, quoted above — and
|
|
1834
|
+
snap *those* to the nearest table entry, exactly as you would a fitted span. Rung 8's
|
|
1835
|
+
second version did this for its adjacent pairs and the samples barely moved (**17.34 →
|
|
1836
|
+
17.26 MAE**) while `curve_kinds` changed wholesale: nil in the frames, large in the
|
|
1837
|
+
structure, which is the signature of a convention rather than a fidelity fix — and
|
|
1838
|
+
this section's whole subject.
|
|
1839
|
+
|
|
1253
1840
|
📗 **Handles are normalised, and that is the shape an `easings` entry takes.** For a
|
|
1254
1841
|
Bezier key, *"the X axis is from 0 to 1 and represents the percent of time between
|
|
1255
1842
|
the two keyframes. The Y axis is from 0 to 1 and represents the percent of the
|