spine-rigc 3.1.1 → 3.2.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/docs/AUTHORING.md +98 -2
- package/package.json +1 -1
- package/src/check.ts +11 -0
- package/src/cli/core_commands.ts +98 -5
- package/src/cli/shared.ts +11 -2
- package/src/core/raw.ts +20 -0
- package/src/render.ts +5 -3
- package/src/render_core.ts +10 -6
- package/src/render_shared.ts +117 -2
- package/src/sheet.ts +10 -3
package/docs/AUTHORING.md
CHANGED
|
@@ -129,7 +129,7 @@ commands take it and what its default is.
|
|
|
129
129
|
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all <a id="cmd-ingest"></a> |
|
|
130
130
|
| `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** — two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices — plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING §3.4.11](../docs/AUTHORING.md)) <a id="cmd-split"></a> |
|
|
131
131
|
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong <a id="cmd-explain"></a> |
|
|
132
|
-
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings; ghost counts only visible contributions after draw order; `exposureA`/`exposureB` report each named slot’s whole-drawing background exposure even without a band ([AUTHORING §8.8](../docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--extremes` adds `extremes.json` and a native six-panel `extremes.png` (setup, geometry-pair min/max and three premultiplied RGBA diffs with a legend), plus one `EXTREMES` line per set. Per-channel minima/maxima and the farthest original-vertex geometry pair are distinct readings; valid channel extrema survive a geometry/image SKIP, with channel refusals reported separately; still or uncomparable geometry reports a named SKIP without changing render exit status. `--extremes-only` writes the same `extremes.json` and readable `extremes.png` bytes while skipping ordinary frame rasterization/encoding, `contact.png` and `frames.json`; it implies `--extremes` and takes no other diagnostic or sheet output flags. Look at min, max and diff before a person does ([AUTHORING §8.11](../docs/AUTHORING.md#811-min-max-and-diff-before-a-person-looks--render---extremes)). `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene. `--sheet [--sheet-format json-hash] [--page-edges pot\|free]` also packs each set's straight-alpha transparent frames into `sheet.png` + `sheet.json` (TexturePacker JSON Hash with a shared world-origin `anchor` and `2`-px clamped edge extrusion, verified through `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`) and `sheet-manifest.json` (`rigc-sheet/1`), leaving diagnostic `f####.png` and `contact.png` unchanged ([AUTHORING §8.10](../docs/AUTHORING.md#810-sprite-sheets-from-a-render--render---sheet)) — a self-contained sprite object per animation set; when animations play and how a scene is composed remains the consumer's <a id="cmd-render"></a> |
|
|
132
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings; ghost counts only visible contributions after draw order; `exposureA`/`exposureB` report each named slot’s whole-drawing background exposure even without a band ([AUTHORING §8.8](../docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--extremes` adds `extremes.json` and a native six-panel `extremes.png` (setup, geometry-pair min/max and three premultiplied RGBA diffs with a legend), plus one `EXTREMES` line per set. Per-channel minima/maxima and the farthest original-vertex geometry pair are distinct readings; valid channel extrema survive a geometry/image SKIP, with channel refusals reported separately; still or uncomparable geometry reports a named SKIP without changing render exit status. `--extremes-only` writes the same `extremes.json` and readable `extremes.png` bytes while skipping ordinary frame rasterization/encoding, `contact.png` and `frames.json`; it implies `--extremes` and takes no other diagnostic or sheet output flags. Look at min, max and diff before a person does ([AUTHORING §8.11](../docs/AUTHORING.md#811-min-max-and-diff-before-a-person-looks--render---extremes)). `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene. `--preroll <n>` walks each set `n` whole loops on a looping track before the loop it records, so a looping idle is drawn with its physics already warm, as a player's looping track carries it ([AUTHORING §8.13](../docs/AUTHORING.md#813-a-loop-with-its-physics-warm--render---preroll)). `--sheet [--sheet-format json-hash] [--page-edges pot\|free]` also packs each set's straight-alpha transparent frames into `sheet.png` + `sheet.json` (TexturePacker JSON Hash with a shared world-origin `anchor` and `2`-px clamped edge extrusion, verified through `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`) and `sheet-manifest.json` (`rigc-sheet/1`), leaving diagnostic `f####.png` and `contact.png` unchanged ([AUTHORING §8.10](../docs/AUTHORING.md#810-sprite-sheets-from-a-render--render---sheet)) — a self-contained sprite object per animation set; when animations play and how a scene is composed remains the consumer's <a id="cmd-render"></a> |
|
|
133
133
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` <a id="cmd-preview"></a> |
|
|
134
134
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` <a id="cmd-vote"></a> |
|
|
135
135
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture <a id="cmd-pose"></a> |
|
|
@@ -389,6 +389,7 @@ What the flags mean:
|
|
|
389
389
|
| `--sheet-format` | `render --sheet` only: `json-hash` (default, TexturePacker JSON Hash with `frames`, `animations` and `meta`, verified against `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`). Refused without `--sheet` or on an unknown value, exit 2, nothing written — **§8.10** |
|
|
390
390
|
| `--viewport`, `--viewport-from` | `render`: give the box every frame is drawn over instead of fitting one — `--viewport <x,y,w,h>`, a world box, y up, `x,y` its bottom-left corner, at the pixel size `--max` gives; or `--viewport-from <frames.json \| dir>`, an earlier render's grid exactly (its box, scale and pixel size), so two candidates land on one pixel grid. A slot that reaches past the box is named by set and frame, and `frames.json` records the box was given under `pinned`. The two together are refused, and so is `--max` beside `--viewport-from` — **§8.6**. On `check` and `inspect`, `--viewport` pins the box those commands draw over instead |
|
|
391
391
|
| `--environment` | `render` only: the wind and gravity the rig's physics constraints answer, in time — a schedule file `{ "spec": "rigc-environment/1", "steps": [{ "t": <seconds>, "wind"?: [x, y], "gravity"?: [x, y] }, …] }`, x right and **y down**, each step holding from its `t` until the next. Both posers set each change before the step that poses the first frame at or after its `t`, and draw the same frames, `geometry.json` and `frames.json` (`RE01`). `frames.json` and `geometry.json` record it, and the framing reads it. A malformed file, a half no physics constraint answers, two steps on one frame and a step after every set's last frame are refused by name, exit 2, nothing written; `check` refuses a frame set drawn under one as a reference. Without it every file is the bytes it always was. An inspection of the response the rig states, not a scene — **§8.4** |
|
|
392
|
+
| `--preroll` | `render` only: `<n>`, a whole number of 1 or more — walk each set `n` whole loops on a **looping** track before the loop recorded, drawn by no frame, the physics carried across every wrap as a player's looping track carries it, so a spring that converges is warm on the first frame. The walk lands on track time `n · duration` exactly, where the track has wrapped `n` times, so frame `i` is the pose at animation time `i / fps` — the time it is filed under — whether or not the duration is a whole number of frames (`PRE06`). Both posers walk it alike and write the same frames, `geometry.json`, sheet and `frames.json` (`PRE01`). `frames.json`, `geometry.json` and `sheet-manifest.json` record it as `preroll`, a sheet's namespace ends `/preroll=<n>`, and the framing reads it. A value that is not a whole number of 1 or more, a walk past what a step list holds, a skeleton with no animation or no physics constraint, a set of 0 frames at the rate, and `--environment` or `--parallax` beside it are refused by name, exit 2, nothing written; `check` refuses a frame set drawn after one as a reference. Without it every file is the bytes it always was — **§8.13** |
|
|
392
393
|
| `--poser` | `render`, `inspect`, `check` and `explain`: `core` or `spine` — on `render`, `inspect` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`, `RC34`). On the entry that links none of spine-core — the installed `rigc` without the runtime beside it, `bun cli_core.ts` in a clone — that refusal ends on the route to the entry that does (``the entry that links it runs them — installed, the same `rigc` once @esotericsoftware/spine-core is installed beside the package; from a source checkout, `bun cli.ts` ``); a runtime that is installed and fails to load is refused without it, since installing it is not what that reader has to do. On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1), ending on the same route where the entry links none of it |
|
|
393
394
|
| `--lever-fold` | `explain` only: a fraction above 0 and at most 1. Also read, on the turn lines' walk, the steps where a physics constraint that drives `scaleX` has its inertia lever divide by less than this fraction of the divisor it takes at the setup pose, and the steps where the scale factor it applies moves by more than the bone's setup scale. **No default**: without it the reading is not taken and one line says so; a value that is not such a fraction is refused by name (exit 2). A reading, not a gate — **§4.6.2** |
|
|
394
395
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
@@ -5615,7 +5616,8 @@ that poses through its posers (`check`, `bonedist`, `inspect`, `--geometry`), ru
|
|
|
5615
5616
|
the physics clock `2⁻³⁰` faster than the frames, so every such frame takes the steps
|
|
5616
5617
|
exact arithmetic takes; the animation time is unchanged. A spring that has converged
|
|
5617
5618
|
by the loop point therefore closes, which the loop-seam suite's `LS06` holds at all
|
|
5618
|
-
five rates. A
|
|
5619
|
+
five rates. A render from rest releases the spring at frame 0, so on a looping idle
|
|
5620
|
+
it has not converged by then; `render --preroll` (§8.13) walks whole loops first. A player that steps the core entry's live tracks at a fixed rate — a
|
|
5619
5621
|
capture, an export, a test harness — is the render's case and takes the render's
|
|
5620
5622
|
clock: `openTrack` and `openTracks` accept `clock`, what each step moves the physics
|
|
5621
5623
|
clock by, and `physicsClockStep` (with `PHYSICS_CLOCK_LEAD`) is exported from
|
|
@@ -9825,6 +9827,100 @@ not draw order, colour, occlusion, tears, ghost lines or rendered optical flow.
|
|
|
9825
9827
|
Compare the two fits, the per-sample curves and an explicit control before
|
|
9826
9828
|
attributing residual to relief. No assertion, pass mark or build gate is added.
|
|
9827
9829
|
|
|
9830
|
+
### 8.13 A loop with its physics warm — `render --preroll`
|
|
9831
|
+
|
|
9832
|
+
```bash
|
|
9833
|
+
bun cli.ts render --candidate path/to/spine --preroll 2 [--animation idle] [--geometry] [--sheet] [--poser core|spine]
|
|
9834
|
+
```
|
|
9835
|
+
|
|
9836
|
+
A render walks one pass of each animation from rest: frame 0 resets the physics,
|
|
9837
|
+
and every later frame steps it. On a looping idle that is the one frame a player
|
|
9838
|
+
never shows — a player's looping track carries the physics across every wrap — so
|
|
9839
|
+
a video or a sprite sheet cut from one pass shows every spring released from rest
|
|
9840
|
+
at every wrap, and trimming or cross-fading frames hides it without making the
|
|
9841
|
+
loop honest. `--preroll <n>` sets the track **looping** and walks `n` whole loops
|
|
9842
|
+
first, drawn by no frame, then records the next loop:
|
|
9843
|
+
|
|
9844
|
+
- **The walk.** `count` is the set's frame count at the rate,
|
|
9845
|
+
`round(duration · fps)`. From the same reset at frame 0 a render from rest
|
|
9846
|
+
takes, the track steps the render's grid `j / fps` up to the last point before
|
|
9847
|
+
track time `n · duration`, takes one shorter step onto it, then the `count`
|
|
9848
|
+
steps of the loop recorded, `n · duration + i / fps` — on the render's own
|
|
9849
|
+
physics clock (§4.6) — and its animation time is the track time wrapped by the
|
|
9850
|
+
duration, what spine-core's `setAnimation(0, name, true)` does and what the
|
|
9851
|
+
core's live track does looping (`openTrack({ loop: true })`). Both posers add
|
|
9852
|
+
the steps up the same way, and every one before the wrap subtracts without
|
|
9853
|
+
rounding, so the walk lands on `n · duration` exactly: recorded frame `i` is
|
|
9854
|
+
the pose at animation time `i / fps`, filed under index `i` and time `i / fps`,
|
|
9855
|
+
and the frame set has the shape a render from rest has.
|
|
9856
|
+
- **Why the shorter step.** A loop that is not a whole number of frames — 1.03 s
|
|
9857
|
+
at 12 fps is 12.36 — is not `count` frames long, so `n · count` steps of
|
|
9858
|
+
`1 / fps` land `n · (count / fps − duration)` seconds off the wrap: the loop
|
|
9859
|
+
recorded would start mid-loop while frame 0 is filed at time 0, and
|
|
9860
|
+
`frames.json` and `geometry.json` would misstate every frame's time. `PRE06`
|
|
9861
|
+
holds a stepped key on such a loop — 0 on its first half, 12 on its second —
|
|
9862
|
+
to the render from rest's at every frame of pre-rolls of 1 and 3, on both
|
|
9863
|
+
posers, and the uniform grid planted on the core is red at frame 0 on both.
|
|
9864
|
+
Both posers read a duration off float32 key times, so `n · duration` is a
|
|
9865
|
+
float and the wrap is hit exactly; for a duration that is not, where the
|
|
9866
|
+
product rounds below the wrap — the loop's end rather than its start — the
|
|
9867
|
+
walk lands one float past it (`prerollStart`).
|
|
9868
|
+
- **What moves.** Only what carries from one step to the next: the physics. A
|
|
9869
|
+
keyed bone is posed at the loop time the render from rest draws it at, to the
|
|
9870
|
+
rounding of the steps' sums — on the skirt fixture's hip, measured on this walk
|
|
9871
|
+
at 7, 13, 24 and 30 fps after 1 and 3 loops, no position or matrix entry moved
|
|
9872
|
+
by more than 1.7e-13 — and on `PRE06`'s stepped key to the bit.
|
|
9873
|
+
- **Both posers, one walk.** The selftest's `render-preroll` suite renders three
|
|
9874
|
+
rigs through the core and through `--poser spine` and holds every PNG,
|
|
9875
|
+
`geometry.json`, sheet and `frames.json` equal (`PRE01`), and plants on the core
|
|
9876
|
+
the two walks rejected — the loop flag ignored, so the track holds at its
|
|
9877
|
+
duration through the pre-roll, and the pre-roll dropped, so the loop recorded is
|
|
9878
|
+
walked from rest — each red against spine-core (`PRE03`).
|
|
9879
|
+
- **What it buys, measured.** On the skirt fixture (six chains of five links on
|
|
9880
|
+
physics, swayed from the hip on a 2 s loop) at 12 fps, frame 0 against the last
|
|
9881
|
+
frame of each physics bone — its tip and its rotation — ends past that bone's own
|
|
9882
|
+
largest step between two adjacent frames on 29 of 30 bones from rest, the worst
|
|
9883
|
+
at 4.5 times it; after one loop of pre-roll every bone ends within 0.015 of it
|
|
9884
|
+
(`PRE02`, which reads it off the `geometry.json` the CLI wrote, on both posers).
|
|
9885
|
+
|
|
9886
|
+
**The files.** `frames.json`, each `geometry.json` and each `sheet-manifest.json`
|
|
9887
|
+
record `preroll: <n>`, and a sheet's frame-key namespace ends `/preroll=<n>`, so a
|
|
9888
|
+
warm sheet and one from rest loaded into one texture cache do not share keys. The
|
|
9889
|
+
framing reads it, for `--environment`'s reason: a spring warmed swings otherwise
|
|
9890
|
+
than one released, and the box holds what the frames draw — so the grid can differ
|
|
9891
|
+
from the render from rest; compare the two in world units, in `geometry.json`.
|
|
9892
|
+
Without the flag every walk holds at its duration as it always did, spine-core's
|
|
9893
|
+
track is set not looping, and every file is the bytes it always was (`PRE04`, and
|
|
9894
|
+
the gallery's render and emit bases, `RH05` and `EH06`). `check` poses its
|
|
9895
|
+
candidate from rest, so a frame set drawn after a pre-roll is refused as a
|
|
9896
|
+
reference by name rather than scored.
|
|
9897
|
+
|
|
9898
|
+
**Refused by name**, exit 2, nothing written (`PRE05`):
|
|
9899
|
+
|
|
9900
|
+
- a value that is not a whole number of 1 or more — `--preroll <v>: negative`,
|
|
9901
|
+
`not a whole number of loops`, `not a number`, and `--preroll 0 walks no loop
|
|
9902
|
+
before the one recorded, which is the render from rest — leave --preroll out
|
|
9903
|
+
for it`;
|
|
9904
|
+
- a walk past what a step list holds — `"<set>" would walk <N> steps at <fps> fps,
|
|
9905
|
+
more than a walk's step list holds (4294967295)`;
|
|
9906
|
+
- a pre-roll this rig would not walk: a skeleton with no animation; one that
|
|
9907
|
+
declares no physics constraint (`nothing carries from one loop into the next`);
|
|
9908
|
+
a set of 0 frames at the rate (`"<set>" lasts <d>s, which is 0 frames at <fps>
|
|
9909
|
+
fps`);
|
|
9910
|
+
- `--environment` beside it: a schedule's `t` could be read from the first frame
|
|
9911
|
+
recorded or from the first loop walked, the two put the physics in different
|
|
9912
|
+
worlds while it warms, and nothing settles which; and `--parallax`, whose dial
|
|
9913
|
+
samples are independent reset poses.
|
|
9914
|
+
|
|
9915
|
+
**Why loops and not a start in seconds.** A whole number of loops lands recorded
|
|
9916
|
+
frame `i` on the animation time the render from rest draws it at, so the two renders
|
|
9917
|
+
compare frame for frame and only the physics differs. A start in seconds would
|
|
9918
|
+
need a second rule for where the recorded window falls on the frame grid, and
|
|
9919
|
+
could put it mid-loop. The mesh comparison's `warmupSteps` (MESH_REDUCTION, P10),
|
|
9920
|
+
still refused above 0, would reach the same looping walk; counted in steps rather
|
|
9921
|
+
than loops, it would move every compared frame off its `FrameRef` time unless the
|
|
9922
|
+
steps were whole loops.
|
|
9923
|
+
|
|
9828
9924
|
## 9. Checking against the frames — `rigc check`
|
|
9829
9925
|
|
|
9830
9926
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/check.ts
CHANGED
|
@@ -1298,6 +1298,17 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1298
1298
|
'difference between two worlds. Render the reference frames without --environment.',
|
|
1299
1299
|
);
|
|
1300
1300
|
}
|
|
1301
|
+
// A frame set drawn after a pre-roll (`render --preroll`, issue #1684) shows the physics warm; check poses the
|
|
1302
|
+
// candidate from rest, so the score would be a number about the difference between a warm loop and a cold one.
|
|
1303
|
+
const referencePreroll = located.sidecar?.preroll;
|
|
1304
|
+
if (referencePreroll !== undefined) {
|
|
1305
|
+
throw new CheckError(
|
|
1306
|
+
`${FRAMES_SIDECAR} records that these frames were rendered after a pre-roll (\`rigc render --preroll ${String(referencePreroll)}\`: ` +
|
|
1307
|
+
'that many loops walked on a looping track before the loop recorded, the physics carried), and check poses the candidate ' +
|
|
1308
|
+
'from rest — so the comparison would be a number about the difference between a warm loop and a cold one. ' +
|
|
1309
|
+
'Render the reference frames without --preroll.',
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1301
1312
|
if (referenceSkin === null && options.skin !== undefined) {
|
|
1302
1313
|
notes.push(
|
|
1303
1314
|
`the candidate is posed under skin ${JSON.stringify(options.skin)} and the reference frames record no skin ` +
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -47,7 +47,7 @@ import { buildSheetSet, SHEET_MANIFEST_FILE, type SheetSetPack, SheetUsageError,
|
|
|
47
47
|
import { readSeamPairs, SEAM_FILE, seamFileOf, seamLine, SeamError, type SeamPair, seamsOf, seamText } from '../seam.ts';
|
|
48
48
|
import { environmentCollisions, environmentFrame, EnvironmentError, environmentResponse, readEnvironmentSchedule, type EnvironmentStep } from '../environment.ts';
|
|
49
49
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
50
|
-
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, frameSetDir, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, sampleSetupPose, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser, type Viewport, viewportFor } from '../render_shared.ts';
|
|
50
|
+
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, frameSetDir, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, prerollStart, prerollWarmPoints, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, sampleSetupPose, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser, type Viewport, viewportFor } from '../render_shared.ts';
|
|
51
51
|
import { type CompileResult } from '../types.ts';
|
|
52
52
|
import { attachmentRegionJoins } from '../region_joins.ts';
|
|
53
53
|
import { cmdRepack } from './repack.ts';
|
|
@@ -507,6 +507,69 @@ function readEnvironmentFlag(flags: Record<string, string>): { path: string; ste
|
|
|
507
507
|
}
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
/**
|
|
511
|
+
* `--preroll <n>` read (issue #1684): how many whole loops of each animation the
|
|
512
|
+
* render walks on a looping track before the loop it records — or refused as a
|
|
513
|
+
* usage error, naming the value found and what is required, before anything is
|
|
514
|
+
* read off the candidate. `undefined` without the flag, which is the render from
|
|
515
|
+
* rest it always was.
|
|
516
|
+
*
|
|
517
|
+
* ⭐ A count of loops rather than a start in seconds: a whole number of loops
|
|
518
|
+
* puts the recorded frame `i` at the loop time the render without the flag draws
|
|
519
|
+
* it at (to the running sum's rounding), so the two renders compare frame for
|
|
520
|
+
* frame and only the physics differs — a start in seconds would need a second
|
|
521
|
+
* rule for where the window falls on the frame grid, and could land mid-loop.
|
|
522
|
+
*/
|
|
523
|
+
function readPrerollFlag(flags: Record<string, string>): number | undefined {
|
|
524
|
+
const given = flags.preroll;
|
|
525
|
+
if (given === undefined) return undefined;
|
|
526
|
+
const wants = 'it takes how many whole loops of each animation to walk, on a looping track, before the loop it records — 1 or more';
|
|
527
|
+
const value = given.trim() === '' ? Number.NaN : Number(given);
|
|
528
|
+
if (!Number.isFinite(value)) throw new UsageError(`--preroll ${JSON.stringify(given)}: not a number — ${wants}`);
|
|
529
|
+
if (value < 0) throw new UsageError(`--preroll ${given}: negative — ${wants}`);
|
|
530
|
+
if (!Number.isInteger(value)) throw new UsageError(`--preroll ${given}: not a whole number of loops — ${wants}`);
|
|
531
|
+
if (value === 0) throw new UsageError(`--preroll 0 walks no loop before the one recorded, which is the render from rest — leave --preroll out for it; ${wants}`);
|
|
532
|
+
return value;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** The most steps a walk's step list holds: the longest array the language makes (`2^32 − 1`). */
|
|
536
|
+
const WALK_STEPS_HELD = 2 ** 32 - 1;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* The refusal of a pre-roll this candidate, at this rate, would not walk (issue
|
|
540
|
+
* #1684), each a frame set from rest printed under a flag that says it is warm,
|
|
541
|
+
* or a walk that could not be taken:
|
|
542
|
+
*
|
|
543
|
+
* - a skeleton with no animation, whose one frame is the setup pose;
|
|
544
|
+
* - a skeleton that declares no physics constraint, where nothing carries from
|
|
545
|
+
* one loop into the next;
|
|
546
|
+
* - a set drawn that spans no frame at `fps`, whose loop takes no step;
|
|
547
|
+
* - a walk longer than a step list holds (`WALK_STEPS_HELD`), which would
|
|
548
|
+
* otherwise die on the array's own error rather than on a name.
|
|
549
|
+
*
|
|
550
|
+
* `durations` are the poser's, of the sets this run draws; `physics` the
|
|
551
|
+
* physics constraints the skeleton declares.
|
|
552
|
+
*/
|
|
553
|
+
function prerollRefusal(preroll: number, fps: number, durations: ReadonlyArray<{ name: string; duration: number }>, physics: readonly string[]): string | null {
|
|
554
|
+
if (durations.length === 0) return `--preroll ${preroll}: the skeleton has no animation — its one frame is the setup pose, where nothing loops and no physics step runs`;
|
|
555
|
+
if (physics.length === 0) {
|
|
556
|
+
return `--preroll ${preroll}: the skeleton declares no physics constraint, so nothing carries from one loop into the next and the loop recorded would be the render from rest`;
|
|
557
|
+
}
|
|
558
|
+
const still = durations.find(({ duration }) => Math.round(duration * fps) === 0);
|
|
559
|
+
if (still !== undefined) {
|
|
560
|
+
return (
|
|
561
|
+
`--preroll ${preroll}: "${still.name}" lasts ${still.duration}s, which is 0 frames at ${fps} fps — a loop of it walks no step, so no pre-roll ` +
|
|
562
|
+
'moves its physics. Name another set with --animation, or raise --fps'
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
for (const { name, duration } of durations) {
|
|
566
|
+
// `prerollTimes`' own length, counted rather than built: the grid before the wrap, the step onto it, and the loop recorded.
|
|
567
|
+
const steps = prerollWarmPoints(prerollStart(duration, preroll), fps) + Math.round(duration * fps);
|
|
568
|
+
if (steps > WALK_STEPS_HELD) return `--preroll ${preroll}: "${name}" would walk ${steps} steps at ${fps} fps, more than a walk's step list holds (${WALK_STEPS_HELD})`;
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
510
573
|
/**
|
|
511
574
|
* The refusals of a well-formed schedule that this candidate, at this rate,
|
|
512
575
|
* would quietly not draw (issue #1416) — each one a render that would print a
|
|
@@ -657,6 +720,15 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
657
720
|
const fps = readPositiveNumber(flags, 'fps', PROTOCOL_FPS, 1);
|
|
658
721
|
// Read before anything is read off the candidate, so a malformed schedule is refused before a line is printed.
|
|
659
722
|
const environment = readEnvironmentFlag(flags);
|
|
723
|
+
// The pre-roll (issue #1684), read and refused as early, and refused beside a schedule: under a pre-roll a schedule's
|
|
724
|
+
// `t` could be read on the recording's clock or on the walk's, the two give different physics, and nothing settles it.
|
|
725
|
+
const preroll = readPrerollFlag(flags);
|
|
726
|
+
if (preroll !== undefined && environment !== undefined) {
|
|
727
|
+
throw new UsageError(
|
|
728
|
+
`--preroll takes no --environment: a schedule's t could be read from the first frame recorded or from the first loop walked, ` +
|
|
729
|
+
'and the two put the physics in different worlds while it warms. Render with one of them',
|
|
730
|
+
);
|
|
731
|
+
}
|
|
660
732
|
const maxSide = readPositiveNumber(flags, 'max', 256, 16);
|
|
661
733
|
// A box given rather than fitted (issue #1414), read before a line is printed, so a malformed one is refused first.
|
|
662
734
|
const given = readGivenViewport(flags);
|
|
@@ -778,7 +850,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
778
850
|
posersAsked(flags),
|
|
779
851
|
);
|
|
780
852
|
if (flags.parallax !== undefined) {
|
|
781
|
-
for (const flag of ['animation', 'fps', 'slot', 'hide', 'geometry', 'folds', 'extremes', 'extremes-only', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'viewport', 'viewport-from']) {
|
|
853
|
+
for (const flag of ['animation', 'fps', 'slot', 'hide', 'geometry', 'folds', 'extremes', 'extremes-only', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'preroll', 'viewport', 'viewport-from']) {
|
|
782
854
|
if (flags[flag] !== undefined) throw new UsageError(`--parallax takes no --${flag}: it samples one named dial on its own 9-value grid; drop --${flag} or run ordinary render separately`);
|
|
783
855
|
}
|
|
784
856
|
if (flags.poser !== undefined && flags.poser !== 'core') throw new UsageError('--parallax requires --poser core: it reads only a rigc compiled model');
|
|
@@ -824,15 +896,22 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
824
896
|
// the frames draw the subset and the box is still the whole rig's.
|
|
825
897
|
// The environment rides on the same object for the same reason (issue #1416): the framing reads it, so the box holds
|
|
826
898
|
// what the frames under it draw.
|
|
899
|
+
// The pre-roll rides on it for the environment's reason (issue #1684): it changes the pose, so the framing reads it.
|
|
827
900
|
const pose =
|
|
828
|
-
skin === undefined && subset === undefined && environment === undefined
|
|
901
|
+
skin === undefined && subset === undefined && environment === undefined && preroll === undefined
|
|
829
902
|
? undefined
|
|
830
|
-
: {
|
|
903
|
+
: {
|
|
904
|
+
...(skin === undefined ? {} : { skin }),
|
|
905
|
+
...subsetFields(subset),
|
|
906
|
+
...(environment === undefined ? {} : { environment: environment.steps }),
|
|
907
|
+
...(preroll === undefined ? {} : { preroll }),
|
|
908
|
+
};
|
|
831
909
|
if (skin !== undefined) console.log(` .. skin ${skin}`);
|
|
832
910
|
if (subset !== undefined) console.log(` .. ${subset.mode.padEnd(8)} ${subset.names.join(', ')}`);
|
|
833
911
|
if (environment !== undefined && facts.animations.length === 0) {
|
|
834
912
|
throw new UsageError(environmentRefusal(flags.environment, environment.steps, skeletonText, fps, []) ?? '');
|
|
835
913
|
}
|
|
914
|
+
if (preroll !== undefined && facts.animations.length === 0) throw new UsageError(prerollRefusal(preroll, fps, [], physicsDeclared) ?? '');
|
|
836
915
|
|
|
837
916
|
// Rigc's own core when the candidate is a rigc build, and spine-core
|
|
838
917
|
// otherwise, or where the core refuses the input by name. Never silently:
|
|
@@ -853,6 +932,11 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
853
932
|
const refusal = environmentRefusal(flags.environment, environment.steps, skeletonText, fps, drawn);
|
|
854
933
|
if (refusal !== null) throw new UsageError(refusal);
|
|
855
934
|
}
|
|
935
|
+
// A pre-roll this candidate would not walk is refused off the same durations, for the same reason (issue #1684).
|
|
936
|
+
if (preroll !== undefined) {
|
|
937
|
+
const refusal = prerollRefusal(preroll, fps, poser.animations.filter((a) => only === undefined || a.name === only), physicsDeclared);
|
|
938
|
+
if (refusal !== null) throw new UsageError(refusal);
|
|
939
|
+
}
|
|
856
940
|
// `null` is a skeleton that posed no vertex at all. One that posed a vertex
|
|
857
941
|
// it cannot frame — Infinity or NaN — is thrown from the framing as a
|
|
858
942
|
// `GeometryError` naming the number (issue #873), and one whose every vertex
|
|
@@ -935,7 +1019,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
935
1019
|
if (geometry) {
|
|
936
1020
|
for (const [name, frames] of sampled) {
|
|
937
1021
|
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
938
|
-
geometryTexts.set(name, geometryText(geometryFileOf(poser, animation, fps, frames, viewport, skin, environment?.steps)));
|
|
1022
|
+
geometryTexts.set(name, geometryText(geometryFileOf(poser, animation, fps, frames, viewport, skin, environment?.steps, preroll)));
|
|
939
1023
|
}
|
|
940
1024
|
}
|
|
941
1025
|
// The fold reading, off the very attachments the frames were drawn from, against the rest table the geometry
|
|
@@ -1066,6 +1150,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1066
1150
|
skin,
|
|
1067
1151
|
subset,
|
|
1068
1152
|
environment: environment?.steps,
|
|
1153
|
+
preroll,
|
|
1069
1154
|
pinned,
|
|
1070
1155
|
}),
|
|
1071
1156
|
);
|
|
@@ -1086,6 +1171,12 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1086
1171
|
`wind answered by ${response.wind.join(', ') || 'none'}; gravity by ${response.gravity.join(', ') || 'none'}`,
|
|
1087
1172
|
);
|
|
1088
1173
|
}
|
|
1174
|
+
if (preroll !== undefined) {
|
|
1175
|
+
console.log(
|
|
1176
|
+
` .. preroll ${preroll} loop(s) of each set walked on a looping track before the loop recorded, drawn by no frame — ` +
|
|
1177
|
+
'the physics carried across every wrap, as a player\'s looping track carries it',
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1089
1180
|
console.log(` .. ${viewport.width}x${viewport.height}px at ${fps} fps, ${sampled.size} set(s) -> ${outRoot}`);
|
|
1090
1181
|
// Whether physics ran (issue #1467): said once for a rig with nothing to step, and per set below for one with.
|
|
1091
1182
|
if (physicsDeclared.length === 0) console.log(` .. ${PHYSICS_NONE_LINE}`);
|
|
@@ -1237,6 +1328,8 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1237
1328
|
...subsetFields(subset),
|
|
1238
1329
|
// Written only when a schedule was given (issue #1416): a render without one says nothing and stays the bytes it was.
|
|
1239
1330
|
...(environment === undefined ? {} : { environment: environment.steps }),
|
|
1331
|
+
// Written only when a pre-roll was asked for (issue #1684), for the same reason.
|
|
1332
|
+
...(preroll === undefined ? {} : { preroll }),
|
|
1240
1333
|
// Written only when the box was given (issue #1414): a fitted render says nothing and stays the bytes it was.
|
|
1241
1334
|
...(given === undefined ? {} : { pinned: pinnedRecord(given.by, outside) }),
|
|
1242
1335
|
background: BACKGROUND,
|
package/src/cli/shared.ts
CHANGED
|
@@ -1071,7 +1071,7 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1071
1071
|
'Each frame with a moving pair also gets an outline plate, overlaps-f<NNNN>.png: every slot in a moving pair as its contour in its own ' +
|
|
1072
1072
|
'colour, dashed where a slot drawn later covers it, named at its top. The render reports what was drawn; who should be in front is the ' +
|
|
1073
1073
|
"author's, and the draw-order key in the motion spec is the remedy. A reading, not a gate. Not with --slot/--hide",
|
|
1074
|
-
parallax: 'read one named slider dial through the core poser on 9 evenly spaced values: parallax.json plus setup/extreme/diff/affine-residual heat-map parallax.png; geometry only, no assertion or pass threshold. A separate dial render mode; takes no animation, fps, other diagnostics, sheet, subset, environment or viewport flags',
|
|
1074
|
+
parallax: 'read one named slider dial through the core poser on 9 evenly spaced values: parallax.json plus setup/extreme/diff/affine-residual heat-map parallax.png; geometry only, no assertion or pass threshold. A separate dial render mode; takes no animation, fps, other diagnostics, sheet, subset, environment, preroll or viewport flags',
|
|
1075
1075
|
'extremes-only': 'write only extremes.json and readable extremes.png per set, with the same posed grid and bytes as --extremes; no ordinary frame PNGs, contact.png or frames.json. Implies --extremes; takes no --sheet, --sheet-format, --page-edges, --geometry, --folds, --pass-through, --seam, --overlaps or --angle-range',
|
|
1076
1076
|
extremes: 'also write extremes.json and a native six-panel extremes.png per set: setup, geometry-pair min/max and premultiplied RGBA diffs, with a legend. Per-channel extrema are recorded separately and valid channels survive geometry/image SKIPs; channel refusals are named separately. Still geometry reports SKIP; a reading, not a gate. Attachment identity/count changes record a named SKIP without changing render exit; supports --slot/--hide on both posers',
|
|
1077
1077
|
seam:
|
|
@@ -1108,6 +1108,13 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1108
1108
|
'runtime\'s, wind [1, 0] and gravity [0, 1]. frames.json and geometry.json record it, and the framing reads it. A ' +
|
|
1109
1109
|
'malformed file, a half no physics constraint answers, two steps on one frame and a step after every set\'s last frame ' +
|
|
1110
1110
|
'are refused by name. An inspection of what the rig offers, not a scene',
|
|
1111
|
+
preroll:
|
|
1112
|
+
'walk each set this many whole loops on a looping track before the loop recorded, drawn by no frame, the physics carried ' +
|
|
1113
|
+
'across every wrap as a player\'s looping track carries it — so a spring that converges is warm on the first frame. Frame i is the ' +
|
|
1114
|
+
'pose at track time (n·count + i)/fps, filed under index i and time i/fps; on either poser. frames.json, geometry.json and ' +
|
|
1115
|
+
'sheet-manifest.json record it, and the framing reads it. A value that is not a whole number of 1 or more, a walk past what a step ' +
|
|
1116
|
+
'list holds, a skeleton with no animation or no physics constraint, a set of 0 frames, and --environment or --parallax beside it ' +
|
|
1117
|
+
'are refused by name; check refuses such a frame set as a reference',
|
|
1111
1118
|
poser:
|
|
1112
1119
|
"`render`, `inspect` and `check`: which implementation poses the frames (on `check`, the candidate's) — `core` (rigc's own, reading the skeleton.model.json a " +
|
|
1113
1120
|
'build writes beside the pair) or `spine` (spine-core). Default: `core` when that document and the atlas sit ' +
|
|
@@ -1300,6 +1307,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1300
1307
|
'texture-from': '<path>',
|
|
1301
1308
|
poser: 'core|spine',
|
|
1302
1309
|
environment: '<schedule.json>',
|
|
1310
|
+
preroll: '<n>',
|
|
1303
1311
|
'pass-through': '<cloth>:<leg>[,<cloth>:<leg>…]',
|
|
1304
1312
|
seam: '<slot>:<slot>[,<slot>:<slot>…]',
|
|
1305
1313
|
'angle-range': '<bone>:<relative-to>[,…]',
|
|
@@ -1768,6 +1776,7 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1768
1776
|
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--skin <name>] [--fps 12] [--max 256] [--geometry] [--folds] [--extremes | --extremes-only] [--poser core|spine] [--out render/]',
|
|
1769
1777
|
'rigc render … --slot <name[,name…]> | --hide <name[,name…]> (a subset of the slots, on the whole rig\'s grid)',
|
|
1770
1778
|
'rigc render … --environment <schedule.json> (the wind and gravity the physics answers, in time)',
|
|
1779
|
+
'rigc render … --preroll <n> (n whole loops walked on a looping track first, so the loop recorded has its physics warm)',
|
|
1771
1780
|
'rigc render … --pass-through <cloth>:<leg>[,…] (the frames where a cloth drawn over a leg passes through it)',
|
|
1772
1781
|
'rigc render … --seam <slot>:<slot>[,…] (joint gaps, visible ghosts and each named slot’s background exposure)',
|
|
1773
1782
|
'rigc render … --parallax <dial> (geometry reading on the named dial, model/core only)',
|
|
@@ -1776,7 +1785,7 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1776
1785
|
'rigc render … --viewport <x,y,w,h> | --viewport-from <frames.json> (a box given, not fitted: two renders on one pixel grid)',
|
|
1777
1786
|
'rigc render … --sheet [--sheet-format json-hash] [--page-edges pot|free] (deterministic sprite sheets + JSON hash table + sheet-manifest.json per set)',
|
|
1778
1787
|
],
|
|
1779
|
-
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'extremes', 'extremes-only', 'parallax', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'poser', 'out'],
|
|
1788
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'extremes', 'extremes-only', 'parallax', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'preroll', 'poser', 'out'],
|
|
1780
1789
|
overrides: {
|
|
1781
1790
|
'page-edges': {
|
|
1782
1791
|
meaning:
|
package/src/core/raw.ts
CHANGED
|
@@ -620,6 +620,26 @@ export function poseRawAnimationEach(doc: CompiledDocument, animation: string, s
|
|
|
620
620
|
walkEach(doc, animation, steps, plant, reset, clock === undefined ? RAW_MODE : { ...RAW_MODE, clock: (dt) => clock(dt) }, RAW_SHAPE, visit, environment);
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
/**
|
|
624
|
+
* The live track's looping mode (`./track.ts`, issue #1276) as a batch walk's:
|
|
625
|
+
* the animation time the track time wrapped by the duration, `reset` keys
|
|
626
|
+
* fired across the wrap, the clipped rows cut as the raw entry cuts them, and
|
|
627
|
+
* no value that is not finite kept.
|
|
628
|
+
*/
|
|
629
|
+
const RAW_LOOPED_MODE: WalkMode = { loop: true, keep: false, live: true };
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* `poseRawAnimationEach` on a looping track (issue #1684): the same walk in the
|
|
633
|
+
* live track's looping mode (`RAW_LOOPED_MODE`) — the walk `render --preroll`
|
|
634
|
+
* takes, which carries the physics across every wrap as a player's looping
|
|
635
|
+
* track does. Not part of the entry's promised surface: `src/render_core.ts`
|
|
636
|
+
* is its one caller, and spine-core's poser takes the same walk through
|
|
637
|
+
* `setAnimation(0, name, true)`.
|
|
638
|
+
*/
|
|
639
|
+
export function poseRawLoopEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, visit: (pose: RawPose, index: number) => void, clock?: (dt: number) => number, environment?: StepEnvironment): void {
|
|
640
|
+
walkEach(doc, animation, steps, plant, reset, clock === undefined ? RAW_LOOPED_MODE : { ...RAW_LOOPED_MODE, clock: (dt) => clock(dt) }, RAW_SHAPE, visit, environment);
|
|
641
|
+
}
|
|
642
|
+
|
|
623
643
|
/**
|
|
624
644
|
* What a batch walk sets on its physics before a pose (issue #1416, part C):
|
|
625
645
|
* pose `index`'s change to the world every physics constraint answers — either
|
package/src/render.ts
CHANGED
|
@@ -503,12 +503,14 @@ export function spinePoser(data: SkeletonData): Poser {
|
|
|
503
503
|
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
504
504
|
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
505
505
|
setup: (skin) => spinePosed(setupPosed(data, skin)),
|
|
506
|
-
animation: (name, skin, fps, count, visit, environment, _channels, times) => {
|
|
506
|
+
animation: (name, skin, fps, count, visit, environment, _channels, times, loop) => {
|
|
507
507
|
const skeleton = skeletonUnderSkin(data, skin);
|
|
508
508
|
const state = new AnimationState(new AnimationStateData(data));
|
|
509
509
|
// Not looping: the last frame sits at the animation's duration, and a looping
|
|
510
|
-
// entry would wrap it back onto the first pose.
|
|
511
|
-
|
|
510
|
+
// entry would wrap it back onto the first pose. Looping only when asked (issue
|
|
511
|
+
// #1684, `render --preroll`): the walk a player's looping track takes, the
|
|
512
|
+
// physics carried across every wrap — the core walks it as `poseRawLoopEach`.
|
|
513
|
+
state.setAnimation(0, name, loop === true);
|
|
512
514
|
skeleton.setupPose();
|
|
513
515
|
const step = 1 / fps;
|
|
514
516
|
// The physics clock runs `PHYSICS_CLOCK_LEAD` fast, so no frame ends on a physics step's tie (issue #1380).
|
package/src/render_core.ts
CHANGED
|
@@ -103,7 +103,7 @@ import { atlasRegionLookup, parseAtlasText } from './atlas.ts';
|
|
|
103
103
|
import { pagesOfAtlas, spineFileSha256, type ModelPage } from './model.ts';
|
|
104
104
|
import { clipThrough, type ClipShape, type ShapeClipper } from './core/clipping.ts';
|
|
105
105
|
import { activeBones, CoreInputError, readModel, sourceOfDoc, underNoSkin, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
|
|
106
|
-
import { PHYSICS_CLOCK_LEAD, physicsClockStep, poseRawAnimationEach, poseRawDial, poseRawSetup, type RawDrawn, type RawPose, type StepEnvironment } from './core/raw.ts';
|
|
106
|
+
import { PHYSICS_CLOCK_LEAD, physicsClockStep, poseRawAnimationEach, poseRawDial, poseRawLoopEach, poseRawSetup, type RawDrawn, type RawPose, type StepEnvironment } from './core/raw.ts';
|
|
107
107
|
import type { TimelinePlant } from './core/animation.ts';
|
|
108
108
|
import { stepPhysics, type PhysicsStepper } from './core/constraints_physics.ts';
|
|
109
109
|
import { CORE_DEFAULT_SKIN, lookupSkins } from './core/skins.ts';
|
|
@@ -367,14 +367,18 @@ export function keptPieceArrays(): PieceArrays {
|
|
|
367
367
|
* one pose is held at a time rather than the animation's whole series. A plant
|
|
368
368
|
* passes another (`RC44`).
|
|
369
369
|
*/
|
|
370
|
-
export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void, environment?: StepEnvironment) => void;
|
|
370
|
+
export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void, environment?: StepEnvironment, loop?: boolean) => void;
|
|
371
371
|
|
|
372
372
|
/**
|
|
373
373
|
* The walk `corePoser` steps with unless a plant passes another: one pose posed, drawn and released before the next, the
|
|
374
374
|
* physics clock moved by `physicsClockStep` (issue #1380), and `render --environment`'s change for each pose set before it
|
|
375
|
-
* is posed (issue #1416, `StepEnvironment`).
|
|
375
|
+
* is posed (issue #1416, `StepEnvironment`). `loop` (issue #1684, `render --preroll`) walks the track looping
|
|
376
|
+
* (`poseRawLoopEach`) — spine-core's poser sets the same track looping — and absent, the walk is the one it always was.
|
|
376
377
|
*/
|
|
377
|
-
export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit, environment) =>
|
|
378
|
+
export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit, environment, loop) =>
|
|
379
|
+
loop === true
|
|
380
|
+
? poseRawLoopEach(doc, animation, steps, plant, 'animation', visit, physicsClockStep, environment)
|
|
381
|
+
: poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit, physicsClockStep, environment);
|
|
378
382
|
|
|
379
383
|
/**
|
|
380
384
|
* The render's physics clock (issue #1380): defined beside the walk that takes it (`./core/raw.ts`), where the mesh
|
|
@@ -790,7 +794,7 @@ export function corePoser(
|
|
|
790
794
|
const pose = poseRawSetup(input.doc, { through, ...observe });
|
|
791
795
|
return corePosed(input, pose, through, [], values);
|
|
792
796
|
},
|
|
793
|
-
animation: (name, skin, fps, count, visit, environment, channels, times) => {
|
|
797
|
+
animation: (name, skin, fps, count, visit, environment, channels, times, loop) => {
|
|
794
798
|
let values: ChannelValue[] | undefined;
|
|
795
799
|
const observe = channels ? { readLocals: (bones: readonly import('./model.ts').ModelBone[], sliders: readonly import('./core/constraints_slider.ts').SliderApplication[]) => { values = localChannels(bones, sliders); } } : {};
|
|
796
800
|
const input = inputOf(skin);
|
|
@@ -819,7 +823,7 @@ export function corePoser(
|
|
|
819
823
|
} catch (thrown) {
|
|
820
824
|
drawFailed.push(thrown);
|
|
821
825
|
}
|
|
822
|
-
}, environment);
|
|
826
|
+
}, environment, loop);
|
|
823
827
|
if (drawFailed.length > 0) throw drawFailed[0];
|
|
824
828
|
},
|
|
825
829
|
rest: (skin, shown) => {
|
package/src/render_shared.ts
CHANGED
|
@@ -283,6 +283,16 @@ export interface FramesSidecar {
|
|
|
283
283
|
* it as a reference by name.
|
|
284
284
|
*/
|
|
285
285
|
environment?: EnvironmentStep[];
|
|
286
|
+
/**
|
|
287
|
+
* The whole loops each set walked on a looping track before the loop its
|
|
288
|
+
* frames record, when `render --preroll` gave them (issue #1684).
|
|
289
|
+
*
|
|
290
|
+
* ⭐ Absent on a render without it, for `skin`'s reason: every frame set
|
|
291
|
+
* written before the field existed is a render from rest, and says the same.
|
|
292
|
+
* A frame set carrying it shows the physics warm, which `check` does not
|
|
293
|
+
* pose, so `check` refuses it as a reference by name.
|
|
294
|
+
*/
|
|
295
|
+
preroll?: number;
|
|
286
296
|
/**
|
|
287
297
|
* The box was given rather than fitted, when `render --viewport` or
|
|
288
298
|
* `render --viewport-from` gave it (issue #1414): which flag, and every slot
|
|
@@ -501,6 +511,23 @@ export interface PoseOptions {
|
|
|
501
511
|
environment?: readonly EnvironmentStep[];
|
|
502
512
|
/** Also record the physics constraints the step that posed each frame stepped — see `Frame.physics` (issue #1467). */
|
|
503
513
|
physics?: boolean;
|
|
514
|
+
/**
|
|
515
|
+
* Whole loops walked before the loop recorded — `render --preroll` (issue
|
|
516
|
+
* #1684): the track set looping, walked and drawn by no frame up to track
|
|
517
|
+
* time `preroll · duration` (`prerollTimes`), then the `count + 1` frames of
|
|
518
|
+
* the next loop, frame `i` the pose at track time `preroll · duration + i /
|
|
519
|
+
* fps` — animation time `i / fps` — filed under index `i` and time `i / fps`.
|
|
520
|
+
* The physics is carried across every wrap, as
|
|
521
|
+
* a player's looping track carries it, so a spring that converges is warm
|
|
522
|
+
* on the first frame recorded. Absent, the track holds at its duration and
|
|
523
|
+
* every pose is the one it always was. A whole number at or above 1; not
|
|
524
|
+
* with `environment`, whose clock under a pre-roll nothing settles.
|
|
525
|
+
*
|
|
526
|
+
* ⭐ **It is part of the shot, so the framing reads it**, for `environment`'s
|
|
527
|
+
* reason: a spring warmed from rest swings differently from one released
|
|
528
|
+
* from it, and the box holds what the frames draw.
|
|
529
|
+
*/
|
|
530
|
+
preroll?: number;
|
|
504
531
|
}
|
|
505
532
|
|
|
506
533
|
/**
|
|
@@ -1165,9 +1192,14 @@ export interface Poser {
|
|
|
1165
1192
|
* answers set before the step that poses frame `i` (issue #1416).
|
|
1166
1193
|
* Optional `times` replaces the uniform grid with increasing absolute times
|
|
1167
1194
|
* starting at 0 (count = times.length - 1), stepped by adjacent differences.
|
|
1168
|
-
* Used by texture-scale to include key times
|
|
1195
|
+
* Used by texture-scale to include key times, and by a pre-roll to land on
|
|
1196
|
+
* the wrap (`prerollTimes`); ordinary renders are unchanged.
|
|
1197
|
+
* `loop` (issue #1684, `render --preroll`) sets the track looping — the
|
|
1198
|
+
* animation time the track time wrapped by the duration, the physics carried
|
|
1199
|
+
* across every wrap, as a player's looping track carries it; absent, the
|
|
1200
|
+
* track holds at its duration as it always did.
|
|
1169
1201
|
*/
|
|
1170
|
-
animation(name: string, skin: string | undefined, fps: number, count: number, visit: (index: number, posed: Posed) => void, environment?: (index: number) => EnvironmentChange | undefined, channels?: boolean, times?: readonly number[]): void;
|
|
1202
|
+
animation(name: string, skin: string | undefined, fps: number, count: number, visit: (index: number, posed: Posed) => void, environment?: (index: number) => EnvironmentChange | undefined, channels?: boolean, times?: readonly number[], loop?: boolean): void;
|
|
1171
1203
|
/** The rest table for the (slot, attachment) pairs `shown` holds — see `AttachmentRest`. */
|
|
1172
1204
|
rest(skin: string | undefined, shown: readonly AttachmentPose[][]): AttachmentRest[];
|
|
1173
1205
|
}
|
|
@@ -1242,6 +1274,19 @@ export function sampleAnimation(source: PoseSource, name: string, fps: number, o
|
|
|
1242
1274
|
const count = Math.round(animation.duration * fps);
|
|
1243
1275
|
const draw = drawResolver(poser, opts);
|
|
1244
1276
|
const frames: Frame[] = [];
|
|
1277
|
+
const preroll = opts?.preroll;
|
|
1278
|
+
if (preroll !== undefined) {
|
|
1279
|
+
// Refused by name at the CLI first (`readPrerollFlag`); here so no other caller walks a pre-roll nobody defined.
|
|
1280
|
+
if (!Number.isInteger(preroll) || preroll < 1) throw new Error(`preroll is ${String(preroll)}, not a whole number of loops at or above 1`);
|
|
1281
|
+
if (opts?.environment !== undefined) throw new Error('preroll and environment were both given: nothing settles which clock the schedule is read on under a pre-roll');
|
|
1282
|
+
// The track loops; every pose before track time `preroll · duration` is walked and drawn by no frame (issue #1684).
|
|
1283
|
+
const { times, first } = prerollTimes(animation.duration, fps, preroll);
|
|
1284
|
+
const recorded = (i: number, posed: Posed): void => {
|
|
1285
|
+
if (i >= first) frames.push(frameOf(i - first, (i - first) * step, posed, draw(), opts));
|
|
1286
|
+
};
|
|
1287
|
+
poser.animation(name, opts?.skin, fps, times.length - 1, recorded, undefined, opts?.channels === true ? true : undefined, times, true);
|
|
1288
|
+
return frames;
|
|
1289
|
+
}
|
|
1245
1290
|
const visit = (i: number, posed: Posed): void => {
|
|
1246
1291
|
frames.push(frameOf(i, i * step, posed, draw(), opts));
|
|
1247
1292
|
};
|
|
@@ -1252,6 +1297,72 @@ export function sampleAnimation(source: PoseSource, name: string, fps: number, o
|
|
|
1252
1297
|
return frames;
|
|
1253
1298
|
}
|
|
1254
1299
|
|
|
1300
|
+
/**
|
|
1301
|
+
* The track time `render --preroll`'s loop recorded starts at (issue #1684): the
|
|
1302
|
+
* end of `preroll` whole loops, `preroll · duration` — where the track has
|
|
1303
|
+
* wrapped `preroll` times and the animation time is 0.
|
|
1304
|
+
*
|
|
1305
|
+
* ⚠️ The product is a float only when `preroll · duration` fits one; where it
|
|
1306
|
+
* does not, it rounds to the nearer float, and that can sit just short of the
|
|
1307
|
+
* wrap — at `duration · 3` for a duration of 1.03, measured — where the
|
|
1308
|
+
* animation time is the duration less an ulp: the pose at the loop's end, not
|
|
1309
|
+
* its start. So a product that lands short is moved to the next float above
|
|
1310
|
+
* it, past the wrap, where the animation time is under one ulp of the product
|
|
1311
|
+
* past 0. No float lies on the wrap there, and this is the one past it.
|
|
1312
|
+
*/
|
|
1313
|
+
export function prerollStart(duration: number, preroll: number): number {
|
|
1314
|
+
let start = preroll * duration;
|
|
1315
|
+
while (duration > 0 && start % duration > duration / 2) start = nextFloatAbove(start);
|
|
1316
|
+
return start;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** The float just above a positive finite `x`. */
|
|
1320
|
+
function nextFloatAbove(x: number): number {
|
|
1321
|
+
const box = new Float64Array([x]);
|
|
1322
|
+
const bits = new BigUint64Array(box.buffer);
|
|
1323
|
+
bits[0] += 1n;
|
|
1324
|
+
return box[0];
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* How many points of the render's grid, `j / fps` from `j = 0`, lie strictly
|
|
1329
|
+
* before track time `start` — the steps a pre-roll walks before the one that
|
|
1330
|
+
* lands on `start` (`prerollTimes`).
|
|
1331
|
+
*/
|
|
1332
|
+
export function prerollWarmPoints(start: number, fps: number): number {
|
|
1333
|
+
if (!(start > 0)) return 0;
|
|
1334
|
+
let m = Math.max(0, Math.ceil(start * fps) - 1);
|
|
1335
|
+
while ((m + 1) / fps < start) m++;
|
|
1336
|
+
while (m > 0 && m / fps >= start) m--;
|
|
1337
|
+
return m + 1;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* The track times a pre-roll of `preroll` loops walks, for `Poser.animation`'s
|
|
1342
|
+
* `times` (issue #1684): the render's grid `j / fps` up to the last point
|
|
1343
|
+
* before `prerollStart`, one shorter step onto it, then the loop recorded,
|
|
1344
|
+
* `start + i / fps` for `i = 1..count`. `first` is the index of `start` — the
|
|
1345
|
+
* pose frame 0 is.
|
|
1346
|
+
*
|
|
1347
|
+
* ⭐ **The walk lands on `start` exactly.** Both posers step a track by the
|
|
1348
|
+
* differences of `times` and add them up (`trackTime += delta`), and every
|
|
1349
|
+
* difference before `start` is exact — two floats within a factor of two of
|
|
1350
|
+
* each other subtract without rounding — so each running sum is the time it
|
|
1351
|
+
* steps to, by induction, and the last is `start`. A uniform grid of
|
|
1352
|
+
* `preroll · count` steps instead lands on `preroll · count / fps`, which is
|
|
1353
|
+
* the wrap only where `duration · fps` is whole: elsewhere the loop recorded
|
|
1354
|
+
* started at the wrong phase while frame 0 was filed at time 0.
|
|
1355
|
+
*/
|
|
1356
|
+
export function prerollTimes(duration: number, fps: number, preroll: number): { times: number[]; first: number } {
|
|
1357
|
+
const start = prerollStart(duration, preroll);
|
|
1358
|
+
const count = Math.round(duration * fps);
|
|
1359
|
+
const first = prerollWarmPoints(start, fps);
|
|
1360
|
+
const times = Array.from({ length: first }, (_, j) => j / fps);
|
|
1361
|
+
times.push(start);
|
|
1362
|
+
for (let i = 1; i <= count; i++) times.push(start + i / fps);
|
|
1363
|
+
return { times, first };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1255
1366
|
/**
|
|
1256
1367
|
* The setup pose as a single frame — what a skeleton with **no animation at all**
|
|
1257
1368
|
* looks like.
|
|
@@ -1570,6 +1681,8 @@ export interface GeometryFile {
|
|
|
1570
1681
|
skin?: string;
|
|
1571
1682
|
/** The schedule the physics answered (`render --environment`, issue #1416) — `frames.json`'s own `environment`; absent without it. */
|
|
1572
1683
|
environment?: EnvironmentStep[];
|
|
1684
|
+
/** The whole loops walked before the loop these frames record (`render --preroll`, issue #1684) — `frames.json`'s own `preroll`; absent without it. */
|
|
1685
|
+
preroll?: number;
|
|
1573
1686
|
fps: number;
|
|
1574
1687
|
/** The box the PNG frames beside this file were drawn over — `frames.json`'s own `viewport`. */
|
|
1575
1688
|
viewport: FramesSidecar['viewport'];
|
|
@@ -1630,6 +1743,7 @@ export function geometryFileOf(
|
|
|
1630
1743
|
viewport: Viewport,
|
|
1631
1744
|
skin: string | undefined,
|
|
1632
1745
|
environment?: EnvironmentStep[],
|
|
1746
|
+
preroll?: number,
|
|
1633
1747
|
): GeometryFile {
|
|
1634
1748
|
const geometryFrame = (frame: Frame, where: string): Omit<GeometryFrame, 'index' | 'time'> => {
|
|
1635
1749
|
if (frame.bones === undefined || frame.attachments === undefined) {
|
|
@@ -1654,6 +1768,7 @@ export function geometryFileOf(
|
|
|
1654
1768
|
animation,
|
|
1655
1769
|
...(skin === undefined ? {} : { skin }),
|
|
1656
1770
|
...(environment === undefined ? {} : { environment }),
|
|
1771
|
+
...(preroll === undefined ? {} : { preroll }),
|
|
1657
1772
|
fps,
|
|
1658
1773
|
viewport: sidecarViewport(viewport),
|
|
1659
1774
|
bones: poser.bones.map(({ name, parent }) => ({ name, parent })),
|
package/src/sheet.ts
CHANGED
|
@@ -118,6 +118,8 @@ export interface SheetManifest {
|
|
|
118
118
|
slots?: string[];
|
|
119
119
|
hidden?: string[];
|
|
120
120
|
environment?: EnvironmentStep[];
|
|
121
|
+
/** The whole loops walked before the loop these frames record (`render --preroll`, issue #1684); absent without it. */
|
|
122
|
+
preroll?: number;
|
|
121
123
|
pinned?: PinnedViewport;
|
|
122
124
|
fps: number;
|
|
123
125
|
duration: number;
|
|
@@ -182,6 +184,7 @@ export interface BuildSheetSetOptions {
|
|
|
182
184
|
skin?: string;
|
|
183
185
|
subset?: SlotSubset;
|
|
184
186
|
environment?: EnvironmentStep[];
|
|
187
|
+
preroll?: number;
|
|
185
188
|
pinned?: PinnedViewport;
|
|
186
189
|
/** For negative-control testing in `selftest.ts` only: reverse the input order handed to `packAtlas`. */
|
|
187
190
|
reversePackInputs?: boolean;
|
|
@@ -262,9 +265,9 @@ function sanitizeNamespaceSegment(segment: string): string {
|
|
|
262
265
|
|
|
263
266
|
/**
|
|
264
267
|
* Build the collision-free frame-key namespace `<rig>/<anim>[/<variant>]` for a
|
|
265
|
-
* rendered set. Non-default skin, slot subset,
|
|
266
|
-
* encoded in `<variant>` in fixed order so multiple exports loaded into a
|
|
267
|
-
* texture cache cannot collide on frame keys.
|
|
268
|
+
* rendered set. Non-default skin, slot subset, environment schedule or pre-roll
|
|
269
|
+
* are encoded in `<variant>` in fixed order so multiple exports loaded into a
|
|
270
|
+
* shared texture cache cannot collide on frame keys.
|
|
268
271
|
*/
|
|
269
272
|
export function sheetNamespaceOf(opts: {
|
|
270
273
|
rigName: string;
|
|
@@ -272,6 +275,7 @@ export function sheetNamespaceOf(opts: {
|
|
|
272
275
|
skin?: string;
|
|
273
276
|
subset?: SlotSubset;
|
|
274
277
|
environment?: EnvironmentStep[];
|
|
278
|
+
preroll?: number;
|
|
275
279
|
}): string {
|
|
276
280
|
const rig = sanitizeNamespaceSegment(opts.rigName);
|
|
277
281
|
const anim = sanitizeNamespaceSegment(opts.setName);
|
|
@@ -282,6 +286,7 @@ export function sheetNamespaceOf(opts: {
|
|
|
282
286
|
const digest = createHash('sha256').update(JSON.stringify(opts.environment)).digest('hex').slice(0, 8);
|
|
283
287
|
variants.push(`env=${digest}`);
|
|
284
288
|
}
|
|
289
|
+
if (opts.preroll !== undefined) variants.push(`preroll=${opts.preroll}`);
|
|
285
290
|
return variants.length === 0 ? `${rig}/${anim}` : `${rig}/${anim}/${variants.join('+')}`;
|
|
286
291
|
}
|
|
287
292
|
|
|
@@ -715,6 +720,7 @@ export function buildSheetSet(opts: BuildSheetSetOptions): SheetSetPack {
|
|
|
715
720
|
skin: opts.skin,
|
|
716
721
|
subset: opts.subset,
|
|
717
722
|
environment: opts.environment,
|
|
723
|
+
preroll: opts.preroll,
|
|
718
724
|
});
|
|
719
725
|
|
|
720
726
|
const padding = SHEET_PADDING;
|
|
@@ -868,6 +874,7 @@ export function buildSheetSet(opts: BuildSheetSetOptions): SheetSetPack {
|
|
|
868
874
|
? { slots: [...opts.subset.names] }
|
|
869
875
|
: { hidden: [...opts.subset.names] }),
|
|
870
876
|
...(opts.environment === undefined ? {} : { environment: opts.environment }),
|
|
877
|
+
...(opts.preroll === undefined ? {} : { preroll: opts.preroll }),
|
|
871
878
|
...(opts.pinned === undefined ? {} : { pinned: opts.pinned }),
|
|
872
879
|
fps: opts.fps,
|
|
873
880
|
duration,
|