spine-rigc 3.2.0 → 3.2.1
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 +32 -3
- package/package.json +1 -1
- package/src/render.ts +3 -2
- package/src/render_core.ts +24 -6
- package/src/render_shared.ts +52 -12
package/docs/AUTHORING.md
CHANGED
|
@@ -389,7 +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
|
+
| `--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, while its physics clock lands on the frame grid point nearest it, so the loop recorded is loop `n + 1` of a plain looping walk to rounding even where the duration is a float32 a hair off a whole number of frames (`PRE07`); 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** |
|
|
393
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 |
|
|
394
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** |
|
|
395
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` |
|
|
@@ -9844,9 +9844,9 @@ first, drawn by no frame, then records the next loop:
|
|
|
9844
9844
|
- **The walk.** `count` is the set's frame count at the rate,
|
|
9845
9845
|
`round(duration · fps)`. From the same reset at frame 0 a render from rest
|
|
9846
9846
|
takes, the track steps the render's grid `j / fps` up to the last point before
|
|
9847
|
-
track time `n · duration`, takes one
|
|
9847
|
+
track time `n · duration`, takes one step onto it, then the `count`
|
|
9848
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
|
|
9849
|
+
physics clock (§4.6), save the step onto the wrap (below) — and its animation time is the track time wrapped by the
|
|
9850
9850
|
duration, what spine-core's `setAnimation(0, name, true)` does and what the
|
|
9851
9851
|
core's live track does looping (`openTrack({ loop: true })`). Both posers add
|
|
9852
9852
|
the steps up the same way, and every one before the wrap subtracts without
|
|
@@ -9865,6 +9865,35 @@ first, drawn by no frame, then records the next loop:
|
|
|
9865
9865
|
float and the wrap is hit exactly; for a duration that is not, where the
|
|
9866
9866
|
product rounds below the wrap — the loop's end rather than its start — the
|
|
9867
9867
|
walk lands one float past it (`prerollStart`).
|
|
9868
|
+
- **The physics clock lands on the grid, not on the wrap** (issue #1687). The
|
|
9869
|
+
track's step onto the wrap is a frame only where the duration is exactly
|
|
9870
|
+
`count / fps`. A duration read as a float32 a hair below it — 8/3 s is read as
|
|
9871
|
+
2.6666665 s, `count / fps` less `δ` = 1.6e-7 s — makes that step a frame short
|
|
9872
|
+
by `n · δ`, far more than the render's clock lead has gathered by then (3.2e-7
|
|
9873
|
+
s against 5.0e-9 s at 2 loops at 12 fps); a physics constraint takes only the
|
|
9874
|
+
whole steps that fit, so the clock moved by it dropped one physics step at the
|
|
9875
|
+
landing, and the loop recorded started a fresh transient instead of continuing
|
|
9876
|
+
the settled cycle. So every step of the walk moves the physics clock by its own
|
|
9877
|
+
length except the step onto the wrap, which moves it to the grid point nearest
|
|
9878
|
+
the wrap: a whole frame where the wrap sits just short of one, nothing where it
|
|
9879
|
+
sits just past one and the step onto it is the sliver beyond. The loop recorded
|
|
9880
|
+
is then loop `n + 1` of a plain looping walk on the uniform grid, its physics
|
|
9881
|
+
clock off that walk's by rounding alone, and frame 0 is still the wrap — the
|
|
9882
|
+
keyed bones re-phased onto it by `n · δ`. Where the duration is exactly `count
|
|
9883
|
+
/ fps` the step onto the wrap is a frame and nothing moves; on a loop of no
|
|
9884
|
+
whole frame count the physics stays on the frame grid the same way and the
|
|
9885
|
+
keyed bones are re-phased by under half a frame. On the selftest's
|
|
9886
|
+
`preroll_probe` — four springs in a chain under loops read just short of 32
|
|
9887
|
+
and 80 frames and just past 48 — the short step moved the springs 26.4 units
|
|
9888
|
+
off the plain walk's loop on a stepped idle and 37.6 on an eased one at 2 loops
|
|
9889
|
+
and 12 fps; now, on the stepped loops at pre-rolls of 2 and 10 on both posers,
|
|
9890
|
+
every keyed bone is the plain walk's to the bit and every spring is within
|
|
9891
|
+
what one more clock lead moves it (`PRE07`, at most 2.2e-5 of it); restoring
|
|
9892
|
+
the short step on the core is red there on every case, by name (`PRE09`). On
|
|
9893
|
+
the eased loop the springs sit 5.3e-5 units off at 2 loops and 2.6e-4 at 10,
|
|
9894
|
+
linear in `n · δ`: that is the re-phase, which frame 0 being the wrap is. After
|
|
9895
|
+
10 loops every spring's seam closes within its largest step on both posers,
|
|
9896
|
+
where the short step opened four of five sets (`PRE08`).
|
|
9868
9897
|
- **What moves.** Only what carries from one step to the next: the physics. A
|
|
9869
9898
|
keyed bone is posed at the loop time the render from rest draws it at, to the
|
|
9870
9899
|
rounding of the steps' sums — on the skirt fixture's hip, measured on this walk
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
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/render.ts
CHANGED
|
@@ -503,7 +503,7 @@ 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, loop) => {
|
|
506
|
+
animation: (name, skin, fps, count, visit, environment, _channels, times, loop, clockSteps) => {
|
|
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
|
|
@@ -522,7 +522,8 @@ export function spinePoser(data: SkeletonData): Poser {
|
|
|
522
522
|
state.update(delta);
|
|
523
523
|
state.apply(skeleton);
|
|
524
524
|
setSkeletonEnvironment(skeleton, environment?.(i));
|
|
525
|
-
|
|
525
|
+
// `clockSteps` (issue #1687, `render --preroll`): what the step moves the physics clock by, where that is not its own length.
|
|
526
|
+
skeleton.update(times === undefined ? clock : physicsClockStep(clockSteps?.[i - 1] ?? delta));
|
|
526
527
|
skeleton.updateWorldTransform(Physics.update);
|
|
527
528
|
} else {
|
|
528
529
|
state.apply(skeleton);
|
package/src/render_core.ts
CHANGED
|
@@ -367,18 +367,35 @@ 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, loop?: boolean) => void;
|
|
370
|
+
export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void, environment?: StepEnvironment, loop?: boolean, clock?: (dt: number) => number) => 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
375
|
* is posed (issue #1416, `StepEnvironment`). `loop` (issue #1684, `render --preroll`) walks the track looping
|
|
376
376
|
* (`poseRawLoopEach`) — spine-core's poser sets the same track looping — and absent, the walk is the one it always was.
|
|
377
|
+
* `clock` (issue #1687) is what each step moves the physics clock by in place of `physicsClockStep` of the step — the
|
|
378
|
+
* pre-roll's walk passes one that reads its own step list (`Poser.animation`'s `clockSteps`) — and absent, the clock is
|
|
379
|
+
* the render's, `physicsClockStep`.
|
|
377
380
|
*/
|
|
378
|
-
export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit, environment, loop) =>
|
|
381
|
+
export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit, environment, loop, clock = physicsClockStep) =>
|
|
379
382
|
loop === true
|
|
380
|
-
? poseRawLoopEach(doc, animation, steps, plant, 'animation', visit,
|
|
381
|
-
: poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit,
|
|
383
|
+
? poseRawLoopEach(doc, animation, steps, plant, 'animation', visit, clock, environment)
|
|
384
|
+
: poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit, clock, environment);
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The physics clock a walk moves by `clockSteps` (issue #1687, `Poser.animation`): the walk asks its clock once for
|
|
388
|
+
* the reset pose — a step of 0, which moves it by `physicsClockStep(0)`, nothing, as spine-core's `update(0)` at frame
|
|
389
|
+
* 0 does — and then once per step in order, and step `i` moves it by `physicsClockStep(clockSteps[i])` in place of
|
|
390
|
+
* `physicsClockStep` of the step's own length. One clock serves one walk: it counts the steps it was asked for.
|
|
391
|
+
*/
|
|
392
|
+
function steppedClock(clockSteps: readonly number[]): (dt: number) => number {
|
|
393
|
+
let next = -1;
|
|
394
|
+
return (dt) => {
|
|
395
|
+
const i = next++;
|
|
396
|
+
return physicsClockStep(i < 0 ? dt : clockSteps[i]);
|
|
397
|
+
};
|
|
398
|
+
}
|
|
382
399
|
|
|
383
400
|
/**
|
|
384
401
|
* The render's physics clock (issue #1380): defined beside the walk that takes it (`./core/raw.ts`), where the mesh
|
|
@@ -794,7 +811,7 @@ export function corePoser(
|
|
|
794
811
|
const pose = poseRawSetup(input.doc, { through, ...observe });
|
|
795
812
|
return corePosed(input, pose, through, [], values);
|
|
796
813
|
},
|
|
797
|
-
animation: (name, skin, fps, count, visit, environment, channels, times, loop) => {
|
|
814
|
+
animation: (name, skin, fps, count, visit, environment, channels, times, loop, clockSteps) => {
|
|
798
815
|
let values: ChannelValue[] | undefined;
|
|
799
816
|
const observe = channels ? { readLocals: (bones: readonly import('./model.ts').ModelBone[], sliders: readonly import('./core/constraints_slider.ts').SliderApplication[]) => { values = localChannels(bones, sliders); } } : {};
|
|
800
817
|
const input = inputOf(skin);
|
|
@@ -814,6 +831,7 @@ export function corePoser(
|
|
|
814
831
|
if (ctx.time > 0 && ctx.states.get(r.name)?.lastTime === ctx.time) steppedNow.add(r.name);
|
|
815
832
|
return moved;
|
|
816
833
|
};
|
|
834
|
+
const clock = clockSteps === undefined ? undefined : steppedClock(clockSteps);
|
|
817
835
|
walk(input.doc, name, times === undefined ? new Array<number>(count).fill(step) : times.slice(1).map((t, i) => t - times[i]), { through, physicsStep, ...observe }, (pose, i) => {
|
|
818
836
|
const stepped = [...steppedNow].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
819
837
|
steppedNow.clear();
|
|
@@ -823,7 +841,7 @@ export function corePoser(
|
|
|
823
841
|
} catch (thrown) {
|
|
824
842
|
drawFailed.push(thrown);
|
|
825
843
|
}
|
|
826
|
-
}, environment, loop);
|
|
844
|
+
}, environment, loop, clock);
|
|
827
845
|
if (drawFailed.length > 0) throw drawFailed[0];
|
|
828
846
|
},
|
|
829
847
|
rest: (skin, shown) => {
|
package/src/render_shared.ts
CHANGED
|
@@ -1198,8 +1198,14 @@ export interface Poser {
|
|
|
1198
1198
|
* animation time the track time wrapped by the duration, the physics carried
|
|
1199
1199
|
* across every wrap, as a player's looping track carries it; absent, the
|
|
1200
1200
|
* track holds at its duration as it always did.
|
|
1201
|
+
* `clockSteps` (issue #1687, `render --preroll`), beside `times`, is what
|
|
1202
|
+
* each step moves the physics clock by before the lead: step `i` (from 1)
|
|
1203
|
+
* moves it by `physicsClockStep(clockSteps[i - 1])` in place of
|
|
1204
|
+
* `physicsClockStep(times[i] - times[i - 1])`, while the track time still
|
|
1205
|
+
* follows `times` (`prerollTimes` says why the two part). Absent, every
|
|
1206
|
+
* step moves the physics clock by its own length, as it always did.
|
|
1201
1207
|
*/
|
|
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;
|
|
1208
|
+
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, clockSteps?: readonly number[]): void;
|
|
1203
1209
|
/** The rest table for the (slot, attachment) pairs `shown` holds — see `AttachmentRest`. */
|
|
1204
1210
|
rest(skin: string | undefined, shown: readonly AttachmentPose[][]): AttachmentRest[];
|
|
1205
1211
|
}
|
|
@@ -1280,11 +1286,11 @@ export function sampleAnimation(source: PoseSource, name: string, fps: number, o
|
|
|
1280
1286
|
if (!Number.isInteger(preroll) || preroll < 1) throw new Error(`preroll is ${String(preroll)}, not a whole number of loops at or above 1`);
|
|
1281
1287
|
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
1288
|
// 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);
|
|
1289
|
+
const { times, first, clock } = prerollTimes(animation.duration, fps, preroll);
|
|
1284
1290
|
const recorded = (i: number, posed: Posed): void => {
|
|
1285
1291
|
if (i >= first) frames.push(frameOf(i - first, (i - first) * step, posed, draw(), opts));
|
|
1286
1292
|
};
|
|
1287
|
-
poser.animation(name, opts?.skin, fps, times.length - 1, recorded, undefined, opts?.channels === true ? true : undefined, times, true);
|
|
1293
|
+
poser.animation(name, opts?.skin, fps, times.length - 1, recorded, undefined, opts?.channels === true ? true : undefined, times, true, clock);
|
|
1288
1294
|
return frames;
|
|
1289
1295
|
}
|
|
1290
1296
|
const visit = (i: number, posed: Posed): void => {
|
|
@@ -1338,13 +1344,14 @@ export function prerollWarmPoints(start: number, fps: number): number {
|
|
|
1338
1344
|
}
|
|
1339
1345
|
|
|
1340
1346
|
/**
|
|
1341
|
-
* The
|
|
1342
|
-
* `
|
|
1343
|
-
* before `prerollStart`, one
|
|
1344
|
-
* `start + i / fps` for `i = 1..count
|
|
1345
|
-
* pose frame 0
|
|
1347
|
+
* The walk a pre-roll of `preroll` loops takes, for `Poser.animation`'s `times`
|
|
1348
|
+
* and `clockSteps` (issues #1684, #1687): the track times — the render's grid
|
|
1349
|
+
* `j / fps` up to the last point before `prerollStart`, one step onto it, then
|
|
1350
|
+
* the loop recorded, `start + i / fps` for `i = 1..count` — and what each step
|
|
1351
|
+
* moves the physics clock by. `first` is the index of `start`, the pose frame 0
|
|
1352
|
+
* is.
|
|
1346
1353
|
*
|
|
1347
|
-
* ⭐ **The
|
|
1354
|
+
* ⭐ **The track lands on `start` exactly.** Both posers step a track by the
|
|
1348
1355
|
* differences of `times` and add them up (`trackTime += delta`), and every
|
|
1349
1356
|
* difference before `start` is exact — two floats within a factor of two of
|
|
1350
1357
|
* each other subtract without rounding — so each running sum is the time it
|
|
@@ -1352,15 +1359,48 @@ export function prerollWarmPoints(start: number, fps: number): number {
|
|
|
1352
1359
|
* `preroll · count` steps instead lands on `preroll · count / fps`, which is
|
|
1353
1360
|
* the wrap only where `duration · fps` is whole: elsewhere the loop recorded
|
|
1354
1361
|
* started at the wrong phase while frame 0 was filed at time 0.
|
|
1355
|
-
|
|
1356
|
-
|
|
1362
|
+
*
|
|
1363
|
+
* ⭐ **The physics clock lands on the grid, not on `start`** (issue #1687). The
|
|
1364
|
+
* step onto `start` is a frame only where the duration is exactly `count /
|
|
1365
|
+
* fps`. Where it is not — a duration read as a float32, `count / fps` less
|
|
1366
|
+
* `δ` — it is a frame short by `preroll · δ`: 3.2e-7 s at 2 loops of a
|
|
1367
|
+
* 2.6666665 s idle at 12 fps, against the 5.0e-9 s of `PHYSICS_CLOCK_LEAD`
|
|
1368
|
+
* the clock has gathered there. A physics constraint takes the whole steps
|
|
1369
|
+
* that fit, so a clock moved by that short step takes one fewer (4 in place
|
|
1370
|
+
* of 5 at 12 fps against 60 Hz) and leaves 0.99998 of a step remaining where
|
|
1371
|
+
* the plain walk leaves 3e-7 of one; every frame after keeps that phase, and
|
|
1372
|
+
* the loop recorded is a fresh transient — measured on the selftest's
|
|
1373
|
+
* four-link `preroll_probe`, its springs 26.4 units off the plain walk's loop
|
|
1374
|
+
* on a stepped idle and 37.6 on an eased one, and its seam 1.38× its largest
|
|
1375
|
+
* step after 10 loops (`PRE08`). ⚠️ The ⭐ above once stood alone as the
|
|
1376
|
+
* walk's whole claim: it is true of the track and said nothing of the
|
|
1377
|
+
* physics, which took the same step.
|
|
1378
|
+
*
|
|
1379
|
+
* So the physics clock is stepped by the same differences except the step
|
|
1380
|
+
* onto `start`, which moves it to the grid point nearest the wrap,
|
|
1381
|
+
* `round(start · fps) / fps`, and not to `start`: a whole frame where the
|
|
1382
|
+
* wrap sits just short of a grid point, and nothing where it sits just past
|
|
1383
|
+
* one — `δ` below 0, where that grid point is the last before `start` and the
|
|
1384
|
+
* step onto `start` is the sliver past it. The loop recorded is then loop
|
|
1385
|
+
* `preroll + 1` of a plain looping walk on the uniform grid, its physics clock
|
|
1386
|
+
* off that walk's by rounding alone (`PRE07`), and frame 0 is still the wrap:
|
|
1387
|
+
* the animation is re-phased onto it by `preroll · δ`, the one difference
|
|
1388
|
+
* between the two that the physics answers. Where the duration is exactly
|
|
1389
|
+
* `count / fps`, `start` is that grid point and the clock steps are the
|
|
1390
|
+
* track's, so nothing moves; on a loop that is no whole number of frames at
|
|
1391
|
+
* all (`PRE06`'s 1.03 s at 12 fps) the physics stays on the frame grid the
|
|
1392
|
+
* same way and the animation is re-phased by under half a frame.
|
|
1393
|
+
*/
|
|
1394
|
+
export function prerollTimes(duration: number, fps: number, preroll: number): { times: number[]; first: number; clock: number[] } {
|
|
1357
1395
|
const start = prerollStart(duration, preroll);
|
|
1358
1396
|
const count = Math.round(duration * fps);
|
|
1359
1397
|
const first = prerollWarmPoints(start, fps);
|
|
1360
1398
|
const times = Array.from({ length: first }, (_, j) => j / fps);
|
|
1361
1399
|
times.push(start);
|
|
1362
1400
|
for (let i = 1; i <= count; i++) times.push(start + i / fps);
|
|
1363
|
-
|
|
1401
|
+
const clock = times.slice(1).map((t, i) => t - times[i]);
|
|
1402
|
+
if (first > 0) clock[first - 1] = Math.round(start * fps) / fps - times[first - 1];
|
|
1403
|
+
return { times, first, clock };
|
|
1364
1404
|
}
|
|
1365
1405
|
|
|
1366
1406
|
/**
|