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/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;
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
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.length === 0)
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 localRot = this.runtimeSkeleton.localRotations[boneIdx];
1101
- const localTrans = this.runtimeSkeleton.localTranslations[boneIdx];
1102
- if (!frameB) {
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
- if (keyFrames.length === 0)
1125
- continue;
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
- const clip = this.animationState.getCurrentClip();
1160
- const frame = this.animationState.getCurrentFrame();
1161
- if (clip !== null && !this.clipApplySuspended) {
1162
- this.applyPoseFromClip(clip, frame);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",