@aura3d/assets 3.0.0 → 3.0.1
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/GLTFAnimationRuntime.d.ts +84 -4
- package/dist/GLTFAnimationRuntime.d.ts.map +1 -1
- package/dist/GLTFAnimationRuntime.js +308 -23
- package/dist/GLTFAnimationRuntime.js.map +1 -1
- 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/GLTFLoader.d.ts.map +1 -1
- package/dist/GLTFLoader.js +21 -7
- package/dist/GLTFLoader.js.map +1 -1
- package/dist/KTX2BasisTextureTranscoder.d.ts.map +1 -1
- package/dist/KTX2BasisTextureTranscoder.js +8 -0
- package/dist/KTX2BasisTextureTranscoder.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AnimationAction, AnimationClip, AnimationMixer, createFootIkRig, normalizeQuat, slerpQuat, solveTwoBoneIk } from "@aura3d/animation";
|
|
2
|
-
import { invertMat4, MAX_RENDERABLE_SKINNING_JOINTS, multiplyMat4, Renderable, Scene, transformPoint } from "@aura3d/scene";
|
|
1
|
+
import { AnimationAction, AnimationClip, AnimationMixer, consumeRootMotion, extractRootMotion, createFootIkRig, normalizeQuat, slerpQuat, solveTwoBoneIk } from "@aura3d/animation";
|
|
2
|
+
import { composeMat4, decomposeMat4, invertMat4, MAX_RENDERABLE_SKINNING_JOINTS, multiplyMat4, Renderable, Scene, transformPoint } from "@aura3d/scene";
|
|
3
3
|
/**
|
|
4
4
|
* Synonym groups for fuzzy clip-name matching. Real catalog assets label the same
|
|
5
5
|
* motion wildly differently ("Loops" vs "Idle", "Take 001" vs "Run", "sprint" vs
|
|
@@ -259,6 +259,10 @@ export class GLTFSceneAnimationRuntime {
|
|
|
259
259
|
nodesByName = new Map();
|
|
260
260
|
morphRenderablesByNodeName = new Map();
|
|
261
261
|
skinningBindings = [];
|
|
262
|
+
footBindMatrices = new Map();
|
|
263
|
+
footOrientationLocks = new Map();
|
|
264
|
+
footDescendantLocks = new Map();
|
|
265
|
+
footSurfaceVertices = new Map();
|
|
262
266
|
footPlanting;
|
|
263
267
|
footRig;
|
|
264
268
|
lastApply;
|
|
@@ -284,9 +288,65 @@ export class GLTFSceneAnimationRuntime {
|
|
|
284
288
|
}
|
|
285
289
|
if (!sameFootPlantingShape(this.footPlanting, config)) {
|
|
286
290
|
this.footRig = undefined;
|
|
291
|
+
this.footOrientationLocks.clear();
|
|
292
|
+
this.footDescendantLocks.clear();
|
|
287
293
|
}
|
|
288
294
|
this.footPlanting = config;
|
|
289
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Extract real authored displacement over unwrapped clip time, hand it to the physical
|
|
298
|
+
* authority once, then apply an in-place root pose and solve feet in the updated actor
|
|
299
|
+
* transform. Rejected movement is returned and discarded rather than accumulating drift.
|
|
300
|
+
* The caller refreshes footPlanting.worldFromLocal inside move after committing movement.
|
|
301
|
+
*/
|
|
302
|
+
applyRootMotionClip(name, options) {
|
|
303
|
+
return this.applyRootMotionClips([{ clipName: name, ...options }], options);
|
|
304
|
+
}
|
|
305
|
+
/** Blend displacement and consumed poses with the same base/additive weights. */
|
|
306
|
+
applyRootMotionClips(samples, options) {
|
|
307
|
+
if (samples.length === 0)
|
|
308
|
+
throw new Error("glTF root motion blend requires at least one clip sample.");
|
|
309
|
+
const accumulators = new Map();
|
|
310
|
+
const motionAccumulators = new Map();
|
|
311
|
+
const unsupportedTracks = [];
|
|
312
|
+
const names = [];
|
|
313
|
+
let maxTime = 0;
|
|
314
|
+
// Finish validation and sampling before the physical authority can mutate the actor.
|
|
315
|
+
for (const sample of samples) {
|
|
316
|
+
const clip = this.clipsByName.get(sample.clipName);
|
|
317
|
+
if (!clip)
|
|
318
|
+
throw new Error(`glTF animation clip "${sample.clipName}" was not found.`);
|
|
319
|
+
const weight = sample.weight ?? 1;
|
|
320
|
+
if (!Number.isFinite(weight) || weight < 0)
|
|
321
|
+
throw new Error("glTF root motion blend weight must be finite and non-negative.");
|
|
322
|
+
const extracted = extractRootMotion(clip, sample);
|
|
323
|
+
if (weight === 0)
|
|
324
|
+
continue;
|
|
325
|
+
const time = sample.loop && clip.duration > 0
|
|
326
|
+
? ((sample.toTime % clip.duration) + clip.duration) % clip.duration
|
|
327
|
+
: Math.max(0, Math.min(clip.duration, sample.toTime));
|
|
328
|
+
maxTime = Math.max(maxTime, time);
|
|
329
|
+
names.push(`${clip.name}@${time}x${weight}${sample.additive ? "+add" : ""}`);
|
|
330
|
+
blendInto(motionAccumulators, "motion", "vector3", extracted.delta, weight, sample.additive === true);
|
|
331
|
+
for (const track of clip.tracks) {
|
|
332
|
+
if (!parseAnimationTarget(track.target)) {
|
|
333
|
+
unsupportedTracks.push(track.target);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
// Consume each clip's own root before blending, including additive root tracks.
|
|
337
|
+
const value = track.sample(track.target === sample.target ? 0 : time);
|
|
338
|
+
blendInto(accumulators, track.target, track.valueType, value, weight, sample.additive === true);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const values = new Map();
|
|
342
|
+
for (const [target, accumulator] of accumulators)
|
|
343
|
+
values.set(target, finalizeTargetBlend(accumulator));
|
|
344
|
+
const motionAccumulator = motionAccumulators.get("motion");
|
|
345
|
+
const delta = motionAccumulator ? finalizeTargetBlend(motionAccumulator) : [0, 0, 0];
|
|
346
|
+
const motion = consumeRootMotion({ target: "blend", fromTime: 0, toTime: maxTime, looped: samples.some(sample => sample.loop), delta }, options.worldFromLocal, options.move);
|
|
347
|
+
this.lastApply = this.applySampledTargets(`root-motion:${names.join(",")}`, maxTime, { sampledTargets: values, unsupportedTracks }, samples.length);
|
|
348
|
+
return { motion, applyResult: this.lastApply };
|
|
349
|
+
}
|
|
290
350
|
applyClipByName(name, time) {
|
|
291
351
|
const clip = this.clipsByName.get(name);
|
|
292
352
|
if (!clip) {
|
|
@@ -525,10 +585,16 @@ export class GLTFSceneAnimationRuntime {
|
|
|
525
585
|
return [...this.clipsByName.values()].map((clip) => this.inspectClipBinding(clip));
|
|
526
586
|
}
|
|
527
587
|
reindexScene() {
|
|
588
|
+
this.options.scene.updateWorldTransforms();
|
|
589
|
+
this.footBindMatrices.clear();
|
|
590
|
+
this.footOrientationLocks.clear();
|
|
591
|
+
this.footDescendantLocks.clear();
|
|
528
592
|
this.nodesByName.clear();
|
|
529
593
|
this.morphRenderablesByNodeName.clear();
|
|
530
594
|
this.skinningBindings.length = 0;
|
|
595
|
+
this.footSurfaceVertices.clear();
|
|
531
596
|
this.options.scene.traverse((node) => {
|
|
597
|
+
this.footBindMatrices.set(node.id, { world: [...node.transform.worldMatrix], local: [...node.transform.localMatrix] });
|
|
532
598
|
const nodes = this.nodesByName.get(node.name) ?? [];
|
|
533
599
|
nodes.push(node);
|
|
534
600
|
this.nodesByName.set(node.name, nodes);
|
|
@@ -551,7 +617,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
551
617
|
const skin = mesh?.skinIndex === undefined ? undefined : this.options.asset.skins[mesh.skinIndex];
|
|
552
618
|
if (!mesh || !skin || skin.joints.length > MAX_RENDERABLE_SKINNING_JOINTS)
|
|
553
619
|
continue;
|
|
554
|
-
this.skinningBindings.push({ node, renderable, mesh, skin });
|
|
620
|
+
this.skinningBindings.push({ node, renderable, mesh, skin, bindWorldMatrix: [...node.transform.worldMatrix] });
|
|
555
621
|
}
|
|
556
622
|
}
|
|
557
623
|
}
|
|
@@ -559,9 +625,9 @@ export class GLTFSceneAnimationRuntime {
|
|
|
559
625
|
return this.options.scene.collectLights().find((light) => light.name === name);
|
|
560
626
|
}
|
|
561
627
|
/**
|
|
562
|
-
* Foot-planting post-pass (E2): solves the configured leg chains against the ground
|
|
563
|
-
*
|
|
564
|
-
*
|
|
628
|
+
* Foot-planting post-pass (E2): solves the configured leg chains against the ground,
|
|
629
|
+
* applies a shared pelvis reach correction, and rotates rigid hip/knee chains toward the
|
|
630
|
+
* solved targets. Missing leg nodes are reported in
|
|
565
631
|
* `missingLegNodes` and skipped, never faked. Returns undefined when unconfigured.
|
|
566
632
|
*/
|
|
567
633
|
applyFootPlanting() {
|
|
@@ -595,11 +661,20 @@ export class GLTFSceneAnimationRuntime {
|
|
|
595
661
|
missingLegNodes.push(`${leg.side}:ankle:${leg.ankle}`);
|
|
596
662
|
if (!hip || !knee || !ankle)
|
|
597
663
|
continue;
|
|
598
|
-
resolved.push({ side: leg.side, hip, knee, ankle, pole: leg.pole });
|
|
664
|
+
resolved.push({ side: leg.side, hip, knee, ankle, pole: leg.pole, ...(leg.contact === undefined ? {} : { contact: leg.contact }), ...(leg.support ? { support: leg.support } : {}) });
|
|
599
665
|
}
|
|
600
666
|
if (resolved.length === 0) {
|
|
601
667
|
return { groundedFeet: 0, averageTargetError: 0, lockedSides: [], missingLegNodes, hipOffset: 0 };
|
|
602
668
|
}
|
|
669
|
+
// Measure each animated ankle-to-sole offset from the exact pre-solve skinning
|
|
670
|
+
// palette. One global value cannot plant asymmetric shoes or a rotated touchdown.
|
|
671
|
+
this.refreshSkinningPalettes();
|
|
672
|
+
const rawSoles = this.sampleFootSurfaces();
|
|
673
|
+
const measuredAnkleHeights = new Map(resolved.map(entry => {
|
|
674
|
+
const ankle = solvePoint(worldPosition(entry.ankle));
|
|
675
|
+
const minimum = Math.min(Infinity, ...rawSoles.filter(point => point.side === entry.side).map(point => point.worldPosition[1]));
|
|
676
|
+
return [entry.side, Number.isFinite(minimum) ? Math.max(0, ankle[1] - minimum) : undefined];
|
|
677
|
+
}));
|
|
603
678
|
if (!this.footRig) {
|
|
604
679
|
this.footRig = createFootIkRig({
|
|
605
680
|
legs: resolved.map((entry) => ({
|
|
@@ -607,6 +682,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
607
682
|
hip: solvePoint(worldPosition(entry.hip)),
|
|
608
683
|
knee: solvePoint(worldPosition(entry.knee)),
|
|
609
684
|
ankle: solvePoint(worldPosition(entry.ankle)),
|
|
685
|
+
...(measuredAnkleHeights.get(entry.side) ?? config.ankleHeight) !== undefined ? { ankleHeight: measuredAnkleHeights.get(entry.side) ?? config.ankleHeight } : {},
|
|
610
686
|
...(entry.pole ? { pole: entry.pole } : {})
|
|
611
687
|
})),
|
|
612
688
|
raycaster: config.ground,
|
|
@@ -617,38 +693,116 @@ export class GLTFSceneAnimationRuntime {
|
|
|
617
693
|
...(config.hipDropFactor !== undefined ? { hipDropFactor: config.hipDropFactor } : {})
|
|
618
694
|
});
|
|
619
695
|
}
|
|
696
|
+
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
697
|
+
const originalLengths = new Map(resolved.map(entry => [entry.side, {
|
|
698
|
+
upper: distance(solvePoint(worldPosition(entry.hip)), solvePoint(worldPosition(entry.knee))),
|
|
699
|
+
lower: distance(solvePoint(worldPosition(entry.knee)), solvePoint(worldPosition(entry.ankle)))
|
|
700
|
+
}]));
|
|
620
701
|
const solved = this.footRig.solveFootPlacement({
|
|
621
702
|
legs: resolved.map((entry) => ({
|
|
622
703
|
side: entry.side,
|
|
623
704
|
hip: solvePoint(worldPosition(entry.hip)),
|
|
624
705
|
knee: solvePoint(worldPosition(entry.knee)),
|
|
625
706
|
ankle: solvePoint(worldPosition(entry.ankle)),
|
|
626
|
-
...(entry.
|
|
707
|
+
...(measuredAnkleHeights.get(entry.side) ?? config.ankleHeight) !== undefined ? { ankleHeight: measuredAnkleHeights.get(entry.side) ?? config.ankleHeight } : {},
|
|
708
|
+
...(entry.pole ? { pole: entry.pole } : {}),
|
|
709
|
+
...(entry.contact === undefined ? {} : { contact: entry.contact }),
|
|
710
|
+
...(entry.support ? { support: entry.support } : {})
|
|
627
711
|
}))
|
|
628
712
|
});
|
|
713
|
+
// A reach correction belongs to the shared pelvis owner. Move it once before
|
|
714
|
+
// rotating either leg; translating each upper-leg root dislocates the chain,
|
|
715
|
+
// while omitting the drop leaves a fully extended stance foot above ground.
|
|
716
|
+
if (solved.hipOffset < 0) {
|
|
717
|
+
const common = commonAncestor(resolved.map(entry => entry.hip));
|
|
718
|
+
const anchors = common && common.parent ? [common] : [...new Set(resolved.map(entry => entry.hip))];
|
|
719
|
+
for (const anchor of anchors) {
|
|
720
|
+
const currentWorld = solvePoint(worldPosition(anchor));
|
|
721
|
+
const dropped = actorPoint([currentWorld[0], currentWorld[1] + solved.hipOffset, currentWorld[2]]);
|
|
722
|
+
setWorldPosition(anchor, dropped);
|
|
723
|
+
}
|
|
724
|
+
this.options.scene.updateWorldTransforms();
|
|
725
|
+
}
|
|
629
726
|
for (const foot of solved.feet) {
|
|
630
727
|
const entry = resolved.find((candidate) => candidate.side === foot.side);
|
|
631
728
|
if (!entry)
|
|
632
729
|
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();
|
|
730
|
+
// Apply the two-bone solution with joint rotations. Moving hip/knee/ankle
|
|
731
|
+
// translations directly changes authored bone lengths and deforms every weighted
|
|
732
|
+
// mesh. The solver targets are world-space; orient each parent toward its solved
|
|
733
|
+
// child while preserving the animation's local translations and scales.
|
|
644
734
|
const kneeTarget = actorPoint(foot.knee);
|
|
645
|
-
|
|
646
|
-
entry.
|
|
735
|
+
orientJointToward(entry.hip, entry.knee, kneeTarget);
|
|
736
|
+
entry.hip.updateWorldTransform();
|
|
647
737
|
const ankleTarget = actorPoint(foot.ankle);
|
|
648
|
-
|
|
738
|
+
orientJointToward(entry.knee, entry.ankle, ankleTarget);
|
|
739
|
+
entry.knee.updateWorldTransform();
|
|
740
|
+
}
|
|
741
|
+
this.options.scene.updateWorldTransforms();
|
|
742
|
+
for (const foot of solved.feet) {
|
|
743
|
+
const entry = resolved.find(candidate => candidate.side === foot.side);
|
|
744
|
+
if (!foot.locked || config.lockFootRotation === false) {
|
|
745
|
+
this.footOrientationLocks.delete(foot.side);
|
|
746
|
+
this.footDescendantLocks.delete(foot.side);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
let locked = this.footOrientationLocks.get(foot.side);
|
|
750
|
+
if (!locked) {
|
|
751
|
+
// Capture the actual animated sole orientation and toe pose at touchdown.
|
|
752
|
+
// Reusing the asset bind pose snaps arbitrary rigs and moves their sole vertices.
|
|
753
|
+
locked = toWorld ? multiplyMat4(toWorld, entry.ankle.transform.worldMatrix) : [...entry.ankle.transform.worldMatrix];
|
|
754
|
+
const descendants = new Map();
|
|
755
|
+
entry.ankle.children.forEach(child => child.traverse(node => descendants.set(node.id, [...node.transform.localMatrix])));
|
|
756
|
+
this.footDescendantLocks.set(foot.side, descendants);
|
|
757
|
+
const normal = foot.sample.groundNormal;
|
|
758
|
+
const norm = Math.hypot(...normal);
|
|
759
|
+
if (!(norm > 0))
|
|
760
|
+
throw new Error("Foot contact requires a nonzero ground normal.");
|
|
761
|
+
const nx = normal[0] / norm, ny = normal[1] / norm, nz = normal[2] / norm;
|
|
762
|
+
if (ny <= -0.999)
|
|
763
|
+
throw new Error("Foot contact cannot plant on a downward facing surface.");
|
|
764
|
+
const vx = nz, vz = -nx, t = 1 / (1 + ny);
|
|
765
|
+
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];
|
|
766
|
+
locked = multiplyMat4(tilt, locked);
|
|
767
|
+
this.footOrientationLocks.set(foot.side, locked);
|
|
768
|
+
}
|
|
769
|
+
// Keep the solved ankle position while holding the sole orientation in world space.
|
|
770
|
+
const ankleWorld = solvePoint(worldPosition(entry.ankle));
|
|
771
|
+
const desired = [...locked];
|
|
772
|
+
desired[12] = ankleWorld[0];
|
|
773
|
+
desired[13] = ankleWorld[1];
|
|
774
|
+
desired[14] = ankleWorld[2];
|
|
775
|
+
const actorMatrix = toLocal ? multiplyMat4(toLocal, desired) : desired;
|
|
776
|
+
const parent = entry.ankle.parent;
|
|
777
|
+
const local = parent ? multiplyMat4(invertMat4(parent.transform.worldMatrix), actorMatrix) : actorMatrix;
|
|
778
|
+
entry.ankle.transform.setFromLocalMatrix(local);
|
|
779
|
+
// Toe joints remain in their touchdown relationship during stance; animated
|
|
780
|
+
// toe curl otherwise moves the sole while the ankle still reports zero slip.
|
|
781
|
+
const touchdown = this.footDescendantLocks.get(foot.side);
|
|
782
|
+
const restore = (node) => {
|
|
783
|
+
const rest = touchdown?.get(node.id);
|
|
784
|
+
if (rest)
|
|
785
|
+
node.transform.setFromLocalMatrix(rest);
|
|
786
|
+
node.children.forEach(restore);
|
|
787
|
+
};
|
|
788
|
+
entry.ankle.children.forEach(restore);
|
|
789
|
+
entry.ankle.updateWorldTransform();
|
|
649
790
|
}
|
|
650
791
|
this.options.scene.updateWorldTransforms();
|
|
651
792
|
return {
|
|
793
|
+
legDeformation: resolved.map(entry => {
|
|
794
|
+
const before = originalLengths.get(entry.side);
|
|
795
|
+
const upperAfter = distance(solvePoint(worldPosition(entry.hip)), solvePoint(worldPosition(entry.knee)));
|
|
796
|
+
const lowerAfter = distance(solvePoint(worldPosition(entry.knee)), solvePoint(worldPosition(entry.ankle)));
|
|
797
|
+
return { side: entry.side, upperBefore: before.upper, lowerBefore: before.lower, upperAfter, lowerAfter,
|
|
798
|
+
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)) };
|
|
799
|
+
}),
|
|
800
|
+
feet: solved.feet.map(foot => {
|
|
801
|
+
const entry = resolved.find(candidate => candidate.side === foot.side);
|
|
802
|
+
const position = solvePoint(worldPosition(entry.ankle));
|
|
803
|
+
return { side: foot.side, worldPosition: position, locked: foot.locked,
|
|
804
|
+
contactError: Math.hypot(...position.map((value, axis) => value - foot.sample.plantedFoot[axis])) };
|
|
805
|
+
}),
|
|
652
806
|
groundedFeet: solved.groundedFeet,
|
|
653
807
|
averageTargetError: solved.averageTargetError,
|
|
654
808
|
lockedSides: solved.feet.filter((foot) => foot.locked).map((foot) => foot.side),
|
|
@@ -699,6 +853,91 @@ export class GLTFSceneAnimationRuntime {
|
|
|
699
853
|
light.range = asPositiveScalar(value, trackTarget);
|
|
700
854
|
return true;
|
|
701
855
|
}
|
|
856
|
+
/** CPU skinning oracle using the exact palette submitted to rendering, not IK targets. */
|
|
857
|
+
sampleFootSurfaces() {
|
|
858
|
+
const config = this.footPlanting;
|
|
859
|
+
if (!config)
|
|
860
|
+
return [];
|
|
861
|
+
const samples = [];
|
|
862
|
+
for (const leg of config.legs) {
|
|
863
|
+
const ankle = this.nodesByName.get(leg.ankle)?.[0];
|
|
864
|
+
if (!ankle)
|
|
865
|
+
continue;
|
|
866
|
+
const footNames = new Set();
|
|
867
|
+
const visit = (node) => { footNames.add(node.name); node.children.forEach(visit); };
|
|
868
|
+
visit(ankle);
|
|
869
|
+
for (const binding of this.skinningBindings) {
|
|
870
|
+
const palette = binding.renderable.skinning;
|
|
871
|
+
if (!palette)
|
|
872
|
+
continue;
|
|
873
|
+
const mesh = binding.mesh;
|
|
874
|
+
const cacheKey = `${leg.ankle}:${binding.node.id}:${mesh.name}`;
|
|
875
|
+
let selected = this.footSurfaceVertices.get(cacheKey);
|
|
876
|
+
if (!selected) {
|
|
877
|
+
const candidates = [];
|
|
878
|
+
for (let vertex = 0; vertex < mesh.positions.length; vertex++) {
|
|
879
|
+
let footWeight = 0;
|
|
880
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
881
|
+
if (footNames.has(binding.skin.jointNames[mesh.joints[vertex]?.[lane] ?? -1] ?? ""))
|
|
882
|
+
footWeight += mesh.weights[vertex]?.[lane] ?? 0;
|
|
883
|
+
if (footNames.has(binding.skin.jointNames[mesh.joints1?.[vertex]?.[lane] ?? -1] ?? ""))
|
|
884
|
+
footWeight += mesh.weights1?.[vertex]?.[lane] ?? 0;
|
|
885
|
+
}
|
|
886
|
+
if (footWeight >= 0.5)
|
|
887
|
+
candidates.push(vertex);
|
|
888
|
+
}
|
|
889
|
+
if (!candidates.length) {
|
|
890
|
+
this.footSurfaceVertices.set(cacheKey, []);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
// Fixed bind-space sole strip: never reselect vertices from animated positions.
|
|
894
|
+
const bindHeights = new Map(candidates.map(vertex => [vertex, transformPoint(binding.bindWorldMatrix, [...mesh.positions[vertex]])[1]]));
|
|
895
|
+
const ys = [...bindHeights.values()];
|
|
896
|
+
const minimum = Math.min(...ys), maximum = Math.max(...ys);
|
|
897
|
+
const strip = Math.max(1e-6, (maximum - minimum) * 0.001);
|
|
898
|
+
selected = candidates.filter(index => bindHeights.get(index) <= minimum + strip);
|
|
899
|
+
this.footSurfaceVertices.set(cacheKey, selected);
|
|
900
|
+
}
|
|
901
|
+
const toWorld = config.worldFromLocal ? multiplyMat4(config.worldFromLocal, binding.node.transform.worldMatrix) : binding.node.transform.worldMatrix;
|
|
902
|
+
for (const vertex of selected) {
|
|
903
|
+
const authored = [...mesh.positions[vertex]];
|
|
904
|
+
for (let morph = 0; morph < mesh.morphTargets.length; morph++) {
|
|
905
|
+
const weight = binding.renderable.morphWeights?.[morph] ?? 0;
|
|
906
|
+
const delta = mesh.morphTargets[morph]?.positions[vertex];
|
|
907
|
+
if (delta && weight)
|
|
908
|
+
for (let axis = 0; axis < 3; axis++)
|
|
909
|
+
authored[axis] += delta[axis] * weight;
|
|
910
|
+
}
|
|
911
|
+
const skinned = [0, 0, 0];
|
|
912
|
+
let total = 0;
|
|
913
|
+
for (let set = 0; set < 2; set++)
|
|
914
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
915
|
+
const weight = (set ? mesh.weights1?.[vertex]?.[lane] : mesh.weights[vertex]?.[lane]) ?? 0;
|
|
916
|
+
const joint = (set ? mesh.joints1?.[vertex]?.[lane] : mesh.joints[vertex]?.[lane]) ?? -1;
|
|
917
|
+
if (weight <= 0)
|
|
918
|
+
continue;
|
|
919
|
+
if (joint < 0 || joint >= palette.jointCount)
|
|
920
|
+
throw new Error("Foot surface vertex has an invalid palette influence.");
|
|
921
|
+
const point = transformPoint(Array.from(palette.matrices.subarray(joint * 16, joint * 16 + 16)), authored);
|
|
922
|
+
for (let axis = 0; axis < 3; axis++)
|
|
923
|
+
skinned[axis] += point[axis] * weight;
|
|
924
|
+
total += weight;
|
|
925
|
+
}
|
|
926
|
+
if (Math.abs(total - 1) > 0.001)
|
|
927
|
+
throw new Error("Foot surface vertex weights must sum to one.");
|
|
928
|
+
const worldPosition = transformPoint(toWorld, skinned);
|
|
929
|
+
const hit = config.ground.raycastDown([worldPosition[0], worldPosition[1] + 2, worldPosition[2]], 4);
|
|
930
|
+
samples.push({ side: leg.side, mesh: `${binding.node.userData.gltfNodeIndex ?? binding.node.name}:${mesh.name}`, vertex, worldPosition,
|
|
931
|
+
groundError: hit ? worldPosition[1] - hit.point[1] : null,
|
|
932
|
+
...(vertex === selected[0] ? { skinningProbe: { position: authored,
|
|
933
|
+
joints: [...(mesh.joints[vertex] ?? [0, 0, 0, 0]), ...(mesh.joints1?.[vertex] ?? [0, 0, 0, 0])],
|
|
934
|
+
weights: [...(mesh.weights[vertex] ?? [0, 0, 0, 0]), ...(mesh.weights1?.[vertex] ?? [0, 0, 0, 0])],
|
|
935
|
+
matrices: Array.from(palette.matrices), modelMatrix: [...toWorld], expectedWorldPosition: worldPosition } } : {}) });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return samples;
|
|
940
|
+
}
|
|
702
941
|
refreshSkinningPalettes() {
|
|
703
942
|
if (this.skinningBindings.length === 0) {
|
|
704
943
|
return { updated: 0, missingTargets: [] };
|
|
@@ -865,6 +1104,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
865
1104
|
this.options.scene.updateWorldTransforms();
|
|
866
1105
|
const footPlanting = this.applyFootPlanting();
|
|
867
1106
|
const skinning = this.refreshSkinningPalettes();
|
|
1107
|
+
const measuredFootPlanting = footPlanting ? { ...footPlanting, surfaces: this.sampleFootSurfaces() } : undefined;
|
|
868
1108
|
return {
|
|
869
1109
|
clipName,
|
|
870
1110
|
time,
|
|
@@ -874,7 +1114,7 @@ export class GLTFSceneAnimationRuntime {
|
|
|
874
1114
|
morphWeightTracksApplied,
|
|
875
1115
|
materialTracksApplied,
|
|
876
1116
|
lightTracksApplied,
|
|
877
|
-
...(
|
|
1117
|
+
...(measuredFootPlanting === undefined ? {} : { footPlanting: measuredFootPlanting }),
|
|
878
1118
|
skinningPalettesUpdated: skinning.updated,
|
|
879
1119
|
missingTargets: [...missingTargets, ...skinning.missingTargets],
|
|
880
1120
|
unsupportedTracks: sampled.unsupportedTracks
|
|
@@ -896,6 +1136,7 @@ export class GLTFSceneAnimationMixerBinding {
|
|
|
896
1136
|
constructor(options) {
|
|
897
1137
|
this.runtime = new GLTFSceneAnimationRuntime(options);
|
|
898
1138
|
this.mixer = new AnimationMixer({
|
|
1139
|
+
...(options.consumeRootMotion ? { consumeRootMotion: options.consumeRootMotion } : {}),
|
|
899
1140
|
setAnimationValue: (target, value) => {
|
|
900
1141
|
this.pendingValues.set(target, cloneAnimationValue(value));
|
|
901
1142
|
}
|
|
@@ -1200,6 +1441,8 @@ function sameFootPlantingShape(previous, next) {
|
|
|
1200
1441
|
return false;
|
|
1201
1442
|
if (previous.hipDropFactor !== next.hipDropFactor)
|
|
1202
1443
|
return false;
|
|
1444
|
+
if (previous.lockFootRotation !== next.lockFootRotation)
|
|
1445
|
+
return false;
|
|
1203
1446
|
if (previous.legs.length !== next.legs.length)
|
|
1204
1447
|
return false;
|
|
1205
1448
|
return previous.legs.every((leg, index) => {
|
|
@@ -1209,6 +1452,7 @@ function sameFootPlantingShape(previous, next) {
|
|
|
1209
1452
|
&& leg.hip === other.hip
|
|
1210
1453
|
&& leg.knee === other.knee
|
|
1211
1454
|
&& leg.ankle === other.ankle
|
|
1455
|
+
&& leg.ankleHeight === other.ankleHeight
|
|
1212
1456
|
&& (leg.pole ?? []).join(",") === (other.pole ?? []).join(",");
|
|
1213
1457
|
});
|
|
1214
1458
|
}
|
|
@@ -1216,6 +1460,47 @@ function worldPosition(node) {
|
|
|
1216
1460
|
node.updateWorldTransform();
|
|
1217
1461
|
return [node.transform.worldMatrix[12], node.transform.worldMatrix[13], node.transform.worldMatrix[14]];
|
|
1218
1462
|
}
|
|
1463
|
+
function commonAncestor(nodes) {
|
|
1464
|
+
if (nodes.length === 0)
|
|
1465
|
+
return undefined;
|
|
1466
|
+
const first = [];
|
|
1467
|
+
for (let node = nodes[0]; node; node = node.parent)
|
|
1468
|
+
first.push(node);
|
|
1469
|
+
return first.find(candidate => nodes.every(node => node === candidate || candidate.isAncestorOf(node)));
|
|
1470
|
+
}
|
|
1471
|
+
function orientJointToward(node, child, target) {
|
|
1472
|
+
node.updateWorldTransform();
|
|
1473
|
+
child.updateWorldTransform();
|
|
1474
|
+
const origin = worldPosition(node);
|
|
1475
|
+
const current = worldPosition(child);
|
|
1476
|
+
const from = [current[0] - origin[0], current[1] - origin[1], current[2] - origin[2]];
|
|
1477
|
+
const to = [target[0] - origin[0], target[1] - origin[1], target[2] - origin[2]];
|
|
1478
|
+
const delta = quatFromUnitVectors(from, to);
|
|
1479
|
+
const worldRotation = multiplyRigidQuat(delta, decomposeMat4(node.transform.worldMatrix).rotation);
|
|
1480
|
+
const parentRotation = node.parent ? decomposeMat4(node.parent.transform.worldMatrix).rotation : [0, 0, 0, 1];
|
|
1481
|
+
const localRotation = multiplyRigidQuat(invertRigidQuat(parentRotation), worldRotation);
|
|
1482
|
+
node.transform.setRotation(localRotation[0], localRotation[1], localRotation[2], localRotation[3]);
|
|
1483
|
+
}
|
|
1484
|
+
function quatFromUnitVectors(from, to) {
|
|
1485
|
+
const fl = Math.hypot(...from), tl = Math.hypot(...to);
|
|
1486
|
+
if (!(fl > 1e-12) || !(tl > 1e-12))
|
|
1487
|
+
return [0, 0, 0, 1];
|
|
1488
|
+
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;
|
|
1489
|
+
const dot = fx * tx + fy * ty + fz * tz;
|
|
1490
|
+
if (dot < -0.999999) {
|
|
1491
|
+
const axis = Math.abs(fx) > 0.1 ? [-fz, 0, fx] : [0, fz, -fy];
|
|
1492
|
+
const length = Math.hypot(...axis);
|
|
1493
|
+
return [axis[0] / length, axis[1] / length, axis[2] / length, 0];
|
|
1494
|
+
}
|
|
1495
|
+
if (dot > 0.999999)
|
|
1496
|
+
return [0, 0, 0, 1];
|
|
1497
|
+
return normalizeQuaternion([fy * tz - fz * ty, fz * tx - fx * tz, fx * ty - fy * tx, 1 + dot]);
|
|
1498
|
+
}
|
|
1499
|
+
function multiplyRigidQuat(a, b) {
|
|
1500
|
+
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]]);
|
|
1501
|
+
}
|
|
1502
|
+
function invertRigidQuat(q) { return [-q[0], -q[1], -q[2], q[3]]; }
|
|
1503
|
+
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
1504
|
function setWorldPosition(node, position) {
|
|
1220
1505
|
const local = node.parent
|
|
1221
1506
|
? transformPoint(invertMat4(node.parent.transform.worldMatrix), [position[0], position[1], position[2]])
|