@aura3d/assets 3.0.1 → 4.0.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/dist/AdvancedAssetCorpus.d.ts.map +1 -1
- package/dist/AdvancedAssetCorpus.js +7 -12
- package/dist/AdvancedAssetCorpus.js.map +1 -1
- package/dist/AssetDecoderRegistry.d.ts +56 -0
- package/dist/AssetDecoderRegistry.d.ts.map +1 -0
- package/dist/AssetDecoderRegistry.js +225 -0
- package/dist/AssetDecoderRegistry.js.map +1 -0
- package/dist/GLTFAnimationRuntime.d.ts +216 -2
- package/dist/GLTFAnimationRuntime.d.ts.map +1 -1
- package/dist/GLTFAnimationRuntime.js +674 -38
- package/dist/GLTFAnimationRuntime.js.map +1 -1
- package/dist/GLTFAssetParseCache.d.ts +24 -0
- package/dist/GLTFAssetParseCache.d.ts.map +1 -0
- package/dist/GLTFAssetParseCache.js +100 -0
- package/dist/GLTFAssetParseCache.js.map +1 -0
- package/dist/GLTFExtensionSupport.js +3 -3
- package/dist/GLTFExtensionSupport.js.map +1 -1
- package/dist/GLTFLoader.d.ts +28 -1
- package/dist/GLTFLoader.d.ts.map +1 -1
- package/dist/GLTFLoader.js +176 -23
- package/dist/GLTFLoader.js.map +1 -1
- package/dist/GLTFRenderResources.d.ts +77 -2
- package/dist/GLTFRenderResources.d.ts.map +1 -1
- package/dist/GLTFRenderResources.js +310 -153
- package/dist/GLTFRenderResources.js.map +1 -1
- package/dist/KTX2BasisTextureTranscoder.d.ts +70 -12
- package/dist/KTX2BasisTextureTranscoder.d.ts.map +1 -1
- package/dist/KTX2BasisTextureTranscoder.js +225 -192
- package/dist/KTX2BasisTextureTranscoder.js.map +1 -1
- package/dist/KTX2TargetSelection.d.ts +25 -0
- package/dist/KTX2TargetSelection.d.ts.map +1 -0
- package/dist/KTX2TargetSelection.js +36 -0
- package/dist/KTX2TargetSelection.js.map +1 -0
- package/dist/KTX2TranscodeDriver.d.ts +70 -0
- package/dist/KTX2TranscodeDriver.d.ts.map +1 -0
- package/dist/KTX2TranscodeDriver.js +104 -0
- package/dist/KTX2TranscodeDriver.js.map +1 -0
- package/dist/KTX2TranscodeWorker.d.ts +30 -0
- package/dist/KTX2TranscodeWorker.d.ts.map +1 -0
- package/dist/KTX2TranscodeWorker.js +230 -0
- package/dist/KTX2TranscodeWorker.js.map +1 -0
- package/dist/MikkTSpaceTangents.d.ts +43 -0
- package/dist/MikkTSpaceTangents.d.ts.map +1 -0
- package/dist/MikkTSpaceTangents.js +149 -0
- package/dist/MikkTSpaceTangents.js.map +1 -0
- package/dist/asset-corpus/ProductionAssetCorpus.d.ts.map +1 -1
- package/dist/asset-corpus/ProductionAssetCorpus.js +7 -13
- package/dist/asset-corpus/ProductionAssetCorpus.js.map +1 -1
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.d.ts +43 -0
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.d.ts.map +1 -1
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.js +146 -22
- package/dist/asset-corpus/ProductionGLTFRenderPipeline.js.map +1 -1
- package/dist/assetPhaseMarks.d.ts +14 -0
- package/dist/assetPhaseMarks.d.ts.map +1 -0
- package/dist/assetPhaseMarks.js +23 -0
- package/dist/assetPhaseMarks.js.map +1 -0
- package/dist/browser-index.d.ts +5 -1
- package/dist/browser-index.d.ts.map +1 -1
- package/dist/browser-index.js +3 -1
- package/dist/browser-index.js.map +1 -1
- package/dist/contracts/decoders.d.ts +15 -0
- package/dist/contracts/decoders.d.ts.map +1 -0
- package/dist/contracts/decoders.js +8 -0
- package/dist/contracts/decoders.js.map +1 -0
- package/dist/decoderLoad.d.ts +28 -0
- package/dist/decoderLoad.d.ts.map +1 -0
- package/dist/decoderLoad.js +49 -0
- package/dist/decoderLoad.js.map +1 -0
- package/dist/gltf/ImageDecode.d.ts +18 -0
- package/dist/gltf/ImageDecode.d.ts.map +1 -0
- package/dist/gltf/ImageDecode.js +140 -0
- package/dist/gltf/ImageDecode.js.map +1 -0
- package/dist/gltf-runtime.d.ts +1 -1
- package/dist/gltf-runtime.d.ts.map +1 -1
- package/dist/gltf-runtime.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lanes/index.d.ts +17 -0
- package/dist/lanes/index.d.ts.map +1 -0
- package/dist/lanes/index.js +17 -0
- package/dist/lanes/index.js.map +1 -0
- package/dist/lanes/prd01.d.ts +6 -0
- package/dist/lanes/prd01.d.ts.map +1 -0
- package/dist/lanes/prd01.js +6 -0
- package/dist/lanes/prd01.js.map +1 -0
- package/dist/lanes/prd02.d.ts +6 -0
- package/dist/lanes/prd02.d.ts.map +1 -0
- package/dist/lanes/prd02.js +6 -0
- package/dist/lanes/prd02.js.map +1 -0
- package/dist/lanes/prd03.d.ts +6 -0
- package/dist/lanes/prd03.d.ts.map +1 -0
- package/dist/lanes/prd03.js +6 -0
- package/dist/lanes/prd03.js.map +1 -0
- package/dist/lanes/prd04.d.ts +6 -0
- package/dist/lanes/prd04.d.ts.map +1 -0
- package/dist/lanes/prd04.js +6 -0
- package/dist/lanes/prd04.js.map +1 -0
- package/dist/lanes/prd05.d.ts +14 -0
- package/dist/lanes/prd05.d.ts.map +1 -0
- package/dist/lanes/prd05.js +14 -0
- package/dist/lanes/prd05.js.map +1 -0
- package/dist/lanes/prd06.d.ts +6 -0
- package/dist/lanes/prd06.d.ts.map +1 -0
- package/dist/lanes/prd06.js +6 -0
- package/dist/lanes/prd06.js.map +1 -0
- package/dist/lanes/prd07.d.ts +6 -0
- package/dist/lanes/prd07.d.ts.map +1 -0
- package/dist/lanes/prd07.js +6 -0
- package/dist/lanes/prd07.js.map +1 -0
- package/dist/lanes/prd08.d.ts +6 -0
- package/dist/lanes/prd08.d.ts.map +1 -0
- package/dist/lanes/prd08.js +6 -0
- package/dist/lanes/prd08.js.map +1 -0
- package/dist/lanes/prd09.d.ts +6 -0
- package/dist/lanes/prd09.d.ts.map +1 -0
- package/dist/lanes/prd09.js +6 -0
- package/dist/lanes/prd09.js.map +1 -0
- package/dist/lanes/prd10.d.ts +6 -0
- package/dist/lanes/prd10.d.ts.map +1 -0
- package/dist/lanes/prd10.js +6 -0
- package/dist/lanes/prd10.js.map +1 -0
- package/dist/lanes/prd11.d.ts +6 -0
- package/dist/lanes/prd11.d.ts.map +1 -0
- package/dist/lanes/prd11.js +6 -0
- package/dist/lanes/prd11.js.map +1 -0
- package/dist/lanes/prd12.d.ts +6 -0
- package/dist/lanes/prd12.d.ts.map +1 -0
- package/dist/lanes/prd12.js +6 -0
- package/dist/lanes/prd12.js.map +1 -0
- package/dist/lanes/prd13.d.ts +6 -0
- package/dist/lanes/prd13.d.ts.map +1 -0
- package/dist/lanes/prd13.js +6 -0
- package/dist/lanes/prd13.js.map +1 -0
- package/dist/lanes/prd14.d.ts +6 -0
- package/dist/lanes/prd14.d.ts.map +1 -0
- package/dist/lanes/prd14.js +6 -0
- package/dist/lanes/prd14.js.map +1 -0
- package/dist/lanes/prd15.d.ts +6 -0
- package/dist/lanes/prd15.d.ts.map +1 -0
- package/dist/lanes/prd15.js +6 -0
- package/dist/lanes/prd15.js.map +1 -0
- package/dist/vite/auraDecoders.d.ts +31 -0
- package/dist/vite/auraDecoders.d.ts.map +1 -0
- package/dist/vite/auraDecoders.js +89 -0
- package/dist/vite/auraDecoders.js.map +1 -0
- package/package.json +13 -5
- package/vendor/basis/README.md +25 -0
- package/vendor/basis/basis_transcoder.js +19 -0
- package/vendor/basis/basis_transcoder.wasm +0 -0
- package/vendor/draco/README.md +23 -0
- package/vendor/draco/draco_decoder.js +33 -0
- package/vendor/draco/draco_decoder.wasm +0 -0
- package/vendor/meshopt/README.md +21 -0
- package/vendor/meshopt/meshopt_decoder.cjs +199 -0
- package/vendor/meshopt/meshopt_decoder.mjs +196 -0
- package/vendor/meshopt_decoder.mjs +196 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { AnimationAction, AnimationClip, AnimationMixer, consumeRootMotion, extractRootMotion, createFootIkRig, normalizeQuat, slerpQuat, solveTwoBoneIk } from "@aura3d/animation";
|
|
2
|
+
import { bindSkeleton, compileClip, createPoseBuffer, makeClipAdditive, PoseMixer } from "@aura3d/animation/lanes";
|
|
3
|
+
import { AURA3D_RETARGET_ENGINE_VERSION, bakeClipsInWorker, bakeRetargetedClipMap, createRetargetWorker, decompileCompiledClip, readRetargetCache, retargetCacheKey, retargetClipsHash, retargetSkeletonHash, writeRetargetCache } from "@aura3d/animation/lanes";
|
|
2
4
|
import { composeMat4, decomposeMat4, invertMat4, MAX_RENDERABLE_SKINNING_JOINTS, multiplyMat4, Renderable, Scene, transformPoint } from "@aura3d/scene";
|
|
5
|
+
import { skinningPaletteCache } from "@aura3d/rendering";
|
|
3
6
|
/**
|
|
4
7
|
* Synonym groups for fuzzy clip-name matching. Real catalog assets label the same
|
|
5
8
|
* motion wildly differently ("Loops" vs "Idle", "Take 001" vs "Run", "sprint" vs
|
|
@@ -17,19 +20,31 @@ const GLTF_CLIP_SYNONYM_GROUPS = [
|
|
|
17
20
|
function normalizeClipToken(name) {
|
|
18
21
|
return name.toLowerCase().replace(/[\s_.\-]+/g, "");
|
|
19
22
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
const animationClipDefaultsAre31 = () => {
|
|
24
|
+
const parseList = (value) => value === undefined ? [] : value.split(/[\s,]+/).map((entry) => entry.trim().toLowerCase()).filter((entry) => entry !== "");
|
|
25
|
+
const env = typeof process !== "undefined" ? process.env : undefined;
|
|
26
|
+
if (env !== undefined) {
|
|
27
|
+
const scoped = env["A3D_QR_ANIMATION"];
|
|
28
|
+
if (scoped !== undefined && scoped !== "0" && scoped.toLowerCase() !== "false" && scoped.toLowerCase() !== "off")
|
|
29
|
+
return true;
|
|
30
|
+
const root = parseList(env["A3D_QR"] ?? env["A3D_QR_FLAGS"]);
|
|
31
|
+
if (root.includes("animation") || root.includes("all") || root.includes("true"))
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (typeof location !== "undefined" && typeof location.search === "string") {
|
|
35
|
+
try {
|
|
36
|
+
const params = new URLSearchParams(location.search);
|
|
37
|
+
const urlFlags = parseList(params.get("a3d-qr") ?? undefined);
|
|
38
|
+
if (urlFlags.includes("animation") || urlFlags.includes("all") || urlFlags.includes("true"))
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// malformed URLSearchParams input — ignore URL source
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
};
|
|
47
|
+
export function resolveGLTFClipName(requested, available, options) {
|
|
33
48
|
if (available.length === 0)
|
|
34
49
|
return undefined;
|
|
35
50
|
const requestedToken = normalizeClipToken(requested);
|
|
@@ -50,7 +65,8 @@ export function resolveGLTFClipName(requested, available) {
|
|
|
50
65
|
return name;
|
|
51
66
|
}
|
|
52
67
|
}
|
|
53
|
-
|
|
68
|
+
const fallback = options?.fallback ?? (animationClipDefaultsAre31() ? "error" : "first");
|
|
69
|
+
return fallback === "first" ? available[0] : undefined;
|
|
54
70
|
}
|
|
55
71
|
function clipMaskAllowsNode(mask, nodeName) {
|
|
56
72
|
if (!mask)
|
|
@@ -253,12 +269,73 @@ export class GLTFImportedSkeletonIKController {
|
|
|
253
269
|
};
|
|
254
270
|
}
|
|
255
271
|
}
|
|
272
|
+
/** PRD-06 §9.8 — wall clock, ms (`performance.now` where it exists). */
|
|
273
|
+
function nowMs() {
|
|
274
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
275
|
+
}
|
|
276
|
+
/** Column-major mat4 multiply written into a Float32Array slot — the T0.11 no-alloc palette path. Exported so the §13 palette-build micro-budget test measures the exact per-joint write the runtime performs. `a` is hoisted into scalars once per call so the inner writes are pure FMA-grade flops — the indexed-read loop measured ~1.3× over the §13 palette budget. */
|
|
277
|
+
export function multiplyMat4Into(out, outOffset, a, b) {
|
|
278
|
+
const a00 = a[0], a10 = a[1], a20 = a[2], a30 = a[3];
|
|
279
|
+
const a01 = a[4], a11 = a[5], a21 = a[6], a31 = a[7];
|
|
280
|
+
const a02 = a[8], a12 = a[9], a22 = a[10], a32 = a[11];
|
|
281
|
+
const a03 = a[12], a13 = a[13], a23 = a[14], a33 = a[15];
|
|
282
|
+
for (let col = 0; col < 4; col += 1) {
|
|
283
|
+
const b0 = b[col * 4 + 0], b1 = b[col * 4 + 1], b2 = b[col * 4 + 2], b3 = b[col * 4 + 3];
|
|
284
|
+
out[outOffset + col * 4 + 0] = a00 * b0 + a01 * b1 + a02 * b2 + a03 * b3;
|
|
285
|
+
out[outOffset + col * 4 + 1] = a10 * b0 + a11 * b1 + a12 * b2 + a13 * b3;
|
|
286
|
+
out[outOffset + col * 4 + 2] = a20 * b0 + a21 * b1 + a22 * b2 + a23 * b3;
|
|
287
|
+
out[outOffset + col * 4 + 3] = a30 * b0 + a31 * b1 + a32 * b2 + a33 * b3;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Column-major general 4×4 inverse into `out` (adjugate / determinant, gl-matrix `mat4.invert`; 06-T0: out[7], out[10], out[11], out[14], out[15] previously used the wrong cofactor terms, so even inverse(identity) had out[11] = -1 and every skinning palette carried a non-affine row 3 that projected skinned vertices off-screen on the GPU). §13: `invertMat4` allocates two objects per call — over 600 frames that alone blows the steady-state budget. */
|
|
291
|
+
export function invertMat4Into(out, a) {
|
|
292
|
+
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
|
|
293
|
+
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
|
|
294
|
+
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
|
|
295
|
+
const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
|
|
296
|
+
const b00 = a00 * a11 - a01 * a10;
|
|
297
|
+
const b01 = a00 * a12 - a02 * a10;
|
|
298
|
+
const b02 = a00 * a13 - a03 * a10;
|
|
299
|
+
const b03 = a01 * a12 - a02 * a11;
|
|
300
|
+
const b04 = a01 * a13 - a03 * a11;
|
|
301
|
+
const b05 = a02 * a13 - a03 * a12;
|
|
302
|
+
const b06 = a20 * a31 - a21 * a30;
|
|
303
|
+
const b07 = a20 * a32 - a22 * a30;
|
|
304
|
+
const b08 = a20 * a33 - a23 * a30;
|
|
305
|
+
const b09 = a21 * a32 - a22 * a31;
|
|
306
|
+
const b10 = a21 * a33 - a23 * a31;
|
|
307
|
+
const b11 = a22 * a33 - a23 * a32;
|
|
308
|
+
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
|
|
309
|
+
if (!det) {
|
|
310
|
+
out.fill(0);
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
det = 1.0 / det;
|
|
314
|
+
out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
|
|
315
|
+
out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
|
|
316
|
+
out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
|
|
317
|
+
out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
|
|
318
|
+
out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
|
|
319
|
+
out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
|
|
320
|
+
out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
|
|
321
|
+
out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
|
|
322
|
+
out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
|
|
323
|
+
out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
|
|
324
|
+
out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
|
|
325
|
+
out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
|
|
326
|
+
out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
|
|
327
|
+
out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
|
|
328
|
+
out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
|
|
329
|
+
out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
256
332
|
export class GLTFSceneAnimationRuntime {
|
|
257
333
|
options;
|
|
258
334
|
clipsByName = new Map();
|
|
259
335
|
nodesByName = new Map();
|
|
260
336
|
morphRenderablesByNodeName = new Map();
|
|
261
337
|
skinningBindings = [];
|
|
338
|
+
disposed = false;
|
|
262
339
|
footBindMatrices = new Map();
|
|
263
340
|
footOrientationLocks = new Map();
|
|
264
341
|
footDescendantLocks = new Map();
|
|
@@ -266,6 +343,30 @@ export class GLTFSceneAnimationRuntime {
|
|
|
266
343
|
footPlanting;
|
|
267
344
|
footRig;
|
|
268
345
|
lastApply;
|
|
346
|
+
/** T0.11 scratch for the per-joint palette multiply (inverseMeshWorld * jointWorld). */
|
|
347
|
+
paletteScratch = new Float32Array(16);
|
|
348
|
+
/** §13: reusable scratch for the per-binding inverse world matrix (no alloc/frame). */
|
|
349
|
+
inverseWorldScratch = new Float32Array(16);
|
|
350
|
+
/** §13: persistent result object for `refreshSkinningPalettes` (0 bytes/frame). */
|
|
351
|
+
paletteResult = { updated: 0, missingTargets: [] };
|
|
352
|
+
/**
|
|
353
|
+
* T1.7 (PRD-06) — the per-runtime pose pipeline backing `applyClips`: a
|
|
354
|
+
* scene-wide SkeletonBinding (every traversed node in order, rest pose = the
|
|
355
|
+
* node's local TRS at bind time), one PoseMixer, and the compiled clips.
|
|
356
|
+
* Rebuilt whenever `reindexScene` re-reads the node set.
|
|
357
|
+
*/
|
|
358
|
+
poseState;
|
|
359
|
+
/**
|
|
360
|
+
* T3.5 — world→model matrix provider for pose constraints. Defaults to the
|
|
361
|
+
* scene root's world matrix; the `prd06.animation` actor extension installs
|
|
362
|
+
* the app-node transform when one is known.
|
|
363
|
+
*/
|
|
364
|
+
poseConstraintModelMatrix;
|
|
365
|
+
/**
|
|
366
|
+
* Wall-clock (ms) of the last constraint evaluation under `applyClips` —
|
|
367
|
+
* its caller passes no dt, so constraint dynamics measure the real gap.
|
|
368
|
+
*/
|
|
369
|
+
lastConstraintEvalAtMs;
|
|
269
370
|
constructor(options) {
|
|
270
371
|
this.options = options;
|
|
271
372
|
for (const clip of options.clips) {
|
|
@@ -347,6 +448,91 @@ export class GLTFSceneAnimationRuntime {
|
|
|
347
448
|
this.lastApply = this.applySampledTargets(`root-motion:${names.join(",")}`, maxTime, { sampledTargets: values, unsupportedTracks }, samples.length);
|
|
348
449
|
return { motion, applyResult: this.lastApply };
|
|
349
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* T3.7 (PRD-06 §6.8) — resolve the root-motion translation track target for
|
|
453
|
+
* spec-level `play(clip, {rootMotion})`: with `bone` set, the bone's own
|
|
454
|
+
* `translation`/`position` track; without it, the clip's root-motion
|
|
455
|
+
* candidate (largest planar displacement on a hips/root/pelvis node, then
|
|
456
|
+
* any translation track by displacement). Throws when nothing qualifies.
|
|
457
|
+
*/
|
|
458
|
+
rootMotionTargetFor(clipName, bone) {
|
|
459
|
+
const clip = this.clipsByName.get(clipName);
|
|
460
|
+
if (!clip)
|
|
461
|
+
throw new Error(`glTF animation clip "${clipName}" was not found.`);
|
|
462
|
+
const isTranslation = (target) => target.endsWith(".translation") || target.endsWith(".position");
|
|
463
|
+
if (bone !== undefined) {
|
|
464
|
+
for (const leaf of ["translation", "position"]) {
|
|
465
|
+
const candidate = `${bone}.${leaf}`;
|
|
466
|
+
if (clip.tracks.some(track => track.target === candidate && track.valueType === "vector3"))
|
|
467
|
+
return candidate;
|
|
468
|
+
}
|
|
469
|
+
throw new Error(`Root motion bone "${bone}" has no translation track in glTF animation clip "${clipName}".`);
|
|
470
|
+
}
|
|
471
|
+
let best;
|
|
472
|
+
let bestDistance = -1;
|
|
473
|
+
let fallback;
|
|
474
|
+
for (const track of clip.tracks) {
|
|
475
|
+
if (track.valueType !== "vector3" || !isTranslation(track.target))
|
|
476
|
+
continue;
|
|
477
|
+
fallback ??= track.target;
|
|
478
|
+
const keys = track.keyframes ?? [];
|
|
479
|
+
if (keys.length < 2)
|
|
480
|
+
continue;
|
|
481
|
+
const first = vec3OfKeyframeValue(keys[0].value);
|
|
482
|
+
const last = vec3OfKeyframeValue(keys[keys.length - 1].value);
|
|
483
|
+
if (!first || !last)
|
|
484
|
+
continue;
|
|
485
|
+
const distance = Math.hypot(last[0] - first[0], last[2] - first[2]);
|
|
486
|
+
const rooted = /hips|root|pelvis/i.test(track.target);
|
|
487
|
+
if (distance > 0.05 && (rooted || best === undefined || distance > bestDistance)) {
|
|
488
|
+
if (rooted || best === undefined || !/hips|root|pelvis/i.test(best)) {
|
|
489
|
+
best = track.target;
|
|
490
|
+
bestDistance = distance;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const resolved = best ?? fallback;
|
|
495
|
+
if (resolved === undefined) {
|
|
496
|
+
throw new Error(`glTF animation clip "${clipName}" has no translation track for root motion.`);
|
|
497
|
+
}
|
|
498
|
+
return resolved;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* T3.7 — yaw delta (radians, about local Y) authored on `bone`'s rotation
|
|
502
|
+
* track between `fromTime`/`toTime` (loop-aware). Returns 0 when the clip
|
|
503
|
+
* has no matching rotation track.
|
|
504
|
+
*/
|
|
505
|
+
rootMotionYawDelta(clipName, bone, fromTime, toTime, loop) {
|
|
506
|
+
const clip = this.clipsByName.get(clipName);
|
|
507
|
+
if (!clip)
|
|
508
|
+
throw new Error(`glTF animation clip "${clipName}" was not found.`);
|
|
509
|
+
const node = bone ?? this.rootMotionTargetFor(clipName).replace(/\.(translation|position)$/, "");
|
|
510
|
+
const track = clip.tracks.find(candidate => candidate.valueType === "quaternion" &&
|
|
511
|
+
(candidate.target === `${node}.rotation` || candidate.target === `${node}.quaternion`));
|
|
512
|
+
if (!track)
|
|
513
|
+
return 0;
|
|
514
|
+
const yawAt = (time) => {
|
|
515
|
+
const q = track.sample(time);
|
|
516
|
+
const arr = (Array.isArray(q) || ArrayBuffer.isView(q)) ? q : undefined;
|
|
517
|
+
const [x, y, z, w] = arr !== undefined && arr.length >= 4
|
|
518
|
+
? [arr[0], arr[1], arr[2], arr[3]]
|
|
519
|
+
: [0, 0, 0, 1];
|
|
520
|
+
// forward = q * (0,0,1); yaw = atan2(forward.x, forward.z)
|
|
521
|
+
const fx = 2 * (x * z + w * y);
|
|
522
|
+
const fz = 1 - 2 * (x * x + y * y);
|
|
523
|
+
return Math.atan2(fx, fz);
|
|
524
|
+
};
|
|
525
|
+
const wrap01 = (t) => ((t % clip.duration) + clip.duration) % clip.duration;
|
|
526
|
+
const unwrap = (delta) => delta - Math.round(delta / (Math.PI * 2)) * Math.PI * 2;
|
|
527
|
+
if (!loop || clip.duration <= 0) {
|
|
528
|
+
const clamp = (t) => Math.max(0, Math.min(clip.duration, t));
|
|
529
|
+
return unwrap(yawAt(clamp(toTime)) - yawAt(clamp(fromTime)));
|
|
530
|
+
}
|
|
531
|
+
// Continuous-loop yaw: same-cycle delta plus per-cycle contribution per wrap.
|
|
532
|
+
const perCycle = unwrap(yawAt(clip.duration) - yawAt(0));
|
|
533
|
+
const crossings = Math.floor(toTime / clip.duration) - Math.floor(fromTime / clip.duration);
|
|
534
|
+
return unwrap(yawAt(wrap01(toTime)) - yawAt(wrap01(fromTime))) + crossings * perCycle;
|
|
535
|
+
}
|
|
350
536
|
applyClipByName(name, time) {
|
|
351
537
|
const clip = this.clipsByName.get(name);
|
|
352
538
|
if (!clip) {
|
|
@@ -358,13 +544,27 @@ export class GLTFSceneAnimationRuntime {
|
|
|
358
544
|
clipNames() {
|
|
359
545
|
return [...this.clipsByName.keys()];
|
|
360
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* T0.6 (PRD-06) — the C-19 `AuraResolvedClipInfo` surface that backs the
|
|
549
|
+
* `prd06.animation` handle extension's `resolveAnimationClips()`: real GLB
|
|
550
|
+
* clip durations (plus channel counts and the 5 cm XZ-displacement
|
|
551
|
+
* root-motion candidate heuristic of T0.7) after the asset has loaded.
|
|
552
|
+
*/
|
|
553
|
+
resolvedClipInfos() {
|
|
554
|
+
return [...this.clipsByName.values()].map((clip) => ({
|
|
555
|
+
name: clip.name,
|
|
556
|
+
duration: clip.duration,
|
|
557
|
+
channelCount: clip.tracks.length,
|
|
558
|
+
hasRootMotionCandidate: clipHasRootMotionCandidate(clip)
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
361
561
|
/**
|
|
362
562
|
* Resolve a requested clip name to the best available registered clip name
|
|
363
563
|
* using fuzzy matching (exact -> synonym group -> substring -> first clip).
|
|
364
564
|
* Returns `undefined` when the runtime has no clips.
|
|
365
565
|
*/
|
|
366
|
-
resolveClipName(name) {
|
|
367
|
-
return resolveGLTFClipName(name, this.clipNames());
|
|
566
|
+
resolveClipName(name, options) {
|
|
567
|
+
return resolveGLTFClipName(name, this.clipNames(), options);
|
|
368
568
|
}
|
|
369
569
|
/**
|
|
370
570
|
* Apply a clip selected by fuzzy name resolution. Unlike {@link applyClipByName}
|
|
@@ -386,14 +586,35 @@ export class GLTFSceneAnimationRuntime {
|
|
|
386
586
|
this.lastApply = this.applySampledTargets(clip.name, wrappedTime, sampleClipTracks(clip, wrappedTime, 1, false));
|
|
387
587
|
return this.lastApply;
|
|
388
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* T1.7 (PRD-06 §10) — re-implemented on the per-runtime `PoseMixer` in
|
|
591
|
+
* stateless mode (`evaluateSamples` with explicit per-sample times). Node
|
|
592
|
+
* translation/rotation/scale tracks bound to a scene node go through the
|
|
593
|
+
* pose pipeline (incremental-weight accumulate + rest fill + additive
|
|
594
|
+
* accumulators, three r185 `AnimationMixer` order); morph-weight, material
|
|
595
|
+
* and light pointer tracks keep the existing accumulator path, and so do
|
|
596
|
+
* node tracks aimed at nodes the binding does not cover (they keep their
|
|
597
|
+
* `missingTargets` reporting). Two documented semantic changes vs the old
|
|
598
|
+
* accumulator blend: partial-weight mixes fill the remainder from the rest
|
|
599
|
+
* pose instead of renormalising (rest blend), and a bone covered by the
|
|
600
|
+
* blend but not by a given channel resets that channel to rest (rest reset).
|
|
601
|
+
*/
|
|
389
602
|
applyClips(samples) {
|
|
390
603
|
if (samples.length === 0) {
|
|
391
604
|
throw new Error("glTF animation runtime blend requires at least one clip sample.");
|
|
392
605
|
}
|
|
606
|
+
const phaseStart = nowMs();
|
|
607
|
+
let mixerMs = 0;
|
|
608
|
+
let constraintsMs = 0;
|
|
609
|
+
let springsMs = 0;
|
|
393
610
|
const accumulators = new Map();
|
|
394
611
|
const unsupportedTracks = [];
|
|
395
612
|
let maxTime = 0;
|
|
396
613
|
const names = [];
|
|
614
|
+
const pose = this.poseRuntime();
|
|
615
|
+
const poseSpecs = [];
|
|
616
|
+
const touchedBones = new Set();
|
|
617
|
+
const maskWeights = new Map();
|
|
397
618
|
for (const sample of samples) {
|
|
398
619
|
const clip = this.clipsByName.get(sample.clipName);
|
|
399
620
|
if (!clip) {
|
|
@@ -411,25 +632,353 @@ export class GLTFSceneAnimationRuntime {
|
|
|
411
632
|
const wrappedTime = clip.duration > 0 && sample.time > clip.duration ? sample.time % clip.duration : Math.min(sample.time, clip.duration);
|
|
412
633
|
maxTime = Math.max(maxTime, wrappedTime);
|
|
413
634
|
names.push(`${clip.name}@${Number(wrappedTime.toFixed(4))}x${Number(weight.toFixed(4))}${sample.additive ? "+add" : ""}`);
|
|
635
|
+
let bindsNodes = false;
|
|
414
636
|
for (const track of clip.tracks) {
|
|
415
637
|
const target = parseAnimationTarget(track.target);
|
|
416
638
|
if (!target) {
|
|
417
639
|
unsupportedTracks.push(track.target);
|
|
418
640
|
continue;
|
|
419
641
|
}
|
|
420
|
-
|
|
421
|
-
|
|
642
|
+
if (target.kind === "node" && target.path !== "weights") {
|
|
643
|
+
// Node TRS: pose path when the name binds and the mask allows it.
|
|
644
|
+
const boneIndices = pose.binding.jointIndicesByName.get(target.nodeName);
|
|
645
|
+
if (boneIndices !== undefined && boneIndices.length > 0 && clipMaskAllowsNode(sample.mask, target.nodeName)) {
|
|
646
|
+
bindsNodes = true;
|
|
647
|
+
touchedBones.add(boneIndices[0]);
|
|
648
|
+
}
|
|
649
|
+
else if (boneIndices === undefined) {
|
|
650
|
+
// Track points at a node absent from the scene — legacy path so
|
|
651
|
+
// `missingTargets` reporting is unchanged.
|
|
652
|
+
blendInto(accumulators, track.target, track.valueType, track.sample(wrappedTime), weight, sample.additive === true);
|
|
653
|
+
}
|
|
422
654
|
continue;
|
|
655
|
+
}
|
|
423
656
|
blendInto(accumulators, track.target, track.valueType, track.sample(wrappedTime), weight, sample.additive === true);
|
|
424
657
|
}
|
|
658
|
+
if (bindsNodes) {
|
|
659
|
+
let mask = null;
|
|
660
|
+
if (sample.mask !== undefined) {
|
|
661
|
+
let cached = maskWeights.get(sample.mask);
|
|
662
|
+
if (cached === undefined) {
|
|
663
|
+
cached = new Float32Array(pose.binding.boneCount);
|
|
664
|
+
for (let i = 0; i < pose.binding.boneCount; i += 1) {
|
|
665
|
+
cached[i] = clipMaskAllowsNode(sample.mask, pose.binding.jointNames[i]) ? 1 : 0;
|
|
666
|
+
}
|
|
667
|
+
maskWeights.set(sample.mask, cached);
|
|
668
|
+
}
|
|
669
|
+
mask = cached;
|
|
670
|
+
}
|
|
671
|
+
poseSpecs.push({ clipName: clip.name, time: wrappedTime, weight, additive: sample.additive === true, mask });
|
|
672
|
+
}
|
|
425
673
|
}
|
|
426
674
|
const sampledTargets = new Map();
|
|
675
|
+
if (poseSpecs.length > 0) {
|
|
676
|
+
const mixerStart = nowMs();
|
|
677
|
+
pose.mixer.evaluateSamples(poseSpecs, pose.pose);
|
|
678
|
+
mixerMs = nowMs() - mixerStart;
|
|
679
|
+
// T3.5 — constraints apply to the freshly-mixed pose before palette
|
|
680
|
+
// build; touched joints union into the emitted sampled targets. Their
|
|
681
|
+
// dynamics (look-at half-life, spring substeps) integrate in wall-clock
|
|
682
|
+
// seconds, so measure the real gap between applies — a hardcoded 1/60
|
|
683
|
+
// makes smoothing converge ~6× too slowly in sub-60fps sessions.
|
|
684
|
+
const now = nowMs();
|
|
685
|
+
const constraintDt = this.lastConstraintEvalAtMs === undefined
|
|
686
|
+
? 1 / 60
|
|
687
|
+
: Math.min(0.25, Math.max(1e-4, (now - this.lastConstraintEvalAtMs) / 1000));
|
|
688
|
+
this.lastConstraintEvalAtMs = now;
|
|
689
|
+
const constrained = this.runPoseConstraints(pose.pose, pose.binding, constraintDt);
|
|
690
|
+
constraintsMs = constrained.constraintsMs;
|
|
691
|
+
springsMs = constrained.springsMs;
|
|
692
|
+
for (const boneIndex of constrained.touched)
|
|
693
|
+
touchedBones.add(boneIndex);
|
|
694
|
+
for (const boneIndex of touchedBones) {
|
|
695
|
+
const name = pose.binding.jointNames[boneIndex];
|
|
696
|
+
const p = boneIndex * 3;
|
|
697
|
+
const q = boneIndex * 4;
|
|
698
|
+
sampledTargets.set(`${name}.translation`, [pose.pose.positions[p], pose.pose.positions[p + 1], pose.pose.positions[p + 2]]);
|
|
699
|
+
sampledTargets.set(`${name}.rotation`, [pose.pose.rotations[q], pose.pose.rotations[q + 1], pose.pose.rotations[q + 2], pose.pose.rotations[q + 3]]);
|
|
700
|
+
sampledTargets.set(`${name}.scale`, [pose.pose.scales[p], pose.pose.scales[p + 1], pose.pose.scales[p + 2]]);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
427
703
|
for (const [target, accumulator] of accumulators) {
|
|
428
704
|
sampledTargets.set(target, finalizeTargetBlend(accumulator));
|
|
429
705
|
}
|
|
430
|
-
this.lastApply = this.applySampledTargets(`blend:${names.join(",")}`, maxTime, { sampledTargets, unsupportedTracks }, samples.length);
|
|
706
|
+
this.lastApply = this.applySampledTargets(`blend:${names.join(",")}`, maxTime, { sampledTargets, unsupportedTracks }, samples.length, { mixerMs, constraintsMs, springsMs, paletteBytes: this.paletteBufferBytes(), cpuMs: nowMs() - phaseStart });
|
|
431
707
|
return this.lastApply;
|
|
432
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* §10 (PRD-06) — the scene-wide pose binding the pose path evaluates on.
|
|
711
|
+
* Joints are every scene node in `traverse` order, resolved by node index
|
|
712
|
+
* (duplicate names bind all slots); the rest pose is each node's local TRS
|
|
713
|
+
* captured at bind time.
|
|
714
|
+
*/
|
|
715
|
+
skeletons() {
|
|
716
|
+
return [this.poseRuntime().binding];
|
|
717
|
+
}
|
|
718
|
+
/** §10 (PRD-06) — the per-clip compiled tracks used by the pose path. */
|
|
719
|
+
compiledClips() {
|
|
720
|
+
return this.poseRuntime().compiled;
|
|
721
|
+
}
|
|
722
|
+
/** §10 (PRD-06) — the per-runtime PoseMixer `applyClips` evaluates on. */
|
|
723
|
+
mixer() {
|
|
724
|
+
return this.poseRuntime().mixer;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* T3.8 (PRD-06 §7.2, C-19) — `actor.animation.addClipsFrom(source)` retarget-
|
|
728
|
+
* bakes another skeleton's compiled clips onto this runtime's skeleton and
|
|
729
|
+
* registers them by name (raw + compiled + mixer) so `play`/`playLayer`
|
|
730
|
+
* resolve them like loaded clips. The bake runs in `pose/retarget.worker.ts`
|
|
731
|
+
* when the platform has workers, otherwise in-process, and its output is
|
|
732
|
+
* cached in IndexedDB under `(engineVersion, sourceHash, targetHash)` so a
|
|
733
|
+
* rollback ignores newer caches. Returns the registered clip names.
|
|
734
|
+
*/
|
|
735
|
+
async addClipsFrom(source, options = {}) {
|
|
736
|
+
const pose = this.poseRuntime();
|
|
737
|
+
const sourceSkeleton = "skeletons" in source ? source.skeletons()[0] : source.skeleton;
|
|
738
|
+
const sourceClips = "compiledClips" in source ? source.compiledClips() : source.clips;
|
|
739
|
+
if (sourceSkeleton === undefined) {
|
|
740
|
+
throw new Error("addClipsFrom: source has no skeleton to retarget from.");
|
|
741
|
+
}
|
|
742
|
+
const engineVersion = options.engineVersion ?? AURA3D_RETARGET_ENGINE_VERSION;
|
|
743
|
+
const sourceHash = `${retargetSkeletonHash(sourceSkeleton)}:${retargetClipsHash(sourceClips)}`;
|
|
744
|
+
const targetHash = retargetSkeletonHash(pose.binding);
|
|
745
|
+
const key = retargetCacheKey(engineVersion, sourceHash, targetHash);
|
|
746
|
+
let baked = options.cache === false ? undefined : await readRetargetCache(key);
|
|
747
|
+
if (baked === undefined) {
|
|
748
|
+
const bakeOptions = {
|
|
749
|
+
...(options.map !== undefined ? { map: options.map } : {}),
|
|
750
|
+
hipsScale: options.hipsScale ?? "leg-length",
|
|
751
|
+
...(options.fingers !== undefined ? { fingers: options.fingers } : {}),
|
|
752
|
+
...(options.retarget !== undefined ? { retarget: options.retarget } : {})
|
|
753
|
+
};
|
|
754
|
+
const worker = options.worker === false ? undefined : createRetargetWorker();
|
|
755
|
+
baked = worker !== undefined
|
|
756
|
+
? await bakeClipsInWorker(worker, {
|
|
757
|
+
sourceSkeleton,
|
|
758
|
+
targetSkeleton: pose.binding,
|
|
759
|
+
clips: sourceClips,
|
|
760
|
+
options: bakeOptions
|
|
761
|
+
})
|
|
762
|
+
: bakeRetargetedClipMap({ skeleton: sourceSkeleton, clips: sourceClips }, pose.binding, bakeOptions);
|
|
763
|
+
if (options.cache !== false)
|
|
764
|
+
void writeRetargetCache(key, baked);
|
|
765
|
+
}
|
|
766
|
+
const names = [];
|
|
767
|
+
for (const [name, compiled] of baked) {
|
|
768
|
+
const raw = decompileCompiledClip(name, compiled);
|
|
769
|
+
this.clipsByName.set(name, raw);
|
|
770
|
+
pose.compiled.set(name, compiled);
|
|
771
|
+
pose.mixer.addCompiledClip(name, compiled, raw);
|
|
772
|
+
names.push(name);
|
|
773
|
+
}
|
|
774
|
+
return names;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* T1.9 (PRD-06 §10) — advance the per-runtime `PoseMixer` by `dt` seconds
|
|
778
|
+
* and write the evaluated pose to scene nodes in `applySampledTargets`
|
|
779
|
+
* order. `restPoseReset !== false` writes every bound bone (rest-filled
|
|
780
|
+
* channels reset); `false` writes only the bones covered by active actions'
|
|
781
|
+
* bound tracks, so uncovered bones keep their current scene values.
|
|
782
|
+
* Non-pose tracks (morph weights, material/light pointer, node tracks
|
|
783
|
+
* aimed at nodes outside the binding) keep the legacy accumulator path,
|
|
784
|
+
* sampled at each active action's own clock and effective weight.
|
|
785
|
+
*/
|
|
786
|
+
applyPoseMixer(dt, options) {
|
|
787
|
+
const phaseStart = nowMs();
|
|
788
|
+
const pose = this.poseRuntime();
|
|
789
|
+
const mixerStart = nowMs();
|
|
790
|
+
pose.mixer.update(dt);
|
|
791
|
+
pose.mixer.evaluate(pose.pose);
|
|
792
|
+
const mixerMs = nowMs() - mixerStart;
|
|
793
|
+
// T3.5 — constraints evaluate post-mixer / pre-palette; their touched
|
|
794
|
+
// bones union into `covered` so the write-back emits them.
|
|
795
|
+
this.lastConstraintEvalAtMs = nowMs();
|
|
796
|
+
const constrained = this.runPoseConstraints(pose.pose, pose.binding, dt);
|
|
797
|
+
const constraintsMs = constrained.constraintsMs;
|
|
798
|
+
const springsMs = constrained.springsMs;
|
|
799
|
+
const actions = pose.mixer.activeActions();
|
|
800
|
+
let covered;
|
|
801
|
+
if (options?.restPoseReset === false) {
|
|
802
|
+
const set = new Set();
|
|
803
|
+
for (const action of actions) {
|
|
804
|
+
for (const binding of action.bindings)
|
|
805
|
+
set.add(binding.boneIndex);
|
|
806
|
+
}
|
|
807
|
+
for (const boneIndex of constrained.touched)
|
|
808
|
+
set.add(boneIndex);
|
|
809
|
+
covered = set;
|
|
810
|
+
}
|
|
811
|
+
// `covered === undefined` already emits every bound bone below — the
|
|
812
|
+
// constraint union matters only on the covered path.
|
|
813
|
+
const sampledTargets = new Map();
|
|
814
|
+
const writeBone = (boneIndex) => {
|
|
815
|
+
const name = pose.binding.jointNames[boneIndex];
|
|
816
|
+
const p = boneIndex * 3;
|
|
817
|
+
const q = boneIndex * 4;
|
|
818
|
+
sampledTargets.set(`${name}.translation`, [pose.pose.positions[p], pose.pose.positions[p + 1], pose.pose.positions[p + 2]]);
|
|
819
|
+
sampledTargets.set(`${name}.rotation`, [pose.pose.rotations[q], pose.pose.rotations[q + 1], pose.pose.rotations[q + 2], pose.pose.rotations[q + 3]]);
|
|
820
|
+
sampledTargets.set(`${name}.scale`, [pose.pose.scales[p], pose.pose.scales[p + 1], pose.pose.scales[p + 2]]);
|
|
821
|
+
};
|
|
822
|
+
if (covered === undefined) {
|
|
823
|
+
for (let i = 0; i < pose.binding.boneCount; i += 1)
|
|
824
|
+
writeBone(i);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
for (const boneIndex of covered)
|
|
828
|
+
writeBone(boneIndex);
|
|
829
|
+
}
|
|
830
|
+
// Legacy accumulator path for every track the pose binding does not
|
|
831
|
+
// cover — sampled at each action's clock and effective weight, in active
|
|
832
|
+
// action order (same semantics as `applyClips`' per-sample partition).
|
|
833
|
+
const accumulators = new Map();
|
|
834
|
+
const unsupportedTracks = [];
|
|
835
|
+
for (const action of actions) {
|
|
836
|
+
const source = this.clipsByName.get(action.clipName);
|
|
837
|
+
if (source === undefined)
|
|
838
|
+
continue;
|
|
839
|
+
for (const track of source.tracks) {
|
|
840
|
+
const target = parseAnimationTarget(track.target);
|
|
841
|
+
if (target === undefined) {
|
|
842
|
+
unsupportedTracks.push(track.target);
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (target.kind === "node" && target.path !== "weights") {
|
|
846
|
+
const boneIndices = pose.binding.jointIndicesByName.get(target.nodeName);
|
|
847
|
+
if (boneIndices !== undefined && boneIndices.length > 0)
|
|
848
|
+
continue; // bound → mixer path
|
|
849
|
+
}
|
|
850
|
+
blendInto(accumulators, track.target, track.valueType, track.sample(action.time), action.effectiveWeight, action.additive);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
for (const [target, accumulator] of accumulators) {
|
|
854
|
+
sampledTargets.set(target, finalizeTargetBlend(accumulator));
|
|
855
|
+
}
|
|
856
|
+
const active = actions.filter((action) => action.effectiveWeight > 0);
|
|
857
|
+
this.lastApply = this.applySampledTargets(`mixer:${options?.label ?? (actions.map((action) => action.clipName).join("+") || "idle")}`, active[0]?.time ?? 0, { sampledTargets, unsupportedTracks }, actions.length > 0 ? actions.length : undefined, { mixerMs, constraintsMs, springsMs, paletteBytes: this.paletteBufferBytes(), cpuMs: nowMs() - phaseStart });
|
|
858
|
+
return this.lastApply;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* T1.10 (PRD-06 §10, C-19 `blendMode: "additive"`/`additiveReference`) — lazily
|
|
862
|
+
* compile an additive variant of `clipName` via `makeClipAdditive` (reference
|
|
863
|
+
* defaults to the clip's own first frame) and register it on the mixer under
|
|
864
|
+
* a synthesized name. Returns the registered clip key for `clipAction`/`playLayer`.
|
|
865
|
+
*/
|
|
866
|
+
ensureAdditiveClip(clipName, reference) {
|
|
867
|
+
const pose = this.poseRuntime();
|
|
868
|
+
const source = this.clipsByName.get(clipName);
|
|
869
|
+
if (source === undefined) {
|
|
870
|
+
return clipName;
|
|
871
|
+
}
|
|
872
|
+
const referenceClip = reference?.clip === undefined ? undefined : this.clipsByName.get(reference.clip);
|
|
873
|
+
const referenceTime = reference?.time ?? 0;
|
|
874
|
+
const name = `${clipName}#additive:${reference?.clip ?? "self"}:${referenceTime}`;
|
|
875
|
+
if (!pose.compiled.has(name)) {
|
|
876
|
+
const additive = makeClipAdditive(source, referenceClip === undefined ? referenceTime : { clip: referenceClip, time: referenceTime });
|
|
877
|
+
const compiledClip = compileClip(additive);
|
|
878
|
+
pose.compiled.set(name, compiledClip);
|
|
879
|
+
pose.mixer.addCompiledClip(name, compiledClip, additive);
|
|
880
|
+
}
|
|
881
|
+
return name;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* T1.10 (C-19 `animation.mask`) — bind-order joint names for `createBoneMask`.
|
|
885
|
+
*/
|
|
886
|
+
skeletonJointNames() {
|
|
887
|
+
return this.poseRuntime().binding.jointNames;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* T3.5 — register a pose constraint. Entries evaluate in insertion order
|
|
891
|
+
* after each mixer write; returns a disposer that removes exactly this
|
|
892
|
+
* constraint (by identity — a duplicate spec stays).
|
|
893
|
+
*/
|
|
894
|
+
addPoseConstraint(constraint) {
|
|
895
|
+
const state = this.poseRuntime();
|
|
896
|
+
state.constraints.push(constraint);
|
|
897
|
+
let live = true;
|
|
898
|
+
return () => {
|
|
899
|
+
if (!live)
|
|
900
|
+
return;
|
|
901
|
+
live = false;
|
|
902
|
+
const index = state.constraints.indexOf(constraint);
|
|
903
|
+
if (index >= 0)
|
|
904
|
+
state.constraints.splice(index, 1);
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/** T3.5 — drop every pose constraint (constant-time identity sweep). */
|
|
908
|
+
clearPoseConstraints() {
|
|
909
|
+
if (this.poseState === undefined)
|
|
910
|
+
return;
|
|
911
|
+
this.poseState.constraints.length = 0;
|
|
912
|
+
}
|
|
913
|
+
/** T3.5 — the count of live pose constraints (diagnostics/tests). */
|
|
914
|
+
poseConstraintCount() {
|
|
915
|
+
return this.poseState?.constraints.length ?? 0;
|
|
916
|
+
}
|
|
917
|
+
/** T3.5 — install the world→model matrix provider (actor extension). */
|
|
918
|
+
setPoseConstraintModelMatrix(provider) {
|
|
919
|
+
this.poseConstraintModelMatrix = provider;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* T3.5 — evaluate the constraint list in order onto the just-mixed pose and
|
|
923
|
+
* return the union of touched joint indices (empty when no constraints),
|
|
924
|
+
* plus the §9.8 wall-clock split between `"springs"`-tagged constraints and
|
|
925
|
+
* the rest.
|
|
926
|
+
*/
|
|
927
|
+
runPoseConstraints(pose, binding, dt) {
|
|
928
|
+
const touched = new Set();
|
|
929
|
+
const constraints = this.poseState?.constraints;
|
|
930
|
+
let constraintsMs = 0;
|
|
931
|
+
let springsMs = 0;
|
|
932
|
+
if (constraints === undefined || constraints.length === 0)
|
|
933
|
+
return { touched, constraintsMs, springsMs };
|
|
934
|
+
const modelMatrix = this.poseConstraintModelMatrix?.() ?? this.options.scene.root.transform.worldMatrix;
|
|
935
|
+
for (const constraint of constraints) {
|
|
936
|
+
const start = nowMs();
|
|
937
|
+
constraint.evaluate(pose, binding, modelMatrix, { dt });
|
|
938
|
+
const elapsed = nowMs() - start;
|
|
939
|
+
if (constraint.kind === "springs")
|
|
940
|
+
springsMs += elapsed;
|
|
941
|
+
else
|
|
942
|
+
constraintsMs += elapsed;
|
|
943
|
+
for (const bone of constraint.bones)
|
|
944
|
+
touched.add(bone);
|
|
945
|
+
}
|
|
946
|
+
return { touched, constraintsMs, springsMs };
|
|
947
|
+
}
|
|
948
|
+
/** §9.8/§13 — CPU-side palette payload this runtime owns, bytes (16 f32/joint). */
|
|
949
|
+
paletteBufferBytes() {
|
|
950
|
+
let bytes = 0;
|
|
951
|
+
for (const binding of this.skinningBindings)
|
|
952
|
+
bytes += binding.skin.joints.length * 16 * 4;
|
|
953
|
+
return bytes;
|
|
954
|
+
}
|
|
955
|
+
poseRuntime() {
|
|
956
|
+
if (this.poseState === undefined) {
|
|
957
|
+
const nodes = [];
|
|
958
|
+
this.options.scene.traverse((node) => nodes.push(node));
|
|
959
|
+
const indexByNode = new Map(nodes.map((node, index) => [node, index]));
|
|
960
|
+
const binding = bindSkeleton({
|
|
961
|
+
joints: nodes.map((_, index) => index),
|
|
962
|
+
resolveNode: (index) => {
|
|
963
|
+
const node = nodes[index];
|
|
964
|
+
if (node === undefined)
|
|
965
|
+
return undefined;
|
|
966
|
+
return { name: node.name, position: node.transform.position, rotation: node.transform.rotation, scale: node.transform.scale };
|
|
967
|
+
},
|
|
968
|
+
jointNames: nodes.map((node) => node.name),
|
|
969
|
+
parentIndices: nodes.map((node) => (node.parent === null ? -1 : indexByNode.get(node.parent) ?? -1))
|
|
970
|
+
});
|
|
971
|
+
const mixer = new PoseMixer({ skeleton: binding });
|
|
972
|
+
const compiled = new Map();
|
|
973
|
+
for (const [name, clip] of this.clipsByName) {
|
|
974
|
+
const compiledClip = compileClip(clip);
|
|
975
|
+
compiled.set(name, compiledClip);
|
|
976
|
+
mixer.addCompiledClip(name, compiledClip, clip);
|
|
977
|
+
}
|
|
978
|
+
this.poseState = { binding, mixer, compiled, pose: createPoseBuffer(binding.boneCount), constraints: [] };
|
|
979
|
+
}
|
|
980
|
+
return this.poseState;
|
|
981
|
+
}
|
|
433
982
|
applyAnimationValues(label, time, values, unsupportedTracks = []) {
|
|
434
983
|
if (label.trim().length === 0) {
|
|
435
984
|
throw new Error("glTF animation runtime value application label cannot be empty.");
|
|
@@ -443,6 +992,22 @@ export class GLTFSceneAnimationRuntime {
|
|
|
443
992
|
});
|
|
444
993
|
return this.lastApply;
|
|
445
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* §16 S3 (PRD-06) — teardown frees the C-18 palette textures this runtime
|
|
997
|
+
* stamped, keyed on each skin binding (`skinningPaletteCache.release`).
|
|
998
|
+
* Morph array textures and CPU-morph scratch are released by the actor's
|
|
999
|
+
* own teardown (`prd06.animation` extension dispose / pipeline lifecycle),
|
|
1000
|
+
* which is the layer that can see resolved `Geometry` objects — the scene
|
|
1001
|
+
* layer here only knows geometry handles.
|
|
1002
|
+
*/
|
|
1003
|
+
dispose() {
|
|
1004
|
+
if (this.disposed)
|
|
1005
|
+
return;
|
|
1006
|
+
for (const binding of this.skinningBindings) {
|
|
1007
|
+
skinningPaletteCache.release(binding.paletteView?.paletteKey ?? binding);
|
|
1008
|
+
}
|
|
1009
|
+
this.disposed = true;
|
|
1010
|
+
}
|
|
446
1011
|
/**
|
|
447
1012
|
* Render-time bridge: drive the loaded GLB from an externally-computed pose whose keys are GLB
|
|
448
1013
|
* **node names** (e.g. the output of `@aura3d/animation`'s `retargetHumanoidPose`, whose
|
|
@@ -585,6 +1150,8 @@ export class GLTFSceneAnimationRuntime {
|
|
|
585
1150
|
return [...this.clipsByName.values()].map((clip) => this.inspectClipBinding(clip));
|
|
586
1151
|
}
|
|
587
1152
|
reindexScene() {
|
|
1153
|
+
this.poseState = undefined;
|
|
1154
|
+
this.lastConstraintEvalAtMs = undefined;
|
|
588
1155
|
this.options.scene.updateWorldTransforms();
|
|
589
1156
|
this.footBindMatrices.clear();
|
|
590
1157
|
this.footOrientationLocks.clear();
|
|
@@ -617,7 +1184,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
617
1184
|
const skin = mesh?.skinIndex === undefined ? undefined : this.options.asset.skins[mesh.skinIndex];
|
|
618
1185
|
if (!mesh || !skin || skin.joints.length > MAX_RENDERABLE_SKINNING_JOINTS)
|
|
619
1186
|
continue;
|
|
620
|
-
this.skinningBindings.push({ node, renderable, mesh, skin, bindWorldMatrix: [...node.transform.worldMatrix] });
|
|
1187
|
+
this.skinningBindings.push({ node, renderable, mesh, skin, bindWorldMatrix: [...node.transform.worldMatrix], paletteMatrices: new Float32Array(skin.joints.length * 16) });
|
|
621
1188
|
}
|
|
622
1189
|
}
|
|
623
1190
|
}
|
|
@@ -939,36 +1506,64 @@ export class GLTFSceneAnimationRuntime {
|
|
|
939
1506
|
return samples;
|
|
940
1507
|
}
|
|
941
1508
|
refreshSkinningPalettes() {
|
|
1509
|
+
const result = this.paletteResult;
|
|
1510
|
+
result.updated = 0;
|
|
1511
|
+
result.missingTargets.length = 0;
|
|
942
1512
|
if (this.skinningBindings.length === 0) {
|
|
943
|
-
return
|
|
1513
|
+
return result;
|
|
944
1514
|
}
|
|
945
|
-
|
|
946
|
-
const missingTargets = [];
|
|
1515
|
+
const missingTargets = result.missingTargets;
|
|
947
1516
|
for (const binding of this.skinningBindings) {
|
|
948
|
-
|
|
949
|
-
|
|
1517
|
+
// T0.11: write into the binding's persistent palette buffer — no per-frame
|
|
1518
|
+
// Float32Array allocation, and the renderer's C-18 cache can key on it.
|
|
1519
|
+
const matrices = binding.paletteMatrices;
|
|
1520
|
+
const inverseMeshWorld = invertMat4Into(this.inverseWorldScratch, binding.node.transform.worldMatrix);
|
|
1521
|
+
// §13: joint nodes resolved once per binding instead of a string Map
|
|
1522
|
+
// lookup per joint per frame; the cache rebuilds while any joint is
|
|
1523
|
+
// missing so late-registered nodes still resolve.
|
|
1524
|
+
let jointNodes = binding.jointNodes;
|
|
1525
|
+
if (!jointNodes) {
|
|
1526
|
+
jointNodes = new Array(binding.skin.jointNames.length);
|
|
1527
|
+
let allResolved = true;
|
|
1528
|
+
for (let index = 0; index < binding.skin.jointNames.length; index += 1) {
|
|
1529
|
+
const node = this.nodesByName.get(binding.skin.jointNames[index])?.[0];
|
|
1530
|
+
jointNodes[index] = node;
|
|
1531
|
+
if (!node)
|
|
1532
|
+
allResolved = false;
|
|
1533
|
+
}
|
|
1534
|
+
if (allResolved)
|
|
1535
|
+
binding.jointNodes = jointNodes;
|
|
1536
|
+
}
|
|
950
1537
|
let complete = true;
|
|
951
1538
|
for (let index = 0; index < binding.skin.jointNames.length; index += 1) {
|
|
952
|
-
const
|
|
953
|
-
const jointNode = this.nodesByName.get(jointName)?.[0];
|
|
1539
|
+
const jointNode = jointNodes[index];
|
|
954
1540
|
const inverseBind = binding.skin.inverseBindMatrices[index];
|
|
955
1541
|
if (!jointNode || !inverseBind) {
|
|
956
|
-
missingTargets.push(`${binding.skin.name}.${
|
|
1542
|
+
missingTargets.push(`${binding.skin.name}.${binding.skin.jointNames[index]}`);
|
|
957
1543
|
complete = false;
|
|
958
1544
|
break;
|
|
959
1545
|
}
|
|
960
|
-
|
|
961
|
-
|
|
1546
|
+
// (inverseMeshWorld * jointWorld) * inverseBind, straight into the palette.
|
|
1547
|
+
multiplyMat4Into(this.paletteScratch, 0, inverseMeshWorld, jointNode.transform.worldMatrix);
|
|
1548
|
+
multiplyMat4Into(matrices, index * 16, this.paletteScratch, inverseBind);
|
|
962
1549
|
}
|
|
963
1550
|
if (!complete)
|
|
964
1551
|
continue;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1552
|
+
let skinningPalette = binding.paletteView;
|
|
1553
|
+
if (!skinningPalette) {
|
|
1554
|
+
skinningPalette = {
|
|
1555
|
+
jointCount: binding.skin.joints.length,
|
|
1556
|
+
matrices
|
|
1557
|
+
};
|
|
1558
|
+
binding.paletteView = skinningPalette;
|
|
1559
|
+
}
|
|
1560
|
+
// C-18/§9.2: the stable per-skin key the palette cache binds on is this
|
|
1561
|
+
// binding object, not the per-frame `renderable.skinning` wrapper (E40).
|
|
1562
|
+
skinningPalette.paletteKey = binding;
|
|
1563
|
+
binding.renderable.skinning = skinningPalette;
|
|
1564
|
+
result.updated += 1;
|
|
970
1565
|
}
|
|
971
|
-
return
|
|
1566
|
+
return result;
|
|
972
1567
|
}
|
|
973
1568
|
inspectClipBinding(clip) {
|
|
974
1569
|
let supportedTrackCount = 0;
|
|
@@ -1049,7 +1644,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
1049
1644
|
animatesSkeleton: this.skinningBindings.length > 0 && boundTrackCount > 0 && transformTrackCount > 0
|
|
1050
1645
|
};
|
|
1051
1646
|
}
|
|
1052
|
-
applySampledTargets(clipName, time, sampled, blendedClipCount) {
|
|
1647
|
+
applySampledTargets(clipName, time, sampled, blendedClipCount, phaseTimings) {
|
|
1053
1648
|
let transformTracksApplied = 0;
|
|
1054
1649
|
let morphWeightTracksApplied = 0;
|
|
1055
1650
|
let materialTracksApplied = 0;
|
|
@@ -1117,7 +1712,8 @@ export class GLTFSceneAnimationRuntime {
|
|
|
1117
1712
|
...(measuredFootPlanting === undefined ? {} : { footPlanting: measuredFootPlanting }),
|
|
1118
1713
|
skinningPalettesUpdated: skinning.updated,
|
|
1119
1714
|
missingTargets: [...missingTargets, ...skinning.missingTargets],
|
|
1120
|
-
unsupportedTracks: sampled.unsupportedTracks
|
|
1715
|
+
unsupportedTracks: sampled.unsupportedTracks,
|
|
1716
|
+
...(phaseTimings === undefined ? {} : { phaseTimings })
|
|
1121
1717
|
};
|
|
1122
1718
|
}
|
|
1123
1719
|
}
|
|
@@ -1727,4 +2323,44 @@ function multiplyQuat(a, b) {
|
|
|
1727
2323
|
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
|
|
1728
2324
|
];
|
|
1729
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* T0.6/T0.7 (PRD-06) — a clip "has a root-motion candidate" when a translation
|
|
2328
|
+
* track on a hips/root-named target nets more than 5 cm of XZ displacement
|
|
2329
|
+
* between its first and last keyframes.
|
|
2330
|
+
*/
|
|
2331
|
+
function clipHasRootMotionCandidate(clip) {
|
|
2332
|
+
for (const track of clip.tracks) {
|
|
2333
|
+
const target = parseAnimationTarget(track.target);
|
|
2334
|
+
if (target?.kind !== "node" || target.path !== "translation")
|
|
2335
|
+
continue;
|
|
2336
|
+
if (!/hips|root|pelvis/i.test(target.nodeName))
|
|
2337
|
+
continue;
|
|
2338
|
+
const keys = track.keyframes ?? [];
|
|
2339
|
+
if (keys.length < 2)
|
|
2340
|
+
continue;
|
|
2341
|
+
const first = vec3OfKeyframeValue(keys[0].value);
|
|
2342
|
+
const last = vec3OfKeyframeValue(keys[keys.length - 1].value);
|
|
2343
|
+
if (!first || !last)
|
|
2344
|
+
continue;
|
|
2345
|
+
const dx = last[0] - first[0];
|
|
2346
|
+
const dz = last[2] - first[2];
|
|
2347
|
+
if (Math.hypot(dx, dz) > 0.05)
|
|
2348
|
+
return true;
|
|
2349
|
+
}
|
|
2350
|
+
return false;
|
|
2351
|
+
}
|
|
2352
|
+
function vec3OfKeyframeValue(value) {
|
|
2353
|
+
if (Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
2354
|
+
const arr = value;
|
|
2355
|
+
if (arr.length >= 3)
|
|
2356
|
+
return [arr[0], arr[1], arr[2]];
|
|
2357
|
+
return undefined;
|
|
2358
|
+
}
|
|
2359
|
+
if (value && typeof value === "object") {
|
|
2360
|
+
const v = value;
|
|
2361
|
+
if (typeof v.x === "number" && typeof v.z === "number")
|
|
2362
|
+
return [v.x, v.y ?? 0, v.z];
|
|
2363
|
+
}
|
|
2364
|
+
return undefined;
|
|
2365
|
+
}
|
|
1730
2366
|
//# sourceMappingURL=GLTFAnimationRuntime.js.map
|