@rig-c/core 2.70.0 → 2.72.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 +222 -13
- package/package.json +2 -1
- package/src/assertions/report.ts +1 -1
- package/src/compile.ts +126 -24
- package/src/core/animation.ts +56 -24
- package/src/core/events.ts +17 -5
- package/src/core/index.ts +62 -1
- package/src/core/raw.ts +38 -12
- package/src/core/tracks.ts +23 -8
- package/src/core/vertices.ts +182 -44
- package/src/deformgen.ts +25 -9
- package/src/depth.ts +17 -4
- package/src/meshcompare.ts +146 -22
- package/src/motion.ts +4 -4
- package/src/rig.ts +7 -4
- package/src/runtime/curves.ts +106 -0
- package/src/runtime/index.ts +84 -0
- package/src/runtime/model.ts +736 -0
- package/src/runtime/pose.ts +1054 -0
- package/src/trackgen.ts +27 -4
package/src/core/raw.ts
CHANGED
|
@@ -113,8 +113,8 @@
|
|
|
113
113
|
* naming why (`CoreInputError`): a renderer handed a pose with a block
|
|
114
114
|
* missing would draw a different picture in silence.
|
|
115
115
|
*/
|
|
116
|
-
import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber,
|
|
117
|
-
import {
|
|
116
|
+
import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, shownAttachment, sourceOfDoc, underNoSkin, underSkin, viewColours, WHITE, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
|
|
117
|
+
import { posedBoneWorldAlone, posedSlots, type CoreAnimationTimelines, type LocalAdjust, type TimelinePlant } from './animation.ts';
|
|
118
118
|
import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
|
|
119
119
|
import { freshStepContext, rotatePhysics, steppedPreviousPassWhy, translatePhysics, DEFAULT_PHYSICS_ENVIRONMENT, type PhysicsEnvironment, type PhysicsStepContext } from './constraints_physics.ts';
|
|
120
120
|
import { attachmentStates } from './deform.ts';
|
|
@@ -342,7 +342,7 @@ function scanBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>)
|
|
|
342
342
|
*/
|
|
343
343
|
function drawnGeometry(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode): { rows: CoreAttachmentRow[]; clips: CoreClipRow[]; drawnClipped: CoreClippedRow[] } {
|
|
344
344
|
const draw = drawWalkOf(doc, order, plant);
|
|
345
|
-
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && mode.live !== true && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
345
|
+
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices, deformedVertices: plant.deformedVertices }, mode.loop && mode.live !== true && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
346
346
|
if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
|
|
347
347
|
// The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
|
|
348
348
|
if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
|
|
@@ -423,12 +423,20 @@ export interface ScanPlant {
|
|
|
423
423
|
|
|
424
424
|
/** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
|
|
425
425
|
function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode = RAW_MODE): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
|
|
426
|
+
const stage = plant.stage;
|
|
427
|
+
if (stage !== undefined) stage.begin('geometry');
|
|
426
428
|
const geometry = drawnGeometry(doc, shown, world, order, plant, mode);
|
|
429
|
+
if (stage !== undefined) {
|
|
430
|
+
stage.end('geometry');
|
|
431
|
+
stage.begin('readout');
|
|
432
|
+
}
|
|
427
433
|
// Under a concave or inverse clip the oracle's `clipped` block is absent (the runtime's own triangle list); what the core draws is `drawnClipped`,
|
|
428
434
|
// its own decomposition, and the render samples each drawn triangle at its source triangle's affine UV, so the pixels are the decomposition's
|
|
429
435
|
// coverage alone (issue #964, `Mesh.source` in src/render.ts).
|
|
430
436
|
const rows = geometry.rows;
|
|
431
437
|
const drawn: RawDrawn[] = [];
|
|
438
|
+
// Each record's stated colour, read once per view (`viewColours`) and copied into the pose's own tuple, as the pose's arrays are its own.
|
|
439
|
+
const stated = viewColours(doc);
|
|
432
440
|
let k = 0;
|
|
433
441
|
for (const s of shown) {
|
|
434
442
|
const g = s.geometry;
|
|
@@ -436,7 +444,8 @@ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world:
|
|
|
436
444
|
const row = rows[k++];
|
|
437
445
|
if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
|
|
438
446
|
const record = doc.skins.find((x) => x.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
|
|
439
|
-
const
|
|
447
|
+
const read = record?.color === undefined ? WHITE : stated(record.color);
|
|
448
|
+
const colour: [number, number, number, number] = [read[0], read[1], read[2], read[3]];
|
|
440
449
|
let uvs: readonly number[];
|
|
441
450
|
let triangles: readonly number[];
|
|
442
451
|
let hull: number | null = null;
|
|
@@ -454,6 +463,7 @@ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world:
|
|
|
454
463
|
}
|
|
455
464
|
const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], numbersOf(c[3], `clip vertex of slot "${c[0]}"`, mode)]);
|
|
456
465
|
const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], numbersOf(c[3], `clipped vertex of slot "${c[0]}"`, mode), numbersOf(c[4], `clipped uv of slot "${c[0]}"`, mode), c[5]]);
|
|
466
|
+
if (stage !== undefined) stage.end('readout');
|
|
457
467
|
return { drawn, clips, clipped };
|
|
458
468
|
}
|
|
459
469
|
|
|
@@ -785,12 +795,16 @@ interface WalkStep {
|
|
|
785
795
|
* (`walkEach`) is one body: every number a pose carries is computed there and
|
|
786
796
|
* in the two assemblers below, by the same calls, whatever the shape. A shape
|
|
787
797
|
* chooses only what is assembled from what was posed — the scan drops the
|
|
788
|
-
* readings
|
|
798
|
+
* readings and copies nothing it reads is computed from — and keeps the
|
|
789
799
|
* order of every call that can refuse.
|
|
800
|
+
*
|
|
801
|
+
* No shape forms the oracle's bone rows: no walk pose carries them, and the
|
|
802
|
+
* rounding they are formed through refuses nothing, so every step poses the
|
|
803
|
+
* world alone (`posedBoneWorldAlone`). The rows are the sample's
|
|
804
|
+
* (`posedBoneRows`, `posedBoneWorld`), formed there as before. The core
|
|
805
|
+
* suite's `CO62` reads the walk's modules for a call that forms them.
|
|
790
806
|
*/
|
|
791
807
|
interface PoseShape<P> {
|
|
792
|
-
/** Whether a step forms the oracle's bone rows beside its world (`posedBoneWorld`), which no walk pose carries; `false` poses the world alone (`posedBoneWorldAlone`). */
|
|
793
|
-
rows: boolean;
|
|
794
808
|
/** The reset pose at the setup (`reset: 'setup'`). */
|
|
795
809
|
setup: (doc: CompiledDocument, world: Map<string, CoreWorld>, slots: CoreSlotRow[], drawOrder: string[], shown: ShownGeometry[], plant: CorePlant, mode: WalkMode) => P;
|
|
796
810
|
/** One step's pose. */
|
|
@@ -799,19 +813,21 @@ interface PoseShape<P> {
|
|
|
799
813
|
|
|
800
814
|
/** The raw entry's pose, assembled as it always was: at the reset the bones, then the drawn rows; at a step the drawn rows, the events, then the bones. */
|
|
801
815
|
const RAW_SHAPE: PoseShape<RawPose> = {
|
|
802
|
-
rows: true,
|
|
803
816
|
setup: (doc, world, slots, drawOrder, shown, plant, mode) => ({ trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...rawDrawn(doc, shown, world, drawOrder, plant, mode), events: [], shown: drawOrderOf(shown, drawOrder) }),
|
|
804
817
|
step: (doc, p, plant, mode) => {
|
|
805
818
|
const drawn = rawDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode);
|
|
819
|
+
const stage = plant.stage;
|
|
820
|
+
if (stage !== undefined) stage.begin('readout');
|
|
806
821
|
const events = p.events();
|
|
807
|
-
|
|
822
|
+
const pose: RawPose = { trackTime: p.trackTime, animationTime: p.animationTime, bones: rawBones(doc, p.world), slots: p.slots, drawOrder: p.drawOrder, ...drawn, events, shown: p.shown };
|
|
823
|
+
if (stage !== undefined) stage.end('readout');
|
|
824
|
+
return pose;
|
|
808
825
|
},
|
|
809
826
|
};
|
|
810
827
|
|
|
811
828
|
/** The scan's pose (`ScanPose`), in the raw shape's order: the scan's walk is a looping one, which fires no event. */
|
|
812
829
|
function scanShape(scanPlant: ScanPlant): PoseShape<ScanPose> {
|
|
813
830
|
return {
|
|
814
|
-
rows: false,
|
|
815
831
|
setup: (doc, world, slots, drawOrder, shown, plant, mode) => {
|
|
816
832
|
const bones = scanBones(doc, world);
|
|
817
833
|
return { animationTime: 0, bones, slots, drawn: scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant) };
|
|
@@ -908,7 +924,7 @@ function stepWalk<P>(track: TrackState<P>, dt: number, first = false): P {
|
|
|
908
924
|
const keys = anim.timelines.events;
|
|
909
925
|
const adjustment = adjust === undefined ? undefined : { callback: adjust, context: { trackTime, animation: anim.name, globalTime: trackTime } };
|
|
910
926
|
const pose = poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, t, {
|
|
911
|
-
world: (sliders) =>
|
|
927
|
+
world: (sliders) => posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjustment }),
|
|
912
928
|
slots: (sliders, placeholders) => posedSlots(doc, anim.timelines, t, raw, sliders, placeholders),
|
|
913
929
|
drawOrder: anim.timelines.drawOrder,
|
|
914
930
|
at: t,
|
|
@@ -948,20 +964,30 @@ export interface StepPosers {
|
|
|
948
964
|
* and a live tracks' step share.
|
|
949
965
|
*/
|
|
950
966
|
function poseStepFrom<P>(doc: CompiledDocument, plant: TimelinePlant, resolve: NonNullable<TimelinePlant['shown']>, shape: PoseShape<P>, mode: WalkMode, ctx: ReturnType<typeof freshStepContext>, first: boolean, trackTime: number, animationTime: number, posers: StepPosers): P {
|
|
967
|
+
const stage = plant.stage;
|
|
951
968
|
const sliders: SliderApplication[] = [];
|
|
952
969
|
const world = posers.world(sliders);
|
|
953
970
|
if (first) ctx.phase = 'update';
|
|
971
|
+
if (stage !== undefined) stage.begin('readout');
|
|
954
972
|
const placeholders = new Map<string, string | null>();
|
|
955
973
|
const slots = posers.slots(sliders, placeholders);
|
|
956
974
|
if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
|
|
957
975
|
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
958
|
-
|
|
976
|
+
const baseOrder = evalOrder(doc.slots.length, posers.drawOrder, posers.at) ?? doc.slots.map((_s, k) => k);
|
|
977
|
+
let order = baseOrder;
|
|
959
978
|
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
960
979
|
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
980
|
+
if (stage !== undefined) {
|
|
981
|
+
stage.end('readout');
|
|
982
|
+
stage.begin('timeline');
|
|
983
|
+
}
|
|
961
984
|
const states = attachmentStates(doc, resolve, placeholders, posers.sample, sliders, plant, posers.mixed);
|
|
985
|
+
if (stage !== undefined) stage.end('timeline');
|
|
962
986
|
if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
|
|
987
|
+
if (stage !== undefined) stage.begin('readout');
|
|
963
988
|
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
964
989
|
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
990
|
+
if (stage !== undefined) stage.end('readout');
|
|
965
991
|
const events = (): RawEvent[] => posers.events().map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
|
|
966
992
|
return shape.step(doc, { trackTime, animationTime, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
|
|
967
993
|
}
|
package/src/core/tracks.ts
CHANGED
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
* every later call refuses naming the call it was refused at.
|
|
161
161
|
*/
|
|
162
162
|
import type { ModelBone, ModelSlot, ModelVertices } from '../model.ts';
|
|
163
|
-
import { activeBones, CoreInputError, rawNumber,
|
|
163
|
+
import { activeBones, CoreInputError, rawNumber, shownAttachment, viewColours, WHITE, type CompiledDocument, type CoreShown, type StatedColour } from './index.ts';
|
|
164
164
|
import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, slotRowsFrom, steppedLocalsWorld, type BoneTimelineKind, type CoreKey, type LocalAdjust, type SlotBase, type SlotColourKind, type TimelinePlant } from './animation.ts';
|
|
165
165
|
import { deformAt, setupArray, type CoreAttachmentTimeline } from './deform.ts';
|
|
166
166
|
import { eventsFired, type CoreEventKey, type CoreEventRow } from './events.ts';
|
|
@@ -440,6 +440,9 @@ function trackComplete(e: Entry): number {
|
|
|
440
440
|
const signum = (v: number): number => (v > 0 ? 1 : v < 0 ? -1 : 0);
|
|
441
441
|
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
442
442
|
|
|
443
|
+
/** A slot's setup dark colour where it states none: never read, since such a slot carries no dark channel and a two-colour timeline returns before it (`applyColour`). */
|
|
444
|
+
const NO_DARK: readonly number[] = [0, 0, 0];
|
|
445
|
+
|
|
443
446
|
// ---------------------------------------------------------------------------
|
|
444
447
|
// one step's skeleton — the pose the entries are applied to
|
|
445
448
|
// ---------------------------------------------------------------------------
|
|
@@ -473,6 +476,8 @@ class Pose {
|
|
|
473
476
|
private readonly active: ReadonlySet<string>;
|
|
474
477
|
private readonly live: boolean[];
|
|
475
478
|
private readonly resolved = new Map<string, Resolved>();
|
|
479
|
+
/** The view's stated colours (`viewColours`). */
|
|
480
|
+
readonly stated: (hex: string) => StatedColour;
|
|
476
481
|
|
|
477
482
|
constructor(
|
|
478
483
|
private readonly doc: CompiledDocument,
|
|
@@ -480,12 +485,16 @@ class Pose {
|
|
|
480
485
|
) {
|
|
481
486
|
this.bones = [...doc.bones];
|
|
482
487
|
this.active = activeBones(doc);
|
|
483
|
-
|
|
488
|
+
// The view's stated colours, read once (`viewColours`) and copied into the step's own channels; a planted reader of the light colour is called as it always was.
|
|
489
|
+
const stated = viewColours(doc);
|
|
490
|
+
this.stated = stated;
|
|
491
|
+
const colour = plant.colour ?? stated;
|
|
484
492
|
const gate = plant.slotTimelines ?? slotTimelinesApply;
|
|
485
493
|
this.live = doc.slots.map((s) => gate(doc, s, this.active));
|
|
486
494
|
this.slots = doc.slots.map((slot): SlotState => {
|
|
487
|
-
const setupDark = slot.dark === undefined ? null :
|
|
488
|
-
|
|
495
|
+
const setupDark = slot.dark === undefined ? null : stated(slot.dark);
|
|
496
|
+
const setupLight = slot.color === undefined ? WHITE : colour(slot.color);
|
|
497
|
+
return { placeholder: slot.setup, light: [setupLight[0], setupLight[1], setupLight[2], setupLight[3]], dark: setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]], deform: null, seen: [], attachmentState: ATTACH_NONE };
|
|
489
498
|
});
|
|
490
499
|
}
|
|
491
500
|
|
|
@@ -1103,9 +1112,9 @@ class Mixer {
|
|
|
1103
1112
|
const s = pose.slots[tl.slot];
|
|
1104
1113
|
const l = s.light;
|
|
1105
1114
|
const d = s.dark;
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
const sd =
|
|
1115
|
+
// The setup colours, read only: the view's (`viewColours`), or a planted reader's light colour.
|
|
1116
|
+
const sl = slot.color === undefined ? WHITE : (this.plant.colour ?? pose.stated)(slot.color);
|
|
1117
|
+
const sd = slot.dark === undefined ? NO_DARK : pose.stated(slot.dark);
|
|
1109
1118
|
const clampLight = (): void => {
|
|
1110
1119
|
for (let k = 0; k < 4; k++) l[k] = clamp01(l[k]);
|
|
1111
1120
|
};
|
|
@@ -1466,7 +1475,10 @@ class Tracks implements LiveTracks {
|
|
|
1466
1475
|
if (typeof dt !== 'number' || !Number.isFinite(dt) || dt < 0) throw new CoreInputError(`the live tracks' step ${n} is ${String(dt)}, not a finite time at or above 0`);
|
|
1467
1476
|
return this.guard(`step ${n}`, () => {
|
|
1468
1477
|
this.time += dt;
|
|
1478
|
+
const stage = this.plant.stage;
|
|
1479
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1469
1480
|
this.mixer.update(dt);
|
|
1481
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1470
1482
|
const pose = this.pose(dt, false);
|
|
1471
1483
|
this.steps = n;
|
|
1472
1484
|
return pose;
|
|
@@ -1489,6 +1501,8 @@ class Tracks implements LiveTracks {
|
|
|
1489
1501
|
const ctx = path.ctx;
|
|
1490
1502
|
const before = ctx.time;
|
|
1491
1503
|
ctx.time += this.clock === undefined ? dt : this.clock(dt);
|
|
1504
|
+
const stage = this.plant.stage;
|
|
1505
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1492
1506
|
const pose = new Pose(doc, this.plant);
|
|
1493
1507
|
const animationTime = this.mixer.apply(pose);
|
|
1494
1508
|
const entry = this.mixer.tracks[0] ?? null;
|
|
@@ -1497,8 +1511,9 @@ class Tracks implements LiveTracks {
|
|
|
1497
1511
|
context: { trackTime: entry?.trackTime ?? null, animation: entry?.anim.name ?? null, globalTime: this.time },
|
|
1498
1512
|
};
|
|
1499
1513
|
const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));
|
|
1514
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1500
1515
|
return path.step(first, this.time, animationTime, {
|
|
1501
|
-
world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjustment }
|
|
1516
|
+
world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjustment }),
|
|
1502
1517
|
slots: (sliders, placeholders) => slotRowsFrom(doc, path.raw, sliders, placeholders, (_slot, _live): SlotBase => {
|
|
1503
1518
|
const s = pose.slots[doc.slots.indexOf(_slot)];
|
|
1504
1519
|
return { placeholder: s.placeholder, light: s.light, dark: s.dark };
|
package/src/core/vertices.ts
CHANGED
|
@@ -191,6 +191,8 @@ export type CoreClipRow = [string, string, string | null, Array<number | null>];
|
|
|
191
191
|
export type RegionPoser = (region: CoreRegionGeometry & { atlas: ModelAtlasRect }, bone: CoreWorld) => number[];
|
|
192
192
|
/** What computes a vertex array's world positions: `worldVertices` unless a caller passes another. */
|
|
193
193
|
export type VertexPoser = (vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords?: (v: number) => number) => number[];
|
|
194
|
+
/** What computes a deformed vertex array's world positions in one pass: `deformedWorldVertices` unless a caller passes another. */
|
|
195
|
+
export type DeformedVertexPoser = (vertices: ModelVertices, deform: readonly number[], bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>) => number[];
|
|
194
196
|
|
|
195
197
|
/** A deformed array's coordinates are read as they are (`./deform.ts`, `deformedVertices`): they are not the stored float32s. */
|
|
196
198
|
export const EXACT_COORDS = (v: number): number => v;
|
|
@@ -222,31 +224,157 @@ export function regionCorners(region: CoreRegionGeometry & { atlas: ModelAtlasRe
|
|
|
222
224
|
return out;
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
/** A weighted vertex array as the model states it. */
|
|
228
|
+
export type WeightedVertices = Extract<ModelVertices, { weighted: true }>;
|
|
229
|
+
|
|
225
230
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
231
|
+
* One influence of a compiled weighted vertex (issue #1580): its bone as an
|
|
232
|
+
* index into `CompiledWeightedVertices.bones`, its raw coordinates (`x`, `y`)
|
|
233
|
+
* for a caller passing a custom `coords`, and its `Math.fround`-rounded
|
|
234
|
+
* coordinates (`fx`, `fy`) and `weight` for the default posing path.
|
|
230
235
|
*/
|
|
231
|
-
export
|
|
236
|
+
export interface CompiledBinding {
|
|
237
|
+
readonly bone: number;
|
|
238
|
+
readonly x: number;
|
|
239
|
+
readonly y: number;
|
|
240
|
+
readonly fx: number;
|
|
241
|
+
readonly fy: number;
|
|
242
|
+
readonly weight: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* A weighted vertex array's bone bindings compiled to integer indices into
|
|
247
|
+
* `bones` (issue #1580): `bones` lists the distinct bone names in exact
|
|
248
|
+
* first-occurrence order across `vertices.bindings`, and `bindings` keeps the
|
|
249
|
+
* outer vertex order and inner influence order untouched so the summation
|
|
250
|
+
* order and every posed coordinate stay bit-for-bit (`Object.is`) identical.
|
|
251
|
+
*/
|
|
252
|
+
export interface CompiledWeightedVertices {
|
|
253
|
+
readonly bones: readonly string[];
|
|
254
|
+
readonly bindings: ReadonlyArray<readonly CompiledBinding[]>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Each weighted vertex array's compiled index table, keyed on the `WeightedVertices` object itself. */
|
|
258
|
+
const compiledWeightedByVertices = new WeakMap<WeightedVertices, CompiledWeightedVertices>();
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compile `vertices`'s bone names to integer indices in first-occurrence order
|
|
262
|
+
* (issue #1580). With `mapIndex`, each binding's index is passed through it
|
|
263
|
+
* instead (the core suite's wrong-index plant).
|
|
264
|
+
*/
|
|
265
|
+
export function compileWeightedVertices(
|
|
266
|
+
vertices: WeightedVertices,
|
|
267
|
+
mapIndex?: (boneIndex: number, boneCount: number) => number,
|
|
268
|
+
): CompiledWeightedVertices {
|
|
269
|
+
const bones: string[] = [];
|
|
270
|
+
const indexByName = new Map<string, number>();
|
|
271
|
+
for (const influences of vertices.bindings) {
|
|
272
|
+
for (const binding of influences) {
|
|
273
|
+
if (!indexByName.has(binding.bone)) {
|
|
274
|
+
indexByName.set(binding.bone, bones.length);
|
|
275
|
+
bones.push(binding.bone);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const count = bones.length;
|
|
280
|
+
const bindings: CompiledBinding[][] = vertices.bindings.map((influences) =>
|
|
281
|
+
influences.map((binding) => {
|
|
282
|
+
const idx = indexByName.get(binding.bone) as number;
|
|
283
|
+
return {
|
|
284
|
+
bone: mapIndex === undefined ? idx : mapIndex(idx, count),
|
|
285
|
+
x: binding.x,
|
|
286
|
+
y: binding.y,
|
|
287
|
+
fx: Math.fround(binding.x),
|
|
288
|
+
fy: Math.fround(binding.y),
|
|
289
|
+
weight: Math.fround(binding.weight),
|
|
290
|
+
};
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
return { bones, bindings };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The cached `CompiledWeightedVertices` for `vertices`, compiled once per
|
|
298
|
+
* `WeightedVertices` object and never shared across two different vertex
|
|
299
|
+
* objects.
|
|
300
|
+
*/
|
|
301
|
+
export function compiledWeightedOf(vertices: WeightedVertices): CompiledWeightedVertices {
|
|
302
|
+
let kept = compiledWeightedByVertices.get(vertices);
|
|
303
|
+
if (kept === undefined) {
|
|
304
|
+
kept = compileWeightedVertices(vertices);
|
|
305
|
+
compiledWeightedByVertices.set(vertices, kept);
|
|
306
|
+
}
|
|
307
|
+
return kept;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Resolve each distinct bone of `bones` once per pose call into a dense array
|
|
312
|
+
* indexed by `CompiledBinding.bone`. Because `bones` is in first-occurrence
|
|
313
|
+
* order across `vertices.bindings`, a missing bone throws the same error on
|
|
314
|
+
* the same bone name as a per-influence lookup.
|
|
315
|
+
*/
|
|
316
|
+
function resolveBindingBones(bones: readonly string[], world: ReadonlyMap<string, CoreWorld>): CoreWorld[] {
|
|
317
|
+
const transforms = new Array<CoreWorld>(bones.length);
|
|
318
|
+
for (let i = 0; i < bones.length; i++) {
|
|
319
|
+
const name = bones[i];
|
|
320
|
+
const t = world.get(name);
|
|
321
|
+
if (t === undefined) throw new Error(`a binding names bone "${name}", which has no world transform (readModel refuses it first)`);
|
|
322
|
+
transforms[i] = t;
|
|
323
|
+
}
|
|
324
|
+
return transforms;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Evaluate a compiled weighted vertex table against `world` (issue #1580),
|
|
329
|
+
* looking up each distinct bone once per call and summing influences in their
|
|
330
|
+
* original order.
|
|
331
|
+
*/
|
|
332
|
+
export function compiledWorldVertices(
|
|
333
|
+
compiled: CompiledWeightedVertices,
|
|
334
|
+
world: ReadonlyMap<string, CoreWorld>,
|
|
335
|
+
coords: (v: number) => number = Math.fround,
|
|
336
|
+
): number[] {
|
|
337
|
+
const transforms = resolveBindingBones(compiled.bones, world);
|
|
232
338
|
const out: number[] = [];
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
339
|
+
const fround = coords === Math.fround;
|
|
340
|
+
for (const influences of compiled.bindings) {
|
|
341
|
+
let wx = 0;
|
|
342
|
+
let wy = 0;
|
|
343
|
+
for (const binding of influences) {
|
|
344
|
+
const t = transforms[binding.bone];
|
|
345
|
+
const x = fround ? binding.fx : coords(binding.x);
|
|
346
|
+
const y = fround ? binding.fy : coords(binding.y);
|
|
347
|
+
const weight = binding.weight;
|
|
348
|
+
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
349
|
+
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
238
350
|
}
|
|
239
|
-
|
|
351
|
+
out.push(wx, wy);
|
|
240
352
|
}
|
|
241
|
-
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Evaluate a compiled weighted vertex table with `deform` in one pass
|
|
358
|
+
* (issues #1530, #1580), looking up each distinct bone once per call and
|
|
359
|
+
* summing influences in their original order.
|
|
360
|
+
*/
|
|
361
|
+
export function compiledDeformedWorldVertices(
|
|
362
|
+
compiled: CompiledWeightedVertices,
|
|
363
|
+
deform: readonly number[],
|
|
364
|
+
world: ReadonlyMap<string, CoreWorld>,
|
|
365
|
+
): number[] {
|
|
366
|
+
const transforms = resolveBindingBones(compiled.bones, world);
|
|
367
|
+
const out: number[] = [];
|
|
368
|
+
let k = 0;
|
|
369
|
+
for (const influences of compiled.bindings) {
|
|
242
370
|
let wx = 0;
|
|
243
371
|
let wy = 0;
|
|
244
372
|
for (const binding of influences) {
|
|
245
|
-
const t =
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
const weight =
|
|
373
|
+
const t = transforms[binding.bone];
|
|
374
|
+
const x = binding.fx + deform[k];
|
|
375
|
+
const y = binding.fy + deform[k + 1];
|
|
376
|
+
k += 2;
|
|
377
|
+
const weight = binding.weight;
|
|
250
378
|
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
251
379
|
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
252
380
|
}
|
|
@@ -255,20 +383,42 @@ export function worldVertices(vertices: ModelVertices, bone: CoreWorld, world: R
|
|
|
255
383
|
return out;
|
|
256
384
|
}
|
|
257
385
|
|
|
386
|
+
/**
|
|
387
|
+
* A vertex array's world positions — the header's mesh rule, every stored
|
|
388
|
+
* number through `Math.fround`; with `coords`, the coordinates through it
|
|
389
|
+
* instead (a deformed array's are read as they are, `EXACT_COORDS`), the
|
|
390
|
+
* weights through `Math.fround` still. Weighted bone bindings are compiled once
|
|
391
|
+
* per vertex object to integer indices (`compiledWeightedOf`, issue #1580) so
|
|
392
|
+
* each pose looks up each distinct bone once rather than per influence.
|
|
393
|
+
*/
|
|
394
|
+
export function worldVertices(vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords: (v: number) => number = Math.fround): number[] {
|
|
395
|
+
if (!vertices.weighted) {
|
|
396
|
+
const out: number[] = [];
|
|
397
|
+
for (let i = 0; i + 1 < vertices.xy.length; i += 2) {
|
|
398
|
+
const x = coords(vertices.xy[i]);
|
|
399
|
+
const y = coords(vertices.xy[i + 1]);
|
|
400
|
+
out.push(x * bone.a + y * bone.b + bone.worldX, x * bone.c + y * bone.d + bone.worldY);
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
return compiledWorldVertices(compiledWeightedOf(vertices), world, coords);
|
|
405
|
+
}
|
|
406
|
+
|
|
258
407
|
/**
|
|
259
408
|
* `worldVertices(deformedVertices(vertices, deform), bone, world, EXACT_COORDS)`
|
|
260
|
-
* in one pass (
|
|
409
|
+
* in one pass (issues #1530, #1580): the same operations on the same numbers in the
|
|
261
410
|
* same order — an unweighted vertex's coordinates are the deform array's,
|
|
262
411
|
* read as they are; a weighted binding's are its float32 coordinate plus the
|
|
263
|
-
* deform's pair, its weight through `Math.fround
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
412
|
+
* deform's pair, its weight through `Math.fround`, its bone transform indexed
|
|
413
|
+
* through `compiledWeightedOf` — without building the deformed array (and,
|
|
414
|
+
* weighted, one binding record per influence) on every pose only to map it
|
|
415
|
+
* again. A planted vertex poser still reads the deformed array (`poseGeometry`).
|
|
416
|
+
* Posed output is held equal by the core gate's sample attachments and by
|
|
417
|
+
* `tools/posed_frames.ts` across the change.
|
|
268
418
|
*/
|
|
269
419
|
export function deformedWorldVertices(vertices: ModelVertices, deform: readonly number[], bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>): number[] {
|
|
270
|
-
const out: number[] = [];
|
|
271
420
|
if (!vertices.weighted) {
|
|
421
|
+
const out: number[] = [];
|
|
272
422
|
for (let i = 0; i + 1 < deform.length; i += 2) {
|
|
273
423
|
const x = deform[i];
|
|
274
424
|
const y = deform[i + 1];
|
|
@@ -276,23 +426,7 @@ export function deformedWorldVertices(vertices: ModelVertices, deform: readonly
|
|
|
276
426
|
}
|
|
277
427
|
return out;
|
|
278
428
|
}
|
|
279
|
-
|
|
280
|
-
for (const influences of vertices.bindings) {
|
|
281
|
-
let wx = 0;
|
|
282
|
-
let wy = 0;
|
|
283
|
-
for (const binding of influences) {
|
|
284
|
-
const t = world.get(binding.bone);
|
|
285
|
-
if (t === undefined) throw new Error(`a binding names bone "${binding.bone}", which has no world transform (readModel refuses it first)`);
|
|
286
|
-
const x = Math.fround(binding.x) + deform[k];
|
|
287
|
-
const y = Math.fround(binding.y) + deform[k + 1];
|
|
288
|
-
k += 2;
|
|
289
|
-
const weight = Math.fround(binding.weight);
|
|
290
|
-
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
291
|
-
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
292
|
-
}
|
|
293
|
-
out.push(wx, wy);
|
|
294
|
-
}
|
|
295
|
-
return out;
|
|
429
|
+
return compiledDeformedWorldVertices(compiledWeightedOf(vertices), deform, world);
|
|
296
430
|
}
|
|
297
431
|
|
|
298
432
|
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -342,7 +476,10 @@ function readVertices(value: unknown, where: string, bones: ReadonlySet<string>,
|
|
|
342
476
|
for (const key of ['x', 'y', 'weight'] as const) if (!finite(b[key])) problems.push(`${at}: ${key} is ${JSON.stringify(b[key]) ?? 'absent'}, not a finite number`);
|
|
343
477
|
});
|
|
344
478
|
});
|
|
345
|
-
|
|
479
|
+
if (problems.length !== before) return undefined;
|
|
480
|
+
const weighted: WeightedVertices = { weighted: true, bindings: bindings as ModelBinding[][] };
|
|
481
|
+
compiledWeightedByVertices.set(weighted, compileWeightedVertices(weighted));
|
|
482
|
+
return weighted;
|
|
346
483
|
}
|
|
347
484
|
|
|
348
485
|
/**
|
|
@@ -529,11 +666,12 @@ export function poseGeometry(
|
|
|
529
666
|
world: ReadonlyMap<string, CoreWorld>,
|
|
530
667
|
sourceOf: SourceOf,
|
|
531
668
|
round: (v: number) => number | null,
|
|
532
|
-
plant: { region?: RegionPoser; vertices?: VertexPoser } = {},
|
|
669
|
+
plant: { region?: RegionPoser; vertices?: VertexPoser; deformedVertices?: DeformedVertexPoser } = {},
|
|
533
670
|
draw?: DrawWalk,
|
|
534
671
|
): { attachments: CoreAttachmentRow[] | null; attachmentsWhy: string | null; clips: CoreClipRow[]; clipped: CoreClippedRow[] | null; clippedWhy: string | null; drawnClipped: CoreClippedRow[] | null; drawnClippedWhy: string | null } {
|
|
535
672
|
const region = plant.region ?? regionCorners;
|
|
536
673
|
const vertices = plant.vertices ?? worldVertices;
|
|
674
|
+
const deformPoser = plant.deformedVertices ?? deformedWorldVertices;
|
|
537
675
|
const attachments: CoreAttachmentRow[] = [];
|
|
538
676
|
const clips: CoreClipRow[] = [];
|
|
539
677
|
const nulls: string[] = [];
|
|
@@ -548,7 +686,7 @@ export function poseGeometry(
|
|
|
548
686
|
// A deform array replaces the stored vertices, its coordinates read as they are (`./deform.ts`).
|
|
549
687
|
// Unplanted, the deformed positions are read in one pass (`deformedWorldVertices`, issue #1530) rather than through a deformed copy of the array.
|
|
550
688
|
const deform = s.deform;
|
|
551
|
-
const drawn = (v: ModelVertices): number[] => (deform === undefined ? vertices(v, bone, world) : plant.vertices === undefined ?
|
|
689
|
+
const drawn = (v: ModelVertices): number[] => (deform === undefined ? vertices(v, bone, world) : plant.vertices === undefined || plant.deformedVertices !== undefined ? deformPoser(v, deform, bone, world) : vertices(deformedVertices(v, deform), bone, world, EXACT_COORDS));
|
|
552
690
|
if (g.kind === 'region') {
|
|
553
691
|
const rect = s.frame === undefined ? g.region.atlas : (g.region.frames?.[s.frame] ?? g.region.atlas);
|
|
554
692
|
if (rect === null) {
|
package/src/deformgen.ts
CHANGED
|
@@ -121,6 +121,16 @@ export interface DeformTurn {
|
|
|
121
121
|
degrees: number;
|
|
122
122
|
/** Where the axis crosses the driving coordinate. Default 0. */
|
|
123
123
|
about?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Whether the displacement includes the foreshortening of the vertex's setup
|
|
126
|
+
* position `(u − about)·(cos t − 1)`. Default `true`.
|
|
127
|
+
*
|
|
128
|
+
* When `false`, each vertex moves by `−z·sin t` alone — for a rig whose plate
|
|
129
|
+
* proportions are held at their front-view width across the turn and whose
|
|
130
|
+
* triangles therefore fold at `sin t = A₀ / A_axis` (`asin`) rather than
|
|
131
|
+
* `tan t = A₀ / A_axis` (`atan`) (issue #1584).
|
|
132
|
+
*/
|
|
133
|
+
narrowing?: boolean;
|
|
124
134
|
/** Scales each vertex's offset by a weight in [0, 1] — see `DeformEnvelope`. */
|
|
125
135
|
envelope?: DeformEnvelope;
|
|
126
136
|
}
|
|
@@ -364,7 +374,7 @@ export function evaluateDeformTransform(
|
|
|
364
374
|
round: Rounder,
|
|
365
375
|
where: string,
|
|
366
376
|
/**
|
|
367
|
-
* Per-vertex `z` for this attachment, when its generator named a depth map.
|
|
377
|
+
* Per-vertex `z` for this attachment, when it or its generator named a depth map.
|
|
368
378
|
* Null when it named none — which is what makes `"depth": true` refusable by
|
|
369
379
|
* name rather than silently falling back to a cylinder.
|
|
370
380
|
*/
|
|
@@ -438,7 +448,7 @@ export function evaluateDeformTransform(
|
|
|
438
448
|
if (fromDepth && depth === null) {
|
|
439
449
|
throw new CompileError(
|
|
440
450
|
`${where}: transform ${kind} says "depth": true and this attachment has no depth map. The map is named on ` +
|
|
441
|
-
'
|
|
451
|
+
'an authored attachment or its contour/grid generator, as `"depth": { "image": …, "near": …, "zScale": … }`, because it describes ' +
|
|
442
452
|
'the art rather than this key. Name it there, or give this key a radius.', 'CE_DEFORM_DEPTH_MAP_REQUIRED',
|
|
443
453
|
);
|
|
444
454
|
}
|
|
@@ -457,6 +467,7 @@ export function evaluateDeformTransform(
|
|
|
457
467
|
const rad = (degrees * Math.PI) / 180;
|
|
458
468
|
const cosMinus1 = Math.cos(rad) - 1;
|
|
459
469
|
const sin = Math.sin(rad);
|
|
470
|
+
const narrowing = t.narrowing !== false;
|
|
460
471
|
// The driving coordinate is the one the axis is perpendicular to: x for a
|
|
461
472
|
// yaw (a vertical axis), y for a pitch (a horizontal one). The other
|
|
462
473
|
// component of every pair is 0, because a turn about an axis moves
|
|
@@ -481,11 +492,11 @@ export function evaluateDeformTransform(
|
|
|
481
492
|
// mesh was built (`sampleContourDepth`) rather than here, where the
|
|
482
493
|
// sheet is long out of reach.
|
|
483
494
|
const z = fromDepth ? (depth as readonly number[])[v] : Math.sqrt(radius * radius - u * u);
|
|
484
|
-
const d = u * cosMinus1 - z * sin;
|
|
495
|
+
const d = (narrowing ? u * cosMinus1 : 0) - z * sin;
|
|
485
496
|
offsets[2 * v + along] = round(widen(d));
|
|
486
497
|
offsets[2 * v + (1 - along)] = 0;
|
|
487
498
|
}
|
|
488
|
-
identity = round(cosMinus1) === 0 && round(sin) === 0;
|
|
499
|
+
identity = (narrowing ? round(cosMinus1) === 0 : true) && round(sin) === 0;
|
|
489
500
|
identitySpelling = 'degrees 0';
|
|
490
501
|
sampledTo = fromDepth
|
|
491
502
|
? `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
|
|
@@ -494,15 +505,20 @@ export function evaluateDeformTransform(
|
|
|
494
505
|
: `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
|
|
495
506
|
`${kind === 'yaw' ? 'x' : 'y'} — check that this attachment's setup geometry is the shape the radius says it is`;
|
|
496
507
|
const c = kind === 'yaw' ? 'x' : 'y';
|
|
508
|
+
const narrowingNote = narrowing ? '' : ' narrowing=false';
|
|
497
509
|
stated = fromDepth
|
|
498
|
-
? `depth=true degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`
|
|
499
|
-
: `radius=${radius} degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`;
|
|
510
|
+
? `depth=true degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}${narrowingNote}`
|
|
511
|
+
: `radius=${radius} degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}${narrowingNote}`;
|
|
500
512
|
formula = fromDepth
|
|
501
|
-
?
|
|
502
|
-
|
|
513
|
+
? narrowing
|
|
514
|
+
? `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = the vertex's sampled depth`
|
|
515
|
+
: `d${c} = −z·sin t, z = the vertex's sampled depth`
|
|
516
|
+
: narrowing
|
|
517
|
+
? `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = √(radius² − (${c}−about)²)`
|
|
518
|
+
: `d${c} = −z·sin t, z = √(radius² − (${c}−about)²)`;
|
|
503
519
|
derived = [
|
|
504
520
|
`t = ${round(rad)} rad`,
|
|
505
|
-
`cos t − 1 = ${round(cosMinus1)}
|
|
521
|
+
...(narrowing ? [`cos t − 1 = ${round(cosMinus1)}`] : []),
|
|
506
522
|
`sin t = ${round(sin)}`,
|
|
507
523
|
];
|
|
508
524
|
if (fromDepth) {
|