@rig-c/core 2.75.0 → 2.77.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 +47 -10
- package/package.json +1 -1
- package/src/runtime/curves.ts +3 -1
- package/src/runtime/index.ts +3 -2
- package/src/runtime/model.ts +255 -29
- package/src/runtime/physics.ts +235 -0
- package/src/runtime/pose.ts +861 -24
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 4): 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.
|
|
@@ -9,21 +9,23 @@
|
|
|
9
9
|
* The oracle's sample, as `src/core/animation.ts` poses it (`poseAnimations`):
|
|
10
10
|
* every pose is posed from the setup pose — the bones' setup local values,
|
|
11
11
|
* then the animation's bone timelines at `t` at full weight, then the world
|
|
12
|
-
* transforms parents first, then
|
|
12
|
+
* transforms parents first, then each slot's colour, attachment and own deform
|
|
13
|
+
* timelines, then the ik, transform, path, physics and slider constraints in the
|
|
13
14
|
* document's order with their timelines' values at `t`, each followed by the
|
|
14
|
-
* bones below what it moved posed again
|
|
15
|
-
*
|
|
16
|
-
* of one pose survives
|
|
17
|
-
*
|
|
15
|
+
* bones below what it moved posed again, then the sliders' slot and deform
|
|
16
|
+
* timelines; then, when asked, every shown attachment's world vertices. Nothing
|
|
17
|
+
* of one unstepped pose survives but key cursors. A stepped pose also keeps
|
|
18
|
+
* its physics clock, offsets, velocities and lags; sliders select values
|
|
19
|
+
* within this pose rather than advancing an independent track.
|
|
18
20
|
*
|
|
19
21
|
* ## The arithmetic is the oracle's
|
|
20
22
|
*
|
|
21
23
|
* Every formula below is the one the named function of `src/core/` computes,
|
|
22
24
|
* in its order of operations, on the same doubles: a bone's frame and its
|
|
23
25
|
* five inherit modes (`world.ts`), the timelines (`animation.ts`), the one-
|
|
24
|
-
* and two-bone ik, the world-space transform, the
|
|
25
|
-
* local values and the update order (`constraints.ts`),
|
|
26
|
-
* and a mesh's vertices (`vertices.ts`). Each of those orders was fixed
|
|
26
|
+
* and two-bone ik, the world-space transform, the path constraint (`constraints_path.ts`),
|
|
27
|
+
* the read-back of a moved bone's local values and the update order (`constraints.ts`),
|
|
28
|
+
* a region's corners and a mesh's vertices (`vertices.ts`). Each of those orders was fixed
|
|
27
29
|
* there by measurement against spine-core 4.3.13 to the last bit; the runtime
|
|
28
30
|
* does not re-measure them, it is held to the oracle at tolerance 0
|
|
29
31
|
* (`tools/core_gate.ts`, *The runtime poser*), so an order changed here is a
|
|
@@ -31,12 +33,14 @@
|
|
|
31
33
|
*
|
|
32
34
|
* ⚠️ The exactness hazards, each kept as the oracle has it: the setup value
|
|
33
35
|
* plus 0 before a timeline's first key (which turns a `-0` setup into `0`);
|
|
34
|
-
* a keyed ik or
|
|
36
|
+
* a keyed ik, transform or path value reaching the pose through the setup blend
|
|
35
37
|
* `setup + (value − setup)·1`; `Math.PI` for a folded uniform two-bone ik and
|
|
36
38
|
* the runtime's 3.1415927 everywhere else; the `(−180, 180]` wrap; a mix of
|
|
37
39
|
* exactly 0 touching nothing; an inactive bone posed as zeros by the
|
|
38
40
|
* hierarchy and never posed again by a constraint, nor the bones below it.
|
|
39
41
|
*/
|
|
42
|
+
import type { CorePhysicsRecord } from '../core/constraints_physics.ts';
|
|
43
|
+
import { freshPhysicsState, resetPhysicsState, stepRuntimePhysics, type PhysicsState } from './physics.ts';
|
|
40
44
|
import { COLLAPSED_X_AXIS_SQ, RUNTIME_DEG, RUNTIME_PI } from '../core/world.ts';
|
|
41
45
|
import { channelValue, keyAt, deformFraction, CURVE_STEPPED } from './curves.ts';
|
|
42
46
|
import {
|
|
@@ -59,6 +63,13 @@ import {
|
|
|
59
63
|
MODE_NO_SCALE_OR_REFLECTION,
|
|
60
64
|
MODE_NORMAL,
|
|
61
65
|
MODE_ONLY_TRANSLATION,
|
|
66
|
+
PATH_POSITION_PERCENT,
|
|
67
|
+
PATH_ROTATE_CHAIN,
|
|
68
|
+
PATH_ROTATE_CHAIN_SCALE,
|
|
69
|
+
PATH_ROTATE_TANGENT,
|
|
70
|
+
PATH_SPACING_LENGTH,
|
|
71
|
+
PATH_SPACING_PERCENT,
|
|
72
|
+
PATH_SPACING_PROPORTIONAL,
|
|
62
73
|
RAD,
|
|
63
74
|
RuntimeInputError,
|
|
64
75
|
SCALE_Y_UNIFORM,
|
|
@@ -71,14 +82,19 @@ import {
|
|
|
71
82
|
W_Y,
|
|
72
83
|
WORLDS,
|
|
73
84
|
type RuntimeAnimation,
|
|
85
|
+
type RuntimeConstraint,
|
|
74
86
|
type RuntimeEvent,
|
|
75
87
|
type RuntimeIk,
|
|
76
88
|
type RuntimeModel,
|
|
89
|
+
type RuntimePath,
|
|
77
90
|
type RuntimeTransform,
|
|
91
|
+
type RuntimeSlider,
|
|
92
|
+
type RuntimePhysicsTimeline,
|
|
78
93
|
} from './model.ts';
|
|
79
94
|
|
|
80
95
|
const DEG = RUNTIME_DEG;
|
|
81
96
|
const TWO_PI = 2 * RUNTIME_PI;
|
|
97
|
+
const PATH_EPSILON = 0.00001;
|
|
82
98
|
// The transform properties, by `TRANSFORM_PROPERTIES` index (src/core/constraints.ts).
|
|
83
99
|
const P_ROTATE = 0;
|
|
84
100
|
const P_X = 1;
|
|
@@ -98,6 +114,10 @@ function volumeScaleY(scaleY: number, s: number): number {
|
|
|
98
114
|
|
|
99
115
|
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
100
116
|
|
|
117
|
+
/** A caller-supplied physics clock; animation/track time still uses dt itself. */
|
|
118
|
+
export interface RuntimePoseOptions { readonly clock?: (dt: number) => number }
|
|
119
|
+
export type RuntimePhysicsPhase = 'none' | 'reset' | 'update';
|
|
120
|
+
|
|
101
121
|
/** One skeleton's pose, every value in place. Read `world`, `light`, `shown` and `vertices` after a pose. */
|
|
102
122
|
export class RuntimePose {
|
|
103
123
|
readonly model: RuntimeModel;
|
|
@@ -125,11 +145,20 @@ export class RuntimePose {
|
|
|
125
145
|
|
|
126
146
|
private current: RuntimeAnimation | null = null;
|
|
127
147
|
private readonly cursors: Int32Array;
|
|
128
|
-
// The constraints' values as posed: an ik's mix, softness and flags, a transform's six mixes.
|
|
148
|
+
// The constraints' values as posed: an ik's mix, softness and flags, a transform's six mixes, a path's position, spacing and three mixes.
|
|
129
149
|
private readonly ikMix: Float64Array;
|
|
130
150
|
private readonly ikSoftness: Float64Array;
|
|
131
151
|
private readonly ikFlags: Uint8Array;
|
|
132
152
|
private readonly mixes: Float64Array;
|
|
153
|
+
private readonly pathValues: Float64Array;
|
|
154
|
+
private readonly pathSlotSign: Int8Array;
|
|
155
|
+
private readonly pathSpaces: Float64Array;
|
|
156
|
+
private readonly pathLengths: Float64Array;
|
|
157
|
+
private readonly pathPositions: Float64Array;
|
|
158
|
+
private readonly pathWorldPts: Float64Array;
|
|
159
|
+
private readonly pathChain: Float64Array;
|
|
160
|
+
private readonly pathCurveEnds: Float64Array;
|
|
161
|
+
private readonly pathSegmentEnds = new Float64Array(10);
|
|
133
162
|
// The update order's scratch: the bones a constraint changed and moved in world space, in order (repeats kept, as the oracle keeps them), and per-bone marks.
|
|
134
163
|
private readonly changed: Int32Array;
|
|
135
164
|
private readonly inWorld: Int32Array;
|
|
@@ -139,11 +168,34 @@ export class RuntimePose {
|
|
|
139
168
|
private readonly visit: Uint8Array;
|
|
140
169
|
private readonly w = new Float64Array(WORLDS);
|
|
141
170
|
|
|
142
|
-
|
|
171
|
+
/** Sum of caller-supplied physics deltas, independent of animation time. */
|
|
172
|
+
physicsTime = 0;
|
|
173
|
+
private physicsLast = -1;
|
|
174
|
+
private physicsPhase: RuntimePhysicsPhase = 'none';
|
|
175
|
+
private physicsReady = false;
|
|
176
|
+
private readonly hasPhysics: boolean;
|
|
177
|
+
private readonly physicsStates: PhysicsState[];
|
|
178
|
+
private readonly physicsRecords: Array<CorePhysicsRecord | null>;
|
|
179
|
+
private readonly physicsScope: Uint8Array;
|
|
180
|
+
private readonly environment = new Float64Array([1, 0, 0, 1]);
|
|
181
|
+
private readonly sliderTime: Float64Array;
|
|
182
|
+
private readonly sliderMix: Float64Array;
|
|
183
|
+
private readonly sliderAt: Float64Array;
|
|
184
|
+
private readonly clock: ((dt: number) => number) | undefined;
|
|
185
|
+
|
|
186
|
+
constructor(model: RuntimeModel, options: RuntimePoseOptions = {}) {
|
|
143
187
|
this.model = model;
|
|
188
|
+
this.clock = options.clock;
|
|
189
|
+
this.hasPhysics = model.constraints.some(c => c.kind === 'physics');
|
|
190
|
+
this.physicsStates = model.constraints.map(() => freshPhysicsState());
|
|
191
|
+
this.physicsRecords = model.constraints.map(c => c.kind === 'physics' ? { ...c.setup } : null);
|
|
192
|
+
this.sliderTime = new Float64Array(model.constraints.length);
|
|
193
|
+
this.sliderMix = new Float64Array(model.constraints.length);
|
|
194
|
+
this.sliderAt = new Float64Array(model.constraints.length);
|
|
144
195
|
const nb = model.boneNames.length;
|
|
145
196
|
const ns = model.slots.length;
|
|
146
197
|
const nc = model.constraints.length;
|
|
198
|
+
this.physicsScope = new Uint8Array(nc);
|
|
147
199
|
this.locals = new Float64Array(nb * LOCALS);
|
|
148
200
|
this.world = new Float64Array(nb * WORLDS);
|
|
149
201
|
this.light = new Float64Array(ns * 4);
|
|
@@ -156,6 +208,16 @@ export class RuntimePose {
|
|
|
156
208
|
this.ikSoftness = new Float64Array(nc);
|
|
157
209
|
this.ikFlags = new Uint8Array(nc);
|
|
158
210
|
this.mixes = new Float64Array(nc * 6);
|
|
211
|
+
this.pathValues = new Float64Array(nc * 5);
|
|
212
|
+
this.pathSlotSign = new Int8Array(nc);
|
|
213
|
+
const ms = model.maxPathSpaces;
|
|
214
|
+
const mv = model.maxPathVertices;
|
|
215
|
+
this.pathSpaces = new Float64Array(ms);
|
|
216
|
+
this.pathLengths = new Float64Array(ms);
|
|
217
|
+
this.pathPositions = new Float64Array(ms > 0 ? ms * 3 + 2 : 0);
|
|
218
|
+
this.pathWorldPts = new Float64Array(mv);
|
|
219
|
+
this.pathChain = new Float64Array(mv > 0 ? mv + 4 : 0);
|
|
220
|
+
this.pathCurveEnds = new Float64Array(mv > 0 ? Math.ceil(mv / 6) + 1 : 0);
|
|
159
221
|
// Sized by the longest target list (`RuntimeModel.scratch`): a transform may name one bone several times, and every entry is applied and reposed.
|
|
160
222
|
this.changed = new Int32Array(model.scratch);
|
|
161
223
|
this.inWorld = new Int32Array(model.scratch);
|
|
@@ -168,6 +230,10 @@ export class RuntimePose {
|
|
|
168
230
|
/** The animation this pose plays (a covered one: `model.animation` throws a `RuntimeRefusal` for a refused one), from track time 0. */
|
|
169
231
|
setAnimation(name: string): void {
|
|
170
232
|
this.current = this.model.animation(name);
|
|
233
|
+
this.physicsTime = 0;
|
|
234
|
+
this.physicsLast = -1;
|
|
235
|
+
this.physicsReady = false;
|
|
236
|
+
for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
|
|
171
237
|
this.cursors.fill(-1);
|
|
172
238
|
this.trackTime = 0;
|
|
173
239
|
this.time = 0;
|
|
@@ -190,40 +256,61 @@ export class RuntimePose {
|
|
|
190
256
|
}
|
|
191
257
|
|
|
192
258
|
/** The setup pose: every constraint applied with its own values, every slot as it is set up. */
|
|
193
|
-
setupPose(): void {
|
|
259
|
+
setupPose(physics: RuntimePhysicsPhase = 'none'): void {
|
|
260
|
+
this.physicsPhase = physics;
|
|
194
261
|
this.current = null;
|
|
195
262
|
this.time = 0;
|
|
196
263
|
this.eventTime = -1;
|
|
197
264
|
this.fired.length = 0;
|
|
265
|
+
this.physicsValues(null, 0, false);
|
|
198
266
|
this.locals.set(this.model.setup);
|
|
199
267
|
this.hierarchy();
|
|
200
|
-
this.constraintValues(null, 0);
|
|
201
|
-
this.solveConstraints();
|
|
202
268
|
this.slotValues(null, 0);
|
|
203
269
|
this.deformValues(null, 0);
|
|
270
|
+
this.constraintValues(null, 0);
|
|
271
|
+
this.solveConstraints();
|
|
272
|
+
this.sliderAttachments();
|
|
204
273
|
}
|
|
205
274
|
|
|
206
275
|
/** The animation posed at its time `t`, from the setup pose (the header). */
|
|
207
|
-
apply(t: number): void {
|
|
276
|
+
apply(t: number, physics: RuntimePhysicsPhase = 'none', delta = 0): void {
|
|
208
277
|
const anim = this.current;
|
|
209
278
|
if (anim === null) throw new RuntimeInputError('apply: no animation is set (setAnimation first)');
|
|
210
279
|
if (!Number.isFinite(t)) throw new RuntimeInputError(`apply: the time ${t} is not finite`);
|
|
280
|
+
if (!Number.isFinite(delta) || delta < 0) throw new RuntimeInputError(`apply: physics delta ${delta} is not finite and at or above 0`);
|
|
281
|
+
this.physicsPhase = physics;
|
|
282
|
+
// Reset keys see the old clock. The skeleton moves by delta only after application.
|
|
283
|
+
this.physicsValues(anim, t, physics !== 'none');
|
|
284
|
+
if (physics !== 'none') {
|
|
285
|
+
this.physicsTime += delta;
|
|
286
|
+
this.physicsReady = true;
|
|
287
|
+
}
|
|
211
288
|
this.collectEvents(anim, t);
|
|
212
289
|
this.time = t;
|
|
213
290
|
this.boneTimelines(anim, t);
|
|
214
291
|
this.hierarchy();
|
|
215
|
-
this.constraintValues(anim, t);
|
|
216
|
-
this.solveConstraints();
|
|
217
292
|
this.slotValues(anim, t);
|
|
218
293
|
this.deformValues(anim, t);
|
|
294
|
+
this.constraintValues(anim, t);
|
|
295
|
+
this.solveConstraints();
|
|
296
|
+
this.sliderAttachments();
|
|
219
297
|
}
|
|
220
298
|
|
|
221
299
|
/** 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
300
|
step(dt: number): void {
|
|
223
301
|
const anim = this.current;
|
|
224
302
|
if (anim === null) throw new RuntimeInputError('step: no animation is set (setAnimation first)');
|
|
303
|
+
if (!Number.isFinite(dt) || dt < 0) throw new RuntimeInputError(`step: delta ${dt} is not finite and at or above 0`);
|
|
304
|
+
if (!this.physicsReady && this.hasPhysics) {
|
|
305
|
+
// Initial physics setup must not consume the event interval of this step.
|
|
306
|
+
const lastEventTime = this.eventTime;
|
|
307
|
+
this.apply(this.time, 'reset', 0);
|
|
308
|
+
this.eventTime = lastEventTime;
|
|
309
|
+
}
|
|
310
|
+
const moved = this.clock === undefined ? dt : this.clock(dt);
|
|
311
|
+
if (!Number.isFinite(moved) || moved < 0) throw new RuntimeInputError(`step: physics clock moved by ${moved}, not finite and at or above 0`);
|
|
225
312
|
this.trackTime += dt;
|
|
226
|
-
this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration);
|
|
313
|
+
this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration, 'update', moved);
|
|
227
314
|
}
|
|
228
315
|
|
|
229
316
|
/** The oracle interval (last,t], tail then head on a wrap; never copies a payload. */
|
|
@@ -427,9 +514,55 @@ export class RuntimePose {
|
|
|
427
514
|
this.ikSoftness[k] = c.softness + (channelValue(tl.curve, i, 1, t) - c.softness) * 1;
|
|
428
515
|
this.ikFlags[k] = (tl.flags as Uint8Array)[i];
|
|
429
516
|
}
|
|
430
|
-
} else {
|
|
517
|
+
} else if (c.kind === 'transform') {
|
|
431
518
|
const o = k * 6;
|
|
432
519
|
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;
|
|
520
|
+
} else if (c.kind === 'slider') {
|
|
521
|
+
const keyed = anim === null ? null : anim.sliders[k];
|
|
522
|
+
let time = c.setup.time, mix = c.setup.mix;
|
|
523
|
+
if (keyed !== null) {
|
|
524
|
+
if (keyed.time !== null) {
|
|
525
|
+
const q = keyAt(keyed.time, this.cursors, keyed.timeCursor, t);
|
|
526
|
+
if (q >= 0) time += (channelValue(keyed.time, q, 0, t) - time) * 1;
|
|
527
|
+
}
|
|
528
|
+
if (keyed.mix !== null) {
|
|
529
|
+
const q = keyAt(keyed.mix, this.cursors, keyed.mixCursor, t);
|
|
530
|
+
if (q >= 0) mix += (channelValue(keyed.mix, q, 0, t) - mix) * 1;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
this.sliderTime[k] = time;
|
|
534
|
+
this.sliderMix[k] = mix;
|
|
535
|
+
} else if (c.kind === 'path') {
|
|
536
|
+
const keyed = anim === null ? null : anim.paths[k];
|
|
537
|
+
let position = c.position;
|
|
538
|
+
let spacing = c.spacing;
|
|
539
|
+
let mixRotate = c.mixRotate;
|
|
540
|
+
let mixX = c.mixX;
|
|
541
|
+
let mixY = c.mixY;
|
|
542
|
+
if (keyed !== null) {
|
|
543
|
+
if (keyed.position !== null) {
|
|
544
|
+
const q = keyAt(keyed.position, this.cursors, keyed.positionCursor, t);
|
|
545
|
+
if (q >= 0) position = c.position + (channelValue(keyed.position, q, 0, t) - c.position) * 1;
|
|
546
|
+
}
|
|
547
|
+
if (keyed.spacing !== null) {
|
|
548
|
+
const q = keyAt(keyed.spacing, this.cursors, keyed.spacingCursor, t);
|
|
549
|
+
if (q >= 0) spacing = c.spacing + (channelValue(keyed.spacing, q, 0, t) - c.spacing) * 1;
|
|
550
|
+
}
|
|
551
|
+
if (keyed.mix !== null) {
|
|
552
|
+
const q = keyAt(keyed.mix, this.cursors, keyed.mixCursor, t);
|
|
553
|
+
if (q >= 0) {
|
|
554
|
+
mixRotate = c.mixRotate + (channelValue(keyed.mix, q, 0, t) - c.mixRotate) * 1;
|
|
555
|
+
mixX = c.mixX + (channelValue(keyed.mix, q, 1, t) - c.mixX) * 1;
|
|
556
|
+
mixY = c.mixY + (channelValue(keyed.mix, q, 2, t) - c.mixY) * 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const o = k * 5;
|
|
561
|
+
this.pathValues[o] = position;
|
|
562
|
+
this.pathValues[o + 1] = spacing;
|
|
563
|
+
this.pathValues[o + 2] = mixRotate;
|
|
564
|
+
this.pathValues[o + 3] = mixX;
|
|
565
|
+
this.pathValues[o + 4] = mixY;
|
|
433
566
|
}
|
|
434
567
|
}
|
|
435
568
|
}
|
|
@@ -437,11 +570,661 @@ export class RuntimePose {
|
|
|
437
570
|
/** Every applied constraint in the document's order, each followed by the update order's repose (`applyConstraints`). */
|
|
438
571
|
private solveConstraints(): void {
|
|
439
572
|
const cs = this.model.constraints;
|
|
573
|
+
const hasPath = this.model.hasPath;
|
|
440
574
|
for (let k = 0; k < cs.length; k++) {
|
|
575
|
+
if (hasPath) this.snapSlotBones(k, false);
|
|
441
576
|
const c = cs[k];
|
|
442
577
|
if (!c.applied) continue;
|
|
443
578
|
if (c.kind === 'ik') this.solveIk(c, k);
|
|
444
|
-
else this.solveTransform(c, k);
|
|
579
|
+
else if (c.kind === 'transform') this.solveTransform(c, k);
|
|
580
|
+
else if (c.kind === 'slider') this.solveSlider(c, k);
|
|
581
|
+
else if (c.kind === 'path') this.solvePath(c, k);
|
|
582
|
+
else if (this.physicsPhase !== 'none' && !this.model.omitPhysics) {
|
|
583
|
+
const b = c.bones[0];
|
|
584
|
+
if (stepRuntimePhysics(this.physicsRecords[k]!, this.physicsStates[k], this.world, b * WORLDS, this.model.lengths[b], this.physicsTime, this.physicsPhase === 'reset', this.environment)) {
|
|
585
|
+
this.changed[0] = this.inWorld[0] = b;
|
|
586
|
+
this.repose(1, 1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (hasPath) this.snapSlotBones(k, true);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private snapSlotBones(at: number, after: boolean): void {
|
|
594
|
+
const cs = this.model.constraints;
|
|
595
|
+
const W = this.world;
|
|
596
|
+
for (let k = 0; k < cs.length; k++) {
|
|
597
|
+
const c = cs[k];
|
|
598
|
+
if (c.kind !== 'path' || c.slotEventAt !== at || c.slotEventAfter !== after || (k === at && !after)) continue;
|
|
599
|
+
const sb = c.slotBone * WORLDS;
|
|
600
|
+
this.pathSlotSign[k] = W[sb + W_A] * W[sb + W_D] - W[sb + W_B] * W[sb + W_C] > 0 ? 1 : -1;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Reset where the consumer starts: the setup pose or animation pose zero. */
|
|
605
|
+
resetPhysics(at: 'setup' | 'animation' = 'animation'): void {
|
|
606
|
+
this.physicsTime = 0;
|
|
607
|
+
this.physicsLast = -1;
|
|
608
|
+
for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
|
|
609
|
+
if (at === 'animation') this.apply(0, 'reset', 0);
|
|
610
|
+
else {
|
|
611
|
+
const current = this.current;
|
|
612
|
+
this.setupPose('reset');
|
|
613
|
+
this.current = current;
|
|
614
|
+
}
|
|
615
|
+
this.physicsReady = true;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private checkPhysicsScope(scope: readonly string[] | undefined): Uint8Array | null {
|
|
619
|
+
if (scope === undefined) return null;
|
|
620
|
+
if (!Array.isArray(scope)) throw new RuntimeInputError(`physics movement scope is ${String(scope)}, not an array of physics constraint names`);
|
|
621
|
+
const selected = this.physicsScope;
|
|
622
|
+
selected.fill(0);
|
|
623
|
+
const cs = this.model.constraints;
|
|
624
|
+
for (let i = 0; i < scope.length; i++) {
|
|
625
|
+
const name = scope[i];
|
|
626
|
+
if (typeof name !== 'string') throw new RuntimeInputError(`physics movement scope name ${String(name)} is not a string`);
|
|
627
|
+
let found: RuntimeConstraint | undefined;
|
|
628
|
+
for (let k = 0; k < cs.length; k++) {
|
|
629
|
+
if (cs[k].kind === 'physics' && cs[k].name === name) {
|
|
630
|
+
found = cs[k];
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (found === undefined) {
|
|
635
|
+
for (let k = 0; k < cs.length; k++) {
|
|
636
|
+
if (cs[k].name === name) {
|
|
637
|
+
found = cs[k];
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (found === undefined) throw new RuntimeInputError(`physics movement scope: no constraint "${name}"; this document declares [${cs.map((c) => c.name).join(', ')}]`);
|
|
643
|
+
if (found.kind !== 'physics') throw new RuntimeInputError(`physics movement scope: constraint "${name}" is ${found.kind}, not physics`);
|
|
644
|
+
selected[found.index] = 1;
|
|
645
|
+
}
|
|
646
|
+
return this.model.ignorePhysicsScope ? null : selected;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** External motion changes the previous origin, never the authored locals. */
|
|
650
|
+
physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
|
|
651
|
+
const selected = this.checkPhysicsScope(scope);
|
|
652
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new RuntimeInputError('physicsTranslate: x and y must be finite');
|
|
653
|
+
for (let k = 0; k < this.physicsStates.length; k++) {
|
|
654
|
+
if (selected !== null && selected[k] === 0) continue;
|
|
655
|
+
const s = this.physicsStates[k];
|
|
656
|
+
s.ux -= x; s.uy -= y; s.cx -= x; s.cy -= y;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
|
|
661
|
+
const selected = this.checkPhysicsScope(scope);
|
|
662
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(degrees)) throw new RuntimeInputError('physicsRotate: x, y and degrees must be finite');
|
|
663
|
+
const r = degrees * RAD, cos = Math.cos(r), sin = Math.sin(r);
|
|
664
|
+
for (let k = 0; k < this.physicsStates.length; k++) {
|
|
665
|
+
if (selected !== null && selected[k] === 0) continue;
|
|
666
|
+
const s = this.physicsStates[k];
|
|
667
|
+
const dx = s.cx - x, dy = s.cy - y;
|
|
668
|
+
const tx = dx * cos - dy * sin - dx, ty = dx * sin + dy * cos - dy;
|
|
669
|
+
s.ux -= tx; s.uy -= ty; s.cx -= tx; s.cy -= ty;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
setEnvironment(env: { readonly wind: readonly [number, number]; readonly gravity: readonly [number, number] }): void {
|
|
674
|
+
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');
|
|
675
|
+
this.environment[0] = env.wind[0]; this.environment[1] = env.wind[1];
|
|
676
|
+
this.environment[2] = env.gravity[0]; this.environment[3] = env.gravity[1];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** The per-pass values start at setup, before any slider writes. */
|
|
680
|
+
private physicsValues(anim: RuntimeAnimation | null, t: number, resets: boolean): void {
|
|
681
|
+
const cs = this.model.constraints;
|
|
682
|
+
for (let k = 0; k < cs.length; k++) {
|
|
683
|
+
const c = cs[k];
|
|
684
|
+
if (c.kind !== 'physics') continue;
|
|
685
|
+
const r = this.physicsRecords[k]!;
|
|
686
|
+
r.inertia = c.setup.inertia; r.strength = c.setup.strength; r.damping = c.setup.damping;
|
|
687
|
+
r.massInverse = c.setup.massInverse; r.wind = c.setup.wind; r.gravity = c.setup.gravity; r.mix = c.setup.mix;
|
|
688
|
+
}
|
|
689
|
+
if (anim !== null) this.physicsTimelines(anim.physics, t, 1, false, true, resets);
|
|
690
|
+
if (resets) this.physicsLast = t;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private physicsTimelines(timelines: readonly RuntimePhysicsTimeline[], t: number, alpha: number, additive: boolean, setup: boolean, resets = false): void {
|
|
694
|
+
for (let q = 0; q < timelines.length; q++) {
|
|
695
|
+
const tl = timelines[q];
|
|
696
|
+
if (tl.kind === 'reset') {
|
|
697
|
+
if (!resets) continue; // a slider applies at one value; no key is crossed
|
|
698
|
+
let crossed = false;
|
|
699
|
+
const last = this.physicsLast;
|
|
700
|
+
for (let i = 0; i < tl.curve.keys; i++) {
|
|
701
|
+
const time = tl.curve.times[i];
|
|
702
|
+
if (t < last ? time > last || time <= t : time > last && time <= t) crossed = true;
|
|
703
|
+
}
|
|
704
|
+
if (crossed) for (let j = 0; j < tl.targets.length; j++) resetPhysicsState(this.physicsStates[tl.targets[j]], this.physicsTime);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
708
|
+
if (i < 0) {
|
|
709
|
+
// A track's before-first key restores setup even if an earlier global
|
|
710
|
+
// timeline wrote this parameter. A slider's before-first key writes nothing.
|
|
711
|
+
if (setup) for (let j = 0; j < tl.targets.length; j++) {
|
|
712
|
+
const k = tl.targets[j], r = this.physicsRecords[k]!;
|
|
713
|
+
const original = (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup;
|
|
714
|
+
if (tl.kind === 'mass') r.massInverse = original.massInverse;
|
|
715
|
+
else r[tl.kind] = original[tl.kind];
|
|
716
|
+
}
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const v = channelValue(tl.curve, i, 0, t);
|
|
720
|
+
for (let j = 0; j < tl.targets.length; j++) {
|
|
721
|
+
const k = tl.targets[j], r = this.physicsRecords[k]!;
|
|
722
|
+
const from = setup ? (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup : r;
|
|
723
|
+
if (tl.kind === 'mass') {
|
|
724
|
+
const mass = 1 / from.massInverse;
|
|
725
|
+
r.massInverse = 1 / (mass + (v - mass) * alpha);
|
|
726
|
+
} else {
|
|
727
|
+
r[tl.kind] = additive && (tl.kind === 'wind' || tl.kind === 'gravity') ? r[tl.kind] + v * alpha : from[tl.kind] + (v - from[tl.kind]) * alpha;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Read the dial where previous constraints left it; this value is never track time. */
|
|
734
|
+
private solveSlider(c: RuntimeSlider, k: number): void {
|
|
735
|
+
const alpha = this.sliderMix[k];
|
|
736
|
+
if (alpha === 0) return;
|
|
737
|
+
let time = this.sliderTime[k];
|
|
738
|
+
const r = c.setup, a = c.animation;
|
|
739
|
+
if (c.bone >= 0) {
|
|
740
|
+
const b = c.bone, w = b * WORLDS, l = b * LOCALS;
|
|
741
|
+
let value: number;
|
|
742
|
+
if (r.local) {
|
|
743
|
+
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;
|
|
744
|
+
value = this.locals[l + field] + 0;
|
|
745
|
+
} else {
|
|
746
|
+
const W = this.world, x = W[w], y = W[w + 2], bx = W[w + 1], by = W[w + 3];
|
|
747
|
+
switch (c.property) {
|
|
748
|
+
case 0: value = Math.atan2(y, x) * DEG + (x * by - bx * y > 0 ? 0 : -0); if (value < 0) value += 360; break;
|
|
749
|
+
case 1: value = x * 0 + bx * 0 + W[w + W_X]; break;
|
|
750
|
+
case 2: value = y * 0 + by * 0 + W[w + W_Y]; break;
|
|
751
|
+
case 3: value = Math.sqrt(x * x + y * y) + 0; break;
|
|
752
|
+
case 4: value = Math.sqrt(bx * bx + by * by) + 0; break;
|
|
753
|
+
default: value = (Math.atan2(by, bx) - Math.atan2(y, x)) * DEG - 90 + 0;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
time = r.to + (value - r.from) * r.scale;
|
|
757
|
+
time = r.loop ? a.duration === 0 ? 0 : a.duration + time % a.duration : Math.max(0, time);
|
|
758
|
+
}
|
|
759
|
+
const at = this.model.sliderAsTrack ? this.time : r.loop && a.duration !== 0 ? time % a.duration : time;
|
|
760
|
+
this.sliderAt[k] = at;
|
|
761
|
+
if (this.physicsPhase !== 'none') this.physicsTimelines(a.physics, at, alpha, r.additive, false);
|
|
762
|
+
const L = this.locals, S = this.model.setup;
|
|
763
|
+
for (let q = 0; q < a.sliderBones.length; q++) {
|
|
764
|
+
const tl = a.sliderBones[q], i = keyAt(tl.curve, this.cursors, tl.cursor, at);
|
|
765
|
+
if (i < 0) continue;
|
|
766
|
+
const o = tl.bone * LOCALS;
|
|
767
|
+
for (let f = 0; f < tl.fields.length; f++) {
|
|
768
|
+
const n = o + tl.fields[f], v = channelValue(tl.curve, i, f, at), current = L[n], setup = S[n];
|
|
769
|
+
if (tl.multiply[f]) {
|
|
770
|
+
if (r.additive) L[n] = current + (v * setup - setup) * alpha;
|
|
771
|
+
else {
|
|
772
|
+
const target = setup * v, from = Math.abs(current) * Math.sign(target);
|
|
773
|
+
L[n] = alpha === 1 ? target : from + (target - from) * alpha;
|
|
774
|
+
}
|
|
775
|
+
} else L[n] = r.additive ? current + v * alpha : current + (setup + v - current) * alpha;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
for (let q = 0; q < c.bones.length; q++) this.changed[q] = c.bones[q];
|
|
779
|
+
if (c.bones.length > 0) this.repose(c.bones.length, 0);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** A path constraint (`solvePath` in `src/core/constraints_path.ts`), every buffer overwritten in place. */
|
|
783
|
+
private solvePath(c: RuntimePath, k: number): void {
|
|
784
|
+
if (c.attachment < 0 || this.model.omitPath) return;
|
|
785
|
+
const o5 = k * 5;
|
|
786
|
+
const position = this.pathValues[o5];
|
|
787
|
+
const spacing = this.pathValues[o5 + 1];
|
|
788
|
+
const mixRotate = this.pathValues[o5 + 2];
|
|
789
|
+
const mixX = this.pathValues[o5 + 3];
|
|
790
|
+
const mixY = this.pathValues[o5 + 4];
|
|
791
|
+
if (mixRotate === 0 && mixX === 0 && mixY === 0) return;
|
|
792
|
+
const wantsTangent = c.rotateMode === PATH_ROTATE_TANGENT;
|
|
793
|
+
const scale = c.rotateMode === PATH_ROTATE_CHAIN_SCALE;
|
|
794
|
+
const bones = c.bones;
|
|
795
|
+
const boneCount = bones.length;
|
|
796
|
+
const spaceCount = wantsTangent ? boneCount : boneCount + 1;
|
|
797
|
+
const spaces = this.pathSpaces;
|
|
798
|
+
const lengths = this.pathLengths;
|
|
799
|
+
const W = this.world;
|
|
800
|
+
const modelLengths = this.model.lengths;
|
|
801
|
+
spaces[0] = 0;
|
|
802
|
+
if (c.spacingMode === PATH_SPACING_PERCENT) {
|
|
803
|
+
if (scale) {
|
|
804
|
+
for (let i = 0; i < spaceCount - 1; i++) {
|
|
805
|
+
const bi = bones[i];
|
|
806
|
+
const boneLength = modelLengths[bi];
|
|
807
|
+
const w = bi * WORLDS;
|
|
808
|
+
const x = boneLength * W[w + W_A];
|
|
809
|
+
const y = boneLength * W[w + W_C];
|
|
810
|
+
lengths[i] = Math.sqrt(x * x + y * y);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (let i = 1; i < spaceCount; i++) spaces[i] = spacing;
|
|
814
|
+
} else if (c.spacingMode === PATH_SPACING_PROPORTIONAL) {
|
|
815
|
+
let sum = 0;
|
|
816
|
+
for (let i = 0, n = spaceCount - 1; i < n; ) {
|
|
817
|
+
const bi = bones[i];
|
|
818
|
+
const boneLength = modelLengths[bi];
|
|
819
|
+
if (boneLength < PATH_EPSILON) {
|
|
820
|
+
if (scale) lengths[i] = 0;
|
|
821
|
+
spaces[++i] = spacing;
|
|
822
|
+
} else {
|
|
823
|
+
const w = bi * WORLDS;
|
|
824
|
+
const x = boneLength * W[w + W_A];
|
|
825
|
+
const y = boneLength * W[w + W_C];
|
|
826
|
+
const length = Math.sqrt(x * x + y * y);
|
|
827
|
+
if (scale) lengths[i] = length;
|
|
828
|
+
spaces[++i] = length;
|
|
829
|
+
sum += length;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (sum > 0) {
|
|
833
|
+
sum = (spaceCount / sum) * spacing;
|
|
834
|
+
for (let i = 1; i < spaceCount; i++) spaces[i] *= sum;
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
const addsLength = c.spacingMode === PATH_SPACING_LENGTH;
|
|
838
|
+
for (let i = 0, n = spaceCount - 1; i < n; ) {
|
|
839
|
+
const bi = bones[i];
|
|
840
|
+
const boneLength = modelLengths[bi];
|
|
841
|
+
if (boneLength < PATH_EPSILON) {
|
|
842
|
+
if (scale) lengths[i] = 0;
|
|
843
|
+
spaces[++i] = spacing;
|
|
844
|
+
} else {
|
|
845
|
+
const w = bi * WORLDS;
|
|
846
|
+
const x = boneLength * W[w + W_A];
|
|
847
|
+
const y = boneLength * W[w + W_C];
|
|
848
|
+
const length = Math.sqrt(x * x + y * y);
|
|
849
|
+
if (scale) lengths[i] = length;
|
|
850
|
+
spaces[++i] = ((addsLength ? Math.max(0, boneLength + spacing) : spacing) * length) / boneLength;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
this.positionsOfPath(c, position, spaceCount, wantsTangent);
|
|
855
|
+
const positions = this.pathPositions;
|
|
856
|
+
let atX = positions[0];
|
|
857
|
+
let atY = positions[1];
|
|
858
|
+
let offsetRotation = c.offsetRotation;
|
|
859
|
+
let fromTip: boolean;
|
|
860
|
+
if (offsetRotation === 0) fromTip = c.rotateMode === PATH_ROTATE_CHAIN;
|
|
861
|
+
else {
|
|
862
|
+
fromTip = false;
|
|
863
|
+
let positive: boolean;
|
|
864
|
+
if (c.slotEventAt === k && !c.slotEventAfter) {
|
|
865
|
+
const sb = c.slotBone * WORLDS;
|
|
866
|
+
positive = W[sb + W_A] * W[sb + W_D] - W[sb + W_B] * W[sb + W_C] > 0;
|
|
867
|
+
} else {
|
|
868
|
+
positive = c.slotEventAt >= 0 && this.pathSlotSign[k] > 0;
|
|
869
|
+
}
|
|
870
|
+
offsetRotation *= positive ? RAD : -RAD;
|
|
871
|
+
}
|
|
872
|
+
for (let i = 0, p = 3; i < boneCount; i++, p += 3) {
|
|
873
|
+
const bi = bones[i];
|
|
874
|
+
const o = bi * WORLDS;
|
|
875
|
+
let wa = W[o + W_A];
|
|
876
|
+
let wb = W[o + W_B];
|
|
877
|
+
let wc = W[o + W_C];
|
|
878
|
+
let wd = W[o + W_D];
|
|
879
|
+
const wx = W[o + W_X] + (atX - W[o + W_X]) * mixX;
|
|
880
|
+
const wy = W[o + W_Y] + (atY - W[o + W_Y]) * mixY;
|
|
881
|
+
const x = positions[p];
|
|
882
|
+
const y = positions[p + 1];
|
|
883
|
+
const dx = x - atX;
|
|
884
|
+
const dy = y - atY;
|
|
885
|
+
if (scale) {
|
|
886
|
+
const length = lengths[i];
|
|
887
|
+
if (length >= PATH_EPSILON) {
|
|
888
|
+
const s = (Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
|
|
889
|
+
wa *= s;
|
|
890
|
+
wc *= s;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
atX = x;
|
|
894
|
+
atY = y;
|
|
895
|
+
if (mixRotate > 0) {
|
|
896
|
+
const a = wa;
|
|
897
|
+
const bb = wb;
|
|
898
|
+
const cc = wc;
|
|
899
|
+
const d = wd;
|
|
900
|
+
let r: number;
|
|
901
|
+
if (wantsTangent) r = positions[p - 1];
|
|
902
|
+
else if (spaces[i + 1] === 0) r = positions[p + 2];
|
|
903
|
+
else r = Math.atan2(dy, dx);
|
|
904
|
+
r -= Math.atan2(cc, a);
|
|
905
|
+
if (fromTip) {
|
|
906
|
+
const cos = Math.cos(r);
|
|
907
|
+
const sin = Math.sin(r);
|
|
908
|
+
const length = modelLengths[bi];
|
|
909
|
+
atX += (length * (cos * a - sin * cc) - dx) * mixRotate;
|
|
910
|
+
atY += (length * (sin * a + cos * cc) - dy) * mixRotate;
|
|
911
|
+
} else r += offsetRotation;
|
|
912
|
+
if (r > RUNTIME_PI) r -= TWO_PI;
|
|
913
|
+
else if (r < -RUNTIME_PI) r += TWO_PI;
|
|
914
|
+
r *= mixRotate;
|
|
915
|
+
const cos = Math.cos(r);
|
|
916
|
+
const sin = Math.sin(r);
|
|
917
|
+
wa = cos * a - sin * cc;
|
|
918
|
+
wb = cos * bb - sin * d;
|
|
919
|
+
wc = sin * a + cos * cc;
|
|
920
|
+
wd = sin * bb + cos * d;
|
|
921
|
+
}
|
|
922
|
+
W[o + W_A] = wa;
|
|
923
|
+
W[o + W_B] = wb;
|
|
924
|
+
W[o + W_C] = wc;
|
|
925
|
+
W[o + W_D] = wd;
|
|
926
|
+
W[o + W_X] = wx;
|
|
927
|
+
W[o + W_Y] = wy;
|
|
928
|
+
this.changed[i] = bi;
|
|
929
|
+
this.inWorld[i] = bi;
|
|
930
|
+
}
|
|
931
|
+
if (boneCount > 0) this.repose(boneCount, boneCount);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private computePathWorld(c: RuntimePath): void {
|
|
935
|
+
const at = this.model.attachments[c.attachment];
|
|
936
|
+
const W = this.world;
|
|
937
|
+
const pts = this.pathWorldPts;
|
|
938
|
+
const deformed = this.deformed[c.attachment] !== 0;
|
|
939
|
+
if (at.bindingCounts.length === 0) {
|
|
940
|
+
const b = c.slotBone * WORLDS;
|
|
941
|
+
const a = W[b + W_A];
|
|
942
|
+
const bb = W[b + W_B];
|
|
943
|
+
const cc = W[b + W_C];
|
|
944
|
+
const d = W[b + W_D];
|
|
945
|
+
const wx = W[b + W_X];
|
|
946
|
+
const wy = W[b + W_Y];
|
|
947
|
+
const local = deformed ? this.deform : at.local;
|
|
948
|
+
const lo = deformed ? at.deformOffset : 0;
|
|
949
|
+
for (let q = 0; q < c.pathVertices; q += 2) {
|
|
950
|
+
const x = local[lo + q];
|
|
951
|
+
const y = local[lo + q + 1];
|
|
952
|
+
pts[q] = x * a + y * bb + wx;
|
|
953
|
+
pts[q + 1] = x * cc + y * d + wy;
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const counts = at.bindingCounts;
|
|
958
|
+
const bones = at.bindingBones;
|
|
959
|
+
const xs = at.bindingX;
|
|
960
|
+
const ys = at.bindingY;
|
|
961
|
+
const weights = at.bindingWeights;
|
|
962
|
+
let n = 0;
|
|
963
|
+
let o = 0;
|
|
964
|
+
for (let v = 0; v < counts.length; v++) {
|
|
965
|
+
let wx = 0;
|
|
966
|
+
let wy = 0;
|
|
967
|
+
const end = n + counts[v];
|
|
968
|
+
for (; n < end; n++) {
|
|
969
|
+
const b = bones[n] * WORLDS;
|
|
970
|
+
const weight = weights[n];
|
|
971
|
+
const x = deformed ? xs[n] + this.deform[at.deformOffset + n * 2] : xs[n];
|
|
972
|
+
const y = deformed ? ys[n] + this.deform[at.deformOffset + n * 2 + 1] : ys[n];
|
|
973
|
+
wx += (x * W[b + W_A] + y * W[b + W_B] + W[b + W_X]) * weight;
|
|
974
|
+
wy += (x * W[b + W_C] + y * W[b + W_D] + W[b + W_Y]) * weight;
|
|
975
|
+
}
|
|
976
|
+
pts[o++] = wx;
|
|
977
|
+
pts[o++] = wy;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
private positionsOfPath(c: RuntimePath, position: number, spaceCount: number, wantsTangent: boolean): void {
|
|
982
|
+
this.computePathWorld(c);
|
|
983
|
+
const pts = this.pathWorldPts;
|
|
984
|
+
const vc = c.pathVertices;
|
|
985
|
+
const n = vc / 2;
|
|
986
|
+
const spaces = this.pathSpaces;
|
|
987
|
+
const out = this.pathPositions;
|
|
988
|
+
const outLen = spaceCount * 3 + 2;
|
|
989
|
+
for (let i = 0; i < outLen; i++) out[i] = 0;
|
|
990
|
+
const closed = c.closed;
|
|
991
|
+
if (!c.constantSpeed) {
|
|
992
|
+
const lengths = c.lengths;
|
|
993
|
+
const curveCount = n / 3 - (closed ? 1 : 2);
|
|
994
|
+
const total = lengths[curveCount];
|
|
995
|
+
if (c.positionMode === PATH_POSITION_PERCENT) position *= total;
|
|
996
|
+
const spaceScale = c.spacingMode === PATH_SPACING_PERCENT ? total : c.spacingMode === PATH_SPACING_PROPORTIONAL ? total / spaceCount : 1;
|
|
997
|
+
let curve = 0;
|
|
998
|
+
for (let i = 0, o = 0; i < spaceCount; i++, o += 3) {
|
|
999
|
+
const space = spaces[i] * spaceScale;
|
|
1000
|
+
position += space;
|
|
1001
|
+
let p = position;
|
|
1002
|
+
if (closed) {
|
|
1003
|
+
p %= total;
|
|
1004
|
+
if (p < 0) p += total;
|
|
1005
|
+
curve = 0;
|
|
1006
|
+
} else if (p < 0) {
|
|
1007
|
+
onLineBefore(p, pts[2], pts[3], pts[4], pts[5], out, o);
|
|
1008
|
+
continue;
|
|
1009
|
+
} else if (p > total) {
|
|
1010
|
+
const m0 = 2 * (n - 3);
|
|
1011
|
+
const m1 = 2 * (n - 2);
|
|
1012
|
+
onLinePast(p - total, pts[m0], pts[m0 + 1], pts[m1], pts[m1 + 1], out, o);
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
for (;; curve++) {
|
|
1016
|
+
const length = lengths[curve];
|
|
1017
|
+
if (p > length) continue;
|
|
1018
|
+
if (curve === 0) p /= length;
|
|
1019
|
+
else {
|
|
1020
|
+
const prev = lengths[curve - 1];
|
|
1021
|
+
p = (p - prev) / (length - prev);
|
|
1022
|
+
}
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
const kk = 3 * curve + 1;
|
|
1026
|
+
const i0 = (kk % n) * 2;
|
|
1027
|
+
const i1 = ((kk + 1) % n) * 2;
|
|
1028
|
+
const i2 = ((kk + 2) % n) * 2;
|
|
1029
|
+
const i3 = ((kk + 3) % n) * 2;
|
|
1030
|
+
onCurve(p, pts[i0], pts[i0 + 1], pts[i1], pts[i1 + 1], pts[i2], pts[i2 + 1], pts[i3], pts[i3 + 1], out, o, wantsTangent || (i > 0 && space === 0));
|
|
1031
|
+
}
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const chain = this.pathChain;
|
|
1035
|
+
let chainLen = 0;
|
|
1036
|
+
if (closed) {
|
|
1037
|
+
for (let q = 2; q < vc; q++) chain[chainLen++] = pts[q];
|
|
1038
|
+
chain[chainLen++] = pts[0];
|
|
1039
|
+
chain[chainLen++] = pts[1];
|
|
1040
|
+
chain[chainLen++] = pts[2];
|
|
1041
|
+
chain[chainLen++] = pts[3];
|
|
1042
|
+
} else {
|
|
1043
|
+
for (let q = 2; q < vc - 2; q++) chain[chainLen++] = pts[q];
|
|
1044
|
+
}
|
|
1045
|
+
const curveCount = closed ? n / 3 : n / 3 - 1;
|
|
1046
|
+
const curveEnds = this.pathCurveEnds;
|
|
1047
|
+
let total = 0;
|
|
1048
|
+
let cx0 = chain[0];
|
|
1049
|
+
let cy0 = chain[1];
|
|
1050
|
+
for (let i = 0, w = 2; i < curveCount; i++, w += 6) {
|
|
1051
|
+
const cx1 = chain[w];
|
|
1052
|
+
const cy1 = chain[w + 1];
|
|
1053
|
+
const cx2 = chain[w + 2];
|
|
1054
|
+
const cy2 = chain[w + 3];
|
|
1055
|
+
const x2 = chain[w + 4];
|
|
1056
|
+
const y2 = chain[w + 5];
|
|
1057
|
+
const h2x = (cx0 - cx1 * 2 + cx2) * 0.1875;
|
|
1058
|
+
const h2y = (cy0 - cy1 * 2 + cy2) * 0.1875;
|
|
1059
|
+
const d3x = ((cx1 - cx2) * 3 - cx0 + x2) * 0.09375;
|
|
1060
|
+
const d3y = ((cy1 - cy2) * 3 - cy0 + y2) * 0.09375;
|
|
1061
|
+
let d2x = h2x * 2 + d3x;
|
|
1062
|
+
let d2y = h2y * 2 + d3y;
|
|
1063
|
+
let d1x = (cx1 - cx0) * 0.75 + h2x + d3x * 0.16666667;
|
|
1064
|
+
let d1y = (cy1 - cy0) * 0.75 + h2y + d3y * 0.16666667;
|
|
1065
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1066
|
+
d1x += d2x;
|
|
1067
|
+
d1y += d2y;
|
|
1068
|
+
d2x += d3x;
|
|
1069
|
+
d2y += d3y;
|
|
1070
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1071
|
+
d1x += d2x;
|
|
1072
|
+
d1y += d2y;
|
|
1073
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1074
|
+
d1x += d2x + d3x;
|
|
1075
|
+
d1y += d2y + d3y;
|
|
1076
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1077
|
+
curveEnds[i] = total;
|
|
1078
|
+
cx0 = x2;
|
|
1079
|
+
cy0 = y2;
|
|
1080
|
+
}
|
|
1081
|
+
const pathTotal = curveCount > 0 ? curveEnds[curveCount - 1] : 0;
|
|
1082
|
+
if (c.positionMode === PATH_POSITION_PERCENT) position *= pathTotal;
|
|
1083
|
+
const spaceScale = c.spacingMode === PATH_SPACING_PERCENT ? pathTotal : c.spacingMode === PATH_SPACING_PROPORTIONAL ? pathTotal / spaceCount : 1;
|
|
1084
|
+
const segmentEnds = this.pathSegmentEnds;
|
|
1085
|
+
for (let s = 0; s < 10; s++) segmentEnds[s] = 0;
|
|
1086
|
+
let curveTotal = 0;
|
|
1087
|
+
let lastCurve = -1;
|
|
1088
|
+
let x1 = 0, y1 = 0, cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
|
|
1089
|
+
for (let i = 0, o = 0, curve = 0, segment = 0; i < spaceCount; i++, o += 3) {
|
|
1090
|
+
const space = spaces[i] * spaceScale;
|
|
1091
|
+
position += space;
|
|
1092
|
+
let p = position;
|
|
1093
|
+
if (closed) {
|
|
1094
|
+
p %= pathTotal;
|
|
1095
|
+
if (p < 0) p += pathTotal;
|
|
1096
|
+
curve = 0;
|
|
1097
|
+
segment = 0;
|
|
1098
|
+
} else if (p < 0) {
|
|
1099
|
+
onLineBefore(p, chain[0], chain[1], chain[2], chain[3], out, o);
|
|
1100
|
+
continue;
|
|
1101
|
+
} else if (p > pathTotal) {
|
|
1102
|
+
onLinePast(p - pathTotal, chain[chainLen - 4], chain[chainLen - 3], chain[chainLen - 2], chain[chainLen - 1], out, o);
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
for (;; curve++) {
|
|
1106
|
+
const length = curveEnds[curve];
|
|
1107
|
+
if (p > length) continue;
|
|
1108
|
+
if (curve === 0) p /= length;
|
|
1109
|
+
else {
|
|
1110
|
+
const prev = curveEnds[curve - 1];
|
|
1111
|
+
p = (p - prev) / (length - prev);
|
|
1112
|
+
}
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
if (curve !== lastCurve) {
|
|
1116
|
+
lastCurve = curve;
|
|
1117
|
+
const ii = curve * 6;
|
|
1118
|
+
x1 = chain[ii];
|
|
1119
|
+
y1 = chain[ii + 1];
|
|
1120
|
+
cx1 = chain[ii + 2];
|
|
1121
|
+
cy1 = chain[ii + 3];
|
|
1122
|
+
cx2 = chain[ii + 4];
|
|
1123
|
+
cy2 = chain[ii + 5];
|
|
1124
|
+
x2 = chain[ii + 6];
|
|
1125
|
+
y2 = chain[ii + 7];
|
|
1126
|
+
const h2x = (x1 - cx1 * 2 + cx2) * 0.03;
|
|
1127
|
+
const h2y = (y1 - cy1 * 2 + cy2) * 0.03;
|
|
1128
|
+
const d3x = ((cx1 - cx2) * 3 - x1 + x2) * 0.006;
|
|
1129
|
+
const d3y = ((cy1 - cy2) * 3 - y1 + y2) * 0.006;
|
|
1130
|
+
let d2x = h2x * 2 + d3x;
|
|
1131
|
+
let d2y = h2y * 2 + d3y;
|
|
1132
|
+
let d1x = (cx1 - x1) * 0.3 + h2x + d3x * 0.16666667;
|
|
1133
|
+
let d1y = (cy1 - y1) * 0.3 + h2y + d3y * 0.16666667;
|
|
1134
|
+
curveTotal = Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1135
|
+
segmentEnds[0] = curveTotal;
|
|
1136
|
+
for (let s = 1; s < 8; s++) {
|
|
1137
|
+
d1x += d2x;
|
|
1138
|
+
d1y += d2y;
|
|
1139
|
+
d2x += d3x;
|
|
1140
|
+
d2y += d3y;
|
|
1141
|
+
curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1142
|
+
segmentEnds[s] = curveTotal;
|
|
1143
|
+
}
|
|
1144
|
+
d1x += d2x;
|
|
1145
|
+
d1y += d2y;
|
|
1146
|
+
curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1147
|
+
segmentEnds[8] = curveTotal;
|
|
1148
|
+
d1x += d2x + d3x;
|
|
1149
|
+
d1y += d2y + d3y;
|
|
1150
|
+
curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
1151
|
+
segmentEnds[9] = curveTotal;
|
|
1152
|
+
segment = 0;
|
|
1153
|
+
}
|
|
1154
|
+
p *= curveTotal;
|
|
1155
|
+
for (;; segment++) {
|
|
1156
|
+
const length = segmentEnds[segment];
|
|
1157
|
+
if (p > length) continue;
|
|
1158
|
+
if (segment === 0) p /= length;
|
|
1159
|
+
else {
|
|
1160
|
+
const prev = segmentEnds[segment - 1];
|
|
1161
|
+
p = segment + (p - prev) / (length - prev);
|
|
1162
|
+
}
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
onCurve(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, wantsTangent || (i > 0 && space === 0));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
private sliderAttachments(): void {
|
|
1170
|
+
for (let k = 0; k < this.model.constraints.length; k++) {
|
|
1171
|
+
this.sliderSlots(k);
|
|
1172
|
+
this.sliderDeforms(k);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private sliderSlots(index: number): void {
|
|
1177
|
+
const cs = this.model.constraints;
|
|
1178
|
+
{
|
|
1179
|
+
const k = index, c = cs[k];
|
|
1180
|
+
if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
|
|
1181
|
+
const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
|
|
1182
|
+
for (let q = 0; q < a.slots.length; q++) {
|
|
1183
|
+
const tl = a.slots[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
1184
|
+
if (i < 0) continue;
|
|
1185
|
+
if (tl.attachments !== null) {
|
|
1186
|
+
const next = tl.attachments[i];
|
|
1187
|
+
if (next !== this.shown[tl.slot] && next >= 0) this.deformed[next] = 0;
|
|
1188
|
+
this.shown[tl.slot] = next;
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
const channels = tl.channels!;
|
|
1192
|
+
for (let f = 0; f < channels.length; f++) {
|
|
1193
|
+
const n = tl.slot * 4 + channels[f], v = channelValue(tl.curve, i, f, t);
|
|
1194
|
+
this.light[n] = clamp01(alpha === 1 ? v : this.light[n] + (v - this.light[n]) * alpha);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
private sliderDeforms(index: number): void {
|
|
1202
|
+
if (this.model.omitDeform) return;
|
|
1203
|
+
const cs = this.model.constraints;
|
|
1204
|
+
{
|
|
1205
|
+
const k = index, c = cs[k];
|
|
1206
|
+
if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
|
|
1207
|
+
const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
|
|
1208
|
+
for (let q = 0; q < a.deforms.length; q++) {
|
|
1209
|
+
const tl = a.deforms[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
1210
|
+
if (i < 0) continue;
|
|
1211
|
+
const held = i + 1 === tl.curve.keys || tl.curve.kinds[i] === CURVE_STEPPED;
|
|
1212
|
+
const p = held ? 0 : deformFraction(tl.curve, i, t), start = i * tl.count;
|
|
1213
|
+
for (let slot = 0; slot < this.shown.length; slot++) {
|
|
1214
|
+
const index = this.shown[slot];
|
|
1215
|
+
if (index < 0 || !this.model.slots[slot].live) continue;
|
|
1216
|
+
const at = this.model.attachments[index];
|
|
1217
|
+
if (at.timeline !== tl.identity || at.deformCount === 0) continue;
|
|
1218
|
+
const current = this.deformed[index] !== 0;
|
|
1219
|
+
for (let f = 0; f < tl.count; f++) {
|
|
1220
|
+
const v = tl.held[start + f], target = held ? v : v + (tl.held[start + tl.count + f] - v) * p;
|
|
1221
|
+
const setup = at.draw === DRAW_MESH ? at.local[f] : 0, n = at.deformOffset + f;
|
|
1222
|
+
const from = current ? this.deform[n] : setup;
|
|
1223
|
+
this.deform[n] = alpha === 1 && (!c.setup.additive || !current) ? target : c.setup.additive ? from + (target - setup) * alpha : from + (target - from) * alpha;
|
|
1224
|
+
}
|
|
1225
|
+
this.deformed[index] = 1;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
445
1228
|
}
|
|
446
1229
|
}
|
|
447
1230
|
|
|
@@ -868,6 +1651,20 @@ export class RuntimePose {
|
|
|
868
1651
|
const wb = W[w + W_B];
|
|
869
1652
|
const wc = W[w + W_C];
|
|
870
1653
|
const wd = W[w + W_D];
|
|
1654
|
+
if (mode === MODE_NO_SCALE || mode === MODE_NO_SCALE_OR_REFLECTION) {
|
|
1655
|
+
const det = pa * pd - pb * pc;
|
|
1656
|
+
const flip = mode === MODE_NO_SCALE && det < 0 ? -1 : 1;
|
|
1657
|
+
const sign = det < 0 ? -1 : 1;
|
|
1658
|
+
const rotation = Math.atan2((pa * wc - pc * wa) * sign, (pd * wa - pb * wc) * sign) * DEG;
|
|
1659
|
+
const rr = rotation * RAD;
|
|
1660
|
+
const cos = Math.cos(rr), sin = Math.sin(rr);
|
|
1661
|
+
let ux = pa * cos + pb * sin, uy = pc * cos + pd * sin;
|
|
1662
|
+
const length = 1 / Math.sqrt(ux * ux + uy * uy);
|
|
1663
|
+
ux *= length; uy *= length;
|
|
1664
|
+
this.decompose(l, ux * wa + uy * wc, (ux * wc - uy * wa) * flip, ux * wb + uy * wd, (ux * wd - uy * wb) * flip, true);
|
|
1665
|
+
L[l + L_ROTATION] = rotation;
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
871
1668
|
if (mode === MODE_ONLY_TRANSLATION) {
|
|
872
1669
|
this.decompose(l, wa, wc, wb, wd);
|
|
873
1670
|
return;
|
|
@@ -887,7 +1684,7 @@ export class RuntimePose {
|
|
|
887
1684
|
}
|
|
888
1685
|
|
|
889
1686
|
/** 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 {
|
|
1687
|
+
private decompose(l: number, la: number, lc: number, lb: number, ld: number, shearX = false): void {
|
|
891
1688
|
const L = this.locals;
|
|
892
1689
|
const sx = Math.sqrt(la * la + lc * lc);
|
|
893
1690
|
const yLength = Math.sqrt(lb * lb + ld * ld);
|
|
@@ -901,11 +1698,11 @@ export class RuntimePose {
|
|
|
901
1698
|
const xAngle = Math.atan2(lc, la) * DEG;
|
|
902
1699
|
const det = la * ld - lb * lc;
|
|
903
1700
|
const yAngle = Math.atan2(ld, lb) * DEG;
|
|
904
|
-
L[l + L_ROTATION] = xAngle;
|
|
1701
|
+
L[l + (shearX ? L_SHEAR_X : L_ROTATION)] = xAngle;
|
|
905
1702
|
L[l + L_SCALE_X] = sx;
|
|
906
1703
|
L[l + L_SCALE_Y] = det < 0 ? -yLength : yLength;
|
|
907
1704
|
const right = det < 0 ? -90 : 90;
|
|
908
|
-
let shear = yAngle - (xAngle + right);
|
|
1705
|
+
let shear = yAngle - ((shearX ? 0 : xAngle) + right);
|
|
909
1706
|
shear = shear > 180 ? shear - 360 : shear < -180 ? shear + 360 : shear;
|
|
910
1707
|
L[l + L_SHEAR_Y] = yLength > 0.00001 ? shear : 0;
|
|
911
1708
|
}
|
|
@@ -1093,3 +1890,43 @@ function applyWorld(w: Float64Array, property: number, v: number, mix: number):
|
|
|
1093
1890
|
}
|
|
1094
1891
|
}
|
|
1095
1892
|
}
|
|
1893
|
+
|
|
1894
|
+
function onLineBefore(p: number, x1: number, y1: number, x2: number, y2: number, out: Float64Array, o: number): void {
|
|
1895
|
+
const r = Math.atan2(y2 - y1, x2 - x1);
|
|
1896
|
+
out[o] = x1 + p * Math.cos(r);
|
|
1897
|
+
out[o + 1] = y1 + p * Math.sin(r);
|
|
1898
|
+
out[o + 2] = r;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function onLinePast(p: number, x0: number, y0: number, x1: number, y1: number, out: Float64Array, o: number): void {
|
|
1902
|
+
const r = Math.atan2(y1 - y0, x1 - x0);
|
|
1903
|
+
out[o] = x1 + p * Math.cos(r);
|
|
1904
|
+
out[o + 1] = y1 + p * Math.sin(r);
|
|
1905
|
+
out[o + 2] = r;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function onCurve(p: number, x1: number, y1: number, cx1: number, cy1: number, cx2: number, cy2: number, x2: number, y2: number, out: Float64Array, o: number, wantsTangent: boolean): void {
|
|
1909
|
+
if (Number.isNaN(p)) {
|
|
1910
|
+
out[o] = x1;
|
|
1911
|
+
out[o + 1] = y1;
|
|
1912
|
+
out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const t2 = p * p;
|
|
1916
|
+
const t3 = t2 * p;
|
|
1917
|
+
const u = 1 - p;
|
|
1918
|
+
const u2 = u * u;
|
|
1919
|
+
const u3 = u2 * u;
|
|
1920
|
+
const ut = u * p;
|
|
1921
|
+
const ut3x = ut * 3;
|
|
1922
|
+
const w1 = u * ut3x;
|
|
1923
|
+
const w2 = ut3x * p;
|
|
1924
|
+
const x = x1 * u3 + cx1 * w1 + cx2 * w2 + x2 * t3;
|
|
1925
|
+
const y = y1 * u3 + cy1 * w1 + cy2 * w2 + y2 * t3;
|
|
1926
|
+
out[o] = x;
|
|
1927
|
+
out[o + 1] = y;
|
|
1928
|
+
if (wantsTangent) {
|
|
1929
|
+
if (p < 0.001) out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
|
|
1930
|
+
else out[o + 2] = Math.atan2(y - (y1 * u2 + cy1 * ut * 2 + cy2 * t2), x - (x1 * u2 + cx1 * ut * 2 + cx2 * t2));
|
|
1931
|
+
}
|
|
1932
|
+
}
|