reze-engine 0.28.0 → 0.29.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/README.md +18 -1
- package/dist/animation.d.ts +14 -0
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +15 -0
- package/dist/engine.d.ts +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +37 -8
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/locomotion.d.ts +90 -0
- package/dist/locomotion.d.ts.map +1 -0
- package/dist/locomotion.js +179 -0
- package/dist/model.d.ts +51 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +343 -53
- package/package.json +1 -1
- package/src/animation.ts +26 -0
- package/src/engine.ts +46 -8
- package/src/index.ts +9 -0
- package/src/locomotion.ts +254 -0
- package/src/model.ts +354 -65
package/dist/model.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Mat4, Quat, Vec3 } from "./math";
|
|
2
2
|
import { type AssetReader } from "./asset-reader";
|
|
3
3
|
import { Rigidbody, Joint } from "./physics";
|
|
4
|
-
import { AnimationClip, AnimationPlayOptions, AnimationProgress } from "./animation";
|
|
4
|
+
import { AnimationClip, AnimationPlayOptions, AnimationProgress, BlendEntry } from "./animation";
|
|
5
5
|
export interface Texture {
|
|
6
6
|
path: string;
|
|
7
7
|
name: string;
|
|
@@ -155,6 +155,17 @@ export declare class Model {
|
|
|
155
155
|
private boneTrackIndices;
|
|
156
156
|
private morphTrackIndices;
|
|
157
157
|
private lastAppliedClip;
|
|
158
|
+
private blendEntries;
|
|
159
|
+
private crossfade;
|
|
160
|
+
private readonly blendBoneCursors;
|
|
161
|
+
private readonly blendMorphCursors;
|
|
162
|
+
private blendRotAcc;
|
|
163
|
+
private blendTransAcc;
|
|
164
|
+
private blendWeightAcc;
|
|
165
|
+
private blendBoneGen;
|
|
166
|
+
private blendMorphAcc;
|
|
167
|
+
private blendMorphGen;
|
|
168
|
+
private blendGenCounter;
|
|
158
169
|
private assetReader;
|
|
159
170
|
private assetBasePath;
|
|
160
171
|
/** Called by Engine when registering the model; enables loadVmd to resolve relative paths for folder uploads. */
|
|
@@ -181,6 +192,13 @@ export declare class Model {
|
|
|
181
192
|
getSkinning(): Skinning;
|
|
182
193
|
hasPhysicsData(): boolean;
|
|
183
194
|
getLoadWarnings(): readonly string[];
|
|
195
|
+
/** Post-pose local rotation offsets by bone index (see setBoneRotationOffset). */
|
|
196
|
+
private readonly boneRotationOffsets;
|
|
197
|
+
/** Compose a constant local rotation onto a bone AFTER every pose source (clip,
|
|
198
|
+
* blend, tween) each frame — the classic MMD "heel correction": pitch the 足首
|
|
199
|
+
* bones so a motion authored for flat shoes grounds a heeled model. Persists
|
|
200
|
+
* across play()/show(); pass null to clear. */
|
|
201
|
+
setBoneRotationOffset(boneName: string, rotation: Quat | null): boolean;
|
|
184
202
|
getRigidbodies(): Rigidbody[];
|
|
185
203
|
getJoints(): Joint[];
|
|
186
204
|
getMorphing(): Morphing;
|
|
@@ -213,6 +231,20 @@ export declare class Model {
|
|
|
213
231
|
play(name: string): boolean;
|
|
214
232
|
play(name: string, options?: AnimationPlayOptions): boolean;
|
|
215
233
|
show(name: string): void;
|
|
234
|
+
/** Drive the pose from N weighted clips; the caller owns every clock (see BlendEntry).
|
|
235
|
+
* The entries array is held by reference and read each update, so a per-frame driver
|
|
236
|
+
* can mutate it in place without re-calling. Cleared by play(name)/show()/stop()/
|
|
237
|
+
* clearAnimation()/crossfadeTo(). The single-clip player keeps ticking underneath
|
|
238
|
+
* but stops writing the pose while a blend is set. */
|
|
239
|
+
setBlendPose(entries: BlendEntry[]): void;
|
|
240
|
+
clearBlendPose(): void;
|
|
241
|
+
/** Fade from the currently playing clip (or from the rest pose when nothing plays)
|
|
242
|
+
* into `name` over `seconds`. The target starts at frame 0 and becomes the current
|
|
243
|
+
* clip immediately — progress, looping and the camera clock report the target for
|
|
244
|
+
* the whole fade. Bones only the outgoing clip animates ease back to rest. */
|
|
245
|
+
crossfadeTo(name: string, seconds: number, options?: {
|
|
246
|
+
loop?: boolean;
|
|
247
|
+
}): boolean;
|
|
216
248
|
playAnimation(): void;
|
|
217
249
|
pause(): void;
|
|
218
250
|
pauseAnimation(): void;
|
|
@@ -238,7 +270,25 @@ export declare class Model {
|
|
|
238
270
|
/** Step the IK state to `frame`. VMD IK keys are steps: a state holds until
|
|
239
271
|
* the next one changes it, so this is a lookup, not an interpolation. */
|
|
240
272
|
private applyIkFromClip;
|
|
273
|
+
/** Sample one bone track at `frame` into outRot + outVmdTrans. The translation is
|
|
274
|
+
* VMD-space (bind-relative), NOT yet converted to bone-local — callers convert with
|
|
275
|
+
* convertVMDTranslationToLocal once they know the final rotation. The cursor cache
|
|
276
|
+
* belongs to the caller (single-clip path vs per-clip blend caches). Returns false
|
|
277
|
+
* for an empty track. */
|
|
278
|
+
private sampleBoneTrackInto;
|
|
279
|
+
/** Sample one morph track at `frame` (linear weight lerp). Returns NaN for an empty track. */
|
|
280
|
+
private sampleMorphTrack;
|
|
241
281
|
private applyPoseFromClip;
|
|
282
|
+
/** Weighted N-clip pose blend. Rotations accumulate by hemisphere-aligned weighted
|
|
283
|
+
* sum then normalize (nlerp generalized to N inputs); VMD-space translations lerp.
|
|
284
|
+
* A bone missing from a clip contributes that clip's share of the REST pose, so a
|
|
285
|
+
* weight sum below 1 fades toward rest — that is also how crossfading from "no
|
|
286
|
+
* clip" works. IK on/off state follows the highest-weight entry. */
|
|
287
|
+
private applyBlendedPose;
|
|
288
|
+
/** One crossfade step: advance the outgoing clock (the target's clock lives in
|
|
289
|
+
* animationState, already ticked by update), shape the weight with easeInOut,
|
|
290
|
+
* and hand both to the blend sampler. Holds in place while paused. */
|
|
291
|
+
private applyCrossfade;
|
|
242
292
|
update(deltaTime: number, ikEnabled?: boolean): boolean;
|
|
243
293
|
private solveIKChains;
|
|
244
294
|
private ikComputedSet;
|
package/dist/model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
|
package/dist/model.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3, scratchMat4Values, scratchQuat } from "./math";
|
|
1
|
+
import { Mat4, Quat, Vec3, easeInOut, scratchMat4Values, scratchQuat } from "./math";
|
|
2
2
|
import { Engine } from "./engine";
|
|
3
3
|
import { joinAssetPath } from "./asset-reader";
|
|
4
4
|
import { IKSolverSystem } from "./ik-solver";
|
|
5
5
|
import { VMDLoader } from "./vmd-loader";
|
|
6
6
|
import { VMDWriter } from "./vmd-writer";
|
|
7
|
-
import { AnimationState, interpolateControlPoints, rawInterpolationToBoneInterpolation, } from "./animation";
|
|
7
|
+
import { AnimationState, FPS, interpolateControlPoints, rawInterpolationToBoneInterpolation, } from "./animation";
|
|
8
8
|
const VERTEX_STRIDE = 8;
|
|
9
9
|
// Animation-sampling scratch (applyPoseFromClip → convertVMDTranslationToLocal). These
|
|
10
10
|
// run sequentially and are not reentrant with the world-matrix scratch in math.ts, so
|
|
@@ -13,6 +13,13 @@ const _animSlerp = new Quat(0, 0, 0, 1);
|
|
|
13
13
|
const _animInterpT = new Vec3(0, 0, 0);
|
|
14
14
|
const _convOut = new Vec3(0, 0, 0);
|
|
15
15
|
const _convMat = new Float32Array(16);
|
|
16
|
+
// Blend-path scratch: per-entry sample target and the crossfade's two fixed entries.
|
|
17
|
+
const _blendQ = new Quat(0, 0, 0, 1);
|
|
18
|
+
const _blendT = new Vec3(0, 0, 0);
|
|
19
|
+
const _fadeEntries = [
|
|
20
|
+
{ name: "", time: 0, weight: 0 },
|
|
21
|
+
{ name: "", time: 0, weight: 0 },
|
|
22
|
+
];
|
|
16
23
|
export class Model {
|
|
17
24
|
get name() {
|
|
18
25
|
return this._name;
|
|
@@ -94,6 +101,22 @@ export class Model {
|
|
|
94
101
|
this.boneTrackIndices = new Map();
|
|
95
102
|
this.morphTrackIndices = new Map();
|
|
96
103
|
this.lastAppliedClip = null;
|
|
104
|
+
// Blended pose: declarative N-clip mix (setBlendPose) or a running crossfade.
|
|
105
|
+
// Cursor caches are per clip so sampling several clips in one frame doesn't
|
|
106
|
+
// thrash the single-clip caches above; WeakMap so removed clips can collect.
|
|
107
|
+
this.blendEntries = null;
|
|
108
|
+
this.crossfade = null;
|
|
109
|
+
this.blendBoneCursors = new WeakMap();
|
|
110
|
+
this.blendMorphCursors = new WeakMap();
|
|
111
|
+
// Per-bone accumulators (lazy — sized to the skeleton on first blend). Generation
|
|
112
|
+
// marks avoid a clear pass: a slot is live this frame only when gen matches.
|
|
113
|
+
this.blendRotAcc = null;
|
|
114
|
+
this.blendTransAcc = null;
|
|
115
|
+
this.blendWeightAcc = null;
|
|
116
|
+
this.blendBoneGen = null;
|
|
117
|
+
this.blendMorphAcc = null;
|
|
118
|
+
this.blendMorphGen = null;
|
|
119
|
+
this.blendGenCounter = 0;
|
|
97
120
|
this.assetReader = null;
|
|
98
121
|
this.assetBasePath = "";
|
|
99
122
|
// When true, update() skips applyPoseFromClip, so whatever was last written to
|
|
@@ -102,6 +125,8 @@ export class Model {
|
|
|
102
125
|
// overwriting manual edits each frame. Auto-cleared on play()/seek() so the user
|
|
103
126
|
// gets back to normal playback without having to manage this flag explicitly.
|
|
104
127
|
this.clipApplySuspended = false;
|
|
128
|
+
/** Post-pose local rotation offsets by bone index (see setBoneRotationOffset). */
|
|
129
|
+
this.boneRotationOffsets = new Map();
|
|
105
130
|
/**
|
|
106
131
|
* IK bones whose chains are switched OFF at the current frame.
|
|
107
132
|
*
|
|
@@ -414,6 +439,20 @@ export class Model {
|
|
|
414
439
|
getLoadWarnings() {
|
|
415
440
|
return this.loadWarnings;
|
|
416
441
|
}
|
|
442
|
+
/** Compose a constant local rotation onto a bone AFTER every pose source (clip,
|
|
443
|
+
* blend, tween) each frame — the classic MMD "heel correction": pitch the 足首
|
|
444
|
+
* bones so a motion authored for flat shoes grounds a heeled model. Persists
|
|
445
|
+
* across play()/show(); pass null to clear. */
|
|
446
|
+
setBoneRotationOffset(boneName, rotation) {
|
|
447
|
+
const idx = this.runtimeSkeleton.nameIndex[boneName];
|
|
448
|
+
if (idx === undefined || idx < 0)
|
|
449
|
+
return false;
|
|
450
|
+
if (rotation === null)
|
|
451
|
+
this.boneRotationOffsets.delete(idx);
|
|
452
|
+
else
|
|
453
|
+
this.boneRotationOffsets.set(idx, rotation.clone());
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
417
456
|
getRigidbodies() {
|
|
418
457
|
return this.rigidbodies;
|
|
419
458
|
}
|
|
@@ -973,18 +1012,57 @@ export class Model {
|
|
|
973
1012
|
play(name, options) {
|
|
974
1013
|
this.clipApplySuspended = false;
|
|
975
1014
|
if (name === undefined) {
|
|
1015
|
+
// Resume: a paused crossfade continues from where it held.
|
|
976
1016
|
this.animationState.play();
|
|
977
1017
|
return;
|
|
978
1018
|
}
|
|
1019
|
+
this.blendEntries = null;
|
|
1020
|
+
this.crossfade = null;
|
|
979
1021
|
this.resetAllBones();
|
|
980
1022
|
this.resetAllMorphs();
|
|
981
1023
|
return this.animationState.play(name, options);
|
|
982
1024
|
}
|
|
983
1025
|
show(name) {
|
|
1026
|
+
this.blendEntries = null;
|
|
1027
|
+
this.crossfade = null;
|
|
984
1028
|
this.resetAllBones();
|
|
985
1029
|
this.resetAllMorphs();
|
|
986
1030
|
this.animationState.show(name);
|
|
987
1031
|
}
|
|
1032
|
+
/** Drive the pose from N weighted clips; the caller owns every clock (see BlendEntry).
|
|
1033
|
+
* The entries array is held by reference and read each update, so a per-frame driver
|
|
1034
|
+
* can mutate it in place without re-calling. Cleared by play(name)/show()/stop()/
|
|
1035
|
+
* clearAnimation()/crossfadeTo(). The single-clip player keeps ticking underneath
|
|
1036
|
+
* but stops writing the pose while a blend is set. */
|
|
1037
|
+
setBlendPose(entries) {
|
|
1038
|
+
this.blendEntries = entries;
|
|
1039
|
+
this.crossfade = null;
|
|
1040
|
+
this.clipApplySuspended = false;
|
|
1041
|
+
}
|
|
1042
|
+
clearBlendPose() {
|
|
1043
|
+
this.blendEntries = null;
|
|
1044
|
+
}
|
|
1045
|
+
/** Fade from the currently playing clip (or from the rest pose when nothing plays)
|
|
1046
|
+
* into `name` over `seconds`. The target starts at frame 0 and becomes the current
|
|
1047
|
+
* clip immediately — progress, looping and the camera clock report the target for
|
|
1048
|
+
* the whole fade. Bones only the outgoing clip animates ease back to rest. */
|
|
1049
|
+
crossfadeTo(name, seconds, options) {
|
|
1050
|
+
if (!this.animationState.hasAnimation(name))
|
|
1051
|
+
return false;
|
|
1052
|
+
this.blendEntries = null;
|
|
1053
|
+
this.clipApplySuspended = false;
|
|
1054
|
+
const fromName = this.animationState.getCurrentAnimation();
|
|
1055
|
+
const fromFrame = this.animationState.getCurrentFrame();
|
|
1056
|
+
const fromLoop = this.animationState.getProgress().looping;
|
|
1057
|
+
this.animationState.forcePlay(name, options?.loop ?? false);
|
|
1058
|
+
// Fading a clip into itself has no second pose to hold — just restart.
|
|
1059
|
+
if (seconds <= 0 || fromName === name) {
|
|
1060
|
+
this.crossfade = null;
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
this.crossfade = { fromName, fromFrame, fromLoop, elapsed: 0, duration: seconds };
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
988
1066
|
// @deprecated Use model.play()
|
|
989
1067
|
playAnimation() {
|
|
990
1068
|
this.animationState.play();
|
|
@@ -997,6 +1075,8 @@ export class Model {
|
|
|
997
1075
|
this.animationState.pause();
|
|
998
1076
|
}
|
|
999
1077
|
stop() {
|
|
1078
|
+
this.blendEntries = null;
|
|
1079
|
+
this.crossfade = null;
|
|
1000
1080
|
this.animationState.stop();
|
|
1001
1081
|
}
|
|
1002
1082
|
// @deprecated Use model.stop()
|
|
@@ -1007,11 +1087,14 @@ export class Model {
|
|
|
1007
1087
|
* pose is no longer re-applied each frame afterwards — follow with
|
|
1008
1088
|
* resetAllBones()/resetAllMorphs() to return to the bind pose. */
|
|
1009
1089
|
clearAnimation() {
|
|
1090
|
+
this.blendEntries = null;
|
|
1091
|
+
this.crossfade = null;
|
|
1010
1092
|
this.animationState.clear();
|
|
1011
1093
|
}
|
|
1012
1094
|
// Seek by absolute timeline seconds, not frame index.
|
|
1013
1095
|
seek(seconds) {
|
|
1014
1096
|
this.clipApplySuspended = false;
|
|
1097
|
+
this.crossfade = null; // a timeline jump mid-fade snaps to the target clip
|
|
1015
1098
|
this.animationState.seek(seconds);
|
|
1016
1099
|
}
|
|
1017
1100
|
// @deprecated Use model.seek()
|
|
@@ -1074,6 +1157,57 @@ export class Model {
|
|
|
1074
1157
|
this.ikDisabled.add(index);
|
|
1075
1158
|
}
|
|
1076
1159
|
}
|
|
1160
|
+
/** Sample one bone track at `frame` into outRot + outVmdTrans. The translation is
|
|
1161
|
+
* VMD-space (bind-relative), NOT yet converted to bone-local — callers convert with
|
|
1162
|
+
* convertVMDTranslationToLocal once they know the final rotation. The cursor cache
|
|
1163
|
+
* belongs to the caller (single-clip path vs per-clip blend caches). Returns false
|
|
1164
|
+
* for an empty track. */
|
|
1165
|
+
sampleBoneTrackInto(boneName, keyFrames, frame, cursors, outRot, outVmdTrans) {
|
|
1166
|
+
if (keyFrames.length === 0)
|
|
1167
|
+
return false;
|
|
1168
|
+
const cachedIdx = cursors.get(boneName) ?? -1;
|
|
1169
|
+
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame));
|
|
1170
|
+
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx);
|
|
1171
|
+
if (idx < 0)
|
|
1172
|
+
return false;
|
|
1173
|
+
cursors.set(boneName, idx);
|
|
1174
|
+
const frameA = keyFrames[idx];
|
|
1175
|
+
const frameB = keyFrames[idx + 1];
|
|
1176
|
+
if (!frameB) {
|
|
1177
|
+
outRot.set(frameA.rotation);
|
|
1178
|
+
outVmdTrans.set(frameA.translation);
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
const frameDelta = frameB.frame - frameA.frame;
|
|
1182
|
+
const gradient = frameDelta > 0 ? (clampedFrame - frameA.frame) / frameDelta : 0;
|
|
1183
|
+
const interp = frameB.interpolation;
|
|
1184
|
+
const rotT = interpolateControlPoints(interp.rotation, gradient);
|
|
1185
|
+
Quat.slerpInto(frameA.rotation, frameB.rotation, rotT, outRot);
|
|
1186
|
+
const txWeight = interpolateControlPoints(interp.translationX, gradient);
|
|
1187
|
+
const tyWeight = interpolateControlPoints(interp.translationY, gradient);
|
|
1188
|
+
const tzWeight = interpolateControlPoints(interp.translationZ, gradient);
|
|
1189
|
+
outVmdTrans.setXYZ(frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight, frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight, frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight);
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
/** Sample one morph track at `frame` (linear weight lerp). Returns NaN for an empty track. */
|
|
1194
|
+
sampleMorphTrack(morphName, keyFrames, frame, cursors) {
|
|
1195
|
+
if (keyFrames.length === 0)
|
|
1196
|
+
return NaN;
|
|
1197
|
+
const cachedIdx = cursors.get(morphName) ?? -1;
|
|
1198
|
+
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame));
|
|
1199
|
+
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx);
|
|
1200
|
+
if (idx < 0)
|
|
1201
|
+
return NaN;
|
|
1202
|
+
cursors.set(morphName, idx);
|
|
1203
|
+
const frameA = keyFrames[idx];
|
|
1204
|
+
const frameB = keyFrames[idx + 1];
|
|
1205
|
+
return frameB
|
|
1206
|
+
? frameA.weight +
|
|
1207
|
+
(frameB.weight - frameA.weight) *
|
|
1208
|
+
(frameB.frame > frameA.frame ? (clampedFrame - frameA.frame) / (frameB.frame - frameA.frame) : 0)
|
|
1209
|
+
: frameA.weight;
|
|
1210
|
+
}
|
|
1077
1211
|
applyPoseFromClip(clip, frame) {
|
|
1078
1212
|
if (!clip)
|
|
1079
1213
|
return;
|
|
@@ -1084,67 +1218,209 @@ export class Model {
|
|
|
1084
1218
|
this.lastAppliedClip = clip;
|
|
1085
1219
|
}
|
|
1086
1220
|
for (const [boneName, keyFrames] of clip.boneTracks.entries()) {
|
|
1087
|
-
if (keyFrames.
|
|
1221
|
+
if (!this.sampleBoneTrackInto(boneName, keyFrames, frame, this.boneTrackIndices, _animSlerp, _animInterpT))
|
|
1088
1222
|
continue;
|
|
1089
|
-
const cachedIdx = this.boneTrackIndices.get(boneName) ?? -1;
|
|
1090
|
-
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame));
|
|
1091
|
-
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx);
|
|
1092
|
-
if (idx < 0)
|
|
1093
|
-
continue;
|
|
1094
|
-
this.boneTrackIndices.set(boneName, idx);
|
|
1095
|
-
const frameA = keyFrames[idx];
|
|
1096
|
-
const frameB = keyFrames[idx + 1];
|
|
1097
1223
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName];
|
|
1098
1224
|
if (boneIdx === undefined)
|
|
1099
1225
|
continue;
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const frameRotation = frameA.rotation;
|
|
1104
|
-
localRot.set(frameRotation);
|
|
1105
|
-
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation);
|
|
1106
|
-
localTrans.set(localTranslation);
|
|
1107
|
-
}
|
|
1108
|
-
else {
|
|
1109
|
-
const frameDelta = frameB.frame - frameA.frame;
|
|
1110
|
-
const gradient = frameDelta > 0 ? (clampedFrame - frameA.frame) / frameDelta : 0;
|
|
1111
|
-
const interp = frameB.interpolation;
|
|
1112
|
-
const rotT = interpolateControlPoints(interp.rotation, gradient);
|
|
1113
|
-
const rotation = Quat.slerpInto(frameA.rotation, frameB.rotation, rotT, _animSlerp);
|
|
1114
|
-
const txWeight = interpolateControlPoints(interp.translationX, gradient);
|
|
1115
|
-
const tyWeight = interpolateControlPoints(interp.translationY, gradient);
|
|
1116
|
-
const tzWeight = interpolateControlPoints(interp.translationZ, gradient);
|
|
1117
|
-
const interpolatedVMDTranslation = _animInterpT.setXYZ(frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight, frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight, frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight);
|
|
1118
|
-
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation);
|
|
1119
|
-
localRot.set(rotation);
|
|
1120
|
-
localTrans.set(localTranslation);
|
|
1121
|
-
}
|
|
1226
|
+
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, _animInterpT, _animSlerp);
|
|
1227
|
+
this.runtimeSkeleton.localRotations[boneIdx].set(_animSlerp);
|
|
1228
|
+
this.runtimeSkeleton.localTranslations[boneIdx].set(localTranslation);
|
|
1122
1229
|
}
|
|
1123
1230
|
for (const [morphName, keyFrames] of clip.morphTracks.entries()) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const cachedIdx = this.morphTrackIndices.get(morphName) ?? -1;
|
|
1127
|
-
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame));
|
|
1128
|
-
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx);
|
|
1129
|
-
if (idx < 0)
|
|
1231
|
+
const weight = this.sampleMorphTrack(morphName, keyFrames, frame, this.morphTrackIndices);
|
|
1232
|
+
if (Number.isNaN(weight))
|
|
1130
1233
|
continue;
|
|
1131
|
-
this.morphTrackIndices.set(morphName, idx);
|
|
1132
|
-
const frameA = keyFrames[idx];
|
|
1133
|
-
const frameB = keyFrames[idx + 1];
|
|
1134
1234
|
const morphIdx = this.runtimeMorph.nameIndex[morphName];
|
|
1135
1235
|
if (morphIdx === undefined)
|
|
1136
1236
|
continue;
|
|
1137
|
-
const weight = frameB
|
|
1138
|
-
? frameA.weight +
|
|
1139
|
-
(frameB.weight - frameA.weight) *
|
|
1140
|
-
(keyFrames[idx + 1].frame > keyFrames[idx].frame
|
|
1141
|
-
? (clampedFrame - keyFrames[idx].frame) / (keyFrames[idx + 1].frame - keyFrames[idx].frame)
|
|
1142
|
-
: 0)
|
|
1143
|
-
: frameA.weight;
|
|
1144
1237
|
this.runtimeMorph.weights[morphIdx] = weight;
|
|
1145
1238
|
this.morphsDirty = true; // Mark as dirty when animation sets morph weights
|
|
1146
1239
|
}
|
|
1147
1240
|
}
|
|
1241
|
+
/** Weighted N-clip pose blend. Rotations accumulate by hemisphere-aligned weighted
|
|
1242
|
+
* sum then normalize (nlerp generalized to N inputs); VMD-space translations lerp.
|
|
1243
|
+
* A bone missing from a clip contributes that clip's share of the REST pose, so a
|
|
1244
|
+
* weight sum below 1 fades toward rest — that is also how crossfading from "no
|
|
1245
|
+
* clip" works. IK on/off state follows the highest-weight entry. */
|
|
1246
|
+
applyBlendedPose(entries) {
|
|
1247
|
+
// Resolve entries; find the total weight and the dominant entry (drives IK state).
|
|
1248
|
+
let total = 0;
|
|
1249
|
+
let live = 0;
|
|
1250
|
+
let domClip = null;
|
|
1251
|
+
let domFrame = 0;
|
|
1252
|
+
let domWeight = 0;
|
|
1253
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1254
|
+
const e = entries[i];
|
|
1255
|
+
if (!(e.weight > 1e-6))
|
|
1256
|
+
continue;
|
|
1257
|
+
const clip = this.animationState.getAnimationClip(e.name);
|
|
1258
|
+
if (!clip)
|
|
1259
|
+
continue;
|
|
1260
|
+
total += e.weight;
|
|
1261
|
+
live++;
|
|
1262
|
+
if (e.weight > domWeight) {
|
|
1263
|
+
domWeight = e.weight;
|
|
1264
|
+
domClip = clip;
|
|
1265
|
+
domFrame = e.time * FPS;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (live === 0 || domClip === null)
|
|
1269
|
+
return;
|
|
1270
|
+
// A sum above 1 normalizes; below 1 stays — the remainder is the rest pose's share.
|
|
1271
|
+
const norm = total > 1 ? 1 / total : 1;
|
|
1272
|
+
this.applyIkFromClip(domClip, domFrame);
|
|
1273
|
+
if (this.blendRotAcc === null) {
|
|
1274
|
+
const boneCount = this.runtimeSkeleton.localRotations.length;
|
|
1275
|
+
this.blendRotAcc = Array.from({ length: boneCount }, () => new Quat(0, 0, 0, 0));
|
|
1276
|
+
this.blendTransAcc = Array.from({ length: boneCount }, () => new Vec3(0, 0, 0));
|
|
1277
|
+
this.blendWeightAcc = new Float32Array(boneCount);
|
|
1278
|
+
this.blendBoneGen = new Int32Array(boneCount).fill(-1);
|
|
1279
|
+
this.blendMorphAcc = new Float32Array(this.runtimeMorph.weights.length);
|
|
1280
|
+
this.blendMorphGen = new Int32Array(this.runtimeMorph.weights.length).fill(-1);
|
|
1281
|
+
}
|
|
1282
|
+
const rotAcc = this.blendRotAcc;
|
|
1283
|
+
const transAcc = this.blendTransAcc;
|
|
1284
|
+
const weightAcc = this.blendWeightAcc;
|
|
1285
|
+
const boneGen = this.blendBoneGen;
|
|
1286
|
+
const morphAcc = this.blendMorphAcc;
|
|
1287
|
+
const morphGen = this.blendMorphGen;
|
|
1288
|
+
const gen = ++this.blendGenCounter;
|
|
1289
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1290
|
+
const e = entries[i];
|
|
1291
|
+
if (!(e.weight > 1e-6))
|
|
1292
|
+
continue;
|
|
1293
|
+
const clip = this.animationState.getAnimationClip(e.name);
|
|
1294
|
+
if (!clip)
|
|
1295
|
+
continue;
|
|
1296
|
+
const w = e.weight * norm;
|
|
1297
|
+
const frame = e.time * FPS;
|
|
1298
|
+
let cursors = this.blendBoneCursors.get(clip);
|
|
1299
|
+
if (!cursors) {
|
|
1300
|
+
cursors = new Map();
|
|
1301
|
+
this.blendBoneCursors.set(clip, cursors);
|
|
1302
|
+
}
|
|
1303
|
+
for (const [boneName, keyFrames] of clip.boneTracks.entries()) {
|
|
1304
|
+
if (!this.sampleBoneTrackInto(boneName, keyFrames, frame, cursors, _blendQ, _blendT))
|
|
1305
|
+
continue;
|
|
1306
|
+
const boneIdx = this.runtimeSkeleton.nameIndex[boneName];
|
|
1307
|
+
if (boneIdx === undefined)
|
|
1308
|
+
continue;
|
|
1309
|
+
const r = rotAcc[boneIdx];
|
|
1310
|
+
const t = transAcc[boneIdx];
|
|
1311
|
+
if (boneGen[boneIdx] !== gen) {
|
|
1312
|
+
boneGen[boneIdx] = gen;
|
|
1313
|
+
r.setXYZW(_blendQ.x * w, _blendQ.y * w, _blendQ.z * w, _blendQ.w * w);
|
|
1314
|
+
t.setXYZ(_blendT.x * w, _blendT.y * w, _blendT.z * w);
|
|
1315
|
+
weightAcc[boneIdx] = w;
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
// Hemisphere-align this sample against the accumulated sum before adding.
|
|
1319
|
+
const d = r.x * _blendQ.x + r.y * _blendQ.y + r.z * _blendQ.z + r.w * _blendQ.w;
|
|
1320
|
+
const s = d < 0 ? -w : w;
|
|
1321
|
+
r.x += _blendQ.x * s;
|
|
1322
|
+
r.y += _blendQ.y * s;
|
|
1323
|
+
r.z += _blendQ.z * s;
|
|
1324
|
+
r.w += _blendQ.w * s;
|
|
1325
|
+
t.x += _blendT.x * w;
|
|
1326
|
+
t.y += _blendT.y * w;
|
|
1327
|
+
t.z += _blendT.z * w;
|
|
1328
|
+
weightAcc[boneIdx] += w;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
let morphCursors = this.blendMorphCursors.get(clip);
|
|
1332
|
+
if (!morphCursors) {
|
|
1333
|
+
morphCursors = new Map();
|
|
1334
|
+
this.blendMorphCursors.set(clip, morphCursors);
|
|
1335
|
+
}
|
|
1336
|
+
for (const [morphName, keyFrames] of clip.morphTracks.entries()) {
|
|
1337
|
+
const weight = this.sampleMorphTrack(morphName, keyFrames, frame, morphCursors);
|
|
1338
|
+
if (Number.isNaN(weight))
|
|
1339
|
+
continue;
|
|
1340
|
+
const morphIdx = this.runtimeMorph.nameIndex[morphName];
|
|
1341
|
+
if (morphIdx === undefined)
|
|
1342
|
+
continue;
|
|
1343
|
+
if (morphGen[morphIdx] !== gen) {
|
|
1344
|
+
morphGen[morphIdx] = gen;
|
|
1345
|
+
morphAcc[morphIdx] = weight * w;
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
morphAcc[morphIdx] += weight * w;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
// Finalize touched bones: fold the rest-pose remainder in, normalize, convert.
|
|
1353
|
+
const boneCount = rotAcc.length;
|
|
1354
|
+
for (let i = 0; i < boneCount; i++) {
|
|
1355
|
+
if (boneGen[i] !== gen)
|
|
1356
|
+
continue;
|
|
1357
|
+
const r = rotAcc[i];
|
|
1358
|
+
const wSum = weightAcc[i];
|
|
1359
|
+
if (wSum < 1) {
|
|
1360
|
+
// Rest local rotation is identity (0,0,0,1); translation contribution is zero.
|
|
1361
|
+
const rest = 1 - wSum;
|
|
1362
|
+
r.w += r.w < 0 ? -rest : rest;
|
|
1363
|
+
}
|
|
1364
|
+
const len = Math.sqrt(r.x * r.x + r.y * r.y + r.z * r.z + r.w * r.w);
|
|
1365
|
+
if (len > 1e-8) {
|
|
1366
|
+
const inv = 1 / len;
|
|
1367
|
+
_blendQ.setXYZW(r.x * inv, r.y * inv, r.z * inv, r.w * inv);
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
_blendQ.setIdentity();
|
|
1371
|
+
}
|
|
1372
|
+
const localTranslation = this.convertVMDTranslationToLocal(i, transAcc[i], _blendQ);
|
|
1373
|
+
this.runtimeSkeleton.localRotations[i].set(_blendQ);
|
|
1374
|
+
this.runtimeSkeleton.localTranslations[i].set(localTranslation);
|
|
1375
|
+
}
|
|
1376
|
+
const morphCount = morphAcc.length;
|
|
1377
|
+
for (let i = 0; i < morphCount; i++) {
|
|
1378
|
+
if (morphGen[i] !== gen)
|
|
1379
|
+
continue;
|
|
1380
|
+
this.runtimeMorph.weights[i] = morphAcc[i];
|
|
1381
|
+
this.morphsDirty = true;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/** One crossfade step: advance the outgoing clock (the target's clock lives in
|
|
1385
|
+
* animationState, already ticked by update), shape the weight with easeInOut,
|
|
1386
|
+
* and hand both to the blend sampler. Holds in place while paused. */
|
|
1387
|
+
applyCrossfade(deltaTime) {
|
|
1388
|
+
const fade = this.crossfade;
|
|
1389
|
+
if (fade === null)
|
|
1390
|
+
return;
|
|
1391
|
+
const playing = this.animationState.getProgress().playing;
|
|
1392
|
+
if (playing) {
|
|
1393
|
+
fade.elapsed += deltaTime;
|
|
1394
|
+
if (fade.fromName !== null) {
|
|
1395
|
+
const fromClip = this.animationState.getAnimationClip(fade.fromName);
|
|
1396
|
+
if (fromClip && fromClip.frameCount > 0 && Number.isFinite(fromClip.frameCount)) {
|
|
1397
|
+
fade.fromFrame += deltaTime * FPS;
|
|
1398
|
+
if (fade.fromLoop) {
|
|
1399
|
+
while (fade.fromFrame >= fromClip.frameCount)
|
|
1400
|
+
fade.fromFrame -= fromClip.frameCount;
|
|
1401
|
+
}
|
|
1402
|
+
else if (fade.fromFrame > fromClip.frameCount) {
|
|
1403
|
+
fade.fromFrame = fromClip.frameCount;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
fade.fromName = null; // outgoing clip was removed mid-fade: fade from rest
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const t = fade.duration > 0 ? Math.min(1, fade.elapsed / fade.duration) : 1;
|
|
1412
|
+
const w = easeInOut(t);
|
|
1413
|
+
const toName = this.animationState.getCurrentAnimation();
|
|
1414
|
+
_fadeEntries[0].name = toName ?? "";
|
|
1415
|
+
_fadeEntries[0].time = this.animationState.getCurrentFrame() / FPS;
|
|
1416
|
+
_fadeEntries[0].weight = toName !== null ? w : 0;
|
|
1417
|
+
_fadeEntries[1].name = fade.fromName ?? "";
|
|
1418
|
+
_fadeEntries[1].time = fade.fromFrame / FPS;
|
|
1419
|
+
_fadeEntries[1].weight = fade.fromName !== null ? 1 - w : 0;
|
|
1420
|
+
this.applyBlendedPose(_fadeEntries);
|
|
1421
|
+
if (t >= 1)
|
|
1422
|
+
this.crossfade = null;
|
|
1423
|
+
}
|
|
1148
1424
|
// Returns true when morphs changed (vertex buffer may need upload). `ikEnabled`
|
|
1149
1425
|
// is the host's runtime switch (engine-wide); the clip decides which chains
|
|
1150
1426
|
// within that. A host driving bones directly — motion capture writing FK
|
|
@@ -1156,10 +1432,24 @@ export class Model {
|
|
|
1156
1432
|
// Update all active tweens (rotations, translations, morphs)
|
|
1157
1433
|
const tweensChangedMorphs = this.updateTweens();
|
|
1158
1434
|
this.animationState.update(deltaTime);
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1435
|
+
if (!this.clipApplySuspended) {
|
|
1436
|
+
if (this.blendEntries !== null && this.blendEntries.length > 0) {
|
|
1437
|
+
this.applyBlendedPose(this.blendEntries);
|
|
1438
|
+
}
|
|
1439
|
+
else if (this.crossfade !== null) {
|
|
1440
|
+
this.applyCrossfade(deltaTime);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
const clip = this.animationState.getCurrentClip();
|
|
1444
|
+
if (clip !== null)
|
|
1445
|
+
this.applyPoseFromClip(clip, this.animationState.getCurrentFrame());
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
// Constant per-bone offsets compose after every pose source, before the
|
|
1449
|
+
// world/IK passes (ankle offsets survive IK — the solver moves thigh/knee).
|
|
1450
|
+
for (const [idx, offset] of this.boneRotationOffsets) {
|
|
1451
|
+
const r = this.runtimeSkeleton.localRotations[idx];
|
|
1452
|
+
Quat.multiplyInto(r, offset, r);
|
|
1163
1453
|
}
|
|
1164
1454
|
// Apply morphs if tweens changed morphs or animation changed morphs
|
|
1165
1455
|
const verticesChanged = this.morphsDirty || tweensChangedMorphs;
|