@rig-c/core 2.73.0 → 2.74.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 +18 -5
- package/package.json +1 -1
- package/src/runtime/curves.ts +35 -0
- package/src/runtime/index.ts +2 -2
- package/src/runtime/model.ts +47 -14
- package/src/runtime/pose.ts +48 -7
package/docs/AUTHORING.md
CHANGED
|
@@ -12610,7 +12610,7 @@ verdict on those figures.
|
|
|
12610
12610
|
| <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.⟧ |
|
|
12611
12611
|
| <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.⟧ |
|
|
12612
12612
|
|
|
12613
|
-
## 15. Posing in place for a player — the runtime poser (stage
|
|
12613
|
+
## 15. Posing in place for a player — the runtime poser (stage 2)
|
|
12614
12614
|
|
|
12615
12615
|
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12616
12616
|
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
@@ -12645,15 +12645,28 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
|
|
|
12645
12645
|
The display `name` may be shared by different placeholders; it is never a table
|
|
12646
12646
|
key to guess the source from. These load-time strings do not change the pose path.
|
|
12647
12647
|
|
|
12648
|
-
| | what stage
|
|
12648
|
+
| | what stage 2 poses |
|
|
12649
12649
|
| --- | --- |
|
|
12650
12650
|
| 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) |
|
|
12651
12651
|
| 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 |
|
|
12652
12652
|
| 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 |
|
|
12653
12653
|
| vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
|
|
12654
|
+
| 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` |
|
|
12654
12655
|
| 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 |
|
|
12655
12656
|
| time | one animation at full weight from the setup pose, at a time (`apply`) or stepped as a looping track (`step`, the track time wrapped by the duration as `openTrack` wraps it) |
|
|
12656
12657
|
|
|
12658
|
+
Deform keys are expanded once at load. Unweighted keys hold float32 positions,
|
|
12659
|
+
`fround(fround(setup) + fround(offset))`; weighted keys hold float32 offsets,
|
|
12660
|
+
**one x/y pair per influence**, in vertex then influence order. At a pose,
|
|
12661
|
+
interpolation stays double. Each weighted offset is added to its float32 binding
|
|
12662
|
+
coordinate before the bone transform, translation and weight, summed in binding
|
|
12663
|
+
order. Bézier uses the oracle's deform recurrence end `0.99999999` and its
|
|
12664
|
+
product-before-division first and last pieces, rather than ordinary channel easing.
|
|
12665
|
+
Attachment switches resolve before deform matching by source skin/slot/placeholder;
|
|
12666
|
+
display names do not identify timelines. Every pose clears deform state, including
|
|
12667
|
+
before the first key, at setup, on backwards seeks and across loop wraps. Buffers
|
|
12668
|
+
and cursors belong to the pose; compiling another skin resolves another identity.
|
|
12669
|
+
|
|
12657
12670
|
Skin switching is a load-time view change: compile `underSkin(document, name)`
|
|
12658
12671
|
(or `underNoSkin(document)`), create the replacement `RuntimePose`, then call
|
|
12659
12672
|
`replacement.restore(animationName, oldPose.trackTime, oldPose.time)`. Restore
|
|
@@ -12681,7 +12694,7 @@ the list; normal compilation never does.
|
|
|
12681
12694
|
|
|
12682
12695
|
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12683
12696
|
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12684
|
-
reads `not posed by the runtime poser (stage
|
|
12697
|
+
reads `not posed by the runtime poser (stage 2), N construct(s) missing:
|
|
12685
12698
|
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12686
12699
|
animation-level one refuses that animation alone (`model.refused`;
|
|
12687
12700
|
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
@@ -12697,7 +12710,7 @@ refused: nothing is posed approximately.
|
|
|
12697
12710
|
| `clipping`, `null-atlas-region` | the model when shown at setup, else the animation that keys it | a clipping attachment; a region whose atlas rectangle the model states as `null` |
|
|
12698
12711
|
| `skin-conflict` | as above | a placeholder the skins fill differently |
|
|
12699
12712
|
| `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`) |
|
|
12700
|
-
| `inherit-timeline`, `
|
|
12713
|
+
| `inherit-timeline`, `sequence-timeline`, `draw-order-timeline`, `dark-colour-timeline` | the animation | a timeline of that kind (`dark-colour` is `rgba2` / `rgb2`) |
|
|
12701
12714
|
|
|
12702
12715
|
A call it cannot answer throws `RuntimeInputError`, naming the call:
|
|
12703
12716
|
`apply: no animation is set (setAnimation first)`, `animation "x" is not one of
|
|
@@ -12717,7 +12730,7 @@ the poses and values compared. A `DIFF` or `REFUSED` turns the run RED; a SKIP
|
|
|
12717
12730
|
never counts as a pass. The page UVs are the atlas's and are compared by neither
|
|
12718
12731
|
side here. The selftest's `runtime-poser` suite holds the gate to its plants (a
|
|
12719
12732
|
1e-6° bone perturbation, a weighted binding bound to the wrong bone index, a
|
|
12720
|
-
|
|
12733
|
+
omitted deform — each caught by name), a seeded population of
|
|
12721
12734
|
every construct above, the looping step against `openTrack`, and a steady-state
|
|
12722
12735
|
frame measured at 0 bytes with the collector off.
|
|
12723
12736
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.74.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
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* Nothing here allocates once compiled.
|
|
26
26
|
*/
|
|
27
27
|
import { bezierPolyline, BEZIER_PIECES, type CoreKey } from '../core/animation.ts';
|
|
28
|
+
import { DEFORM_CURVE_END, type CoreDeformKey } from '../core/deform.ts';
|
|
28
29
|
|
|
29
30
|
/** A segment's curve, per key. */
|
|
30
31
|
export const CURVE_LINEAR = 0;
|
|
@@ -104,3 +105,37 @@ export function channelValue(curve: RuntimeCurve, i: number, c: number, t: numbe
|
|
|
104
105
|
const y0 = p[o + q - 1];
|
|
105
106
|
return y0 + ((t - x0) / (p[o + q] - x0)) * (p[o + q + 1] - y0);
|
|
106
107
|
}
|
|
108
|
+
|
|
109
|
+
/** Deform easing has one fraction channel, a distinct recurrence end and end-piece arithmetic (core/deform.ts). */
|
|
110
|
+
export function compileDeformCurve(keys: readonly CoreDeformKey[]): RuntimeCurve {
|
|
111
|
+
const curve = compileCurve(keys.map(k => ({ time: k.time, values: [0], curve: 'linear' as const, stated: { time: k.stated, values: [0] } })), 1);
|
|
112
|
+
for (let i = 0; i < keys.length; i++) {
|
|
113
|
+
const a = keys[i];
|
|
114
|
+
curve.kinds[i] = a.curve === 'linear' ? CURVE_LINEAR : a.curve === 'stepped' ? CURVE_STEPPED : CURVE_BEZIER;
|
|
115
|
+
if (typeof a.curve === 'string' || i + 1 === keys.length) continue;
|
|
116
|
+
const b = keys[i + 1];
|
|
117
|
+
const c = a.curve;
|
|
118
|
+
const inner = bezierPolyline(a.stated, 0, c[0], c[1], c[2], c[3], b.stated, DEFORM_CURVE_END);
|
|
119
|
+
const o = i * POLYLINE;
|
|
120
|
+
curve.polylines[o] = a.time;
|
|
121
|
+
curve.polylines[o + 1] = 0;
|
|
122
|
+
curve.polylines.set(inner, o + 2);
|
|
123
|
+
curve.polylines[o + POLYLINE - 2] = b.time;
|
|
124
|
+
curve.polylines[o + POLYLINE - 1] = 1;
|
|
125
|
+
}
|
|
126
|
+
return curve;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The oracle's deformPercent, with the polyline compiled once and product-before-division at both ends. */
|
|
130
|
+
export function deformFraction(curve: RuntimeCurve, i: number, t: number): number {
|
|
131
|
+
if (curve.kinds[i] === CURVE_STEPPED) return 0;
|
|
132
|
+
if (curve.kinds[i] === CURVE_LINEAR) return (t - curve.times[i]) / (curve.times[i + 1] - curve.times[i]);
|
|
133
|
+
const p = curve.polylines;
|
|
134
|
+
const o = i * POLYLINE;
|
|
135
|
+
let q = 2;
|
|
136
|
+
while (q < POLYLINE - 2 && p[o + q] < t) q += 2;
|
|
137
|
+
const x0 = p[o + q - 2], y0 = p[o + q - 1], x1 = p[o + q], y1 = p[o + q + 1];
|
|
138
|
+
if (q === 2) return (y1 * (t - x0)) / (x1 - x0);
|
|
139
|
+
if (q === POLYLINE - 2) return y0 + ((1 - y0) * (t - x0)) / (x1 - x0);
|
|
140
|
+
return y0 + ((t - x0) / (x1 - x0)) * (y1 - y0);
|
|
141
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -21,11 +21,11 @@
|
|
|
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 2: 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
|
-
* timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
|
|
28
|
+
* timelines; weighted/unweighted deform timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
|
|
29
29
|
* two-bone ik and world-space, absolute transform constraints 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
|
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 2): 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
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
* differently, or a block the oracle itself leaves out;
|
|
22
22
|
* - **refuses an animation** (`RuntimeModel.refused`; `animation(name)`
|
|
23
23
|
* throws the same `RuntimeRefusal`) when the construct is the animation's
|
|
24
|
-
* — an `inherit` timeline, a
|
|
24
|
+
* — an `inherit` timeline, a sequence timeline, a draw-order
|
|
25
25
|
* timeline, a dark-colour (`rgba2`, `rgb2`) timeline, or an
|
|
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
|
-
* named/no-skin views and
|
|
29
|
+
* `RUNTIME_FEATURES` is the closed list of what can be named. Stage 2 adds mesh deform timelines to the
|
|
30
|
+
* named/no-skin views and event keys, with immutable payloads compiled
|
|
31
31
|
* at load. `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.
|
|
@@ -49,10 +49,11 @@ import type { ModelVertices } from '../model.ts';
|
|
|
49
49
|
import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, readBlend, readColour, shownAttachment, shownRow, sourceOfDoc, underSkin, type CompiledDocument, type CoreAnimation, type CoreBlendMode } from '../core/index.ts';
|
|
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
|
-
import {
|
|
52
|
+
import { heldArray, timelineIdentity } from '../core/deform.ts';
|
|
53
|
+
import { compileCurve, compileDeformCurve, type RuntimeCurve } from './curves.ts';
|
|
53
54
|
|
|
54
55
|
/** The stage of issue #1591 this poser implements. */
|
|
55
|
-
export const RUNTIME_STAGE =
|
|
56
|
+
export const RUNTIME_STAGE = 2;
|
|
56
57
|
|
|
57
58
|
/** Every construct the runtime poser can refuse, by the name its refusals carry. */
|
|
58
59
|
export const RUNTIME_FEATURES = [
|
|
@@ -68,7 +69,6 @@ export const RUNTIME_FEATURES = [
|
|
|
68
69
|
'skin-conflict',
|
|
69
70
|
'oracle-absent',
|
|
70
71
|
'inherit-timeline',
|
|
71
|
-
'deform-timeline',
|
|
72
72
|
'sequence-timeline',
|
|
73
73
|
'draw-order-timeline',
|
|
74
74
|
'dark-colour-timeline',
|
|
@@ -125,6 +125,9 @@ export interface RuntimePlant {
|
|
|
125
125
|
readonly eventWrapOffByOne?: boolean;
|
|
126
126
|
/** The old restore behavior, which repeats keys up to the restored time. */
|
|
127
127
|
readonly restoreEventsFromStart?: boolean;
|
|
128
|
+
/** Omit deform / multiply offsets by weights too early: negative controls only. */
|
|
129
|
+
readonly omitDeform?: boolean;
|
|
130
|
+
readonly deformAfterWeights?: boolean;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
// A bone's local values, seven per bone.
|
|
@@ -184,6 +187,10 @@ export const RAD = RUNTIME_PI / 180;
|
|
|
184
187
|
* bindings (bone index, float32 coordinates, float32 weight) in their order.
|
|
185
188
|
*/
|
|
186
189
|
export interface RuntimeAttachment {
|
|
190
|
+
/** Source timeline identity, including linked-mesh inheritance, resolved once. */
|
|
191
|
+
readonly timeline: string | null;
|
|
192
|
+
readonly deformOffset: number;
|
|
193
|
+
readonly deformCount: number;
|
|
187
194
|
readonly slot: number;
|
|
188
195
|
readonly name: string;
|
|
189
196
|
/** Exact resolved source table identity; display names need not be unique. */
|
|
@@ -289,11 +296,21 @@ export interface RuntimeEvent {
|
|
|
289
296
|
readonly balance: number;
|
|
290
297
|
}
|
|
291
298
|
|
|
299
|
+
/** One source record's deform, its held arrays by key, matched after attachment switches. */
|
|
300
|
+
export interface RuntimeDeformTimeline {
|
|
301
|
+
readonly identity: string;
|
|
302
|
+
readonly curve: RuntimeCurve;
|
|
303
|
+
readonly cursor: number;
|
|
304
|
+
readonly held: Float64Array;
|
|
305
|
+
readonly count: number;
|
|
306
|
+
}
|
|
307
|
+
|
|
292
308
|
/** One covered animation, compiled. */
|
|
293
309
|
export interface RuntimeAnimation {
|
|
294
310
|
readonly name: string;
|
|
295
311
|
readonly duration: number;
|
|
296
312
|
readonly bones: readonly RuntimeBoneTimeline[];
|
|
313
|
+
readonly deforms: readonly RuntimeDeformTimeline[];
|
|
297
314
|
readonly slots: readonly RuntimeSlotTimeline[];
|
|
298
315
|
/** Per constraint of the plan, its timeline in this animation or `null`. */
|
|
299
316
|
readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
|
|
@@ -307,6 +324,9 @@ export interface RuntimeAnimation {
|
|
|
307
324
|
/** The compiled model a pose is made of (`compileRuntime`). Read-only once built. */
|
|
308
325
|
export interface RuntimeModel {
|
|
309
326
|
/** Control-only boundary defect; fixed at load. */
|
|
327
|
+
readonly omitDeform: boolean;
|
|
328
|
+
readonly deformAfterWeights: boolean;
|
|
329
|
+
readonly deformCount: number;
|
|
310
330
|
readonly eventWrapOffByOne: boolean;
|
|
311
331
|
readonly restoreEventsFromStart: boolean;
|
|
312
332
|
readonly doc: CompiledDocument;
|
|
@@ -454,6 +474,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
454
474
|
// --- slots and the records they show ---
|
|
455
475
|
const attachments: RuntimeAttachment[] = [];
|
|
456
476
|
let vertexCount = 0;
|
|
477
|
+
let deformCount = 0;
|
|
457
478
|
const sourceOf = sourceOfDoc(doc);
|
|
458
479
|
const attachmentView = plant.attachmentSkin === undefined ? doc : underSkin(doc, plant.attachmentSkin);
|
|
459
480
|
/** Per slot, each placeholder's attachment index (−1 shows nothing) and what it misses, resolved once and its refusal noted for every animation that shows it. */
|
|
@@ -475,7 +496,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
475
496
|
if ('conflict' in shown) return { index: -1, missing: ['skin-conflict', `slot "${slot.name}" placeholder "${placeholder}" is filled by skins that disagree`] };
|
|
476
497
|
const row = shownRow(shown);
|
|
477
498
|
const g = shown.record.geometry;
|
|
478
|
-
const at = { slot: si, name: row.name, skin: shown.skin, placeholder: shown.placeholder, path: CORE_REGION_KINDS.has(shown.record.kind) ? row.path : null, bone: indexOf(slot.bone), offset: vertexCount };
|
|
499
|
+
const at = { timeline: timelineIdentity(slot.name, shown), deformOffset: deformCount, slot: si, name: row.name, skin: shown.skin, placeholder: shown.placeholder, path: CORE_REGION_KINDS.has(shown.record.kind) ? row.path : null, bone: indexOf(slot.bone), offset: vertexCount };
|
|
479
500
|
let made: RuntimeAttachment | null = null;
|
|
480
501
|
let missing: readonly [RuntimeFeature, string] | null = null;
|
|
481
502
|
if (g?.kind === 'clipping') missing = ['clipping', `slot "${slot.name}" shows clipping attachment "${row.name}"`];
|
|
@@ -519,6 +540,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
519
540
|
if (made === null) made = vertexAttachment(at, DRAW_NONE, new Float64Array(0), 0);
|
|
520
541
|
attachments.push(made);
|
|
521
542
|
vertexCount += made.count;
|
|
543
|
+
deformCount += made.deformCount;
|
|
522
544
|
return { index: attachments.length - 1, missing };
|
|
523
545
|
};
|
|
524
546
|
const slots: RuntimeSlot[] = doc.slots.map((slot, si) => {
|
|
@@ -542,7 +564,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
542
564
|
const refused: Array<{ animation: string; missing: RuntimeMissing[] }> = [];
|
|
543
565
|
let cursorMax = 0;
|
|
544
566
|
for (const anim of doc.animations) {
|
|
545
|
-
const compiled = compileAnimation(anim, refusals, ignoreEvents, boneIndex, slotIndex, slots, constraints, (si, ph) => resolve(si, ph, anim.name));
|
|
567
|
+
const compiled = compileAnimation(anim, refusals, ignoreEvents, boneIndex, slotIndex, slots, constraints, doc, (si, ph) => resolve(si, ph, anim.name));
|
|
546
568
|
const missing = refusals.byAnimation.get(anim.name);
|
|
547
569
|
if (missing !== undefined && missing.length > 0) refused.push({ animation: anim.name, missing });
|
|
548
570
|
else {
|
|
@@ -554,6 +576,9 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
554
576
|
if (refusals.model.length > 0) throw new RuntimeRefusal(refusals.model);
|
|
555
577
|
const names = doc.animations.map((a) => a.name);
|
|
556
578
|
return {
|
|
579
|
+
omitDeform: plant.omitDeform === true,
|
|
580
|
+
deformAfterWeights: plant.deformAfterWeights === true,
|
|
581
|
+
deformCount,
|
|
557
582
|
eventWrapOffByOne: plant.eventWrapOffByOne === true,
|
|
558
583
|
restoreEventsFromStart: plant.restoreEventsFromStart === true,
|
|
559
584
|
doc,
|
|
@@ -583,13 +608,13 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
583
608
|
};
|
|
584
609
|
}
|
|
585
610
|
|
|
586
|
-
type AttachmentHead = { slot: number; name: string; skin: string; placeholder: string; path: string | null; bone: number; offset: number };
|
|
611
|
+
type AttachmentHead = { timeline: string | null; deformOffset: number; slot: number; name: string; skin: string; placeholder: string; path: string | null; bone: number; offset: number };
|
|
587
612
|
|
|
588
613
|
const NO_INTS = new Int32Array(0);
|
|
589
614
|
const NO_NUMBERS = new Float64Array(0);
|
|
590
615
|
|
|
591
616
|
function vertexAttachment(at: AttachmentHead, draw: number, local: Float64Array, count: number): RuntimeAttachment {
|
|
592
|
-
return { ...at, draw, local, count, bindingCounts: NO_INTS, bindingBones: NO_INTS, bindingX: NO_NUMBERS, bindingY: NO_NUMBERS, bindingWeights: NO_NUMBERS };
|
|
617
|
+
return { ...at, draw, local, count, deformCount: draw === DRAW_MESH ? count : 0, bindingCounts: NO_INTS, bindingBones: NO_INTS, bindingX: NO_NUMBERS, bindingY: NO_NUMBERS, bindingWeights: NO_NUMBERS };
|
|
593
618
|
}
|
|
594
619
|
|
|
595
620
|
/** A mesh's (or a linked mesh's source's) vertices: every stored number through `Math.fround` (src/core/vertices.ts, *A mesh's vertices*). */
|
|
@@ -615,7 +640,7 @@ function meshAttachment(at: AttachmentHead, vertices: ModelVertices, indexOf: (n
|
|
|
615
640
|
ws.push(Math.fround(b.weight));
|
|
616
641
|
}
|
|
617
642
|
}
|
|
618
|
-
return { ...at, draw: DRAW_WEIGHTED, local: NO_NUMBERS, count: counts.length * 2, bindingCounts: Int32Array.from(counts), bindingBones: Int32Array.from(ids), bindingX: Float64Array.from(xs), bindingY: Float64Array.from(ys), bindingWeights: Float64Array.from(ws) };
|
|
643
|
+
return { ...at, deformCount: ids.length * 2, draw: DRAW_WEIGHTED, local: NO_NUMBERS, count: counts.length * 2, bindingCounts: Int32Array.from(counts), bindingBones: Int32Array.from(ids), bindingX: Float64Array.from(xs), bindingY: Float64Array.from(ys), bindingWeights: Float64Array.from(ws) };
|
|
619
644
|
}
|
|
620
645
|
|
|
621
646
|
function compileIk(r: CoreIkRecord, index: number, applied: boolean, bones: Int32Array, indexOf: (name: string) => number): RuntimeIk {
|
|
@@ -675,6 +700,7 @@ function compileAnimation(
|
|
|
675
700
|
slotIndex: ReadonlyMap<string, number>,
|
|
676
701
|
slots: readonly RuntimeSlot[],
|
|
677
702
|
constraints: readonly RuntimeConstraint[],
|
|
703
|
+
doc: CompiledDocument,
|
|
678
704
|
resolve: (slot: number, placeholder: string | null) => number,
|
|
679
705
|
): RuntimeAnimation {
|
|
680
706
|
const tl = anim.timelines;
|
|
@@ -682,7 +708,7 @@ function compileAnimation(
|
|
|
682
708
|
let cursors = 0;
|
|
683
709
|
const deform = tl.attachments.filter((a) => a.deform !== null);
|
|
684
710
|
const sequence = tl.attachments.filter((a) => a.sequence !== null);
|
|
685
|
-
|
|
711
|
+
|
|
686
712
|
if (sequence.length > 0) refusals.note('sequence-timeline', `${sequence.length} sequence timeline(s): ${sequence.slice(0, 3).map((a) => `${a.skin}/${a.slot}/${a.attachment}`).join(', ')}${sequence.length > 3 ? ', …' : ''}`, name);
|
|
687
713
|
if (tl.drawOrder.length > 0) refusals.note('draw-order-timeline', `${tl.drawOrder.length} draw-order key(s)`, name);
|
|
688
714
|
|
|
@@ -732,5 +758,12 @@ function compileAnimation(
|
|
|
732
758
|
constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, TRANSFORM_PROPERTIES.length), cursor: cursors++, flags: null };
|
|
733
759
|
}
|
|
734
760
|
}
|
|
735
|
-
|
|
761
|
+
const deforms: RuntimeDeformTimeline[] = deform.map(a => {
|
|
762
|
+
const g = doc.skins.find(s => s.name === a.skin)?.attachments[a.slot]?.[a.attachment]?.geometry;
|
|
763
|
+
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)`);
|
|
764
|
+
const keys = a.deform!;
|
|
765
|
+
const arrays = keys.map(k => heldArray(g.vertices, k));
|
|
766
|
+
return { identity: `${a.skin}/${a.slot}/${a.attachment}`, cursor: cursors++, curve: compileDeformCurve(keys), count: arrays[0].length, held: Float64Array.from(arrays.flat()) };
|
|
767
|
+
});
|
|
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 }))) };
|
|
736
769
|
}
|
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 2): 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.
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* hierarchy and never posed again by a constraint, nor the bones below it.
|
|
39
39
|
*/
|
|
40
40
|
import { COLLAPSED_X_AXIS_SQ, RUNTIME_DEG, RUNTIME_PI } from '../core/world.ts';
|
|
41
|
-
import { channelValue, keyAt } from './curves.ts';
|
|
41
|
+
import { channelValue, keyAt, deformFraction, CURVE_STEPPED } from './curves.ts';
|
|
42
42
|
import {
|
|
43
43
|
DRAW_MESH,
|
|
44
44
|
DRAW_NONE,
|
|
@@ -111,6 +111,9 @@ export class RuntimePose {
|
|
|
111
111
|
readonly shown: Int32Array;
|
|
112
112
|
/** Every attachment's world vertices at its `offset`, written by `computeVertices` for the attachments shown. */
|
|
113
113
|
readonly vertices: Float64Array;
|
|
114
|
+
/** Held/interpolated deform values, one pair per binding when weighted. */
|
|
115
|
+
readonly deform: Float64Array;
|
|
116
|
+
private readonly deformed: Uint8Array;
|
|
114
117
|
/** The running sum of `step`'s steps, and the animation time the last pose was posed at. */
|
|
115
118
|
trackTime = 0;
|
|
116
119
|
time = 0;
|
|
@@ -146,6 +149,8 @@ export class RuntimePose {
|
|
|
146
149
|
this.light = new Float64Array(ns * 4);
|
|
147
150
|
this.shown = new Int32Array(ns);
|
|
148
151
|
this.vertices = new Float64Array(model.vertexCount);
|
|
152
|
+
this.deform = new Float64Array(model.deformCount);
|
|
153
|
+
this.deformed = new Uint8Array(model.attachments.length);
|
|
149
154
|
this.cursors = new Int32Array(Math.max(1, model.cursors)).fill(-1);
|
|
150
155
|
this.ikMix = new Float64Array(nc);
|
|
151
156
|
this.ikSoftness = new Float64Array(nc);
|
|
@@ -195,6 +200,7 @@ export class RuntimePose {
|
|
|
195
200
|
this.constraintValues(null, 0);
|
|
196
201
|
this.solveConstraints();
|
|
197
202
|
this.slotValues(null, 0);
|
|
203
|
+
this.deformValues(null, 0);
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
/** The animation posed at its time `t`, from the setup pose (the header). */
|
|
@@ -209,6 +215,7 @@ export class RuntimePose {
|
|
|
209
215
|
this.constraintValues(anim, t);
|
|
210
216
|
this.solveConstraints();
|
|
211
217
|
this.slotValues(anim, t);
|
|
218
|
+
this.deformValues(anim, t);
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
/** 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). */
|
|
@@ -940,6 +947,32 @@ export class RuntimePose {
|
|
|
940
947
|
}
|
|
941
948
|
}
|
|
942
949
|
|
|
950
|
+
/** Reset on every pose, then match source identity after switches; no old frame's offsets survive. */
|
|
951
|
+
private deformValues(anim: RuntimeAnimation | null, t: number): void {
|
|
952
|
+
this.deformed.fill(0);
|
|
953
|
+
this.deform.fill(0);
|
|
954
|
+
if (anim === null || this.model.omitDeform) return;
|
|
955
|
+
for (let k = 0; k < anim.deforms.length; k++) {
|
|
956
|
+
const tl = anim.deforms[k];
|
|
957
|
+
const i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
958
|
+
if (i < 0) continue;
|
|
959
|
+
const held = i + 1 === tl.curve.keys || tl.curve.kinds[i] === CURVE_STEPPED;
|
|
960
|
+
const p = held ? 0 : deformFraction(tl.curve, i, t);
|
|
961
|
+
const start = i * tl.count;
|
|
962
|
+
for (let s = 0; s < this.shown.length; s++) {
|
|
963
|
+
const index = this.shown[s];
|
|
964
|
+
if (index < 0 || !this.model.slots[s].live) continue;
|
|
965
|
+
const at = this.model.attachments[index];
|
|
966
|
+
if (at.timeline !== tl.identity || at.deformCount === 0) continue;
|
|
967
|
+
this.deformed[index] = 1;
|
|
968
|
+
for (let q = 0; q < tl.count; q++) {
|
|
969
|
+
const v = tl.held[start + q];
|
|
970
|
+
this.deform[at.deformOffset + q] = held ? v : v + (tl.held[start + tl.count + q] - v) * p;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
943
976
|
/** Every shown attachment's world vertices into `vertices` at its offset (`regionCorners`, `worldVertices`). */
|
|
944
977
|
computeVertices(): void {
|
|
945
978
|
const m = this.model;
|
|
@@ -961,7 +994,8 @@ export class RuntimePose {
|
|
|
961
994
|
const d = W[b + W_D];
|
|
962
995
|
const wx = W[b + W_X];
|
|
963
996
|
const wy = W[b + W_Y];
|
|
964
|
-
const local = at.local;
|
|
997
|
+
const local = this.deformed[index] && at.draw === DRAW_MESH ? this.deform : at.local;
|
|
998
|
+
const lo = this.deformed[index] && at.draw === DRAW_MESH ? at.deformOffset : 0;
|
|
965
999
|
if (at.draw === DRAW_REGION) {
|
|
966
1000
|
for (let q = 0; q < 8; q += 2) {
|
|
967
1001
|
const lx = local[q];
|
|
@@ -971,8 +1005,8 @@ export class RuntimePose {
|
|
|
971
1005
|
}
|
|
972
1006
|
} else {
|
|
973
1007
|
for (let q = 0; q < at.count; q += 2) {
|
|
974
|
-
const x = local[q];
|
|
975
|
-
const y = local[q + 1];
|
|
1008
|
+
const x = local[lo + q];
|
|
1009
|
+
const y = local[lo + q + 1];
|
|
976
1010
|
out[o++] = x * a + y * bb + wx;
|
|
977
1011
|
out[o++] = x * c + y * d + wy;
|
|
978
1012
|
}
|
|
@@ -992,9 +1026,16 @@ export class RuntimePose {
|
|
|
992
1026
|
const end = n + counts[v];
|
|
993
1027
|
for (; n < end; n++) {
|
|
994
1028
|
const b = bones[n] * WORLDS;
|
|
995
|
-
const x = xs[n];
|
|
996
|
-
const y = ys[n];
|
|
997
1029
|
const weight = weights[n];
|
|
1030
|
+
// The pair belongs to this influence, before its transform and weight, never to the logical vertex.
|
|
1031
|
+
const deformed = this.deformed[index] !== 0;
|
|
1032
|
+
const x = deformed ? xs[n] + this.deform[at.deformOffset + n * 2] : xs[n];
|
|
1033
|
+
const y = deformed ? ys[n] + this.deform[at.deformOffset + n * 2 + 1] : ys[n];
|
|
1034
|
+
if (deformed && m.deformAfterWeights) {
|
|
1035
|
+
wx += (xs[n] * W[b + W_A] + ys[n] * W[b + W_B] + W[b + W_X]) * weight + this.deform[at.deformOffset + n * 2];
|
|
1036
|
+
wy += (xs[n] * W[b + W_C] + ys[n] * W[b + W_D] + W[b + W_Y]) * weight + this.deform[at.deformOffset + n * 2 + 1];
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
998
1039
|
wx += (x * W[b + W_A] + y * W[b + W_B] + W[b + W_X]) * weight;
|
|
999
1040
|
wy += (x * W[b + W_C] + y * W[b + W_D] + W[b + W_Y]) * weight;
|
|
1000
1041
|
}
|