@aura3d/assets 3.0.0 → 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 +300 -6
- package/dist/GLTFAnimationRuntime.d.ts.map +1 -1
- package/dist/GLTFAnimationRuntime.js +981 -60
- 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/GLTFCompressionDecoders.d.ts +1 -0
- package/dist/GLTFCompressionDecoders.d.ts.map +1 -1
- package/dist/GLTFCompressionDecoders.js +20 -5
- package/dist/GLTFCompressionDecoders.js.map +1 -1
- 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 +197 -30
- 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 -184
- 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
|
-
import { AnimationAction, AnimationClip, AnimationMixer, createFootIkRig, normalizeQuat, slerpQuat, solveTwoBoneIk } from "@aura3d/animation";
|
|
2
|
-
import {
|
|
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";
|
|
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,15 +269,104 @@ 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;
|
|
339
|
+
footBindMatrices = new Map();
|
|
340
|
+
footOrientationLocks = new Map();
|
|
341
|
+
footDescendantLocks = new Map();
|
|
342
|
+
footSurfaceVertices = new Map();
|
|
262
343
|
footPlanting;
|
|
263
344
|
footRig;
|
|
264
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;
|
|
265
370
|
constructor(options) {
|
|
266
371
|
this.options = options;
|
|
267
372
|
for (const clip of options.clips) {
|
|
@@ -284,9 +389,150 @@ export class GLTFSceneAnimationRuntime {
|
|
|
284
389
|
}
|
|
285
390
|
if (!sameFootPlantingShape(this.footPlanting, config)) {
|
|
286
391
|
this.footRig = undefined;
|
|
392
|
+
this.footOrientationLocks.clear();
|
|
393
|
+
this.footDescendantLocks.clear();
|
|
287
394
|
}
|
|
288
395
|
this.footPlanting = config;
|
|
289
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Extract real authored displacement over unwrapped clip time, hand it to the physical
|
|
399
|
+
* authority once, then apply an in-place root pose and solve feet in the updated actor
|
|
400
|
+
* transform. Rejected movement is returned and discarded rather than accumulating drift.
|
|
401
|
+
* The caller refreshes footPlanting.worldFromLocal inside move after committing movement.
|
|
402
|
+
*/
|
|
403
|
+
applyRootMotionClip(name, options) {
|
|
404
|
+
return this.applyRootMotionClips([{ clipName: name, ...options }], options);
|
|
405
|
+
}
|
|
406
|
+
/** Blend displacement and consumed poses with the same base/additive weights. */
|
|
407
|
+
applyRootMotionClips(samples, options) {
|
|
408
|
+
if (samples.length === 0)
|
|
409
|
+
throw new Error("glTF root motion blend requires at least one clip sample.");
|
|
410
|
+
const accumulators = new Map();
|
|
411
|
+
const motionAccumulators = new Map();
|
|
412
|
+
const unsupportedTracks = [];
|
|
413
|
+
const names = [];
|
|
414
|
+
let maxTime = 0;
|
|
415
|
+
// Finish validation and sampling before the physical authority can mutate the actor.
|
|
416
|
+
for (const sample of samples) {
|
|
417
|
+
const clip = this.clipsByName.get(sample.clipName);
|
|
418
|
+
if (!clip)
|
|
419
|
+
throw new Error(`glTF animation clip "${sample.clipName}" was not found.`);
|
|
420
|
+
const weight = sample.weight ?? 1;
|
|
421
|
+
if (!Number.isFinite(weight) || weight < 0)
|
|
422
|
+
throw new Error("glTF root motion blend weight must be finite and non-negative.");
|
|
423
|
+
const extracted = extractRootMotion(clip, sample);
|
|
424
|
+
if (weight === 0)
|
|
425
|
+
continue;
|
|
426
|
+
const time = sample.loop && clip.duration > 0
|
|
427
|
+
? ((sample.toTime % clip.duration) + clip.duration) % clip.duration
|
|
428
|
+
: Math.max(0, Math.min(clip.duration, sample.toTime));
|
|
429
|
+
maxTime = Math.max(maxTime, time);
|
|
430
|
+
names.push(`${clip.name}@${time}x${weight}${sample.additive ? "+add" : ""}`);
|
|
431
|
+
blendInto(motionAccumulators, "motion", "vector3", extracted.delta, weight, sample.additive === true);
|
|
432
|
+
for (const track of clip.tracks) {
|
|
433
|
+
if (!parseAnimationTarget(track.target)) {
|
|
434
|
+
unsupportedTracks.push(track.target);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
// Consume each clip's own root before blending, including additive root tracks.
|
|
438
|
+
const value = track.sample(track.target === sample.target ? 0 : time);
|
|
439
|
+
blendInto(accumulators, track.target, track.valueType, value, weight, sample.additive === true);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const values = new Map();
|
|
443
|
+
for (const [target, accumulator] of accumulators)
|
|
444
|
+
values.set(target, finalizeTargetBlend(accumulator));
|
|
445
|
+
const motionAccumulator = motionAccumulators.get("motion");
|
|
446
|
+
const delta = motionAccumulator ? finalizeTargetBlend(motionAccumulator) : [0, 0, 0];
|
|
447
|
+
const motion = consumeRootMotion({ target: "blend", fromTime: 0, toTime: maxTime, looped: samples.some(sample => sample.loop), delta }, options.worldFromLocal, options.move);
|
|
448
|
+
this.lastApply = this.applySampledTargets(`root-motion:${names.join(",")}`, maxTime, { sampledTargets: values, unsupportedTracks }, samples.length);
|
|
449
|
+
return { motion, applyResult: this.lastApply };
|
|
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
|
+
}
|
|
290
536
|
applyClipByName(name, time) {
|
|
291
537
|
const clip = this.clipsByName.get(name);
|
|
292
538
|
if (!clip) {
|
|
@@ -298,13 +544,27 @@ export class GLTFSceneAnimationRuntime {
|
|
|
298
544
|
clipNames() {
|
|
299
545
|
return [...this.clipsByName.keys()];
|
|
300
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
|
+
}
|
|
301
561
|
/**
|
|
302
562
|
* Resolve a requested clip name to the best available registered clip name
|
|
303
563
|
* using fuzzy matching (exact -> synonym group -> substring -> first clip).
|
|
304
564
|
* Returns `undefined` when the runtime has no clips.
|
|
305
565
|
*/
|
|
306
|
-
resolveClipName(name) {
|
|
307
|
-
return resolveGLTFClipName(name, this.clipNames());
|
|
566
|
+
resolveClipName(name, options) {
|
|
567
|
+
return resolveGLTFClipName(name, this.clipNames(), options);
|
|
308
568
|
}
|
|
309
569
|
/**
|
|
310
570
|
* Apply a clip selected by fuzzy name resolution. Unlike {@link applyClipByName}
|
|
@@ -326,14 +586,35 @@ export class GLTFSceneAnimationRuntime {
|
|
|
326
586
|
this.lastApply = this.applySampledTargets(clip.name, wrappedTime, sampleClipTracks(clip, wrappedTime, 1, false));
|
|
327
587
|
return this.lastApply;
|
|
328
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
|
+
*/
|
|
329
602
|
applyClips(samples) {
|
|
330
603
|
if (samples.length === 0) {
|
|
331
604
|
throw new Error("glTF animation runtime blend requires at least one clip sample.");
|
|
332
605
|
}
|
|
606
|
+
const phaseStart = nowMs();
|
|
607
|
+
let mixerMs = 0;
|
|
608
|
+
let constraintsMs = 0;
|
|
609
|
+
let springsMs = 0;
|
|
333
610
|
const accumulators = new Map();
|
|
334
611
|
const unsupportedTracks = [];
|
|
335
612
|
let maxTime = 0;
|
|
336
613
|
const names = [];
|
|
614
|
+
const pose = this.poseRuntime();
|
|
615
|
+
const poseSpecs = [];
|
|
616
|
+
const touchedBones = new Set();
|
|
617
|
+
const maskWeights = new Map();
|
|
337
618
|
for (const sample of samples) {
|
|
338
619
|
const clip = this.clipsByName.get(sample.clipName);
|
|
339
620
|
if (!clip) {
|
|
@@ -351,25 +632,353 @@ export class GLTFSceneAnimationRuntime {
|
|
|
351
632
|
const wrappedTime = clip.duration > 0 && sample.time > clip.duration ? sample.time % clip.duration : Math.min(sample.time, clip.duration);
|
|
352
633
|
maxTime = Math.max(maxTime, wrappedTime);
|
|
353
634
|
names.push(`${clip.name}@${Number(wrappedTime.toFixed(4))}x${Number(weight.toFixed(4))}${sample.additive ? "+add" : ""}`);
|
|
635
|
+
let bindsNodes = false;
|
|
354
636
|
for (const track of clip.tracks) {
|
|
355
637
|
const target = parseAnimationTarget(track.target);
|
|
356
638
|
if (!target) {
|
|
357
639
|
unsupportedTracks.push(track.target);
|
|
358
640
|
continue;
|
|
359
641
|
}
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
654
|
continue;
|
|
655
|
+
}
|
|
363
656
|
blendInto(accumulators, track.target, track.valueType, track.sample(wrappedTime), weight, sample.additive === true);
|
|
364
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
|
+
}
|
|
673
|
+
}
|
|
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
|
+
}
|
|
703
|
+
for (const [target, accumulator] of accumulators) {
|
|
704
|
+
sampledTargets.set(target, finalizeTargetBlend(accumulator));
|
|
705
|
+
}
|
|
706
|
+
this.lastApply = this.applySampledTargets(`blend:${names.join(",")}`, maxTime, { sampledTargets, unsupportedTracks }, samples.length, { mixerMs, constraintsMs, springsMs, paletteBytes: this.paletteBufferBytes(), cpuMs: nowMs() - phaseStart });
|
|
707
|
+
return this.lastApply;
|
|
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;
|
|
365
810
|
}
|
|
811
|
+
// `covered === undefined` already emits every bound bone below — the
|
|
812
|
+
// constraint union matters only on the covered path.
|
|
366
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
|
+
}
|
|
367
853
|
for (const [target, accumulator] of accumulators) {
|
|
368
854
|
sampledTargets.set(target, finalizeTargetBlend(accumulator));
|
|
369
855
|
}
|
|
370
|
-
|
|
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 });
|
|
371
858
|
return this.lastApply;
|
|
372
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
|
+
}
|
|
373
982
|
applyAnimationValues(label, time, values, unsupportedTracks = []) {
|
|
374
983
|
if (label.trim().length === 0) {
|
|
375
984
|
throw new Error("glTF animation runtime value application label cannot be empty.");
|
|
@@ -383,6 +992,22 @@ export class GLTFSceneAnimationRuntime {
|
|
|
383
992
|
});
|
|
384
993
|
return this.lastApply;
|
|
385
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
|
+
}
|
|
386
1011
|
/**
|
|
387
1012
|
* Render-time bridge: drive the loaded GLB from an externally-computed pose whose keys are GLB
|
|
388
1013
|
* **node names** (e.g. the output of `@aura3d/animation`'s `retargetHumanoidPose`, whose
|
|
@@ -525,10 +1150,18 @@ export class GLTFSceneAnimationRuntime {
|
|
|
525
1150
|
return [...this.clipsByName.values()].map((clip) => this.inspectClipBinding(clip));
|
|
526
1151
|
}
|
|
527
1152
|
reindexScene() {
|
|
1153
|
+
this.poseState = undefined;
|
|
1154
|
+
this.lastConstraintEvalAtMs = undefined;
|
|
1155
|
+
this.options.scene.updateWorldTransforms();
|
|
1156
|
+
this.footBindMatrices.clear();
|
|
1157
|
+
this.footOrientationLocks.clear();
|
|
1158
|
+
this.footDescendantLocks.clear();
|
|
528
1159
|
this.nodesByName.clear();
|
|
529
1160
|
this.morphRenderablesByNodeName.clear();
|
|
530
1161
|
this.skinningBindings.length = 0;
|
|
1162
|
+
this.footSurfaceVertices.clear();
|
|
531
1163
|
this.options.scene.traverse((node) => {
|
|
1164
|
+
this.footBindMatrices.set(node.id, { world: [...node.transform.worldMatrix], local: [...node.transform.localMatrix] });
|
|
532
1165
|
const nodes = this.nodesByName.get(node.name) ?? [];
|
|
533
1166
|
nodes.push(node);
|
|
534
1167
|
this.nodesByName.set(node.name, nodes);
|
|
@@ -551,7 +1184,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
551
1184
|
const skin = mesh?.skinIndex === undefined ? undefined : this.options.asset.skins[mesh.skinIndex];
|
|
552
1185
|
if (!mesh || !skin || skin.joints.length > MAX_RENDERABLE_SKINNING_JOINTS)
|
|
553
1186
|
continue;
|
|
554
|
-
this.skinningBindings.push({ node, renderable, mesh, skin });
|
|
1187
|
+
this.skinningBindings.push({ node, renderable, mesh, skin, bindWorldMatrix: [...node.transform.worldMatrix], paletteMatrices: new Float32Array(skin.joints.length * 16) });
|
|
555
1188
|
}
|
|
556
1189
|
}
|
|
557
1190
|
}
|
|
@@ -559,9 +1192,9 @@ export class GLTFSceneAnimationRuntime {
|
|
|
559
1192
|
return this.options.scene.collectLights().find((light) => light.name === name);
|
|
560
1193
|
}
|
|
561
1194
|
/**
|
|
562
|
-
* Foot-planting post-pass (E2): solves the configured leg chains against the ground
|
|
563
|
-
*
|
|
564
|
-
*
|
|
1195
|
+
* Foot-planting post-pass (E2): solves the configured leg chains against the ground,
|
|
1196
|
+
* applies a shared pelvis reach correction, and rotates rigid hip/knee chains toward the
|
|
1197
|
+
* solved targets. Missing leg nodes are reported in
|
|
565
1198
|
* `missingLegNodes` and skipped, never faked. Returns undefined when unconfigured.
|
|
566
1199
|
*/
|
|
567
1200
|
applyFootPlanting() {
|
|
@@ -595,11 +1228,20 @@ export class GLTFSceneAnimationRuntime {
|
|
|
595
1228
|
missingLegNodes.push(`${leg.side}:ankle:${leg.ankle}`);
|
|
596
1229
|
if (!hip || !knee || !ankle)
|
|
597
1230
|
continue;
|
|
598
|
-
resolved.push({ side: leg.side, hip, knee, ankle, pole: leg.pole });
|
|
1231
|
+
resolved.push({ side: leg.side, hip, knee, ankle, pole: leg.pole, ...(leg.contact === undefined ? {} : { contact: leg.contact }), ...(leg.support ? { support: leg.support } : {}) });
|
|
599
1232
|
}
|
|
600
1233
|
if (resolved.length === 0) {
|
|
601
1234
|
return { groundedFeet: 0, averageTargetError: 0, lockedSides: [], missingLegNodes, hipOffset: 0 };
|
|
602
1235
|
}
|
|
1236
|
+
// Measure each animated ankle-to-sole offset from the exact pre-solve skinning
|
|
1237
|
+
// palette. One global value cannot plant asymmetric shoes or a rotated touchdown.
|
|
1238
|
+
this.refreshSkinningPalettes();
|
|
1239
|
+
const rawSoles = this.sampleFootSurfaces();
|
|
1240
|
+
const measuredAnkleHeights = new Map(resolved.map(entry => {
|
|
1241
|
+
const ankle = solvePoint(worldPosition(entry.ankle));
|
|
1242
|
+
const minimum = Math.min(Infinity, ...rawSoles.filter(point => point.side === entry.side).map(point => point.worldPosition[1]));
|
|
1243
|
+
return [entry.side, Number.isFinite(minimum) ? Math.max(0, ankle[1] - minimum) : undefined];
|
|
1244
|
+
}));
|
|
603
1245
|
if (!this.footRig) {
|
|
604
1246
|
this.footRig = createFootIkRig({
|
|
605
1247
|
legs: resolved.map((entry) => ({
|
|
@@ -607,6 +1249,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
607
1249
|
hip: solvePoint(worldPosition(entry.hip)),
|
|
608
1250
|
knee: solvePoint(worldPosition(entry.knee)),
|
|
609
1251
|
ankle: solvePoint(worldPosition(entry.ankle)),
|
|
1252
|
+
...(measuredAnkleHeights.get(entry.side) ?? config.ankleHeight) !== undefined ? { ankleHeight: measuredAnkleHeights.get(entry.side) ?? config.ankleHeight } : {},
|
|
610
1253
|
...(entry.pole ? { pole: entry.pole } : {})
|
|
611
1254
|
})),
|
|
612
1255
|
raycaster: config.ground,
|
|
@@ -617,38 +1260,116 @@ export class GLTFSceneAnimationRuntime {
|
|
|
617
1260
|
...(config.hipDropFactor !== undefined ? { hipDropFactor: config.hipDropFactor } : {})
|
|
618
1261
|
});
|
|
619
1262
|
}
|
|
1263
|
+
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
1264
|
+
const originalLengths = new Map(resolved.map(entry => [entry.side, {
|
|
1265
|
+
upper: distance(solvePoint(worldPosition(entry.hip)), solvePoint(worldPosition(entry.knee))),
|
|
1266
|
+
lower: distance(solvePoint(worldPosition(entry.knee)), solvePoint(worldPosition(entry.ankle)))
|
|
1267
|
+
}]));
|
|
620
1268
|
const solved = this.footRig.solveFootPlacement({
|
|
621
1269
|
legs: resolved.map((entry) => ({
|
|
622
1270
|
side: entry.side,
|
|
623
1271
|
hip: solvePoint(worldPosition(entry.hip)),
|
|
624
1272
|
knee: solvePoint(worldPosition(entry.knee)),
|
|
625
1273
|
ankle: solvePoint(worldPosition(entry.ankle)),
|
|
626
|
-
...(entry.
|
|
1274
|
+
...(measuredAnkleHeights.get(entry.side) ?? config.ankleHeight) !== undefined ? { ankleHeight: measuredAnkleHeights.get(entry.side) ?? config.ankleHeight } : {},
|
|
1275
|
+
...(entry.pole ? { pole: entry.pole } : {}),
|
|
1276
|
+
...(entry.contact === undefined ? {} : { contact: entry.contact }),
|
|
1277
|
+
...(entry.support ? { support: entry.support } : {})
|
|
627
1278
|
}))
|
|
628
1279
|
});
|
|
1280
|
+
// A reach correction belongs to the shared pelvis owner. Move it once before
|
|
1281
|
+
// rotating either leg; translating each upper-leg root dislocates the chain,
|
|
1282
|
+
// while omitting the drop leaves a fully extended stance foot above ground.
|
|
1283
|
+
if (solved.hipOffset < 0) {
|
|
1284
|
+
const common = commonAncestor(resolved.map(entry => entry.hip));
|
|
1285
|
+
const anchors = common && common.parent ? [common] : [...new Set(resolved.map(entry => entry.hip))];
|
|
1286
|
+
for (const anchor of anchors) {
|
|
1287
|
+
const currentWorld = solvePoint(worldPosition(anchor));
|
|
1288
|
+
const dropped = actorPoint([currentWorld[0], currentWorld[1] + solved.hipOffset, currentWorld[2]]);
|
|
1289
|
+
setWorldPosition(anchor, dropped);
|
|
1290
|
+
}
|
|
1291
|
+
this.options.scene.updateWorldTransforms();
|
|
1292
|
+
}
|
|
629
1293
|
for (const foot of solved.feet) {
|
|
630
1294
|
const entry = resolved.find((candidate) => candidate.side === foot.side);
|
|
631
1295
|
if (!entry)
|
|
632
1296
|
continue;
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
// absorb the pelvis motion by bending, the natural single-pelvis compromise.
|
|
638
|
-
// Refresh down the chain after each write: setWorldPosition reads the parent's world
|
|
639
|
-
// matrix, so the knee must see the new hip world before its own write (same for ankle).
|
|
640
|
-
// Solved targets are world-space; convert to actor-local for the write-back.
|
|
641
|
-
const hipTarget = actorPoint([foot.hip[0], foot.hip[1] + solved.hipOffset, foot.hip[2]]);
|
|
642
|
-
setWorldPosition(entry.hip, [hipTarget[0], hipTarget[1], hipTarget[2]]);
|
|
643
|
-
entry.hip.updateWorldTransform();
|
|
1297
|
+
// Apply the two-bone solution with joint rotations. Moving hip/knee/ankle
|
|
1298
|
+
// translations directly changes authored bone lengths and deforms every weighted
|
|
1299
|
+
// mesh. The solver targets are world-space; orient each parent toward its solved
|
|
1300
|
+
// child while preserving the animation's local translations and scales.
|
|
644
1301
|
const kneeTarget = actorPoint(foot.knee);
|
|
645
|
-
|
|
646
|
-
entry.
|
|
1302
|
+
orientJointToward(entry.hip, entry.knee, kneeTarget);
|
|
1303
|
+
entry.hip.updateWorldTransform();
|
|
647
1304
|
const ankleTarget = actorPoint(foot.ankle);
|
|
648
|
-
|
|
1305
|
+
orientJointToward(entry.knee, entry.ankle, ankleTarget);
|
|
1306
|
+
entry.knee.updateWorldTransform();
|
|
1307
|
+
}
|
|
1308
|
+
this.options.scene.updateWorldTransforms();
|
|
1309
|
+
for (const foot of solved.feet) {
|
|
1310
|
+
const entry = resolved.find(candidate => candidate.side === foot.side);
|
|
1311
|
+
if (!foot.locked || config.lockFootRotation === false) {
|
|
1312
|
+
this.footOrientationLocks.delete(foot.side);
|
|
1313
|
+
this.footDescendantLocks.delete(foot.side);
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
let locked = this.footOrientationLocks.get(foot.side);
|
|
1317
|
+
if (!locked) {
|
|
1318
|
+
// Capture the actual animated sole orientation and toe pose at touchdown.
|
|
1319
|
+
// Reusing the asset bind pose snaps arbitrary rigs and moves their sole vertices.
|
|
1320
|
+
locked = toWorld ? multiplyMat4(toWorld, entry.ankle.transform.worldMatrix) : [...entry.ankle.transform.worldMatrix];
|
|
1321
|
+
const descendants = new Map();
|
|
1322
|
+
entry.ankle.children.forEach(child => child.traverse(node => descendants.set(node.id, [...node.transform.localMatrix])));
|
|
1323
|
+
this.footDescendantLocks.set(foot.side, descendants);
|
|
1324
|
+
const normal = foot.sample.groundNormal;
|
|
1325
|
+
const norm = Math.hypot(...normal);
|
|
1326
|
+
if (!(norm > 0))
|
|
1327
|
+
throw new Error("Foot contact requires a nonzero ground normal.");
|
|
1328
|
+
const nx = normal[0] / norm, ny = normal[1] / norm, nz = normal[2] / norm;
|
|
1329
|
+
if (ny <= -0.999)
|
|
1330
|
+
throw new Error("Foot contact cannot plant on a downward facing surface.");
|
|
1331
|
+
const vx = nz, vz = -nx, t = 1 / (1 + ny);
|
|
1332
|
+
const tilt = [1 - vz * vz * t, vz, vx * vz * t, 0, -vz, 1 - (vx * vx + vz * vz) * t, vx, 0, vx * vz * t, -vx, 1 - vx * vx * t, 0, 0, 0, 0, 1];
|
|
1333
|
+
locked = multiplyMat4(tilt, locked);
|
|
1334
|
+
this.footOrientationLocks.set(foot.side, locked);
|
|
1335
|
+
}
|
|
1336
|
+
// Keep the solved ankle position while holding the sole orientation in world space.
|
|
1337
|
+
const ankleWorld = solvePoint(worldPosition(entry.ankle));
|
|
1338
|
+
const desired = [...locked];
|
|
1339
|
+
desired[12] = ankleWorld[0];
|
|
1340
|
+
desired[13] = ankleWorld[1];
|
|
1341
|
+
desired[14] = ankleWorld[2];
|
|
1342
|
+
const actorMatrix = toLocal ? multiplyMat4(toLocal, desired) : desired;
|
|
1343
|
+
const parent = entry.ankle.parent;
|
|
1344
|
+
const local = parent ? multiplyMat4(invertMat4(parent.transform.worldMatrix), actorMatrix) : actorMatrix;
|
|
1345
|
+
entry.ankle.transform.setFromLocalMatrix(local);
|
|
1346
|
+
// Toe joints remain in their touchdown relationship during stance; animated
|
|
1347
|
+
// toe curl otherwise moves the sole while the ankle still reports zero slip.
|
|
1348
|
+
const touchdown = this.footDescendantLocks.get(foot.side);
|
|
1349
|
+
const restore = (node) => {
|
|
1350
|
+
const rest = touchdown?.get(node.id);
|
|
1351
|
+
if (rest)
|
|
1352
|
+
node.transform.setFromLocalMatrix(rest);
|
|
1353
|
+
node.children.forEach(restore);
|
|
1354
|
+
};
|
|
1355
|
+
entry.ankle.children.forEach(restore);
|
|
1356
|
+
entry.ankle.updateWorldTransform();
|
|
649
1357
|
}
|
|
650
1358
|
this.options.scene.updateWorldTransforms();
|
|
651
1359
|
return {
|
|
1360
|
+
legDeformation: resolved.map(entry => {
|
|
1361
|
+
const before = originalLengths.get(entry.side);
|
|
1362
|
+
const upperAfter = distance(solvePoint(worldPosition(entry.hip)), solvePoint(worldPosition(entry.knee)));
|
|
1363
|
+
const lowerAfter = distance(solvePoint(worldPosition(entry.knee)), solvePoint(worldPosition(entry.ankle)));
|
|
1364
|
+
return { side: entry.side, upperBefore: before.upper, lowerBefore: before.lower, upperAfter, lowerAfter,
|
|
1365
|
+
maxRelativeLengthChange: Math.max(Math.abs(upperAfter - before.upper) / Math.max(1e-9, before.upper), Math.abs(lowerAfter - before.lower) / Math.max(1e-9, before.lower)) };
|
|
1366
|
+
}),
|
|
1367
|
+
feet: solved.feet.map(foot => {
|
|
1368
|
+
const entry = resolved.find(candidate => candidate.side === foot.side);
|
|
1369
|
+
const position = solvePoint(worldPosition(entry.ankle));
|
|
1370
|
+
return { side: foot.side, worldPosition: position, locked: foot.locked,
|
|
1371
|
+
contactError: Math.hypot(...position.map((value, axis) => value - foot.sample.plantedFoot[axis])) };
|
|
1372
|
+
}),
|
|
652
1373
|
groundedFeet: solved.groundedFeet,
|
|
653
1374
|
averageTargetError: solved.averageTargetError,
|
|
654
1375
|
lockedSides: solved.feet.filter((foot) => foot.locked).map((foot) => foot.side),
|
|
@@ -699,37 +1420,150 @@ export class GLTFSceneAnimationRuntime {
|
|
|
699
1420
|
light.range = asPositiveScalar(value, trackTarget);
|
|
700
1421
|
return true;
|
|
701
1422
|
}
|
|
1423
|
+
/** CPU skinning oracle using the exact palette submitted to rendering, not IK targets. */
|
|
1424
|
+
sampleFootSurfaces() {
|
|
1425
|
+
const config = this.footPlanting;
|
|
1426
|
+
if (!config)
|
|
1427
|
+
return [];
|
|
1428
|
+
const samples = [];
|
|
1429
|
+
for (const leg of config.legs) {
|
|
1430
|
+
const ankle = this.nodesByName.get(leg.ankle)?.[0];
|
|
1431
|
+
if (!ankle)
|
|
1432
|
+
continue;
|
|
1433
|
+
const footNames = new Set();
|
|
1434
|
+
const visit = (node) => { footNames.add(node.name); node.children.forEach(visit); };
|
|
1435
|
+
visit(ankle);
|
|
1436
|
+
for (const binding of this.skinningBindings) {
|
|
1437
|
+
const palette = binding.renderable.skinning;
|
|
1438
|
+
if (!palette)
|
|
1439
|
+
continue;
|
|
1440
|
+
const mesh = binding.mesh;
|
|
1441
|
+
const cacheKey = `${leg.ankle}:${binding.node.id}:${mesh.name}`;
|
|
1442
|
+
let selected = this.footSurfaceVertices.get(cacheKey);
|
|
1443
|
+
if (!selected) {
|
|
1444
|
+
const candidates = [];
|
|
1445
|
+
for (let vertex = 0; vertex < mesh.positions.length; vertex++) {
|
|
1446
|
+
let footWeight = 0;
|
|
1447
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
1448
|
+
if (footNames.has(binding.skin.jointNames[mesh.joints[vertex]?.[lane] ?? -1] ?? ""))
|
|
1449
|
+
footWeight += mesh.weights[vertex]?.[lane] ?? 0;
|
|
1450
|
+
if (footNames.has(binding.skin.jointNames[mesh.joints1?.[vertex]?.[lane] ?? -1] ?? ""))
|
|
1451
|
+
footWeight += mesh.weights1?.[vertex]?.[lane] ?? 0;
|
|
1452
|
+
}
|
|
1453
|
+
if (footWeight >= 0.5)
|
|
1454
|
+
candidates.push(vertex);
|
|
1455
|
+
}
|
|
1456
|
+
if (!candidates.length) {
|
|
1457
|
+
this.footSurfaceVertices.set(cacheKey, []);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
// Fixed bind-space sole strip: never reselect vertices from animated positions.
|
|
1461
|
+
const bindHeights = new Map(candidates.map(vertex => [vertex, transformPoint(binding.bindWorldMatrix, [...mesh.positions[vertex]])[1]]));
|
|
1462
|
+
const ys = [...bindHeights.values()];
|
|
1463
|
+
const minimum = Math.min(...ys), maximum = Math.max(...ys);
|
|
1464
|
+
const strip = Math.max(1e-6, (maximum - minimum) * 0.001);
|
|
1465
|
+
selected = candidates.filter(index => bindHeights.get(index) <= minimum + strip);
|
|
1466
|
+
this.footSurfaceVertices.set(cacheKey, selected);
|
|
1467
|
+
}
|
|
1468
|
+
const toWorld = config.worldFromLocal ? multiplyMat4(config.worldFromLocal, binding.node.transform.worldMatrix) : binding.node.transform.worldMatrix;
|
|
1469
|
+
for (const vertex of selected) {
|
|
1470
|
+
const authored = [...mesh.positions[vertex]];
|
|
1471
|
+
for (let morph = 0; morph < mesh.morphTargets.length; morph++) {
|
|
1472
|
+
const weight = binding.renderable.morphWeights?.[morph] ?? 0;
|
|
1473
|
+
const delta = mesh.morphTargets[morph]?.positions[vertex];
|
|
1474
|
+
if (delta && weight)
|
|
1475
|
+
for (let axis = 0; axis < 3; axis++)
|
|
1476
|
+
authored[axis] += delta[axis] * weight;
|
|
1477
|
+
}
|
|
1478
|
+
const skinned = [0, 0, 0];
|
|
1479
|
+
let total = 0;
|
|
1480
|
+
for (let set = 0; set < 2; set++)
|
|
1481
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
1482
|
+
const weight = (set ? mesh.weights1?.[vertex]?.[lane] : mesh.weights[vertex]?.[lane]) ?? 0;
|
|
1483
|
+
const joint = (set ? mesh.joints1?.[vertex]?.[lane] : mesh.joints[vertex]?.[lane]) ?? -1;
|
|
1484
|
+
if (weight <= 0)
|
|
1485
|
+
continue;
|
|
1486
|
+
if (joint < 0 || joint >= palette.jointCount)
|
|
1487
|
+
throw new Error("Foot surface vertex has an invalid palette influence.");
|
|
1488
|
+
const point = transformPoint(Array.from(palette.matrices.subarray(joint * 16, joint * 16 + 16)), authored);
|
|
1489
|
+
for (let axis = 0; axis < 3; axis++)
|
|
1490
|
+
skinned[axis] += point[axis] * weight;
|
|
1491
|
+
total += weight;
|
|
1492
|
+
}
|
|
1493
|
+
if (Math.abs(total - 1) > 0.001)
|
|
1494
|
+
throw new Error("Foot surface vertex weights must sum to one.");
|
|
1495
|
+
const worldPosition = transformPoint(toWorld, skinned);
|
|
1496
|
+
const hit = config.ground.raycastDown([worldPosition[0], worldPosition[1] + 2, worldPosition[2]], 4);
|
|
1497
|
+
samples.push({ side: leg.side, mesh: `${binding.node.userData.gltfNodeIndex ?? binding.node.name}:${mesh.name}`, vertex, worldPosition,
|
|
1498
|
+
groundError: hit ? worldPosition[1] - hit.point[1] : null,
|
|
1499
|
+
...(vertex === selected[0] ? { skinningProbe: { position: authored,
|
|
1500
|
+
joints: [...(mesh.joints[vertex] ?? [0, 0, 0, 0]), ...(mesh.joints1?.[vertex] ?? [0, 0, 0, 0])],
|
|
1501
|
+
weights: [...(mesh.weights[vertex] ?? [0, 0, 0, 0]), ...(mesh.weights1?.[vertex] ?? [0, 0, 0, 0])],
|
|
1502
|
+
matrices: Array.from(palette.matrices), modelMatrix: [...toWorld], expectedWorldPosition: worldPosition } } : {}) });
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return samples;
|
|
1507
|
+
}
|
|
702
1508
|
refreshSkinningPalettes() {
|
|
1509
|
+
const result = this.paletteResult;
|
|
1510
|
+
result.updated = 0;
|
|
1511
|
+
result.missingTargets.length = 0;
|
|
703
1512
|
if (this.skinningBindings.length === 0) {
|
|
704
|
-
return
|
|
1513
|
+
return result;
|
|
705
1514
|
}
|
|
706
|
-
|
|
707
|
-
const missingTargets = [];
|
|
1515
|
+
const missingTargets = result.missingTargets;
|
|
708
1516
|
for (const binding of this.skinningBindings) {
|
|
709
|
-
|
|
710
|
-
|
|
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
|
+
}
|
|
711
1537
|
let complete = true;
|
|
712
1538
|
for (let index = 0; index < binding.skin.jointNames.length; index += 1) {
|
|
713
|
-
const
|
|
714
|
-
const jointNode = this.nodesByName.get(jointName)?.[0];
|
|
1539
|
+
const jointNode = jointNodes[index];
|
|
715
1540
|
const inverseBind = binding.skin.inverseBindMatrices[index];
|
|
716
1541
|
if (!jointNode || !inverseBind) {
|
|
717
|
-
missingTargets.push(`${binding.skin.name}.${
|
|
1542
|
+
missingTargets.push(`${binding.skin.name}.${binding.skin.jointNames[index]}`);
|
|
718
1543
|
complete = false;
|
|
719
1544
|
break;
|
|
720
1545
|
}
|
|
721
|
-
|
|
722
|
-
|
|
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);
|
|
723
1549
|
}
|
|
724
1550
|
if (!complete)
|
|
725
1551
|
continue;
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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;
|
|
731
1565
|
}
|
|
732
|
-
return
|
|
1566
|
+
return result;
|
|
733
1567
|
}
|
|
734
1568
|
inspectClipBinding(clip) {
|
|
735
1569
|
let supportedTrackCount = 0;
|
|
@@ -810,7 +1644,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
810
1644
|
animatesSkeleton: this.skinningBindings.length > 0 && boundTrackCount > 0 && transformTrackCount > 0
|
|
811
1645
|
};
|
|
812
1646
|
}
|
|
813
|
-
applySampledTargets(clipName, time, sampled, blendedClipCount) {
|
|
1647
|
+
applySampledTargets(clipName, time, sampled, blendedClipCount, phaseTimings) {
|
|
814
1648
|
let transformTracksApplied = 0;
|
|
815
1649
|
let morphWeightTracksApplied = 0;
|
|
816
1650
|
let materialTracksApplied = 0;
|
|
@@ -865,6 +1699,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
865
1699
|
this.options.scene.updateWorldTransforms();
|
|
866
1700
|
const footPlanting = this.applyFootPlanting();
|
|
867
1701
|
const skinning = this.refreshSkinningPalettes();
|
|
1702
|
+
const measuredFootPlanting = footPlanting ? { ...footPlanting, surfaces: this.sampleFootSurfaces() } : undefined;
|
|
868
1703
|
return {
|
|
869
1704
|
clipName,
|
|
870
1705
|
time,
|
|
@@ -874,10 +1709,11 @@ export class GLTFSceneAnimationRuntime {
|
|
|
874
1709
|
morphWeightTracksApplied,
|
|
875
1710
|
materialTracksApplied,
|
|
876
1711
|
lightTracksApplied,
|
|
877
|
-
...(
|
|
1712
|
+
...(measuredFootPlanting === undefined ? {} : { footPlanting: measuredFootPlanting }),
|
|
878
1713
|
skinningPalettesUpdated: skinning.updated,
|
|
879
1714
|
missingTargets: [...missingTargets, ...skinning.missingTargets],
|
|
880
|
-
unsupportedTracks: sampled.unsupportedTracks
|
|
1715
|
+
unsupportedTracks: sampled.unsupportedTracks,
|
|
1716
|
+
...(phaseTimings === undefined ? {} : { phaseTimings })
|
|
881
1717
|
};
|
|
882
1718
|
}
|
|
883
1719
|
}
|
|
@@ -896,6 +1732,7 @@ export class GLTFSceneAnimationMixerBinding {
|
|
|
896
1732
|
constructor(options) {
|
|
897
1733
|
this.runtime = new GLTFSceneAnimationRuntime(options);
|
|
898
1734
|
this.mixer = new AnimationMixer({
|
|
1735
|
+
...(options.consumeRootMotion ? { consumeRootMotion: options.consumeRootMotion } : {}),
|
|
899
1736
|
setAnimationValue: (target, value) => {
|
|
900
1737
|
this.pendingValues.set(target, cloneAnimationValue(value));
|
|
901
1738
|
}
|
|
@@ -1200,6 +2037,8 @@ function sameFootPlantingShape(previous, next) {
|
|
|
1200
2037
|
return false;
|
|
1201
2038
|
if (previous.hipDropFactor !== next.hipDropFactor)
|
|
1202
2039
|
return false;
|
|
2040
|
+
if (previous.lockFootRotation !== next.lockFootRotation)
|
|
2041
|
+
return false;
|
|
1203
2042
|
if (previous.legs.length !== next.legs.length)
|
|
1204
2043
|
return false;
|
|
1205
2044
|
return previous.legs.every((leg, index) => {
|
|
@@ -1209,6 +2048,7 @@ function sameFootPlantingShape(previous, next) {
|
|
|
1209
2048
|
&& leg.hip === other.hip
|
|
1210
2049
|
&& leg.knee === other.knee
|
|
1211
2050
|
&& leg.ankle === other.ankle
|
|
2051
|
+
&& leg.ankleHeight === other.ankleHeight
|
|
1212
2052
|
&& (leg.pole ?? []).join(",") === (other.pole ?? []).join(",");
|
|
1213
2053
|
});
|
|
1214
2054
|
}
|
|
@@ -1216,6 +2056,47 @@ function worldPosition(node) {
|
|
|
1216
2056
|
node.updateWorldTransform();
|
|
1217
2057
|
return [node.transform.worldMatrix[12], node.transform.worldMatrix[13], node.transform.worldMatrix[14]];
|
|
1218
2058
|
}
|
|
2059
|
+
function commonAncestor(nodes) {
|
|
2060
|
+
if (nodes.length === 0)
|
|
2061
|
+
return undefined;
|
|
2062
|
+
const first = [];
|
|
2063
|
+
for (let node = nodes[0]; node; node = node.parent)
|
|
2064
|
+
first.push(node);
|
|
2065
|
+
return first.find(candidate => nodes.every(node => node === candidate || candidate.isAncestorOf(node)));
|
|
2066
|
+
}
|
|
2067
|
+
function orientJointToward(node, child, target) {
|
|
2068
|
+
node.updateWorldTransform();
|
|
2069
|
+
child.updateWorldTransform();
|
|
2070
|
+
const origin = worldPosition(node);
|
|
2071
|
+
const current = worldPosition(child);
|
|
2072
|
+
const from = [current[0] - origin[0], current[1] - origin[1], current[2] - origin[2]];
|
|
2073
|
+
const to = [target[0] - origin[0], target[1] - origin[1], target[2] - origin[2]];
|
|
2074
|
+
const delta = quatFromUnitVectors(from, to);
|
|
2075
|
+
const worldRotation = multiplyRigidQuat(delta, decomposeMat4(node.transform.worldMatrix).rotation);
|
|
2076
|
+
const parentRotation = node.parent ? decomposeMat4(node.parent.transform.worldMatrix).rotation : [0, 0, 0, 1];
|
|
2077
|
+
const localRotation = multiplyRigidQuat(invertRigidQuat(parentRotation), worldRotation);
|
|
2078
|
+
node.transform.setRotation(localRotation[0], localRotation[1], localRotation[2], localRotation[3]);
|
|
2079
|
+
}
|
|
2080
|
+
function quatFromUnitVectors(from, to) {
|
|
2081
|
+
const fl = Math.hypot(...from), tl = Math.hypot(...to);
|
|
2082
|
+
if (!(fl > 1e-12) || !(tl > 1e-12))
|
|
2083
|
+
return [0, 0, 0, 1];
|
|
2084
|
+
const fx = from[0] / fl, fy = from[1] / fl, fz = from[2] / fl, tx = to[0] / tl, ty = to[1] / tl, tz = to[2] / tl;
|
|
2085
|
+
const dot = fx * tx + fy * ty + fz * tz;
|
|
2086
|
+
if (dot < -0.999999) {
|
|
2087
|
+
const axis = Math.abs(fx) > 0.1 ? [-fz, 0, fx] : [0, fz, -fy];
|
|
2088
|
+
const length = Math.hypot(...axis);
|
|
2089
|
+
return [axis[0] / length, axis[1] / length, axis[2] / length, 0];
|
|
2090
|
+
}
|
|
2091
|
+
if (dot > 0.999999)
|
|
2092
|
+
return [0, 0, 0, 1];
|
|
2093
|
+
return normalizeQuaternion([fy * tz - fz * ty, fz * tx - fx * tz, fx * ty - fy * tx, 1 + dot]);
|
|
2094
|
+
}
|
|
2095
|
+
function multiplyRigidQuat(a, b) {
|
|
2096
|
+
return normalizeQuaternion([a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1], a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0], a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3], a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]]);
|
|
2097
|
+
}
|
|
2098
|
+
function invertRigidQuat(q) { return [-q[0], -q[1], -q[2], q[3]]; }
|
|
2099
|
+
function normalizeQuaternion(q) { const n = Math.hypot(...q); return n > 0 ? [q[0] / n, q[1] / n, q[2] / n, q[3] / n] : [0, 0, 0, 1]; }
|
|
1219
2100
|
function setWorldPosition(node, position) {
|
|
1220
2101
|
const local = node.parent
|
|
1221
2102
|
? transformPoint(invertMat4(node.parent.transform.worldMatrix), [position[0], position[1], position[2]])
|
|
@@ -1442,4 +2323,44 @@ function multiplyQuat(a, b) {
|
|
|
1442
2323
|
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
|
|
1443
2324
|
];
|
|
1444
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
|
+
}
|
|
1445
2366
|
//# sourceMappingURL=GLTFAnimationRuntime.js.map
|