@rig-c/core 2.75.0 → 2.76.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 +40 -6
- package/package.json +1 -1
- package/src/runtime/curves.ts +3 -1
- package/src/runtime/index.ts +3 -2
- package/src/runtime/model.ts +101 -20
- package/src/runtime/physics.ts +235 -0
- package/src/runtime/pose.ts +311 -16
package/docs/AUTHORING.md
CHANGED
|
@@ -12647,7 +12647,7 @@ verdict on those figures.
|
|
|
12647
12647
|
| <a id="CE_COMPILE_ALPHA_CLIP_CONFLICT"></a>`CE_COMPILE_ALPHA_CLIP_CONFLICT` | Names authored fields or convex/inverse flags conflicting with generated inside-clipping geometry. Use one authored/generated form.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦: alphaClip conflicts with [⟧<br>⟦vertexCount⟧<br>⟦vertices⟧<br>⟦weights⟧<br>⟦boneIndexing⟧<br>⟦, ⟧<br>⟦], convex=⟧<br>⟦, inverse=⟧<br>⟦; required ordinary inside clipping with generated geometry only. Remove authored geometry/flags.⟧ |
|
|
12648
12648
|
| <a id="CE_COMPILE_ALPHA_CLIP_DEFORM"></a>`CE_COMPILE_ALPHA_CLIP_DEFORM` | Names animation and source/clip key/frame counts that cannot share the existing projection contract. Give matching explicit supported transform keys in compatible actual frames, with constant world shares where needed, or author the corresponding clip geometry and motion; weighted binding does not copy source deform.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦ alphaClip from "⟧<br>⟦"⟧<br>⟦: animation "⟧<br>⟦" has ⟧<br>⟦ source and ⟧<br>⟦ clip keys; source frame=⟧<br>⟦no vertex geometry⟧<br>⟦clip frame=⟧<br>⟦, constant world influence shares=⟧<br>⟦Required matching explicit affine/depth-yaw/pitch transforms in compatible DeformGeometry frames, ⟧<br>⟦with constant shares for world-space transfer. Fix the named frame/keys or author corresponding clip motion.⟧ |
|
|
12649
12649
|
|
|
12650
|
-
## 15. Posing in place for a player — the runtime poser (stage
|
|
12650
|
+
## 15. Posing in place for a player — the runtime poser (stage 3)
|
|
12651
12651
|
|
|
12652
12652
|
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12653
12653
|
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
@@ -12682,11 +12682,11 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
|
|
|
12682
12682
|
The display `name` may be shared by different placeholders; it is never a table
|
|
12683
12683
|
key to guess the source from. These load-time strings do not change the pose path.
|
|
12684
12684
|
|
|
12685
|
-
| | what stage
|
|
12685
|
+
| | what stage 3 poses |
|
|
12686
12686
|
| --- | --- |
|
|
12687
12687
|
| bones | local and world transforms, all five inherit modes, skin-required bones under the view's skin (an inactive bone is zeros, as the oracle poses it) |
|
|
12688
12688
|
| timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `alpha`; ik and transform timelines; linear, stepped and Bézier keys, each read from a per-pose key cursor |
|
|
12689
|
-
| constraints | one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
|
|
12689
|
+
| constraints | physics with its exact clock, reset and parameter timelines; value-mapped sliders with bone/slot/deform/physics composition; one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
|
|
12690
12690
|
| vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
|
|
12691
12691
|
| deform | weighted and unweighted mesh timelines, linear/stepped/Bézier, per-pose key cursors and in-place buffers; linked meshes inherit their source's timelines only with `timelines: true` |
|
|
12692
12692
|
| skins and events | named/no-skin views with their skin-required bones and constraints; event delivery in key order over `(last, time]`, tail then head across a wrap |
|
|
@@ -12729,9 +12729,43 @@ back to the definition's **volume**, matching the measured runtime rather than
|
|
|
12729
12729
|
inventing a correction. An explicit `ignore: ['events']` diagnostic still mutes
|
|
12730
12730
|
the list; normal compilation never does.
|
|
12731
12731
|
|
|
12732
|
+
Physics is explicit on a direct pose: `apply(time, 'none')` leaves physics inert;
|
|
12733
|
+
`apply(time, 'reset', delta)` or `apply(time, 'update', delta)` first applies
|
|
12734
|
+
reset keys over `(last, time]` against the old clock, then adds `delta` to the
|
|
12735
|
+
physics clock and solves. A backwards animation time crosses the tail then head
|
|
12736
|
+
of the interval, as a looping track does. `step(dt)` updates physics and the
|
|
12737
|
+
looping track; it takes the initial reset at animation zero if needed.
|
|
12738
|
+
`resetPhysics('animation')` starts at that animation's pose zero;
|
|
12739
|
+
`resetPhysics('setup')` starts from setup before applying the animation.
|
|
12740
|
+
The physics clock is a running sum, never the animation time; constructor option
|
|
12741
|
+
`{ clock: physicsClockStep }` can select the render clock without changing track
|
|
12742
|
+
time. `physicsTime` exposes that sum. A non-finite or negative delta/clock result
|
|
12743
|
+
throws `RuntimeInputError` naming the call. `physicsTranslate(x, y)` and
|
|
12744
|
+
`physicsRotate(x, y, degrees)` shift the previous origins; `setEnvironment({
|
|
12745
|
+
wind: [x, y], gravity: [x, y] })` supplies the skeleton directions (positive y
|
|
12746
|
+
points down), all numbers finite. Offsets, velocities, lags and buffers belong
|
|
12747
|
+
to each pose; a zero physics mix does not consume its constraint's clock.
|
|
12748
|
+
|
|
12749
|
+
A slider reads its dial where earlier constraints left it: local or world
|
|
12750
|
+
property mapped as `to + (value - from) * scale`, clamped at zero or looped by
|
|
12751
|
+
its animation duration. A boneless slider uses its keyed `time`. The selected
|
|
12752
|
+
animation is applied at that value, never advanced as a track, and fires no
|
|
12753
|
+
events or physics reset keys. Mix zero does not re-pose bones; nonzero mix
|
|
12754
|
+
re-poses every keyed bone even before its first key. Its bone targets compose
|
|
12755
|
+
in authored order, including repeated names; each entry uses the same setup
|
|
12756
|
+
values and blends from the current pose. Ordinary tracks still read only the
|
|
12757
|
+
last target for each bone. An earlier unsupported inherit target is refused
|
|
12758
|
+
when a slider applies it, even if the track's last target would hide it.
|
|
12759
|
+
Absolute/additive bone and
|
|
12760
|
+
deform blends and slot switches follow the oracle, including direct scale and
|
|
12761
|
+
colour targets at mix one. Slider physics writes last one pass and affect only
|
|
12762
|
+
constraints later in update order. An unsupported timeline in a slider's
|
|
12763
|
+
animation refuses the whole model under that timeline's existing feature name,
|
|
12764
|
+
so an unselected track cannot hide a construct the slider always applies.
|
|
12765
|
+
|
|
12732
12766
|
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12733
12767
|
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12734
|
-
reads `not posed by the runtime poser (stage
|
|
12768
|
+
reads `not posed by the runtime poser (stage 3), N construct(s) missing:
|
|
12735
12769
|
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12736
12770
|
animation-level one refuses that animation alone (`model.refused`;
|
|
12737
12771
|
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
@@ -12740,7 +12774,7 @@ refused: nothing is posed approximately.
|
|
|
12740
12774
|
|
|
12741
12775
|
| feature | refuses | what it is |
|
|
12742
12776
|
| --- | --- | --- |
|
|
12743
|
-
| `path-constraint
|
|
12777
|
+
| `path-constraint` | the model | a constraint of that kind |
|
|
12744
12778
|
| `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
|
|
12745
12779
|
| `inactive-constrained-bone` | the model | an applied ik or transform constraint naming a bone the view leaves inactive (what the oracle writes there depends on the pass before) |
|
|
12746
12780
|
| `noscale-world-target` | the model | a world-space transform onto a `noScale` or `noScaleOrReflection` bone, whose local values would be read back in that mode |
|
|
@@ -12755,7 +12789,7 @@ this document's [...]`, `apply: the time NaN is not finite`.
|
|
|
12755
12789
|
|
|
12756
12790
|
**The gate.** `tools/core_gate.ts` runs a third dumper (`runtimeDump` in
|
|
12757
12791
|
`tools/pose_oracle.ts`) over every row it builds, at the oracle's samples, and
|
|
12758
|
-
compares
|
|
12792
|
+
compares both unstepped and stepped documents with the oracle's in full doubles at tolerance 0 twice:
|
|
12759
12793
|
through `compare` and cell by cell (which reads the bones `compare`'s
|
|
12760
12794
|
ill-conditioned rule leaves out). One `RUNTIME` line per row reads `IDENTICAL`
|
|
12761
12795
|
(covered whole and equal), `SKIP` (refused at load, or animations or events left
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.76.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/runtime/curves.ts
CHANGED
|
@@ -78,7 +78,9 @@ export function compileCurve(keys: readonly CoreKey[], channels: number): Runtim
|
|
|
78
78
|
/** The last key at or before `t` (−1 before the first), found from `cursors[slot]` and left there — the header's first read. */
|
|
79
79
|
export function keyAt(curve: RuntimeCurve, cursors: Int32Array, slot: number, t: number): number {
|
|
80
80
|
const times = curve.times;
|
|
81
|
-
|
|
81
|
+
// A slider can evaluate another animation between two reads of this one.
|
|
82
|
+
// Shared cursor slots are only search hints: keep the hint inside THIS curve.
|
|
83
|
+
let i = Math.min(cursors[slot], curve.keys - 1);
|
|
82
84
|
while (i >= 0 && times[i] > t) i--;
|
|
83
85
|
while (i + 1 < curve.keys && times[i + 1] <= t) i++;
|
|
84
86
|
cursors[slot] = i;
|
package/src/runtime/index.ts
CHANGED
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
* corpus through both at the oracle's samples and compares full doubles at
|
|
22
22
|
* tolerance 0 (*The runtime poser* there). The oracle is not changed for it.
|
|
23
23
|
*
|
|
24
|
-
* ## Stage
|
|
24
|
+
* ## Stage 3: what it poses, and what it refuses
|
|
25
25
|
*
|
|
26
26
|
* Bones and their five inherit modes; the rotate, translate, scale and shear
|
|
27
27
|
* timelines; a slot's attachment and colour (`rgba`, `rgb`, `alpha`)
|
|
28
28
|
* timelines; weighted/unweighted deform timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
|
|
29
|
-
* two-bone ik
|
|
29
|
+
* two-bone ik, world-space absolute transforms, physics and value-mapped sliders with their
|
|
30
30
|
* timelines; skin-required bones and constraints under the view's skin. One
|
|
31
31
|
* animation at full weight from the setup pose, at a time or stepped as a
|
|
32
32
|
* looping track. Everything else is refused at load by name with the list of
|
|
@@ -81,4 +81,5 @@ export {
|
|
|
81
81
|
L_SHEAR_Y,
|
|
82
82
|
} from './model.ts';
|
|
83
83
|
export type { RuntimeAnimation, RuntimeEvent, RuntimeAttachment, RuntimeFeature, RuntimeMissing, RuntimeModel, RuntimeOptions, RuntimePlant, RuntimeSlot } from './model.ts';
|
|
84
|
+
export type { RuntimePoseOptions, RuntimePhysicsPhase } from './pose.ts';
|
|
84
85
|
export { RuntimePose } from './pose.ts';
|
package/src/runtime/model.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime poser's load-time half (issue #1591, stage
|
|
2
|
+
* The runtime poser's load-time half (issue #1591, stage 3): a skin view of
|
|
3
3
|
* the compiled model (`readModel`, then `underSkin` / `underNoSkin` — the
|
|
4
4
|
* oracle's reader and its views, read and never written) compiled once into
|
|
5
5
|
* index-resolved bones and slots, a constraint plan, compiled timelines and
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* (`RuntimeMissing`), and:
|
|
13
13
|
*
|
|
14
14
|
* - **refuses the model** (`RuntimeRefusal`, every missing feature listed)
|
|
15
|
-
* when one is a property of the whole model — a path
|
|
15
|
+
* when one is a property of the whole model — a path constraint, a transform constraint reading or
|
|
16
16
|
* writing local values or adding (`localSource`, `localTarget`,
|
|
17
17
|
* `additive`), an ik or transform constraint over a bone the view leaves
|
|
18
18
|
* inactive, a world-space transform onto a `noScale` or
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
* attachment key showing a clipping attachment or a region with no atlas
|
|
27
27
|
* rectangle. Every other animation of the model is posed.
|
|
28
28
|
*
|
|
29
|
-
* `RUNTIME_FEATURES` is the closed list of what can be named. Stage
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* `RUNTIME_FEATURES` is the closed list of what can be named. Stage 3 adds indexed physics and value-mapped sliders, including their
|
|
30
|
+
* parameter timelines and per-pass writes. Mesh deforms, named/no-skin views
|
|
31
|
+
* and immutable event deliveries remain covered. `ignore: ['events']` remains an explicit diagnostic opt-out; it is
|
|
32
32
|
* never the default. A merged view with conflicting attachments still refuses
|
|
33
33
|
* by name, as the oracle itself cannot resolve that view without file order.
|
|
34
34
|
*
|
|
@@ -50,16 +50,16 @@ import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, rea
|
|
|
50
50
|
import { constraintInactiveWhy, constraintsAbsentWhy, TRANSFORM_PROPERTIES, type CoreIkRecord, type CoreTransformRecord } from '../core/constraints.ts';
|
|
51
51
|
import { RUNTIME_PI } from '../core/world.ts';
|
|
52
52
|
import { heldArray, timelineIdentity } from '../core/deform.ts';
|
|
53
|
+
import type { CorePhysicsRecord, PhysicsTimelineKind } from '../core/constraints_physics.ts';
|
|
54
|
+
import type { CoreSliderRecord } from '../core/constraints_slider.ts';
|
|
53
55
|
import { compileCurve, compileDeformCurve, type RuntimeCurve } from './curves.ts';
|
|
54
56
|
|
|
55
57
|
/** The stage of issue #1591 this poser implements. */
|
|
56
|
-
export const RUNTIME_STAGE =
|
|
58
|
+
export const RUNTIME_STAGE = 3;
|
|
57
59
|
|
|
58
60
|
/** Every construct the runtime poser can refuse, by the name its refusals carry. */
|
|
59
61
|
export const RUNTIME_FEATURES = [
|
|
60
62
|
'path-constraint',
|
|
61
|
-
'physics-constraint',
|
|
62
|
-
'slider-constraint',
|
|
63
63
|
'local-transform',
|
|
64
64
|
'additive-transform',
|
|
65
65
|
'inactive-constrained-bone',
|
|
@@ -126,6 +126,8 @@ export interface RuntimePlant {
|
|
|
126
126
|
/** The old restore behavior, which repeats keys up to the restored time. */
|
|
127
127
|
readonly restoreEventsFromStart?: boolean;
|
|
128
128
|
/** Omit deform / multiply offsets by weights too early: negative controls only. */
|
|
129
|
+
readonly omitPhysics?: boolean;
|
|
130
|
+
readonly sliderAsTrack?: boolean;
|
|
129
131
|
readonly omitDeform?: boolean;
|
|
130
132
|
readonly deformAfterWeights?: boolean;
|
|
131
133
|
}
|
|
@@ -257,7 +259,41 @@ export interface RuntimeTransform {
|
|
|
257
259
|
readonly clamp: boolean;
|
|
258
260
|
}
|
|
259
261
|
|
|
260
|
-
export
|
|
262
|
+
export interface RuntimePhysics {
|
|
263
|
+
readonly kind: 'physics';
|
|
264
|
+
readonly name: string;
|
|
265
|
+
readonly index: number;
|
|
266
|
+
readonly applied: boolean;
|
|
267
|
+
readonly bones: Int32Array;
|
|
268
|
+
readonly setup: CorePhysicsRecord;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface RuntimeSlider {
|
|
272
|
+
readonly kind: 'slider';
|
|
273
|
+
readonly name: string;
|
|
274
|
+
readonly index: number;
|
|
275
|
+
readonly applied: boolean;
|
|
276
|
+
readonly bones: Int32Array;
|
|
277
|
+
readonly bone: number;
|
|
278
|
+
readonly property: number;
|
|
279
|
+
readonly setup: CoreSliderRecord;
|
|
280
|
+
/** Resolved after every animation is compiled. Never a track. */
|
|
281
|
+
animation: RuntimeAnimation;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface RuntimePhysicsTimeline {
|
|
285
|
+
readonly kind: PhysicsTimelineKind;
|
|
286
|
+
readonly targets: Int32Array;
|
|
287
|
+
readonly curve: RuntimeCurve;
|
|
288
|
+
readonly cursor: number;
|
|
289
|
+
}
|
|
290
|
+
export interface RuntimeSliderTimeline {
|
|
291
|
+
readonly time: RuntimeCurve | null;
|
|
292
|
+
readonly mix: RuntimeCurve | null;
|
|
293
|
+
readonly timeCursor: number;
|
|
294
|
+
readonly mixCursor: number;
|
|
295
|
+
}
|
|
296
|
+
export type RuntimeConstraint = RuntimeIk | RuntimeTransform | RuntimePhysics | RuntimeSlider;
|
|
261
297
|
|
|
262
298
|
/** A bone timeline: the bone, its curve, and per channel the local field it writes and whether it multiplies the setup value (scale) or adds to it. */
|
|
263
299
|
export interface RuntimeBoneTimeline {
|
|
@@ -310,7 +346,11 @@ export interface RuntimeAnimation {
|
|
|
310
346
|
readonly name: string;
|
|
311
347
|
readonly duration: number;
|
|
312
348
|
readonly bones: readonly RuntimeBoneTimeline[];
|
|
349
|
+
/** Every authored target in order; sliders compose duplicates that tracks discard. */
|
|
350
|
+
readonly sliderBones: readonly RuntimeBoneTimeline[];
|
|
313
351
|
readonly deforms: readonly RuntimeDeformTimeline[];
|
|
352
|
+
readonly physics: readonly RuntimePhysicsTimeline[];
|
|
353
|
+
readonly sliders: ReadonlyArray<RuntimeSliderTimeline | null>;
|
|
314
354
|
readonly slots: readonly RuntimeSlotTimeline[];
|
|
315
355
|
/** Per constraint of the plan, its timeline in this animation or `null`. */
|
|
316
356
|
readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
|
|
@@ -326,6 +366,8 @@ export interface RuntimeModel {
|
|
|
326
366
|
/** Control-only boundary defect; fixed at load. */
|
|
327
367
|
readonly omitDeform: boolean;
|
|
328
368
|
readonly deformAfterWeights: boolean;
|
|
369
|
+
readonly omitPhysics: boolean;
|
|
370
|
+
readonly sliderAsTrack: boolean;
|
|
329
371
|
readonly deformCount: number;
|
|
330
372
|
readonly eventWrapOffByOne: boolean;
|
|
331
373
|
readonly restoreEventsFromStart: boolean;
|
|
@@ -449,13 +491,21 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
449
491
|
|
|
450
492
|
// --- constraints, in the document's order ---
|
|
451
493
|
const constraints: RuntimeConstraint[] = [];
|
|
452
|
-
for (const c of doc.constraints) if (c.kind === 'path'
|
|
494
|
+
for (const c of doc.constraints) if (c.kind === 'path') refusals.note(`${c.kind}-constraint`, `${c.kind} constraint "${c.name}"`);
|
|
453
495
|
for (const r of constraintRecords(doc)) {
|
|
454
|
-
if (r.kind
|
|
496
|
+
if (r.kind === 'path') continue;
|
|
455
497
|
const applied = constraintInactiveWhy(r, active) === null;
|
|
456
|
-
const bones = Int32Array.from(r.bones.map(indexOf));
|
|
457
|
-
if (applied && r.bones.some((b) => !active.has(b))) refusals.note('inactive-constrained-bone', `${r.kind} constraint "${r.name}" names bone(s) ${r.bones.filter((b) => !active.has(b)).map((b) => `"${b}"`).join(', ')}, which the view leaves inactive — what the oracle writes there depends on the pass before`);
|
|
498
|
+
const bones = Int32Array.from((r.kind === 'physics' ? [r.bone] : r.kind === 'slider' ? r.timelines.bones.filter(t => t.timelines.length > 0).map(t => t.name) : r.bones).map(indexOf));
|
|
458
499
|
const index = constraints.length;
|
|
500
|
+
if (r.kind === 'physics') {
|
|
501
|
+
constraints.push({ kind: 'physics', name: r.name, index, applied, bones, setup: r });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (r.kind === 'slider') {
|
|
505
|
+
constraints.push({ kind: 'slider', name: r.name, index, applied, bones, setup: r, bone: r.bone === null ? -1 : indexOf(r.bone), property: TRANSFORM_PROPERTIES.indexOf(r.property), animation: null as unknown as RuntimeAnimation });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (applied && r.bones.some((b) => !active.has(b))) refusals.note('inactive-constrained-bone', `${r.kind} constraint "${r.name}" names bone(s) ${r.bones.filter((b) => !active.has(b)).map((b) => `"${b}"`).join(', ')}, which the view leaves inactive — what the oracle writes there depends on the pass before`);
|
|
459
509
|
if (r.kind === 'ik') constraints.push(compileIk(r, index, applied, bones, indexOf));
|
|
460
510
|
else {
|
|
461
511
|
if (r.localSource || r.localTarget) refusals.note('local-transform', `transform constraint "${r.name}" ${[r.localSource ? 'reads local values (localSource)' : '', r.localTarget ? 'writes local values (localTarget)' : ''].filter((x) => x !== '').join(' and ')}`);
|
|
@@ -573,9 +623,16 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
573
623
|
}
|
|
574
624
|
}
|
|
575
625
|
|
|
626
|
+
for (const c of constraints) if (c.kind === 'slider') {
|
|
627
|
+
const a = animations.get(c.setup.animation);
|
|
628
|
+
if (a !== undefined) c.animation = a;
|
|
629
|
+
else for (const m of refusals.byAnimation.get(c.setup.animation) ?? []) refusals.note(m.feature, `slider "${c.name}" applies refused animation "${c.setup.animation}": ${m.detail}`);
|
|
630
|
+
}
|
|
576
631
|
if (refusals.model.length > 0) throw new RuntimeRefusal(refusals.model);
|
|
577
632
|
const names = doc.animations.map((a) => a.name);
|
|
578
633
|
return {
|
|
634
|
+
omitPhysics: plant.omitPhysics === true,
|
|
635
|
+
sliderAsTrack: plant.sliderAsTrack === true,
|
|
579
636
|
omitDeform: plant.omitDeform === true,
|
|
580
637
|
deformAfterWeights: plant.deformAfterWeights === true,
|
|
581
638
|
deformCount,
|
|
@@ -714,18 +771,32 @@ function compileAnimation(
|
|
|
714
771
|
|
|
715
772
|
const lastBy = <T extends { name: string }>(targets: readonly T[]): Map<string, T> => new Map(targets.map((t) => [t.name, t]));
|
|
716
773
|
const bones: RuntimeBoneTimeline[] = [];
|
|
717
|
-
|
|
774
|
+
type BoneTarget = (typeof tl.bones)[number];
|
|
775
|
+
const byTarget = new Map<BoneTarget, RuntimeBoneTimeline[]>();
|
|
776
|
+
const compileBones = (target: BoneTarget, forSlider: boolean): RuntimeBoneTimeline[] => {
|
|
777
|
+
const cached = byTarget.get(target);
|
|
778
|
+
if (cached !== undefined) return cached;
|
|
779
|
+
const keyed: RuntimeBoneTimeline[] = [];
|
|
780
|
+
byTarget.set(target, keyed);
|
|
718
781
|
const bone = boneIndex.get(target.name);
|
|
719
|
-
if (bone === undefined)
|
|
782
|
+
if (bone === undefined) return keyed;
|
|
720
783
|
for (const t of target.timelines) {
|
|
721
784
|
if (t.kind === 'inherit') {
|
|
722
|
-
refusals.note('inherit-timeline', `bone "${target.name}" keys its inherit mode`, name);
|
|
785
|
+
if (!forSlider) refusals.note('inherit-timeline', `bone "${target.name}" keys its inherit mode`, name);
|
|
786
|
+
else for (const c of constraints) if (c.kind === 'slider' && c.setup.animation === name) {
|
|
787
|
+
refusals.note('inherit-timeline', `slider "${c.name}" applies animation "${name}" whose bone "${target.name}" keys its inherit mode`);
|
|
788
|
+
}
|
|
723
789
|
continue;
|
|
724
790
|
}
|
|
725
791
|
const spec = BONE_KINDS[t.kind];
|
|
726
|
-
|
|
792
|
+
keyed.push({ bone, curve: compileCurve(t.keys, spec.length), cursor: cursors++, fields: Int32Array.from(spec.map((s) => s[0])), multiply: Uint8Array.from(spec.map((s) => s[1])) });
|
|
727
793
|
}
|
|
728
|
-
|
|
794
|
+
return keyed;
|
|
795
|
+
};
|
|
796
|
+
// Keep the track's last-target Map order and reuse those exact curves/cursors.
|
|
797
|
+
for (const target of lastBy(tl.bones).values()) bones.push(...compileBones(target, false));
|
|
798
|
+
const sliderBones = constraints.some(c => c.kind === 'slider' && c.setup.animation === name)
|
|
799
|
+
? tl.bones.flatMap(target => compileBones(target, true)) : [];
|
|
729
800
|
const slotTimelines: RuntimeSlotTimeline[] = [];
|
|
730
801
|
const keyedSlots = [...lastBy(tl.slots).values()].flatMap((target) => {
|
|
731
802
|
const si = slotIndex.get(target.name);
|
|
@@ -752,12 +823,22 @@ function compileAnimation(
|
|
|
752
823
|
const keyed = anim.constraints.ik.find((x) => x.name === c.name);
|
|
753
824
|
if (keyed === undefined) continue;
|
|
754
825
|
constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, 2), cursor: cursors++, flags: Uint8Array.from(keyed.keys.map((k) => ikFlags(k.flags.bendPositive, k.flags.compress, k.flags.stretch))) };
|
|
755
|
-
} else {
|
|
826
|
+
} else if (c.kind === 'transform') {
|
|
756
827
|
const keyed = anim.constraints.transform.find((x) => x.name === c.name);
|
|
757
828
|
if (keyed === undefined) continue;
|
|
758
829
|
constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, TRANSFORM_PROPERTIES.length), cursor: cursors++, flags: null };
|
|
759
830
|
}
|
|
760
831
|
}
|
|
832
|
+
const sliders = constraints.map((c): RuntimeSliderTimeline | null => {
|
|
833
|
+
if (c.kind !== 'slider') return null;
|
|
834
|
+
const keyed = anim.constraints.slider.find(t => t.name === c.name);
|
|
835
|
+
if (keyed === undefined) return null;
|
|
836
|
+
return { time: keyed.time === null ? null : compileCurve(keyed.time, 1), mix: keyed.mix === null ? null : compileCurve(keyed.mix, 1), timeCursor: cursors++, mixCursor: cursors++ };
|
|
837
|
+
});
|
|
838
|
+
const physics: RuntimePhysicsTimeline[] = (anim.constraints.physicsKeyed ?? []).map(t => ({
|
|
839
|
+
kind: t.kind, cursor: cursors++, curve: compileCurve(t.keys, t.kind === 'reset' ? 0 : 1),
|
|
840
|
+
targets: Int32Array.from(constraints.filter((c): c is RuntimePhysics => c.kind === 'physics' && c.applied && (t.name === '*' ? t.kind === 'reset' || c.setup.global[t.kind] : c.name === t.name)).map(c => c.index)),
|
|
841
|
+
}));
|
|
761
842
|
const deforms: RuntimeDeformTimeline[] = deform.map(a => {
|
|
762
843
|
const g = doc.skins.find(s => s.name === a.skin)?.attachments[a.slot]?.[a.attachment]?.geometry;
|
|
763
844
|
if (g === undefined || g.kind === 'region' || g.kind === 'linkedmesh') throw new RuntimeInputError(`deform "${a.skin}/${a.slot}/${a.attachment}" has no vertex geometry (readModel refuses it first)`);
|
|
@@ -765,5 +846,5 @@ function compileAnimation(
|
|
|
765
846
|
const arrays = keys.map(k => heldArray(g.vertices, k));
|
|
766
847
|
return { identity: `${a.skin}/${a.slot}/${a.attachment}`, cursor: cursors++, curve: compileDeformCurve(keys), count: arrays[0].length, held: Float64Array.from(arrays.flat()) };
|
|
767
848
|
});
|
|
768
|
-
return { name, duration: tl.duration, deforms, bones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length, eventKeys: Object.freeze(ignoreEvents ? [] : tl.events.map(key => Object.freeze({ name: key.name, time: key.time, int: key.int, float: key.float, string: key.string, volume: key.volume ?? 0, balance: key.balance ?? 0 }))) };
|
|
849
|
+
return { name, duration: tl.duration, physics, sliders, deforms, bones, sliderBones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length, eventKeys: Object.freeze(ignoreEvents ? [] : tl.events.map(key => Object.freeze({ name: key.name, time: key.time, int: key.int, float: key.float, string: key.string, volume: key.volume ?? 0, balance: key.balance ?? 0 }))) };
|
|
769
850
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** Indexed in-place physics. Arithmetic and operation order mirror the measured oracle;
|
|
2
|
+
* state is allocated once per pose, never during an update. */
|
|
3
|
+
import type { CorePhysicsRecord } from '../core/constraints_physics.ts';
|
|
4
|
+
import { RUNTIME_PI } from '../core/world.ts';
|
|
5
|
+
const PI2 = RUNTIME_PI * 2;
|
|
6
|
+
const INV_PI2 = 1 / PI2;
|
|
7
|
+
export interface PhysicsState {
|
|
8
|
+
/** The next update takes the bone where it is and integrates nothing: set by a reset and on a constraint never updated. */
|
|
9
|
+
pending: boolean;
|
|
10
|
+
lastTime: number;
|
|
11
|
+
remaining: number;
|
|
12
|
+
ux: number;
|
|
13
|
+
uy: number;
|
|
14
|
+
cx: number;
|
|
15
|
+
cy: number;
|
|
16
|
+
tx: number;
|
|
17
|
+
ty: number;
|
|
18
|
+
xOffset: number;
|
|
19
|
+
xLag: number;
|
|
20
|
+
xVelocity: number;
|
|
21
|
+
yOffset: number;
|
|
22
|
+
yLag: number;
|
|
23
|
+
yVelocity: number;
|
|
24
|
+
rotateOffset: number;
|
|
25
|
+
rotateLag: number;
|
|
26
|
+
rotateVelocity: number;
|
|
27
|
+
scaleOffset: number;
|
|
28
|
+
scaleLag: number;
|
|
29
|
+
scaleVelocity: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A constraint's state before its first update: pending, its clock at 0. */
|
|
33
|
+
export function freshPhysicsState(): PhysicsState {
|
|
34
|
+
return { pending: true, lastTime: 0, remaining: 0, ux: 0, uy: 0, cx: 0, cy: 0, tx: 0, ty: 0, xOffset: 0, xLag: 0, xVelocity: 0, yOffset: 0, yLag: 0, yVelocity: 0, rotateOffset: 0, rotateLag: 0, rotateVelocity: 0, scaleOffset: 0, scaleLag: 0, scaleVelocity: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A reset: offsets, velocities and lags cleared, nothing remaining, the clock read at `time`, pending. */
|
|
38
|
+
export function resetPhysicsState(s: PhysicsState, time: number): void {
|
|
39
|
+
s.remaining = 0;
|
|
40
|
+
s.lastTime = time;
|
|
41
|
+
s.pending = true;
|
|
42
|
+
s.xOffset = s.xLag = s.xVelocity = 0;
|
|
43
|
+
s.yOffset = s.yLag = s.yVelocity = 0;
|
|
44
|
+
s.rotateOffset = s.rotateLag = s.rotateVelocity = 0;
|
|
45
|
+
s.scaleOffset = s.scaleLag = s.scaleVelocity = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function stepRuntimePhysics(r: CorePhysicsRecord, s: PhysicsState, w: Float64Array, boneOffset: number, length: number, time: number, reset: boolean, env: Float64Array): boolean {
|
|
49
|
+
const mix = r.mix;
|
|
50
|
+
if (mix === 0) return false;
|
|
51
|
+
const x = r.x > 0, y = r.y > 0, rotateOrShearX = r.rotate > 0 || r.shearX > 0, scaleX = r.scaleX > 0;
|
|
52
|
+
const l = length;
|
|
53
|
+
if (reset) resetPhysicsState(s, time);
|
|
54
|
+
const delta = Math.max(time - s.lastTime, 0);
|
|
55
|
+
// The interpolation weight of the previous update, before this one's time is added: the rotate and the scale-only inertia read the pose as it was shown.
|
|
56
|
+
const shown = Math.max(0, 1 - s.remaining / r.step);
|
|
57
|
+
s.remaining += delta;
|
|
58
|
+
s.lastTime = time;
|
|
59
|
+
const bx = w[boneOffset + 4];
|
|
60
|
+
const by = w[boneOffset + 5];
|
|
61
|
+
let z = 0;
|
|
62
|
+
if (s.pending) {
|
|
63
|
+
s.pending = false;
|
|
64
|
+
s.ux = bx;
|
|
65
|
+
s.uy = by;
|
|
66
|
+
} else {
|
|
67
|
+
let a = s.remaining;
|
|
68
|
+
const i = r.inertia;
|
|
69
|
+
const t = r.step;
|
|
70
|
+
const f = r.referenceScale;
|
|
71
|
+
let d = -1;
|
|
72
|
+
const qx = r.limit * delta;
|
|
73
|
+
const qy = qx;
|
|
74
|
+
if (x || y) {
|
|
75
|
+
if (x) {
|
|
76
|
+
const u = (s.ux - bx) * i;
|
|
77
|
+
s.xOffset += u > qx ? qx : u < -qx ? -qx : u;
|
|
78
|
+
s.ux = bx;
|
|
79
|
+
}
|
|
80
|
+
if (y) {
|
|
81
|
+
const u = (s.uy - by) * i;
|
|
82
|
+
s.yOffset += u > qy ? qy : u < -qy ? -qy : u;
|
|
83
|
+
s.uy = by;
|
|
84
|
+
}
|
|
85
|
+
if (a >= t) {
|
|
86
|
+
d = Math.pow(r.damping, 60 * t);
|
|
87
|
+
const m = r.massInverse * t;
|
|
88
|
+
const e = r.strength;
|
|
89
|
+
const wind = r.wind * f;
|
|
90
|
+
const g = r.gravity * f;
|
|
91
|
+
// The environment's directions (issue #1416), as the runtime weighs them; the skeleton's scale is 1 here, so its factor is left out.
|
|
92
|
+
const ax = wind * env[0] + g * env[2];
|
|
93
|
+
const ay = wind * env[1] + g * env[3];
|
|
94
|
+
const xs = s.xOffset;
|
|
95
|
+
const ys = s.yOffset;
|
|
96
|
+
do {
|
|
97
|
+
if (x) {
|
|
98
|
+
s.xVelocity += (ax - s.xOffset * e) * m;
|
|
99
|
+
s.xOffset += s.xVelocity * t;
|
|
100
|
+
s.xVelocity *= d;
|
|
101
|
+
}
|
|
102
|
+
if (y) {
|
|
103
|
+
s.yVelocity -= (ay + s.yOffset * e) * m;
|
|
104
|
+
s.yOffset += s.yVelocity * t;
|
|
105
|
+
s.yVelocity *= d;
|
|
106
|
+
}
|
|
107
|
+
a -= t;
|
|
108
|
+
} while (a >= t);
|
|
109
|
+
s.xLag = s.xOffset - xs;
|
|
110
|
+
s.yLag = s.yOffset - ys;
|
|
111
|
+
}
|
|
112
|
+
z = Math.max(0, 1 - a / t);
|
|
113
|
+
if (x) w[boneOffset + 4] += (s.xOffset - s.xLag * z) * mix * r.x;
|
|
114
|
+
if (y) w[boneOffset + 5] += (s.yOffset - s.yLag * z) * mix * r.y;
|
|
115
|
+
}
|
|
116
|
+
if (rotateOrShearX || scaleX) {
|
|
117
|
+
const ca = Math.atan2(w[boneOffset + 2], w[boneOffset + 0]);
|
|
118
|
+
let c = 0;
|
|
119
|
+
let sn = 0;
|
|
120
|
+
let mr = 0;
|
|
121
|
+
let dx = s.cx - w[boneOffset + 4];
|
|
122
|
+
let dy = s.cy - w[boneOffset + 5];
|
|
123
|
+
if (dx > qx) dx = qx;
|
|
124
|
+
else if (dx < -qx) dx = -qx;
|
|
125
|
+
if (dy > qy) dy = qy;
|
|
126
|
+
else if (dy < -qy) dy = -qy;
|
|
127
|
+
if (rotateOrShearX) {
|
|
128
|
+
mr = (r.rotate + r.shearX) * mix;
|
|
129
|
+
const lagged = s.rotateLag * shown;
|
|
130
|
+
let rr = Math.atan2(dy + s.ty, dx + s.tx) - ca - (s.rotateOffset - lagged) * mr;
|
|
131
|
+
s.rotateOffset += (rr - Math.ceil(rr * INV_PI2 - 0.5) * PI2) * i;
|
|
132
|
+
rr = (s.rotateOffset - lagged) * mr + ca;
|
|
133
|
+
c = Math.cos(rr);
|
|
134
|
+
sn = Math.sin(rr);
|
|
135
|
+
if (scaleX) {
|
|
136
|
+
rr = l * Math.sqrt(w[boneOffset + 0] * w[boneOffset + 0] + w[boneOffset + 2] * w[boneOffset + 2]);
|
|
137
|
+
if (rr > 0) s.scaleOffset += ((dx * c + dy * sn) * i) / rr;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
c = Math.cos(ca);
|
|
141
|
+
sn = Math.sin(ca);
|
|
142
|
+
const rr = l * Math.sqrt(w[boneOffset + 0] * w[boneOffset + 0] + w[boneOffset + 2] * w[boneOffset + 2]) - s.scaleLag * shown;
|
|
143
|
+
if (rr > 0) s.scaleOffset += ((dx * c + dy * sn) * i) / rr;
|
|
144
|
+
}
|
|
145
|
+
a = s.remaining;
|
|
146
|
+
if (a >= t) {
|
|
147
|
+
if (d === -1) d = Math.pow(r.damping, 60 * t);
|
|
148
|
+
const m = r.massInverse * t;
|
|
149
|
+
const e = r.strength;
|
|
150
|
+
const ax = r.wind * env[0] + r.gravity * env[2];
|
|
151
|
+
const ay = r.wind * env[1] + r.gravity * env[3];
|
|
152
|
+
const h = l / f;
|
|
153
|
+
const rs = s.rotateOffset;
|
|
154
|
+
const ss = s.scaleOffset;
|
|
155
|
+
for (;;) {
|
|
156
|
+
a -= t;
|
|
157
|
+
if (scaleX) {
|
|
158
|
+
s.scaleVelocity += (ax * c - ay * sn - s.scaleOffset * e) * m;
|
|
159
|
+
s.scaleOffset += s.scaleVelocity * t;
|
|
160
|
+
s.scaleVelocity *= d;
|
|
161
|
+
}
|
|
162
|
+
if (rotateOrShearX) {
|
|
163
|
+
s.rotateVelocity -= ((ax * sn + ay * c) * h + s.rotateOffset * e) * m;
|
|
164
|
+
s.rotateOffset += s.rotateVelocity * t;
|
|
165
|
+
s.rotateVelocity *= d;
|
|
166
|
+
if (a < t) break;
|
|
167
|
+
const rr = s.rotateOffset * mr + ca;
|
|
168
|
+
c = Math.cos(rr);
|
|
169
|
+
sn = Math.sin(rr);
|
|
170
|
+
} else if (a < t) break;
|
|
171
|
+
}
|
|
172
|
+
s.rotateLag = s.rotateOffset - rs;
|
|
173
|
+
s.scaleLag = s.scaleOffset - ss;
|
|
174
|
+
}
|
|
175
|
+
z = Math.max(0, 1 - a / t);
|
|
176
|
+
}
|
|
177
|
+
s.remaining = a;
|
|
178
|
+
}
|
|
179
|
+
s.cx = w[boneOffset + 4];
|
|
180
|
+
s.cy = w[boneOffset + 5];
|
|
181
|
+
if (rotateOrShearX) {
|
|
182
|
+
let angle = (s.rotateOffset - s.rotateLag * z) * mix;
|
|
183
|
+
let sn = 0;
|
|
184
|
+
let c = 0;
|
|
185
|
+
let a = 0;
|
|
186
|
+
if (r.shearX > 0) {
|
|
187
|
+
let rr = 0;
|
|
188
|
+
if (r.rotate > 0) {
|
|
189
|
+
// The rotation's share turns the y column here, and the x column with the shear below.
|
|
190
|
+
rr = angle * r.rotate;
|
|
191
|
+
sn = Math.sin(rr);
|
|
192
|
+
c = Math.cos(rr);
|
|
193
|
+
a = w[boneOffset + 1];
|
|
194
|
+
w[boneOffset + 1] = c * a - sn * w[boneOffset + 3];
|
|
195
|
+
w[boneOffset + 3] = sn * a + c * w[boneOffset + 3];
|
|
196
|
+
}
|
|
197
|
+
rr += angle * r.shearX;
|
|
198
|
+
sn = Math.sin(rr);
|
|
199
|
+
c = Math.cos(rr);
|
|
200
|
+
a = w[boneOffset + 0];
|
|
201
|
+
w[boneOffset + 0] = c * a - sn * w[boneOffset + 2];
|
|
202
|
+
w[boneOffset + 2] = sn * a + c * w[boneOffset + 2];
|
|
203
|
+
} else {
|
|
204
|
+
angle *= r.rotate;
|
|
205
|
+
sn = Math.sin(angle);
|
|
206
|
+
c = Math.cos(angle);
|
|
207
|
+
a = w[boneOffset + 0];
|
|
208
|
+
w[boneOffset + 0] = c * a - sn * w[boneOffset + 2];
|
|
209
|
+
w[boneOffset + 2] = sn * a + c * w[boneOffset + 2];
|
|
210
|
+
a = w[boneOffset + 1];
|
|
211
|
+
w[boneOffset + 1] = c * a - sn * w[boneOffset + 3];
|
|
212
|
+
w[boneOffset + 3] = sn * a + c * w[boneOffset + 3];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (scaleX) {
|
|
216
|
+
const k = 1 + (s.scaleOffset - s.scaleLag * z) * mix * r.scaleX;
|
|
217
|
+
w[boneOffset + 0] *= k;
|
|
218
|
+
w[boneOffset + 2] *= k;
|
|
219
|
+
// The transverse column follows the authored mode (issue #1453), after
|
|
220
|
+
// the X column: the native Volume branch bounds reciprocal compression
|
|
221
|
+
// below 0.7; it is not an area-preservation promise on that branch.
|
|
222
|
+
if (r.scaleYMode === 'Uniform') {
|
|
223
|
+
w[boneOffset + 1] *= k;
|
|
224
|
+
w[boneOffset + 3] *= k;
|
|
225
|
+
} else if (r.scaleYMode === 'Volume') {
|
|
226
|
+
const magnitude = Math.abs(k);
|
|
227
|
+
const transverse = magnitude >= 0.7 ? 1 / magnitude : 4 - 3.67347 * magnitude;
|
|
228
|
+
w[boneOffset + 1] *= transverse;
|
|
229
|
+
w[boneOffset + 3] *= transverse;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
s.tx = l * w[boneOffset + 0];
|
|
233
|
+
s.ty = l * w[boneOffset + 2];
|
|
234
|
+
return true;
|
|
235
|
+
}
|
package/src/runtime/pose.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime poser's per-instance half (issue #1591, stage
|
|
2
|
+
* The runtime poser's per-instance half (issue #1591, stage 3): one
|
|
3
3
|
* `RuntimePose` per skeleton on screen, every buffer allocated when it is
|
|
4
4
|
* made and overwritten in place on every pose, so that posing a frame
|
|
5
5
|
* allocates nothing.
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* document's order with their timelines' values at `t`, each followed by the
|
|
14
14
|
* bones below what it moved posed again; then each slot's colour and what it
|
|
15
15
|
* shows; then, when asked, every shown attachment's world vertices. Nothing
|
|
16
|
-
* of one pose survives
|
|
17
|
-
*
|
|
16
|
+
* of one unstepped pose survives but key cursors. A stepped pose also keeps
|
|
17
|
+
* its physics clock, offsets, velocities and lags; sliders select values
|
|
18
|
+
* within this pose rather than advancing an independent track.
|
|
18
19
|
*
|
|
19
20
|
* ## The arithmetic is the oracle's
|
|
20
21
|
*
|
|
@@ -37,6 +38,8 @@
|
|
|
37
38
|
* exactly 0 touching nothing; an inactive bone posed as zeros by the
|
|
38
39
|
* hierarchy and never posed again by a constraint, nor the bones below it.
|
|
39
40
|
*/
|
|
41
|
+
import type { CorePhysicsRecord } from '../core/constraints_physics.ts';
|
|
42
|
+
import { freshPhysicsState, resetPhysicsState, stepRuntimePhysics, type PhysicsState } from './physics.ts';
|
|
40
43
|
import { COLLAPSED_X_AXIS_SQ, RUNTIME_DEG, RUNTIME_PI } from '../core/world.ts';
|
|
41
44
|
import { channelValue, keyAt, deformFraction, CURVE_STEPPED } from './curves.ts';
|
|
42
45
|
import {
|
|
@@ -75,6 +78,8 @@ import {
|
|
|
75
78
|
type RuntimeIk,
|
|
76
79
|
type RuntimeModel,
|
|
77
80
|
type RuntimeTransform,
|
|
81
|
+
type RuntimeSlider,
|
|
82
|
+
type RuntimePhysicsTimeline,
|
|
78
83
|
} from './model.ts';
|
|
79
84
|
|
|
80
85
|
const DEG = RUNTIME_DEG;
|
|
@@ -98,6 +103,10 @@ function volumeScaleY(scaleY: number, s: number): number {
|
|
|
98
103
|
|
|
99
104
|
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
100
105
|
|
|
106
|
+
/** A caller-supplied physics clock; animation/track time still uses dt itself. */
|
|
107
|
+
export interface RuntimePoseOptions { readonly clock?: (dt: number) => number }
|
|
108
|
+
export type RuntimePhysicsPhase = 'none' | 'reset' | 'update';
|
|
109
|
+
|
|
101
110
|
/** One skeleton's pose, every value in place. Read `world`, `light`, `shown` and `vertices` after a pose. */
|
|
102
111
|
export class RuntimePose {
|
|
103
112
|
readonly model: RuntimeModel;
|
|
@@ -139,8 +148,29 @@ export class RuntimePose {
|
|
|
139
148
|
private readonly visit: Uint8Array;
|
|
140
149
|
private readonly w = new Float64Array(WORLDS);
|
|
141
150
|
|
|
142
|
-
|
|
151
|
+
/** Sum of caller-supplied physics deltas, independent of animation time. */
|
|
152
|
+
physicsTime = 0;
|
|
153
|
+
private physicsLast = -1;
|
|
154
|
+
private physicsPhase: RuntimePhysicsPhase = 'none';
|
|
155
|
+
private physicsReady = false;
|
|
156
|
+
private readonly hasPhysics: boolean;
|
|
157
|
+
private readonly physicsStates: PhysicsState[];
|
|
158
|
+
private readonly physicsRecords: Array<CorePhysicsRecord | null>;
|
|
159
|
+
private readonly environment = new Float64Array([1, 0, 0, 1]);
|
|
160
|
+
private readonly sliderTime: Float64Array;
|
|
161
|
+
private readonly sliderMix: Float64Array;
|
|
162
|
+
private readonly sliderAt: Float64Array;
|
|
163
|
+
private readonly clock: ((dt: number) => number) | undefined;
|
|
164
|
+
|
|
165
|
+
constructor(model: RuntimeModel, options: RuntimePoseOptions = {}) {
|
|
143
166
|
this.model = model;
|
|
167
|
+
this.clock = options.clock;
|
|
168
|
+
this.hasPhysics = model.constraints.some(c => c.kind === 'physics');
|
|
169
|
+
this.physicsStates = model.constraints.map(() => freshPhysicsState());
|
|
170
|
+
this.physicsRecords = model.constraints.map(c => c.kind === 'physics' ? { ...c.setup } : null);
|
|
171
|
+
this.sliderTime = new Float64Array(model.constraints.length);
|
|
172
|
+
this.sliderMix = new Float64Array(model.constraints.length);
|
|
173
|
+
this.sliderAt = new Float64Array(model.constraints.length);
|
|
144
174
|
const nb = model.boneNames.length;
|
|
145
175
|
const ns = model.slots.length;
|
|
146
176
|
const nc = model.constraints.length;
|
|
@@ -168,6 +198,10 @@ export class RuntimePose {
|
|
|
168
198
|
/** The animation this pose plays (a covered one: `model.animation` throws a `RuntimeRefusal` for a refused one), from track time 0. */
|
|
169
199
|
setAnimation(name: string): void {
|
|
170
200
|
this.current = this.model.animation(name);
|
|
201
|
+
this.physicsTime = 0;
|
|
202
|
+
this.physicsLast = -1;
|
|
203
|
+
this.physicsReady = false;
|
|
204
|
+
for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
|
|
171
205
|
this.cursors.fill(-1);
|
|
172
206
|
this.trackTime = 0;
|
|
173
207
|
this.time = 0;
|
|
@@ -190,40 +224,57 @@ export class RuntimePose {
|
|
|
190
224
|
}
|
|
191
225
|
|
|
192
226
|
/** The setup pose: every constraint applied with its own values, every slot as it is set up. */
|
|
193
|
-
setupPose(): void {
|
|
227
|
+
setupPose(physics: RuntimePhysicsPhase = 'none'): void {
|
|
228
|
+
this.physicsPhase = physics;
|
|
194
229
|
this.current = null;
|
|
195
230
|
this.time = 0;
|
|
196
231
|
this.eventTime = -1;
|
|
197
232
|
this.fired.length = 0;
|
|
233
|
+
this.physicsValues(null, 0, false);
|
|
198
234
|
this.locals.set(this.model.setup);
|
|
199
235
|
this.hierarchy();
|
|
200
236
|
this.constraintValues(null, 0);
|
|
201
237
|
this.solveConstraints();
|
|
202
|
-
this.
|
|
203
|
-
this.deformValues(null, 0);
|
|
238
|
+
this.attachmentValues(null, 0);
|
|
204
239
|
}
|
|
205
240
|
|
|
206
241
|
/** The animation posed at its time `t`, from the setup pose (the header). */
|
|
207
|
-
apply(t: number): void {
|
|
242
|
+
apply(t: number, physics: RuntimePhysicsPhase = 'none', delta = 0): void {
|
|
208
243
|
const anim = this.current;
|
|
209
244
|
if (anim === null) throw new RuntimeInputError('apply: no animation is set (setAnimation first)');
|
|
210
245
|
if (!Number.isFinite(t)) throw new RuntimeInputError(`apply: the time ${t} is not finite`);
|
|
246
|
+
if (!Number.isFinite(delta) || delta < 0) throw new RuntimeInputError(`apply: physics delta ${delta} is not finite and at or above 0`);
|
|
247
|
+
this.physicsPhase = physics;
|
|
248
|
+
// Reset keys see the old clock. The skeleton moves by delta only after application.
|
|
249
|
+
this.physicsValues(anim, t, physics !== 'none');
|
|
250
|
+
if (physics !== 'none') {
|
|
251
|
+
this.physicsTime += delta;
|
|
252
|
+
this.physicsReady = true;
|
|
253
|
+
}
|
|
211
254
|
this.collectEvents(anim, t);
|
|
212
255
|
this.time = t;
|
|
213
256
|
this.boneTimelines(anim, t);
|
|
214
257
|
this.hierarchy();
|
|
215
258
|
this.constraintValues(anim, t);
|
|
216
259
|
this.solveConstraints();
|
|
217
|
-
this.
|
|
218
|
-
this.deformValues(anim, t);
|
|
260
|
+
this.attachmentValues(anim, t);
|
|
219
261
|
}
|
|
220
262
|
|
|
221
263
|
/** One step of a looping track: the track time moved by `dt`, the animation posed at it wrapped by the duration (`loopedTime` in src/core/raw.ts). */
|
|
222
264
|
step(dt: number): void {
|
|
223
265
|
const anim = this.current;
|
|
224
266
|
if (anim === null) throw new RuntimeInputError('step: no animation is set (setAnimation first)');
|
|
267
|
+
if (!Number.isFinite(dt) || dt < 0) throw new RuntimeInputError(`step: delta ${dt} is not finite and at or above 0`);
|
|
268
|
+
if (!this.physicsReady && this.hasPhysics) {
|
|
269
|
+
// Initial physics setup must not consume the event interval of this step.
|
|
270
|
+
const lastEventTime = this.eventTime;
|
|
271
|
+
this.apply(this.time, 'reset', 0);
|
|
272
|
+
this.eventTime = lastEventTime;
|
|
273
|
+
}
|
|
274
|
+
const moved = this.clock === undefined ? dt : this.clock(dt);
|
|
275
|
+
if (!Number.isFinite(moved) || moved < 0) throw new RuntimeInputError(`step: physics clock moved by ${moved}, not finite and at or above 0`);
|
|
225
276
|
this.trackTime += dt;
|
|
226
|
-
this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration);
|
|
277
|
+
this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration, 'update', moved);
|
|
227
278
|
}
|
|
228
279
|
|
|
229
280
|
/** The oracle interval (last,t], tail then head on a wrap; never copies a payload. */
|
|
@@ -427,9 +478,24 @@ export class RuntimePose {
|
|
|
427
478
|
this.ikSoftness[k] = c.softness + (channelValue(tl.curve, i, 1, t) - c.softness) * 1;
|
|
428
479
|
this.ikFlags[k] = (tl.flags as Uint8Array)[i];
|
|
429
480
|
}
|
|
430
|
-
} else {
|
|
481
|
+
} else if (c.kind === 'transform') {
|
|
431
482
|
const o = k * 6;
|
|
432
483
|
for (let p = 0; p < 6; p++) this.mixes[o + p] = tl === null || i < 0 ? c.mixes[p] : c.mixes[p] + (channelValue(tl.curve, i, p, t) - c.mixes[p]) * 1;
|
|
484
|
+
} else if (c.kind === 'slider') {
|
|
485
|
+
const keyed = anim === null ? null : anim.sliders[k];
|
|
486
|
+
let time = c.setup.time, mix = c.setup.mix;
|
|
487
|
+
if (keyed !== null) {
|
|
488
|
+
if (keyed.time !== null) {
|
|
489
|
+
const q = keyAt(keyed.time, this.cursors, keyed.timeCursor, t);
|
|
490
|
+
if (q >= 0) time += (channelValue(keyed.time, q, 0, t) - time) * 1;
|
|
491
|
+
}
|
|
492
|
+
if (keyed.mix !== null) {
|
|
493
|
+
const q = keyAt(keyed.mix, this.cursors, keyed.mixCursor, t);
|
|
494
|
+
if (q >= 0) mix += (channelValue(keyed.mix, q, 0, t) - mix) * 1;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
this.sliderTime[k] = time;
|
|
498
|
+
this.sliderMix[k] = mix;
|
|
433
499
|
}
|
|
434
500
|
}
|
|
435
501
|
}
|
|
@@ -441,7 +507,222 @@ export class RuntimePose {
|
|
|
441
507
|
const c = cs[k];
|
|
442
508
|
if (!c.applied) continue;
|
|
443
509
|
if (c.kind === 'ik') this.solveIk(c, k);
|
|
444
|
-
else this.solveTransform(c, k);
|
|
510
|
+
else if (c.kind === 'transform') this.solveTransform(c, k);
|
|
511
|
+
else if (c.kind === 'slider') this.solveSlider(c, k);
|
|
512
|
+
else if (this.physicsPhase !== 'none' && !this.model.omitPhysics) {
|
|
513
|
+
const b = c.bones[0];
|
|
514
|
+
if (stepRuntimePhysics(this.physicsRecords[k]!, this.physicsStates[k], this.world, b * WORLDS, this.model.lengths[b], this.physicsTime, this.physicsPhase === 'reset', this.environment)) {
|
|
515
|
+
this.changed[0] = this.inWorld[0] = b;
|
|
516
|
+
this.repose(1, 1);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Reset where the consumer starts: the setup pose or animation pose zero. */
|
|
523
|
+
resetPhysics(at: 'setup' | 'animation' = 'animation'): void {
|
|
524
|
+
this.physicsTime = 0;
|
|
525
|
+
this.physicsLast = -1;
|
|
526
|
+
for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
|
|
527
|
+
if (at === 'animation') this.apply(0, 'reset', 0);
|
|
528
|
+
else {
|
|
529
|
+
const current = this.current;
|
|
530
|
+
this.setupPose('reset');
|
|
531
|
+
this.current = current;
|
|
532
|
+
}
|
|
533
|
+
this.physicsReady = true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** External motion changes the previous origin, never the authored locals. */
|
|
537
|
+
physicsTranslate(x: number, y: number): void {
|
|
538
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new RuntimeInputError('physicsTranslate: x and y must be finite');
|
|
539
|
+
for (let k = 0; k < this.physicsStates.length; k++) {
|
|
540
|
+
const s = this.physicsStates[k];
|
|
541
|
+
s.ux -= x; s.uy -= y; s.cx -= x; s.cy -= y;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
physicsRotate(x: number, y: number, degrees: number): void {
|
|
546
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(degrees)) throw new RuntimeInputError('physicsRotate: x, y and degrees must be finite');
|
|
547
|
+
const r = degrees * RAD, cos = Math.cos(r), sin = Math.sin(r);
|
|
548
|
+
for (let k = 0; k < this.physicsStates.length; k++) {
|
|
549
|
+
const s = this.physicsStates[k];
|
|
550
|
+
const dx = s.cx - x, dy = s.cy - y;
|
|
551
|
+
const tx = dx * cos - dy * sin - dx, ty = dx * sin + dy * cos - dy;
|
|
552
|
+
s.ux -= tx; s.uy -= ty; s.cx -= tx; s.cy -= ty;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
setEnvironment(env: { readonly wind: readonly [number, number]; readonly gravity: readonly [number, number] }): void {
|
|
557
|
+
if (!Number.isFinite(env.wind[0]) || !Number.isFinite(env.wind[1]) || !Number.isFinite(env.gravity[0]) || !Number.isFinite(env.gravity[1])) throw new RuntimeInputError('setEnvironment: wind and gravity must be finite');
|
|
558
|
+
this.environment[0] = env.wind[0]; this.environment[1] = env.wind[1];
|
|
559
|
+
this.environment[2] = env.gravity[0]; this.environment[3] = env.gravity[1];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The per-pass values start at setup, before any slider writes. */
|
|
563
|
+
private physicsValues(anim: RuntimeAnimation | null, t: number, resets: boolean): void {
|
|
564
|
+
const cs = this.model.constraints;
|
|
565
|
+
for (let k = 0; k < cs.length; k++) {
|
|
566
|
+
const c = cs[k];
|
|
567
|
+
if (c.kind !== 'physics') continue;
|
|
568
|
+
const r = this.physicsRecords[k]!;
|
|
569
|
+
r.inertia = c.setup.inertia; r.strength = c.setup.strength; r.damping = c.setup.damping;
|
|
570
|
+
r.massInverse = c.setup.massInverse; r.wind = c.setup.wind; r.gravity = c.setup.gravity; r.mix = c.setup.mix;
|
|
571
|
+
}
|
|
572
|
+
if (anim !== null) this.physicsTimelines(anim.physics, t, 1, false, true, resets);
|
|
573
|
+
if (resets) this.physicsLast = t;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private physicsTimelines(timelines: readonly RuntimePhysicsTimeline[], t: number, alpha: number, additive: boolean, setup: boolean, resets = false): void {
|
|
577
|
+
for (let q = 0; q < timelines.length; q++) {
|
|
578
|
+
const tl = timelines[q];
|
|
579
|
+
if (tl.kind === 'reset') {
|
|
580
|
+
if (!resets) continue; // a slider applies at one value; no key is crossed
|
|
581
|
+
let crossed = false;
|
|
582
|
+
const last = this.physicsLast;
|
|
583
|
+
for (let i = 0; i < tl.curve.keys; i++) {
|
|
584
|
+
const time = tl.curve.times[i];
|
|
585
|
+
if (t < last ? time > last || time <= t : time > last && time <= t) crossed = true;
|
|
586
|
+
}
|
|
587
|
+
if (crossed) for (let j = 0; j < tl.targets.length; j++) resetPhysicsState(this.physicsStates[tl.targets[j]], this.physicsTime);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
591
|
+
if (i < 0) {
|
|
592
|
+
// A track's before-first key restores setup even if an earlier global
|
|
593
|
+
// timeline wrote this parameter. A slider's before-first key writes nothing.
|
|
594
|
+
if (setup) for (let j = 0; j < tl.targets.length; j++) {
|
|
595
|
+
const k = tl.targets[j], r = this.physicsRecords[k]!;
|
|
596
|
+
const original = (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup;
|
|
597
|
+
if (tl.kind === 'mass') r.massInverse = original.massInverse;
|
|
598
|
+
else r[tl.kind] = original[tl.kind];
|
|
599
|
+
}
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const v = channelValue(tl.curve, i, 0, t);
|
|
603
|
+
for (let j = 0; j < tl.targets.length; j++) {
|
|
604
|
+
const k = tl.targets[j], r = this.physicsRecords[k]!;
|
|
605
|
+
const from = setup ? (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup : r;
|
|
606
|
+
if (tl.kind === 'mass') {
|
|
607
|
+
const mass = 1 / from.massInverse;
|
|
608
|
+
r.massInverse = 1 / (mass + (v - mass) * alpha);
|
|
609
|
+
} else {
|
|
610
|
+
r[tl.kind] = additive && (tl.kind === 'wind' || tl.kind === 'gravity') ? r[tl.kind] + v * alpha : from[tl.kind] + (v - from[tl.kind]) * alpha;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Read the dial where previous constraints left it; this value is never track time. */
|
|
617
|
+
private solveSlider(c: RuntimeSlider, k: number): void {
|
|
618
|
+
const alpha = this.sliderMix[k];
|
|
619
|
+
if (alpha === 0) return;
|
|
620
|
+
let time = this.sliderTime[k];
|
|
621
|
+
const r = c.setup, a = c.animation;
|
|
622
|
+
if (c.bone >= 0) {
|
|
623
|
+
const b = c.bone, w = b * WORLDS, l = b * LOCALS;
|
|
624
|
+
let value: number;
|
|
625
|
+
if (r.local) {
|
|
626
|
+
const field = c.property === 0 ? L_ROTATION : c.property === 1 ? L_X : c.property === 2 ? L_Y : c.property === 3 ? L_SCALE_X : c.property === 4 ? L_SCALE_Y : L_SHEAR_Y;
|
|
627
|
+
value = this.locals[l + field] + 0;
|
|
628
|
+
} else {
|
|
629
|
+
const W = this.world, x = W[w], y = W[w + 2], bx = W[w + 1], by = W[w + 3];
|
|
630
|
+
switch (c.property) {
|
|
631
|
+
case 0: value = Math.atan2(y, x) * DEG + (x * by - bx * y > 0 ? 0 : -0); if (value < 0) value += 360; break;
|
|
632
|
+
case 1: value = x * 0 + bx * 0 + W[w + W_X]; break;
|
|
633
|
+
case 2: value = y * 0 + by * 0 + W[w + W_Y]; break;
|
|
634
|
+
case 3: value = Math.sqrt(x * x + y * y) + 0; break;
|
|
635
|
+
case 4: value = Math.sqrt(bx * bx + by * by) + 0; break;
|
|
636
|
+
default: value = (Math.atan2(by, bx) - Math.atan2(y, x)) * DEG - 90 + 0;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
time = r.to + (value - r.from) * r.scale;
|
|
640
|
+
time = r.loop ? a.duration === 0 ? 0 : a.duration + time % a.duration : Math.max(0, time);
|
|
641
|
+
}
|
|
642
|
+
const at = this.model.sliderAsTrack ? this.time : r.loop && a.duration !== 0 ? time % a.duration : time;
|
|
643
|
+
this.sliderAt[k] = at;
|
|
644
|
+
if (this.physicsPhase !== 'none') this.physicsTimelines(a.physics, at, alpha, r.additive, false);
|
|
645
|
+
const L = this.locals, S = this.model.setup;
|
|
646
|
+
for (let q = 0; q < a.sliderBones.length; q++) {
|
|
647
|
+
const tl = a.sliderBones[q], i = keyAt(tl.curve, this.cursors, tl.cursor, at);
|
|
648
|
+
if (i < 0) continue;
|
|
649
|
+
const o = tl.bone * LOCALS;
|
|
650
|
+
for (let f = 0; f < tl.fields.length; f++) {
|
|
651
|
+
const n = o + tl.fields[f], v = channelValue(tl.curve, i, f, at), current = L[n], setup = S[n];
|
|
652
|
+
if (tl.multiply[f]) {
|
|
653
|
+
if (r.additive) L[n] = current + (v * setup - setup) * alpha;
|
|
654
|
+
else {
|
|
655
|
+
const target = setup * v, from = Math.abs(current) * Math.sign(target);
|
|
656
|
+
L[n] = alpha === 1 ? target : from + (target - from) * alpha;
|
|
657
|
+
}
|
|
658
|
+
} else L[n] = r.additive ? current + v * alpha : current + (setup + v - current) * alpha;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (let q = 0; q < c.bones.length; q++) this.changed[q] = c.bones[q];
|
|
662
|
+
if (c.bones.length > 0) this.repose(c.bones.length, 0);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private attachmentValues(anim: RuntimeAnimation | null, t: number): void {
|
|
666
|
+
this.slotValues(anim, t);
|
|
667
|
+
this.deformValues(anim, t);
|
|
668
|
+
for (let k = 0; k < this.model.constraints.length; k++) {
|
|
669
|
+
this.sliderSlots(k);
|
|
670
|
+
this.sliderDeforms(k);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private sliderSlots(index: number): void {
|
|
675
|
+
const cs = this.model.constraints;
|
|
676
|
+
{
|
|
677
|
+
const k = index, c = cs[k];
|
|
678
|
+
if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
|
|
679
|
+
const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
|
|
680
|
+
for (let q = 0; q < a.slots.length; q++) {
|
|
681
|
+
const tl = a.slots[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
682
|
+
if (i < 0) continue;
|
|
683
|
+
if (tl.attachments !== null) {
|
|
684
|
+
const next = tl.attachments[i];
|
|
685
|
+
if (next !== this.shown[tl.slot] && next >= 0) this.deformed[next] = 0;
|
|
686
|
+
this.shown[tl.slot] = next;
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
const channels = tl.channels!;
|
|
690
|
+
for (let f = 0; f < channels.length; f++) {
|
|
691
|
+
const n = tl.slot * 4 + channels[f], v = channelValue(tl.curve, i, f, t);
|
|
692
|
+
this.light[n] = clamp01(alpha === 1 ? v : this.light[n] + (v - this.light[n]) * alpha);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private sliderDeforms(index: number): void {
|
|
700
|
+
if (this.model.omitDeform) return;
|
|
701
|
+
const cs = this.model.constraints;
|
|
702
|
+
{
|
|
703
|
+
const k = index, c = cs[k];
|
|
704
|
+
if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
|
|
705
|
+
const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
|
|
706
|
+
for (let q = 0; q < a.deforms.length; q++) {
|
|
707
|
+
const tl = a.deforms[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
708
|
+
if (i < 0) continue;
|
|
709
|
+
const held = i + 1 === tl.curve.keys || tl.curve.kinds[i] === CURVE_STEPPED;
|
|
710
|
+
const p = held ? 0 : deformFraction(tl.curve, i, t), start = i * tl.count;
|
|
711
|
+
for (let slot = 0; slot < this.shown.length; slot++) {
|
|
712
|
+
const index = this.shown[slot];
|
|
713
|
+
if (index < 0 || !this.model.slots[slot].live) continue;
|
|
714
|
+
const at = this.model.attachments[index];
|
|
715
|
+
if (at.timeline !== tl.identity || at.deformCount === 0) continue;
|
|
716
|
+
const current = this.deformed[index] !== 0;
|
|
717
|
+
for (let f = 0; f < tl.count; f++) {
|
|
718
|
+
const v = tl.held[start + f], target = held ? v : v + (tl.held[start + tl.count + f] - v) * p;
|
|
719
|
+
const setup = at.draw === DRAW_MESH ? at.local[f] : 0, n = at.deformOffset + f;
|
|
720
|
+
const from = current ? this.deform[n] : setup;
|
|
721
|
+
this.deform[n] = alpha === 1 && (!c.setup.additive || !current) ? target : c.setup.additive ? from + (target - setup) * alpha : from + (target - from) * alpha;
|
|
722
|
+
}
|
|
723
|
+
this.deformed[index] = 1;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
445
726
|
}
|
|
446
727
|
}
|
|
447
728
|
|
|
@@ -868,6 +1149,20 @@ export class RuntimePose {
|
|
|
868
1149
|
const wb = W[w + W_B];
|
|
869
1150
|
const wc = W[w + W_C];
|
|
870
1151
|
const wd = W[w + W_D];
|
|
1152
|
+
if (mode === MODE_NO_SCALE || mode === MODE_NO_SCALE_OR_REFLECTION) {
|
|
1153
|
+
const det = pa * pd - pb * pc;
|
|
1154
|
+
const flip = mode === MODE_NO_SCALE && det < 0 ? -1 : 1;
|
|
1155
|
+
const sign = det < 0 ? -1 : 1;
|
|
1156
|
+
const rotation = Math.atan2((pa * wc - pc * wa) * sign, (pd * wa - pb * wc) * sign) * DEG;
|
|
1157
|
+
const rr = rotation * RAD;
|
|
1158
|
+
const cos = Math.cos(rr), sin = Math.sin(rr);
|
|
1159
|
+
let ux = pa * cos + pb * sin, uy = pc * cos + pd * sin;
|
|
1160
|
+
const length = 1 / Math.sqrt(ux * ux + uy * uy);
|
|
1161
|
+
ux *= length; uy *= length;
|
|
1162
|
+
this.decompose(l, ux * wa + uy * wc, (ux * wc - uy * wa) * flip, ux * wb + uy * wd, (ux * wd - uy * wb) * flip, true);
|
|
1163
|
+
L[l + L_ROTATION] = rotation;
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
871
1166
|
if (mode === MODE_ONLY_TRANSLATION) {
|
|
872
1167
|
this.decompose(l, wa, wc, wb, wd);
|
|
873
1168
|
return;
|
|
@@ -887,7 +1182,7 @@ export class RuntimePose {
|
|
|
887
1182
|
}
|
|
888
1183
|
|
|
889
1184
|
/** A local matrix with columns `(la, lc)` and `(lb, ld)` as rotation, scales and shear (`decompose`). */
|
|
890
|
-
private decompose(l: number, la: number, lc: number, lb: number, ld: number): void {
|
|
1185
|
+
private decompose(l: number, la: number, lc: number, lb: number, ld: number, shearX = false): void {
|
|
891
1186
|
const L = this.locals;
|
|
892
1187
|
const sx = Math.sqrt(la * la + lc * lc);
|
|
893
1188
|
const yLength = Math.sqrt(lb * lb + ld * ld);
|
|
@@ -901,11 +1196,11 @@ export class RuntimePose {
|
|
|
901
1196
|
const xAngle = Math.atan2(lc, la) * DEG;
|
|
902
1197
|
const det = la * ld - lb * lc;
|
|
903
1198
|
const yAngle = Math.atan2(ld, lb) * DEG;
|
|
904
|
-
L[l + L_ROTATION] = xAngle;
|
|
1199
|
+
L[l + (shearX ? L_SHEAR_X : L_ROTATION)] = xAngle;
|
|
905
1200
|
L[l + L_SCALE_X] = sx;
|
|
906
1201
|
L[l + L_SCALE_Y] = det < 0 ? -yLength : yLength;
|
|
907
1202
|
const right = det < 0 ? -90 : 90;
|
|
908
|
-
let shear = yAngle - (xAngle + right);
|
|
1203
|
+
let shear = yAngle - ((shearX ? 0 : xAngle) + right);
|
|
909
1204
|
shear = shear > 180 ? shear - 360 : shear < -180 ? shear + 360 : shear;
|
|
910
1205
|
L[l + L_SHEAR_Y] = yLength > 0.00001 ? shear : 0;
|
|
911
1206
|
}
|