@lovelace_lol/loom3 1.0.42 → 1.0.44
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/index.cjs +448 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +449 -105
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -74,6 +74,9 @@ function bindingTargets(binding) {
|
|
|
74
74
|
if (targets && targets.length > 0) return targets;
|
|
75
75
|
return binding.morph !== void 0 && binding.morph !== "" ? [binding.morph] : [];
|
|
76
76
|
}
|
|
77
|
+
function normalizeBindingWeight(weight) {
|
|
78
|
+
return Number.isFinite(weight) ? Math.max(0, weight ?? 1) : 1;
|
|
79
|
+
}
|
|
77
80
|
function getProfileVisemeSlots(profile) {
|
|
78
81
|
if (profile.visemeSlots && profile.visemeSlots.length > 0) {
|
|
79
82
|
return [...profile.visemeSlots].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
@@ -99,6 +102,23 @@ function compileVisemeKeys(profile) {
|
|
|
99
102
|
return target ?? profile.visemeKeys?.[index] ?? "";
|
|
100
103
|
});
|
|
101
104
|
}
|
|
105
|
+
function getVisemeBindingTargets(profile, visemeIndex) {
|
|
106
|
+
const slots = getProfileVisemeSlots(profile);
|
|
107
|
+
const slot = slots[visemeIndex];
|
|
108
|
+
const binding = slot ? profile.visemeBindings?.[slot.id] : void 0;
|
|
109
|
+
const boundTargets = binding?.targets?.filter((target) => target.morph !== void 0 && target.morph !== "").map((target) => ({
|
|
110
|
+
morph: target.morph,
|
|
111
|
+
weight: normalizeBindingWeight(target.weight)
|
|
112
|
+
}));
|
|
113
|
+
if (boundTargets && boundTargets.length > 0) {
|
|
114
|
+
return boundTargets;
|
|
115
|
+
}
|
|
116
|
+
if (binding?.morph !== void 0 && binding.morph !== "") {
|
|
117
|
+
return [{ morph: binding.morph, weight: 1 }];
|
|
118
|
+
}
|
|
119
|
+
const legacyTarget = profile.visemeKeys?.[visemeIndex];
|
|
120
|
+
return legacyTarget !== void 0 && legacyTarget !== "" ? [{ morph: legacyTarget, weight: 1 }] : [];
|
|
121
|
+
}
|
|
102
122
|
function getVisemeJawAmounts(profile) {
|
|
103
123
|
const slots = getProfileVisemeSlots(profile);
|
|
104
124
|
if (slots.length === 0) return profile.visemeJawAmounts ? [...profile.visemeJawAmounts] : void 0;
|
|
@@ -510,14 +530,19 @@ var Y_AXIS = new THREE2.Vector3(0, 1, 0);
|
|
|
510
530
|
var Z_AXIS = new THREE2.Vector3(0, 0, 1);
|
|
511
531
|
var CLIP_EVENT_METADATA_KEY = "__loom3ClipEvents";
|
|
512
532
|
var CLIP_EVENT_EPSILON = 1e-4;
|
|
533
|
+
var ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX = "__loom3_additive_delta";
|
|
513
534
|
var BakedAnimationController = class {
|
|
514
535
|
constructor(host) {
|
|
515
536
|
__publicField(this, "host");
|
|
537
|
+
// Clip-backed snippets need a later mixer pass so they can override baked additive tracks.
|
|
516
538
|
__publicField(this, "animationMixer", null);
|
|
539
|
+
__publicField(this, "clipAnimationMixer", null);
|
|
517
540
|
__publicField(this, "mixerFinishedListenerAttached", false);
|
|
541
|
+
__publicField(this, "clipMixerFinishedListenerAttached", false);
|
|
518
542
|
__publicField(this, "animationClips", []);
|
|
519
543
|
__publicField(this, "bakedSourceClips", /* @__PURE__ */ new Map());
|
|
520
544
|
__publicField(this, "bakedRuntimeActions", /* @__PURE__ */ new Map());
|
|
545
|
+
__publicField(this, "bakedAdditiveRuntimeClips", /* @__PURE__ */ new Map());
|
|
521
546
|
__publicField(this, "bakedActionGroups", /* @__PURE__ */ new Map());
|
|
522
547
|
__publicField(this, "bakedRuntimeClipToSource", /* @__PURE__ */ new Map());
|
|
523
548
|
__publicField(this, "animationActions", /* @__PURE__ */ new Map());
|
|
@@ -542,6 +567,173 @@ var BakedAnimationController = class {
|
|
|
542
567
|
action.__actionId = actionId;
|
|
543
568
|
return actionId;
|
|
544
569
|
}
|
|
570
|
+
clearActionId(action) {
|
|
571
|
+
if (!action) return;
|
|
572
|
+
const actionId = this.getActionId(action);
|
|
573
|
+
if (actionId) {
|
|
574
|
+
this.actionIdToClip.delete(actionId);
|
|
575
|
+
}
|
|
576
|
+
this.actionIds.delete(action);
|
|
577
|
+
delete action.__actionId;
|
|
578
|
+
}
|
|
579
|
+
uncacheClip(clip, mixer = this.animationMixer) {
|
|
580
|
+
if (!clip || !mixer) return;
|
|
581
|
+
try {
|
|
582
|
+
mixer.uncacheClip(clip);
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
uncacheAction(action, mixer = this.animationMixer) {
|
|
587
|
+
if (!action || !mixer) return;
|
|
588
|
+
try {
|
|
589
|
+
const clip = action.getClip();
|
|
590
|
+
if (clip) {
|
|
591
|
+
mixer.uncacheAction(clip);
|
|
592
|
+
mixer.uncacheClip(clip);
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
releaseBakedRuntimeAction(runtimeClipName) {
|
|
598
|
+
const action = this.bakedRuntimeActions.get(runtimeClipName);
|
|
599
|
+
if (!action) return;
|
|
600
|
+
try {
|
|
601
|
+
action.stop();
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
this.uncacheAction(action);
|
|
605
|
+
this.clearActionId(action);
|
|
606
|
+
this.bakedRuntimeActions.delete(runtimeClipName);
|
|
607
|
+
}
|
|
608
|
+
clearBakedAdditiveRuntimeClip(runtimeClipName) {
|
|
609
|
+
const clip = this.bakedAdditiveRuntimeClips.get(runtimeClipName);
|
|
610
|
+
if (!clip) return;
|
|
611
|
+
this.uncacheClip(clip);
|
|
612
|
+
this.bakedAdditiveRuntimeClips.delete(runtimeClipName);
|
|
613
|
+
}
|
|
614
|
+
clearAllBakedAdditiveRuntimeClips() {
|
|
615
|
+
for (const runtimeClipName of Array.from(this.bakedAdditiveRuntimeClips.keys())) {
|
|
616
|
+
this.clearBakedAdditiveRuntimeClip(runtimeClipName);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
resolveTrackTarget(model, parsed) {
|
|
620
|
+
const targetKey = parsed.objectName === "bones" && parsed.objectIndex ? parsed.objectIndex : parsed.nodeName;
|
|
621
|
+
if (!targetKey) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
return model.getObjectByProperty("uuid", targetKey) ?? THREE2.PropertyBinding.findNode(model, targetKey) ?? null;
|
|
625
|
+
}
|
|
626
|
+
getMorphTrackBaseValue(target, propertyIndex) {
|
|
627
|
+
if (!target) {
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
const meshTarget = target;
|
|
631
|
+
const influences = meshTarget.morphTargetInfluences;
|
|
632
|
+
if (!influences) {
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
let morphIndex;
|
|
636
|
+
if (typeof propertyIndex === "number" && Number.isInteger(propertyIndex)) {
|
|
637
|
+
morphIndex = propertyIndex;
|
|
638
|
+
} else if (typeof propertyIndex === "string") {
|
|
639
|
+
if (/^\d+$/.test(propertyIndex)) {
|
|
640
|
+
morphIndex = Number(propertyIndex);
|
|
641
|
+
} else {
|
|
642
|
+
morphIndex = meshTarget.morphTargetDictionary?.[propertyIndex];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (morphIndex === void 0) {
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
return influences[morphIndex] ?? 0;
|
|
649
|
+
}
|
|
650
|
+
canCreateFirstFrameReferenceTrack(track) {
|
|
651
|
+
const valueSize = track.getValueSize();
|
|
652
|
+
if (!Number.isFinite(valueSize) || valueSize <= 0 || track.values.length < valueSize) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
return track.ValueTypeName === "number" || track.ValueTypeName === "quaternion" || track.ValueTypeName === "vector";
|
|
656
|
+
}
|
|
657
|
+
createFirstFrameReferenceTrack(track) {
|
|
658
|
+
const valueSize = track.getValueSize();
|
|
659
|
+
if (!this.canCreateFirstFrameReferenceTrack(track)) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
const values = Array.from(track.values.slice(0, valueSize));
|
|
663
|
+
if (track.ValueTypeName === "number") {
|
|
664
|
+
return new THREE2.NumberKeyframeTrack(track.name, [0], values);
|
|
665
|
+
}
|
|
666
|
+
if (track.ValueTypeName === "quaternion") {
|
|
667
|
+
return new THREE2.QuaternionKeyframeTrack(track.name, [0], values);
|
|
668
|
+
}
|
|
669
|
+
if (track.ValueTypeName === "vector") {
|
|
670
|
+
return new THREE2.VectorKeyframeTrack(track.name, [0], values);
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
createAdditiveReferenceTrack(track, model) {
|
|
675
|
+
const trackName = typeof track?.name === "string" ? track.name : "";
|
|
676
|
+
if (!trackName) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
let parsed;
|
|
680
|
+
try {
|
|
681
|
+
parsed = THREE2.PropertyBinding.parseTrackName(trackName);
|
|
682
|
+
} catch {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
const target = this.resolveTrackTarget(model, parsed);
|
|
686
|
+
if (parsed.propertyName === "morphTargetInfluences") {
|
|
687
|
+
return new THREE2.NumberKeyframeTrack(
|
|
688
|
+
track.name,
|
|
689
|
+
[0],
|
|
690
|
+
[this.getMorphTrackBaseValue(target, parsed.propertyIndex)]
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return this.createFirstFrameReferenceTrack(track);
|
|
694
|
+
}
|
|
695
|
+
createAdditiveRuntimeClip(runtimeClip) {
|
|
696
|
+
const model = this.host.getModel();
|
|
697
|
+
if (!model) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
const additiveTracks = [];
|
|
701
|
+
const referenceTracks = [];
|
|
702
|
+
for (const track of runtimeClip.tracks) {
|
|
703
|
+
const referenceTrack = this.createAdditiveReferenceTrack(track, model);
|
|
704
|
+
if (!referenceTrack) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
additiveTracks.push(track.clone());
|
|
708
|
+
referenceTracks.push(referenceTrack);
|
|
709
|
+
}
|
|
710
|
+
const additiveClip = new THREE2.AnimationClip(
|
|
711
|
+
`${runtimeClip.name}${ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX}`,
|
|
712
|
+
runtimeClip.duration,
|
|
713
|
+
additiveTracks
|
|
714
|
+
);
|
|
715
|
+
if (additiveTracks.length > 0) {
|
|
716
|
+
const referenceClip = new THREE2.AnimationClip(
|
|
717
|
+
`${runtimeClip.name}${ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX}_reference`,
|
|
718
|
+
0,
|
|
719
|
+
referenceTracks
|
|
720
|
+
);
|
|
721
|
+
THREE2.AnimationUtils.makeClipAdditive(additiveClip, 0, referenceClip);
|
|
722
|
+
}
|
|
723
|
+
return additiveClip;
|
|
724
|
+
}
|
|
725
|
+
getOrCreateBakedAdditiveRuntimeClip(runtimeClip) {
|
|
726
|
+
const cached = this.bakedAdditiveRuntimeClips.get(runtimeClip.name);
|
|
727
|
+
if (cached) {
|
|
728
|
+
return cached;
|
|
729
|
+
}
|
|
730
|
+
const additiveClip = this.createAdditiveRuntimeClip(runtimeClip);
|
|
731
|
+
if (!additiveClip) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
this.bakedAdditiveRuntimeClips.set(runtimeClip.name, additiveClip);
|
|
735
|
+
return additiveClip;
|
|
736
|
+
}
|
|
545
737
|
setClipEventMetadata(clip, metadata) {
|
|
546
738
|
const userData = clip.userData ?? (clip.userData = {});
|
|
547
739
|
userData[CLIP_EVENT_METADATA_KEY] = metadata;
|
|
@@ -827,21 +1019,28 @@ var BakedAnimationController = class {
|
|
|
827
1019
|
}
|
|
828
1020
|
return 0;
|
|
829
1021
|
}
|
|
830
|
-
getOrCreateBakedRuntimeAction(sourceClipName, channel) {
|
|
1022
|
+
getOrCreateBakedRuntimeAction(sourceClipName, channel, blendMode = "replace") {
|
|
831
1023
|
const bakedClip = this.getBakedSourceClip(sourceClipName);
|
|
832
1024
|
const runtimeClip = bakedClip?.runtimeClips.find((entry) => entry.channel === channel)?.clip;
|
|
833
1025
|
if (!runtimeClip) {
|
|
834
1026
|
return null;
|
|
835
1027
|
}
|
|
1028
|
+
const desiredClip = blendMode === "additive" ? this.getOrCreateBakedAdditiveRuntimeClip(runtimeClip) : runtimeClip;
|
|
1029
|
+
if (!desiredClip) {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
836
1032
|
const existing = this.bakedRuntimeActions.get(runtimeClip.name);
|
|
837
|
-
if (existing) {
|
|
1033
|
+
if (existing?.getClip() === desiredClip) {
|
|
838
1034
|
return existing;
|
|
839
1035
|
}
|
|
840
1036
|
this.ensureMixer();
|
|
841
1037
|
if (!this.animationMixer) {
|
|
842
1038
|
return null;
|
|
843
1039
|
}
|
|
844
|
-
|
|
1040
|
+
if (existing) {
|
|
1041
|
+
this.releaseBakedRuntimeAction(runtimeClip.name);
|
|
1042
|
+
}
|
|
1043
|
+
const action = this.animationMixer.clipAction(desiredClip);
|
|
845
1044
|
this.bakedRuntimeActions.set(runtimeClip.name, action);
|
|
846
1045
|
return action;
|
|
847
1046
|
}
|
|
@@ -859,7 +1058,15 @@ var BakedAnimationController = class {
|
|
|
859
1058
|
}
|
|
860
1059
|
const channelActions = /* @__PURE__ */ new Map();
|
|
861
1060
|
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
862
|
-
const
|
|
1061
|
+
const channelBlendMode = resolveBakedChannelBlendMode(
|
|
1062
|
+
runtimeClip.channel,
|
|
1063
|
+
playbackState.requestedBlendMode
|
|
1064
|
+
) ?? "replace";
|
|
1065
|
+
const action = this.getOrCreateBakedRuntimeAction(
|
|
1066
|
+
clipName,
|
|
1067
|
+
runtimeClip.channel,
|
|
1068
|
+
channelBlendMode
|
|
1069
|
+
);
|
|
863
1070
|
if (action) {
|
|
864
1071
|
channelActions.set(runtimeClip.channel, action);
|
|
865
1072
|
}
|
|
@@ -898,13 +1105,39 @@ var BakedAnimationController = class {
|
|
|
898
1105
|
}
|
|
899
1106
|
return getMeshNamesForVisemeProfile(config);
|
|
900
1107
|
}
|
|
1108
|
+
hasActiveAdditivePlayback() {
|
|
1109
|
+
for (const [clipName, group] of this.bakedActionGroups) {
|
|
1110
|
+
const state = this.playbackState.get(clipName);
|
|
1111
|
+
if (state?.blendMode !== "additive") {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
for (const action of group.channelActions.values()) {
|
|
1115
|
+
if (action.isRunning() && !action.paused) {
|
|
1116
|
+
return true;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
for (const [clipName, action] of this.animationActions) {
|
|
1121
|
+
const state = this.playbackState.get(clipName);
|
|
1122
|
+
if (state?.blendMode !== "additive") {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (action.isRunning() && !action.paused) {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
901
1131
|
update(dtSeconds) {
|
|
902
1132
|
if (this.animationMixer) {
|
|
1133
|
+
this.animationMixer.update(dtSeconds);
|
|
1134
|
+
}
|
|
1135
|
+
if (this.clipAnimationMixer) {
|
|
903
1136
|
const snapshots = Array.from(this.clipMonitors.values()).map((monitor) => ({
|
|
904
1137
|
actionId: monitor.actionId,
|
|
905
1138
|
previousTime: monitor.action.time
|
|
906
1139
|
}));
|
|
907
|
-
this.
|
|
1140
|
+
this.clipAnimationMixer.update(dtSeconds);
|
|
908
1141
|
for (const { actionId, previousTime } of snapshots) {
|
|
909
1142
|
const monitor = this.clipMonitors.get(actionId);
|
|
910
1143
|
if (!monitor) continue;
|
|
@@ -923,16 +1156,27 @@ var BakedAnimationController = class {
|
|
|
923
1156
|
}
|
|
924
1157
|
}
|
|
925
1158
|
}
|
|
1159
|
+
if (this.hasActiveAdditivePlayback()) {
|
|
1160
|
+
this.host.reapplyProceduralState?.();
|
|
1161
|
+
}
|
|
926
1162
|
}
|
|
927
1163
|
dispose() {
|
|
928
1164
|
this.stopAllAnimations();
|
|
1165
|
+
this.clearAllBakedAdditiveRuntimeClips();
|
|
929
1166
|
if (this.animationMixer) {
|
|
930
1167
|
this.animationMixer.stopAllAction();
|
|
931
1168
|
this.animationMixer = null;
|
|
932
1169
|
}
|
|
1170
|
+
if (this.clipAnimationMixer) {
|
|
1171
|
+
this.clipAnimationMixer.stopAllAction();
|
|
1172
|
+
this.clipAnimationMixer = null;
|
|
1173
|
+
}
|
|
1174
|
+
this.mixerFinishedListenerAttached = false;
|
|
1175
|
+
this.clipMixerFinishedListenerAttached = false;
|
|
933
1176
|
this.animationClips = [];
|
|
934
1177
|
this.bakedSourceClips.clear();
|
|
935
1178
|
this.bakedRuntimeActions.clear();
|
|
1179
|
+
this.bakedAdditiveRuntimeClips.clear();
|
|
936
1180
|
this.bakedActionGroups.clear();
|
|
937
1181
|
this.bakedRuntimeClipToSource.clear();
|
|
938
1182
|
this.animationActions.clear();
|
|
@@ -955,6 +1199,8 @@ var BakedAnimationController = class {
|
|
|
955
1199
|
if (this.animationMixer) {
|
|
956
1200
|
for (const bakedClip of this.bakedSourceClips.values()) {
|
|
957
1201
|
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
1202
|
+
this.releaseBakedRuntimeAction(runtimeClip.clip.name);
|
|
1203
|
+
this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
|
|
958
1204
|
try {
|
|
959
1205
|
this.animationMixer.uncacheAction(runtimeClip.clip);
|
|
960
1206
|
} catch {
|
|
@@ -966,6 +1212,7 @@ var BakedAnimationController = class {
|
|
|
966
1212
|
}
|
|
967
1213
|
}
|
|
968
1214
|
}
|
|
1215
|
+
this.clearAllBakedAdditiveRuntimeClips();
|
|
969
1216
|
for (const clipName of this.bakedSourceClips.keys()) {
|
|
970
1217
|
this.playbackState.delete(clipName);
|
|
971
1218
|
this.clipSources.delete(clipName);
|
|
@@ -1006,6 +1253,8 @@ var BakedAnimationController = class {
|
|
|
1006
1253
|
if (this.animationMixer) {
|
|
1007
1254
|
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
1008
1255
|
const action = this.bakedRuntimeActions.get(runtimeClip.clip.name);
|
|
1256
|
+
this.releaseBakedRuntimeAction(runtimeClip.clip.name);
|
|
1257
|
+
this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
|
|
1009
1258
|
try {
|
|
1010
1259
|
this.animationMixer.uncacheAction(runtimeClip.clip);
|
|
1011
1260
|
} catch {
|
|
@@ -1014,7 +1263,6 @@ var BakedAnimationController = class {
|
|
|
1014
1263
|
this.animationMixer.uncacheClip(runtimeClip.clip);
|
|
1015
1264
|
} catch {
|
|
1016
1265
|
}
|
|
1017
|
-
this.bakedRuntimeActions.delete(runtimeClip.clip.name);
|
|
1018
1266
|
this.bakedRuntimeClipToSource.delete(runtimeClip.clip.name);
|
|
1019
1267
|
const actionId = this.getActionId(action);
|
|
1020
1268
|
if (actionId && action) {
|
|
@@ -1023,6 +1271,10 @@ var BakedAnimationController = class {
|
|
|
1023
1271
|
}
|
|
1024
1272
|
}
|
|
1025
1273
|
}
|
|
1274
|
+
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
1275
|
+
this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
|
|
1276
|
+
this.bakedRuntimeActions.delete(runtimeClip.clip.name);
|
|
1277
|
+
}
|
|
1026
1278
|
this.animationClips = this.animationClips.filter((entry) => entry.name !== clipName);
|
|
1027
1279
|
this.bakedSourceClips.delete(clipName);
|
|
1028
1280
|
this.bakedActionGroups.delete(clipName);
|
|
@@ -1083,12 +1335,12 @@ var BakedAnimationController = class {
|
|
|
1083
1335
|
const actionId = this.getActionId(action);
|
|
1084
1336
|
const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
|
|
1085
1337
|
action.stop();
|
|
1086
|
-
if (!isBaked && this.
|
|
1338
|
+
if (!isBaked && this.clipAnimationMixer) {
|
|
1087
1339
|
try {
|
|
1088
1340
|
const clip = action.getClip();
|
|
1089
1341
|
if (clip) {
|
|
1090
|
-
this.
|
|
1091
|
-
this.
|
|
1342
|
+
this.clipAnimationMixer.uncacheAction(clip);
|
|
1343
|
+
this.clipAnimationMixer.uncacheClip(clip);
|
|
1092
1344
|
}
|
|
1093
1345
|
} catch {
|
|
1094
1346
|
}
|
|
@@ -1110,11 +1362,11 @@ var BakedAnimationController = class {
|
|
|
1110
1362
|
const actionId = this.getActionId(clipAction);
|
|
1111
1363
|
try {
|
|
1112
1364
|
clipAction.stop();
|
|
1113
|
-
if (this.
|
|
1365
|
+
if (this.clipAnimationMixer) {
|
|
1114
1366
|
const clip = clipAction.getClip();
|
|
1115
1367
|
if (clip) {
|
|
1116
|
-
this.
|
|
1117
|
-
this.
|
|
1368
|
+
this.clipAnimationMixer.uncacheAction(clip);
|
|
1369
|
+
this.clipAnimationMixer.uncacheClip(clip);
|
|
1118
1370
|
}
|
|
1119
1371
|
}
|
|
1120
1372
|
} catch {
|
|
@@ -1318,8 +1570,23 @@ var BakedAnimationController = class {
|
|
|
1318
1570
|
next.blendMode = this.getBakedAggregateBlendMode(clipName, next);
|
|
1319
1571
|
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
1320
1572
|
if (bakedGroup) {
|
|
1321
|
-
for (const [channel,
|
|
1573
|
+
for (const [channel, currentAction] of Array.from(bakedGroup.channelActions)) {
|
|
1574
|
+
const channelBlendMode = resolveBakedChannelBlendMode(channel, next.requestedBlendMode) ?? "replace";
|
|
1575
|
+
const previousTime = currentAction.time;
|
|
1576
|
+
const wasActive = currentAction.isRunning() || currentAction.paused;
|
|
1577
|
+
const wasPaused = currentAction.paused;
|
|
1578
|
+
const action2 = this.getOrCreateBakedRuntimeAction(clipName, channel, channelBlendMode);
|
|
1579
|
+
if (!action2) {
|
|
1580
|
+
bakedGroup.channelActions.delete(channel);
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1322
1583
|
this.applyPlaybackStateToBakedAction(action2, next, channel);
|
|
1584
|
+
action2.time = Math.max(0, Math.min(action2.getClip().duration, previousTime));
|
|
1585
|
+
if (action2 !== currentAction && wasActive) {
|
|
1586
|
+
action2.play();
|
|
1587
|
+
}
|
|
1588
|
+
action2.paused = wasPaused;
|
|
1589
|
+
bakedGroup.channelActions.set(channel, action2);
|
|
1323
1590
|
}
|
|
1324
1591
|
}
|
|
1325
1592
|
this.setPlaybackState(clipName, next);
|
|
@@ -1334,6 +1601,10 @@ var BakedAnimationController = class {
|
|
|
1334
1601
|
seekAnimation(clipName, time) {
|
|
1335
1602
|
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
1336
1603
|
if (bakedGroup) {
|
|
1604
|
+
const state = this.getPlaybackStateSnapshot(clipName, {
|
|
1605
|
+
loop: true,
|
|
1606
|
+
source: this.clipSources.get(clipName) ?? "baked"
|
|
1607
|
+
});
|
|
1337
1608
|
const duration2 = this.getBakedSourceClip(clipName)?.sourceClip.duration ?? 0;
|
|
1338
1609
|
const clamped = Math.max(0, Math.min(duration2, Number.isFinite(time) ? time : 0));
|
|
1339
1610
|
for (const action2 of bakedGroup.channelActions.values()) {
|
|
@@ -1341,6 +1612,10 @@ var BakedAnimationController = class {
|
|
|
1341
1612
|
}
|
|
1342
1613
|
try {
|
|
1343
1614
|
this.animationMixer?.update(0);
|
|
1615
|
+
this.clipAnimationMixer?.update(0);
|
|
1616
|
+
if (state.blendMode === "additive") {
|
|
1617
|
+
this.host.reapplyProceduralState?.();
|
|
1618
|
+
}
|
|
1344
1619
|
} catch {
|
|
1345
1620
|
}
|
|
1346
1621
|
return;
|
|
@@ -1350,7 +1625,11 @@ var BakedAnimationController = class {
|
|
|
1350
1625
|
const duration = action.getClip().duration;
|
|
1351
1626
|
action.time = Math.max(0, Math.min(duration, Number.isFinite(time) ? time : 0));
|
|
1352
1627
|
try {
|
|
1353
|
-
this.
|
|
1628
|
+
this.clipAnimationMixer?.update(0);
|
|
1629
|
+
const state = this.playbackState.get(clipName);
|
|
1630
|
+
if (state?.blendMode === "additive") {
|
|
1631
|
+
this.host.reapplyProceduralState?.();
|
|
1632
|
+
}
|
|
1354
1633
|
} catch {
|
|
1355
1634
|
}
|
|
1356
1635
|
}
|
|
@@ -1358,6 +1637,9 @@ var BakedAnimationController = class {
|
|
|
1358
1637
|
if (this.animationMixer) {
|
|
1359
1638
|
this.animationMixer.timeScale = timeScale;
|
|
1360
1639
|
}
|
|
1640
|
+
if (this.clipAnimationMixer) {
|
|
1641
|
+
this.clipAnimationMixer.timeScale = timeScale;
|
|
1642
|
+
}
|
|
1361
1643
|
}
|
|
1362
1644
|
getAnimationState(clipName) {
|
|
1363
1645
|
const bakedClip = this.getBakedSourceClip(clipName);
|
|
@@ -1473,12 +1755,13 @@ var BakedAnimationController = class {
|
|
|
1473
1755
|
const globalBalance = options?.balance ?? 0;
|
|
1474
1756
|
const balanceMap = options?.balanceMap;
|
|
1475
1757
|
const meshNames = options?.meshNames;
|
|
1758
|
+
const visemeSlotCount = getProfileVisemeSlots(config).length;
|
|
1476
1759
|
let maxTime = 0;
|
|
1477
1760
|
const isNumericAU = (id) => /^\d+$/.test(id);
|
|
1478
1761
|
const isVisemeIndex = (id) => {
|
|
1479
1762
|
if (options?.snippetCategory !== "visemeSnippet") return false;
|
|
1480
1763
|
const num = Number(id);
|
|
1481
|
-
return !Number.isNaN(num) && num >= 0 && num <
|
|
1764
|
+
return !Number.isNaN(num) && num >= 0 && num < visemeSlotCount;
|
|
1482
1765
|
};
|
|
1483
1766
|
const sampleAt = (arr, t) => {
|
|
1484
1767
|
if (!arr.length) return 0;
|
|
@@ -1516,11 +1799,13 @@ var BakedAnimationController = class {
|
|
|
1516
1799
|
const auId = Number(curveId);
|
|
1517
1800
|
if (isVisemeIndex(curveId)) {
|
|
1518
1801
|
const visemeMeshNames = this.getMeshNamesForViseme(config, meshNames);
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1802
|
+
for (const target of getVisemeBindingTargets(config, auId)) {
|
|
1803
|
+
const effectiveScale = intensityScale * target.weight;
|
|
1804
|
+
if (typeof target.morph === "number") {
|
|
1805
|
+
this.addMorphIndexTracks(tracks, target.morph, keyframes, effectiveScale, visemeMeshNames);
|
|
1806
|
+
} else if (target.morph) {
|
|
1807
|
+
this.addMorphTracks(tracks, target.morph, keyframes, effectiveScale, visemeMeshNames);
|
|
1808
|
+
}
|
|
1524
1809
|
}
|
|
1525
1810
|
} else {
|
|
1526
1811
|
const auMeshNames = this.getMeshNamesForAU(auId, config, meshNames);
|
|
@@ -1563,7 +1848,7 @@ var BakedAnimationController = class {
|
|
|
1563
1848
|
}
|
|
1564
1849
|
const autoVisemeJaw = options?.autoVisemeJaw !== false;
|
|
1565
1850
|
const jawScale = options?.jawScale ?? 1;
|
|
1566
|
-
const visemeJawAmounts = config
|
|
1851
|
+
const visemeJawAmounts = getVisemeJawAmounts(config);
|
|
1567
1852
|
if (autoVisemeJaw && jawScale > 0 && visemeJawAmounts && options?.snippetCategory === "visemeSnippet" && keyframeTimes.length > 0) {
|
|
1568
1853
|
const bones = this.host.getBones();
|
|
1569
1854
|
const jawEntry = bones["JAW"];
|
|
@@ -1571,7 +1856,7 @@ var BakedAnimationController = class {
|
|
|
1571
1856
|
const jawValues = [];
|
|
1572
1857
|
for (const t of keyframeTimes) {
|
|
1573
1858
|
let jawAmount = 0;
|
|
1574
|
-
for (let visemeIdx = 0; visemeIdx <
|
|
1859
|
+
for (let visemeIdx = 0; visemeIdx < visemeSlotCount; visemeIdx++) {
|
|
1575
1860
|
const visemeCurve = curves[String(visemeIdx)];
|
|
1576
1861
|
if (!visemeCurve) continue;
|
|
1577
1862
|
const visemeValue = clampIntensity(sampleAt(visemeCurve, t) * intensityScale);
|
|
@@ -1687,8 +1972,8 @@ var BakedAnimationController = class {
|
|
|
1687
1972
|
return clip;
|
|
1688
1973
|
}
|
|
1689
1974
|
playClip(clip, options) {
|
|
1690
|
-
this.
|
|
1691
|
-
if (!
|
|
1975
|
+
const mixer = this.ensureClipMixer();
|
|
1976
|
+
if (!mixer) {
|
|
1692
1977
|
console.warn("[Loom3] playClip: No model loaded, cannot create mixer");
|
|
1693
1978
|
return null;
|
|
1694
1979
|
}
|
|
@@ -1706,7 +1991,7 @@ var BakedAnimationController = class {
|
|
|
1706
1991
|
actionId = this.setActionId(action, clip.name);
|
|
1707
1992
|
}
|
|
1708
1993
|
if (!action) {
|
|
1709
|
-
action =
|
|
1994
|
+
action = mixer.clipAction(clip);
|
|
1710
1995
|
actionId = this.setActionId(action, clip.name);
|
|
1711
1996
|
}
|
|
1712
1997
|
const existingClip = this.animationClips.find((c) => c.name === clip.name);
|
|
@@ -1767,15 +2052,13 @@ var BakedAnimationController = class {
|
|
|
1767
2052
|
},
|
|
1768
2053
|
stop: () => {
|
|
1769
2054
|
action.stop();
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
} catch {
|
|
1778
|
-
}
|
|
2055
|
+
try {
|
|
2056
|
+
mixer.uncacheAction(clip);
|
|
2057
|
+
} catch {
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
mixer.uncacheClip(clip);
|
|
2061
|
+
} catch {
|
|
1779
2062
|
}
|
|
1780
2063
|
this.clipActions.delete(clip.name);
|
|
1781
2064
|
this.animationActions.delete(clip.name);
|
|
@@ -1816,7 +2099,7 @@ var BakedAnimationController = class {
|
|
|
1816
2099
|
const clamped = Math.max(0, Math.min(clip.duration, t));
|
|
1817
2100
|
action.time = clamped;
|
|
1818
2101
|
try {
|
|
1819
|
-
|
|
2102
|
+
mixer.update(0);
|
|
1820
2103
|
} catch {
|
|
1821
2104
|
}
|
|
1822
2105
|
this.syncClipMonitorTime(monitor, clamped, true);
|
|
@@ -1849,16 +2132,16 @@ var BakedAnimationController = class {
|
|
|
1849
2132
|
return this.playClip(clip, { ...options, source: options?.source ?? "clip" });
|
|
1850
2133
|
}
|
|
1851
2134
|
cleanupSnippet(name) {
|
|
1852
|
-
if (!this.
|
|
2135
|
+
if (!this.host.getModel()) return;
|
|
1853
2136
|
for (const [clipName, action] of Array.from(this.clipActions.entries())) {
|
|
1854
2137
|
if (clipName === name || clipName.startsWith(`${name}_`)) {
|
|
1855
2138
|
const actionId = this.getActionId(action);
|
|
1856
2139
|
try {
|
|
1857
2140
|
action.stop();
|
|
1858
2141
|
const clip = action.getClip();
|
|
1859
|
-
if (clip) {
|
|
1860
|
-
this.
|
|
1861
|
-
this.
|
|
2142
|
+
if (clip && this.clipAnimationMixer) {
|
|
2143
|
+
this.clipAnimationMixer.uncacheAction(clip);
|
|
2144
|
+
this.clipAnimationMixer.uncacheClip(clip);
|
|
1862
2145
|
}
|
|
1863
2146
|
} catch {
|
|
1864
2147
|
}
|
|
@@ -1886,7 +2169,10 @@ var BakedAnimationController = class {
|
|
|
1886
2169
|
clipActions: Array.from(this.clipActions.entries()).map(([k, a]) => ({ name: k, actionId: this.getActionId(a) })),
|
|
1887
2170
|
animationActions: Array.from(this.animationActions.entries()).map(([k, a]) => ({ name: k, actionId: this.getActionId(a) })),
|
|
1888
2171
|
clipHandles: Array.from(this.clipHandles.entries()).map(([k, h]) => ({ name: k, actionId: h.actionId })),
|
|
1889
|
-
mixerActions:
|
|
2172
|
+
mixerActions: [
|
|
2173
|
+
...this.animationMixer?._actions || [],
|
|
2174
|
+
...this.clipAnimationMixer?._actions || []
|
|
2175
|
+
].map((a) => ({ name: a?.getClip?.()?.name || "", actionId: this.getActionId(a) }))
|
|
1890
2176
|
});
|
|
1891
2177
|
console.log("[Loom3] updateClipParams start", debugSnapshot());
|
|
1892
2178
|
const apply = (action) => {
|
|
@@ -2000,35 +2286,48 @@ var BakedAnimationController = class {
|
|
|
2000
2286
|
this.animationMixer = new THREE2.AnimationMixer(model);
|
|
2001
2287
|
}
|
|
2002
2288
|
if (this.animationMixer && !this.mixerFinishedListenerAttached) {
|
|
2003
|
-
this.animationMixer.addEventListener("finished", (event) =>
|
|
2004
|
-
const action = event.action;
|
|
2005
|
-
const actionId = this.getActionId(action);
|
|
2006
|
-
if (actionId) {
|
|
2007
|
-
const monitor = this.clipMonitors.get(actionId);
|
|
2008
|
-
if (monitor) {
|
|
2009
|
-
monitor.finishedPending = true;
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
const clip = action.getClip();
|
|
2014
|
-
const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
|
|
2015
|
-
if (bakedRuntime) {
|
|
2016
|
-
const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
|
|
2017
|
-
if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
|
|
2018
|
-
group.resolveFinished();
|
|
2019
|
-
}
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
const callback = this.animationFinishedCallbacks.get(clip.name);
|
|
2023
|
-
if (callback) {
|
|
2024
|
-
callback();
|
|
2025
|
-
this.animationFinishedCallbacks.delete(clip.name);
|
|
2026
|
-
}
|
|
2027
|
-
});
|
|
2289
|
+
this.animationMixer.addEventListener("finished", (event) => this.handleMixerFinished(event));
|
|
2028
2290
|
this.mixerFinishedListenerAttached = true;
|
|
2029
2291
|
}
|
|
2030
2292
|
return this.animationMixer;
|
|
2031
2293
|
}
|
|
2294
|
+
ensureClipMixer() {
|
|
2295
|
+
const model = this.host.getModel();
|
|
2296
|
+
if (!model) return null;
|
|
2297
|
+
if (!this.clipAnimationMixer) {
|
|
2298
|
+
this.clipAnimationMixer = new THREE2.AnimationMixer(model);
|
|
2299
|
+
}
|
|
2300
|
+
if (this.clipAnimationMixer && !this.clipMixerFinishedListenerAttached) {
|
|
2301
|
+
this.clipAnimationMixer.addEventListener("finished", (event) => this.handleMixerFinished(event));
|
|
2302
|
+
this.clipMixerFinishedListenerAttached = true;
|
|
2303
|
+
}
|
|
2304
|
+
return this.clipAnimationMixer;
|
|
2305
|
+
}
|
|
2306
|
+
handleMixerFinished(event) {
|
|
2307
|
+
const action = event.action;
|
|
2308
|
+
const actionId = this.getActionId(action);
|
|
2309
|
+
if (actionId) {
|
|
2310
|
+
const monitor = this.clipMonitors.get(actionId);
|
|
2311
|
+
if (monitor) {
|
|
2312
|
+
monitor.finishedPending = true;
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
const clip = action.getClip();
|
|
2317
|
+
const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
|
|
2318
|
+
if (bakedRuntime) {
|
|
2319
|
+
const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
|
|
2320
|
+
if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
|
|
2321
|
+
group.resolveFinished();
|
|
2322
|
+
}
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const callback = this.animationFinishedCallbacks.get(clip.name);
|
|
2326
|
+
if (callback) {
|
|
2327
|
+
callback();
|
|
2328
|
+
this.animationFinishedCallbacks.delete(clip.name);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2032
2331
|
createAnimationHandle(clipName, action, finishedPromise) {
|
|
2033
2332
|
return {
|
|
2034
2333
|
actionId: this.getActionId(action),
|
|
@@ -4992,7 +5291,8 @@ var _Loom3 = class _Loom3 {
|
|
|
4992
5291
|
getCompositeRotations: () => this.compositeRotations,
|
|
4993
5292
|
computeSideValues: (base, balance) => this.computeSideValues(base, balance),
|
|
4994
5293
|
getAUMixWeight: (auId) => this.getAUMixWeight(auId),
|
|
4995
|
-
isMixedAU: (auId) => this.isMixedAU(auId)
|
|
5294
|
+
isMixedAU: (auId) => this.isMixedAU(auId),
|
|
5295
|
+
reapplyProceduralState: () => this.reapplyProceduralStateAfterBakedUpdate()
|
|
4996
5296
|
});
|
|
4997
5297
|
this.hairPhysics = new HairPhysicsController({
|
|
4998
5298
|
getMeshByName: (name) => this.meshByName.get(name),
|
|
@@ -5084,10 +5384,15 @@ var _Loom3 = class _Loom3 {
|
|
|
5084
5384
|
};
|
|
5085
5385
|
this.resolvedAUMorphTargets.set(auId, resolved);
|
|
5086
5386
|
}
|
|
5087
|
-
for (let i = 0; i < (this.config
|
|
5088
|
-
const key = this.config.visemeKeys[i];
|
|
5387
|
+
for (let i = 0; i < getProfileVisemeSlots(this.config).length; i += 1) {
|
|
5089
5388
|
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
5090
|
-
const targets =
|
|
5389
|
+
const targets = [];
|
|
5390
|
+
for (const bindingTarget of getVisemeBindingTargets(this.config, i)) {
|
|
5391
|
+
const resolved = typeof bindingTarget.morph === "number" ? this.resolveMorphTargetsByIndex(bindingTarget.morph, visemeMeshNames) : this.resolveMorphTargets(bindingTarget.morph, visemeMeshNames);
|
|
5392
|
+
for (const target of resolved) {
|
|
5393
|
+
targets.push({ ...target, weight: bindingTarget.weight });
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5091
5396
|
this.resolvedVisemeTargets[i] = targets;
|
|
5092
5397
|
}
|
|
5093
5398
|
}
|
|
@@ -5555,46 +5860,33 @@ var _Loom3 = class _Loom3 {
|
|
|
5555
5860
|
// VISEME CONTROL
|
|
5556
5861
|
// ============================================================================
|
|
5557
5862
|
setViseme(visemeIndex, value, jawScale = 1) {
|
|
5558
|
-
if (visemeIndex < 0 || visemeIndex >= this.
|
|
5863
|
+
if (visemeIndex < 0 || visemeIndex >= this.visemeValues.length) return;
|
|
5559
5864
|
const val = clamp012(value);
|
|
5560
5865
|
this.visemeValues[visemeIndex] = val;
|
|
5561
5866
|
this.visemeJawScales[visemeIndex] = jawScale;
|
|
5562
|
-
|
|
5563
|
-
if (targets && targets.length > 0) {
|
|
5564
|
-
this.applyMorphTargets(targets, val);
|
|
5565
|
-
} else {
|
|
5566
|
-
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
5567
|
-
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
5568
|
-
if (typeof morphKey === "number") {
|
|
5569
|
-
this.setMorphInfluence(morphKey, val, visemeMeshNames);
|
|
5570
|
-
} else if (typeof morphKey === "string") {
|
|
5571
|
-
this.setMorph(morphKey, val, visemeMeshNames);
|
|
5572
|
-
}
|
|
5573
|
-
}
|
|
5574
|
-
const jawAmount = this.getVisemeJawAmount(visemeIndex) * val * jawScale;
|
|
5575
|
-
if (Math.abs(jawScale) > 1e-6 && Math.abs(jawAmount) > 1e-6) {
|
|
5576
|
-
this.updateBoneRotation("JAW", "pitch", jawAmount);
|
|
5577
|
-
}
|
|
5867
|
+
this.applyVisemeRuntimeState();
|
|
5578
5868
|
}
|
|
5579
5869
|
transitionViseme(visemeIndex, to, durationMs = 80, jawScale = 1) {
|
|
5580
|
-
if (visemeIndex < 0 || visemeIndex >= this.
|
|
5870
|
+
if (visemeIndex < 0 || visemeIndex >= this.visemeValues.length) {
|
|
5581
5871
|
return { promise: Promise.resolve(), pause: () => {
|
|
5582
5872
|
}, resume: () => {
|
|
5583
5873
|
}, cancel: () => {
|
|
5584
5874
|
} };
|
|
5585
5875
|
}
|
|
5586
|
-
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
5587
5876
|
const target = clamp012(to);
|
|
5588
|
-
this.visemeValues[visemeIndex]
|
|
5877
|
+
const from = this.visemeValues[visemeIndex] ?? 0;
|
|
5589
5878
|
this.visemeJawScales[visemeIndex] = jawScale;
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5879
|
+
return this.animation.addTransition(
|
|
5880
|
+
`viseme_value_${visemeIndex}`,
|
|
5881
|
+
from,
|
|
5882
|
+
target,
|
|
5883
|
+
durationMs,
|
|
5884
|
+
(value) => {
|
|
5885
|
+
this.visemeValues[visemeIndex] = clamp012(value);
|
|
5886
|
+
this.visemeJawScales[visemeIndex] = jawScale;
|
|
5887
|
+
this.applyVisemeRuntimeState();
|
|
5888
|
+
}
|
|
5889
|
+
);
|
|
5598
5890
|
}
|
|
5599
5891
|
setVisemeById(slotId, value, jawScale = 1) {
|
|
5600
5892
|
const index = getVisemeSlotIndex(this.config, slotId);
|
|
@@ -5658,8 +5950,9 @@ var _Loom3 = class _Loom3 {
|
|
|
5658
5950
|
}
|
|
5659
5951
|
resetToNeutral() {
|
|
5660
5952
|
this.auValues = {};
|
|
5661
|
-
|
|
5662
|
-
this.
|
|
5953
|
+
const visemeCount = getProfileVisemeSlots(this.config).length;
|
|
5954
|
+
this.visemeValues = new Array(visemeCount).fill(0);
|
|
5955
|
+
this.visemeJawScales = new Array(visemeCount).fill(1);
|
|
5663
5956
|
this.translations = {};
|
|
5664
5957
|
this.initBoneRotations();
|
|
5665
5958
|
this.clearTransitions();
|
|
@@ -5693,15 +5986,35 @@ var _Loom3 = class _Loom3 {
|
|
|
5693
5986
|
if (Number.isNaN(auId)) continue;
|
|
5694
5987
|
this.setAU(auId, value, this.auBalances[auId]);
|
|
5695
5988
|
}
|
|
5989
|
+
this.applyVisemeRuntimeState();
|
|
5990
|
+
if (this.model) {
|
|
5991
|
+
this.flushPendingComposites();
|
|
5992
|
+
this.model.updateMatrixWorld(true);
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
reapplyProceduralStateAfterBakedUpdate() {
|
|
5996
|
+
if (!this.model) {
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
5999
|
+
let hasActiveOverrides = false;
|
|
6000
|
+
for (const [auIdStr, value] of Object.entries(this.auValues)) {
|
|
6001
|
+
if (value <= 0) continue;
|
|
6002
|
+
const auId = Number(auIdStr);
|
|
6003
|
+
if (Number.isNaN(auId)) continue;
|
|
6004
|
+
hasActiveOverrides = true;
|
|
6005
|
+
this.setAU(auId, value, this.auBalances[auId]);
|
|
6006
|
+
}
|
|
5696
6007
|
for (let visemeIndex = 0; visemeIndex < this.visemeValues.length; visemeIndex += 1) {
|
|
5697
6008
|
const value = this.visemeValues[visemeIndex] ?? 0;
|
|
5698
6009
|
if (value <= 0) continue;
|
|
6010
|
+
hasActiveOverrides = true;
|
|
5699
6011
|
this.setViseme(visemeIndex, value, this.visemeJawScales[visemeIndex] ?? 1);
|
|
5700
6012
|
}
|
|
5701
|
-
if (
|
|
5702
|
-
|
|
5703
|
-
this.model.updateMatrixWorld(true);
|
|
6013
|
+
if (!hasActiveOverrides) {
|
|
6014
|
+
return;
|
|
5704
6015
|
}
|
|
6016
|
+
this.flushPendingComposites();
|
|
6017
|
+
this.model.updateMatrixWorld(true);
|
|
5705
6018
|
}
|
|
5706
6019
|
// ============================================================================
|
|
5707
6020
|
// MESH CONTROL
|
|
@@ -6031,6 +6344,37 @@ var _Loom3 = class _Loom3 {
|
|
|
6031
6344
|
target.infl[target.idx] = val;
|
|
6032
6345
|
}
|
|
6033
6346
|
}
|
|
6347
|
+
applyVisemeRuntimeState() {
|
|
6348
|
+
for (const targets of this.resolvedVisemeTargets) {
|
|
6349
|
+
for (const target of targets || []) {
|
|
6350
|
+
if (target.idx < target.infl.length) {
|
|
6351
|
+
target.infl[target.idx] = 0;
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
for (let index = 0; index < this.visemeValues.length; index += 1) {
|
|
6356
|
+
const value = clamp012(this.visemeValues[index] ?? 0);
|
|
6357
|
+
if (value <= 1e-6) continue;
|
|
6358
|
+
const targets = this.resolvedVisemeTargets[index] || [];
|
|
6359
|
+
for (const target of targets) {
|
|
6360
|
+
if (target.idx >= target.infl.length) continue;
|
|
6361
|
+
const weighted = clamp012(value * target.weight);
|
|
6362
|
+
target.infl[target.idx] = Math.max(target.infl[target.idx] ?? 0, weighted);
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
this.updateBoneRotation("JAW", "pitch", this.getActiveVisemeJawAmount());
|
|
6366
|
+
}
|
|
6367
|
+
getActiveVisemeJawAmount() {
|
|
6368
|
+
let jawAmount = 0;
|
|
6369
|
+
for (let index = 0; index < this.visemeValues.length; index += 1) {
|
|
6370
|
+
const value = clamp012(this.visemeValues[index] ?? 0);
|
|
6371
|
+
if (value <= 1e-6) continue;
|
|
6372
|
+
const jawScale = this.visemeJawScales[index] ?? 1;
|
|
6373
|
+
if (Math.abs(jawScale) <= 1e-6) continue;
|
|
6374
|
+
jawAmount = Math.max(jawAmount, this.getVisemeJawAmount(index) * value * jawScale);
|
|
6375
|
+
}
|
|
6376
|
+
return jawAmount;
|
|
6377
|
+
}
|
|
6034
6378
|
getMorphValue(key) {
|
|
6035
6379
|
if (this.faceMesh) {
|
|
6036
6380
|
const dict = this.faceMesh.morphTargetDictionary;
|
|
@@ -6216,7 +6560,7 @@ var _Loom3 = class _Loom3 {
|
|
|
6216
6560
|
return meshNames?.length ? `idx:${index}@${meshNames.join(",")}` : `idx:${index}`;
|
|
6217
6561
|
}
|
|
6218
6562
|
syncVisemeRuntimeState() {
|
|
6219
|
-
const visemeCount = this.config.
|
|
6563
|
+
const visemeCount = getProfileVisemeSlots(this.config).length;
|
|
6220
6564
|
this.visemeValues = Array.from(
|
|
6221
6565
|
{ length: visemeCount },
|
|
6222
6566
|
(_, index) => this.visemeValues[index] ?? 0
|
|
@@ -8339,6 +8683,7 @@ exports.getModelForwardDirection = getModelForwardDirection;
|
|
|
8339
8683
|
exports.getPreset = getPreset;
|
|
8340
8684
|
exports.getPresetWithProfile = getPresetWithProfile;
|
|
8341
8685
|
exports.getProfileVisemeSlots = getProfileVisemeSlots;
|
|
8686
|
+
exports.getVisemeBindingTargets = getVisemeBindingTargets;
|
|
8342
8687
|
exports.getVisemeJawAmounts = getVisemeJawAmounts;
|
|
8343
8688
|
exports.getVisemeSlotIndex = getVisemeSlotIndex;
|
|
8344
8689
|
exports.hasLeftRightMorphs = hasLeftRightMorphs;
|