reze-engine 0.2.19 → 0.3.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/engine.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bezierInterpolate } from "./bezier-interpolate";
1
2
  import { Camera } from "./camera";
2
3
  import { Quat, Vec3 } from "./math";
3
4
  import { PmxLoader } from "./pmx-loader";
@@ -52,8 +53,10 @@ export class Engine {
52
53
  this.animationTimeouts = [];
53
54
  this.hasAnimation = false; // Set to true when loadAnimation is called
54
55
  this.playingAnimation = false; // Set to true when playAnimation is called
55
- this.breathingTimeout = null;
56
- this.breathingBaseRotations = new Map();
56
+ this.animationStartTime = 0; // When animation started playing
57
+ this.animationDuration = 0; // Total animation duration in seconds
58
+ this.boneTracks = new Map();
59
+ this.morphTracks = new Map();
57
60
  this.canvas = canvas;
58
61
  if (options) {
59
62
  this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
@@ -1127,64 +1130,88 @@ export class Engine {
1127
1130
  this.animationFrames = frames;
1128
1131
  this.hasAnimation = true;
1129
1132
  }
1130
- playAnimation(options) {
1133
+ playAnimation() {
1131
1134
  if (this.animationFrames.length === 0)
1132
1135
  return;
1133
1136
  this.stopAnimation();
1134
- this.stopBreathing();
1135
1137
  this.playingAnimation = true;
1136
- // Enable breathing if breathBones is provided
1137
- const enableBreath = options?.breathBones !== undefined && options.breathBones !== null;
1138
- let breathBones = [];
1139
- let breathRotationRanges = undefined;
1140
- if (enableBreath && options.breathBones) {
1141
- if (Array.isArray(options.breathBones)) {
1142
- breathBones = options.breathBones;
1143
- }
1144
- else {
1145
- breathBones = Object.keys(options.breathBones);
1146
- breathRotationRanges = options.breathBones;
1147
- }
1148
- }
1149
- const breathDuration = options?.breathDuration ?? 4000;
1138
+ // Process bone frames
1150
1139
  const allBoneKeyFrames = [];
1151
1140
  for (const keyFrame of this.animationFrames) {
1152
1141
  for (const boneFrame of keyFrame.boneFrames) {
1153
1142
  allBoneKeyFrames.push({
1154
- boneName: boneFrame.boneName,
1143
+ boneFrame,
1155
1144
  time: keyFrame.time,
1156
- rotation: boneFrame.rotation,
1157
1145
  });
1158
1146
  }
1159
1147
  }
1160
1148
  const boneKeyFramesByBone = new Map();
1161
- for (const boneKeyFrame of allBoneKeyFrames) {
1162
- if (!boneKeyFramesByBone.has(boneKeyFrame.boneName)) {
1163
- boneKeyFramesByBone.set(boneKeyFrame.boneName, []);
1149
+ for (const { boneFrame, time } of allBoneKeyFrames) {
1150
+ if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
1151
+ boneKeyFramesByBone.set(boneFrame.boneName, []);
1164
1152
  }
1165
- boneKeyFramesByBone.get(boneKeyFrame.boneName).push(boneKeyFrame);
1153
+ boneKeyFramesByBone.get(boneFrame.boneName).push({ boneFrame, time });
1166
1154
  }
1167
1155
  for (const keyFrames of boneKeyFramesByBone.values()) {
1168
1156
  keyFrames.sort((a, b) => a.time - b.time);
1169
1157
  }
1170
- const time0Rotations = [];
1171
- const bonesWithTime0 = new Set();
1172
- for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
1173
- if (keyFrames.length > 0 && keyFrames[0].time === 0) {
1174
- time0Rotations.push({
1175
- boneName: boneName,
1176
- rotation: keyFrames[0].rotation,
1158
+ // Process morph frames
1159
+ const allMorphKeyFrames = [];
1160
+ for (const keyFrame of this.animationFrames) {
1161
+ for (const morphFrame of keyFrame.morphFrames) {
1162
+ allMorphKeyFrames.push({
1163
+ morphFrame,
1164
+ time: keyFrame.time,
1177
1165
  });
1178
- bonesWithTime0.add(boneName);
1179
1166
  }
1180
1167
  }
1181
- if (this.currentModel) {
1182
- if (time0Rotations.length > 0) {
1183
- const boneNames = time0Rotations.map((r) => r.boneName);
1184
- const rotations = time0Rotations.map((r) => r.rotation);
1185
- this.rotateBones(boneNames, rotations, 0);
1168
+ const morphKeyFramesByMorph = new Map();
1169
+ for (const { morphFrame, time } of allMorphKeyFrames) {
1170
+ if (!morphKeyFramesByMorph.has(morphFrame.morphName)) {
1171
+ morphKeyFramesByMorph.set(morphFrame.morphName, []);
1186
1172
  }
1173
+ morphKeyFramesByMorph.get(morphFrame.morphName).push({ morphFrame, time });
1174
+ }
1175
+ for (const keyFrames of morphKeyFramesByMorph.values()) {
1176
+ keyFrames.sort((a, b) => a.time - b.time);
1177
+ }
1178
+ // Store tracks for frame-based animation
1179
+ this.boneTracks = boneKeyFramesByBone;
1180
+ this.morphTracks = morphKeyFramesByMorph;
1181
+ // Calculate animation duration from max frame time (already in seconds)
1182
+ let maxFrameTime = 0;
1183
+ for (const keyFrames of this.boneTracks.values()) {
1184
+ if (keyFrames.length > 0) {
1185
+ const lastTime = keyFrames[keyFrames.length - 1].time;
1186
+ if (lastTime > maxFrameTime) {
1187
+ maxFrameTime = lastTime;
1188
+ }
1189
+ }
1190
+ }
1191
+ for (const keyFrames of this.morphTracks.values()) {
1192
+ if (keyFrames.length > 0) {
1193
+ const lastTime = keyFrames[keyFrames.length - 1].time;
1194
+ if (lastTime > maxFrameTime) {
1195
+ maxFrameTime = lastTime;
1196
+ }
1197
+ }
1198
+ }
1199
+ this.animationDuration = maxFrameTime > 0 ? maxFrameTime : 0;
1200
+ this.animationStartTime = performance.now();
1201
+ // Initialize bones and morphs to time 0 pose
1202
+ if (this.currentModel) {
1187
1203
  const skeleton = this.currentModel.getSkeleton();
1204
+ const bonesWithTime0 = new Set();
1205
+ // Apply time 0 bone keyframes
1206
+ for (const [boneName, keyFrames] of this.boneTracks.entries()) {
1207
+ if (keyFrames.length > 0 && keyFrames[0].time === 0) {
1208
+ const boneFrame = keyFrames[0].boneFrame;
1209
+ this.rotateBones([boneName], [boneFrame.rotation], 0);
1210
+ this.moveBones([boneName], [boneFrame.translation], 0);
1211
+ bonesWithTime0.add(boneName);
1212
+ }
1213
+ }
1214
+ // Reset bones without time 0 keyframes
1188
1215
  const bonesToReset = [];
1189
1216
  for (const bone of skeleton.bones) {
1190
1217
  if (!bonesWithTime0.has(bone.name)) {
@@ -1196,6 +1223,13 @@ export class Engine {
1196
1223
  const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
1197
1224
  this.rotateBones(bonesToReset, identityQuats, 0);
1198
1225
  }
1226
+ // Apply time 0 morph keyframes
1227
+ for (const [morphName, keyFrames] of this.morphTracks.entries()) {
1228
+ if (keyFrames.length > 0 && keyFrames[0].time === 0) {
1229
+ const morphFrame = keyFrames[0].morphFrame;
1230
+ this.setMorphWeight(morphName, morphFrame.weight, 0);
1231
+ }
1232
+ }
1199
1233
  // Reset physics immediately and upload matrices to prevent A-pose flash
1200
1234
  if (this.physics) {
1201
1235
  this.currentModel.evaluatePose();
@@ -1208,66 +1242,6 @@ export class Engine {
1208
1242
  this.device.queue.submit([encoder.finish()]);
1209
1243
  }
1210
1244
  }
1211
- for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
1212
- for (let i = 0; i < keyFrames.length; i++) {
1213
- const boneKeyFrame = keyFrames[i];
1214
- const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null;
1215
- if (boneKeyFrame.time === 0)
1216
- continue;
1217
- let durationMs = 0;
1218
- if (i === 0) {
1219
- durationMs = boneKeyFrame.time * 1000;
1220
- }
1221
- else if (previousBoneKeyFrame) {
1222
- durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000;
1223
- }
1224
- const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0;
1225
- const delayMs = scheduleTime * 1000;
1226
- if (delayMs <= 0) {
1227
- this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
1228
- }
1229
- else {
1230
- const timeoutId = window.setTimeout(() => {
1231
- this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
1232
- }, delayMs);
1233
- this.animationTimeouts.push(timeoutId);
1234
- }
1235
- }
1236
- }
1237
- // Setup breathing animation if enabled
1238
- if (enableBreath && this.currentModel) {
1239
- // Find the last frame time
1240
- let maxTime = 0;
1241
- for (const keyFrame of this.animationFrames) {
1242
- if (keyFrame.time > maxTime) {
1243
- maxTime = keyFrame.time;
1244
- }
1245
- }
1246
- // Get last frame rotations directly from animation data for breathing bones
1247
- const lastFrameRotations = new Map();
1248
- for (const bone of breathBones) {
1249
- const keyFrames = boneKeyFramesByBone.get(bone);
1250
- if (keyFrames && keyFrames.length > 0) {
1251
- // Find the rotation at the last frame time (closest keyframe <= maxTime)
1252
- let lastRotation = null;
1253
- for (let i = keyFrames.length - 1; i >= 0; i--) {
1254
- if (keyFrames[i].time <= maxTime) {
1255
- lastRotation = keyFrames[i].rotation;
1256
- break;
1257
- }
1258
- }
1259
- if (lastRotation) {
1260
- lastFrameRotations.set(bone, lastRotation);
1261
- }
1262
- }
1263
- }
1264
- // Start breathing after animation completes
1265
- // Use the last frame rotations directly from animation data (no need to capture from model)
1266
- const animationEndTime = maxTime * 1000 + 200; // Small buffer for final tweens to complete
1267
- this.breathingTimeout = window.setTimeout(() => {
1268
- this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration);
1269
- }, animationEndTime);
1270
- }
1271
1245
  }
1272
1246
  stopAnimation() {
1273
1247
  for (const timeoutId of this.animationTimeouts) {
@@ -1275,54 +1249,170 @@ export class Engine {
1275
1249
  }
1276
1250
  this.animationTimeouts = [];
1277
1251
  this.playingAnimation = false;
1252
+ this.boneTracks.clear();
1253
+ this.morphTracks.clear();
1278
1254
  }
1279
- stopBreathing() {
1280
- if (this.breathingTimeout !== null) {
1281
- clearTimeout(this.breathingTimeout);
1282
- this.breathingTimeout = null;
1283
- }
1284
- this.breathingBaseRotations.clear();
1285
- }
1286
- startBreathing(bones, baseRotations, rotationRanges, durationMs = 4000) {
1255
+ // Frame-based animation update (called every frame)
1256
+ // Similar to reference: MmdRuntimeModelAnimation.animate(frameTime)
1257
+ // frameTime is in seconds (already converted from VMD frame numbers in loader)
1258
+ animate(frameTime) {
1287
1259
  if (!this.currentModel)
1288
1260
  return;
1289
- // Store base rotations directly from last frame of animation data
1290
- // These are the exact rotations from the animation - use them as-is
1291
- for (const bone of bones) {
1292
- const baseRot = baseRotations.get(bone);
1293
- if (baseRot) {
1294
- this.breathingBaseRotations.set(bone, baseRot);
1261
+ // Helper to find upper bound index (binary search)
1262
+ const upperBoundFrameIndex = (time, keyFrames) => {
1263
+ let left = 0;
1264
+ let right = keyFrames.length;
1265
+ while (left < right) {
1266
+ const mid = Math.floor((left + right) / 2);
1267
+ if (keyFrames[mid].time <= time) {
1268
+ left = mid + 1;
1269
+ }
1270
+ else {
1271
+ right = mid;
1272
+ }
1295
1273
  }
1296
- }
1297
- const halfCycleMs = durationMs / 2;
1298
- const defaultRotation = 0.02; // Default rotation range if not specified per bone
1299
- // Start breathing cycle - oscillate around exact base rotation (final pose)
1300
- // Each bone can have its own rotation range, or use default
1301
- const animate = (isInhale) => {
1302
- if (!this.currentModel)
1303
- return;
1304
- const breathingBoneNames = [];
1305
- const breathingQuats = [];
1306
- for (const bone of bones) {
1307
- const baseRot = this.breathingBaseRotations.get(bone);
1308
- if (!baseRot)
1309
- continue;
1310
- // Get rotation range for this bone (per-bone or default)
1311
- const rotation = rotationRanges?.[bone] ?? defaultRotation;
1312
- // Oscillate around base rotation with the bone's rotation range
1313
- // isInhale: base * rotation, exhale: base * (-rotation)
1314
- const oscillationRot = Quat.fromEuler(isInhale ? rotation : -rotation, 0, 0);
1315
- const finalRot = baseRot.multiply(oscillationRot);
1316
- breathingBoneNames.push(bone);
1317
- breathingQuats.push(finalRot);
1274
+ return left;
1275
+ };
1276
+ const boneNamesToRotate = [];
1277
+ const rotationsToApply = [];
1278
+ const boneNamesToMove = [];
1279
+ const translationsToApply = [];
1280
+ const morphNamesToSet = [];
1281
+ const morphWeightsToSet = [];
1282
+ // Process each bone track
1283
+ for (const [boneName, keyFrames] of this.boneTracks.entries()) {
1284
+ if (keyFrames.length === 0)
1285
+ continue;
1286
+ // Clamp frame time to track range (all times are in seconds)
1287
+ const startTime = keyFrames[0].time;
1288
+ const endTime = keyFrames[keyFrames.length - 1].time;
1289
+ const clampedFrameTime = Math.max(startTime, Math.min(endTime, frameTime));
1290
+ const upperBoundIndex = upperBoundFrameIndex(clampedFrameTime, keyFrames);
1291
+ const upperBoundIndexMinusOne = upperBoundIndex - 1;
1292
+ if (upperBoundIndexMinusOne < 0)
1293
+ continue;
1294
+ const timeB = keyFrames[upperBoundIndex]?.time;
1295
+ const boneFrameA = keyFrames[upperBoundIndexMinusOne].boneFrame;
1296
+ if (timeB === undefined) {
1297
+ // Last keyframe or beyond - use the last keyframe value
1298
+ boneNamesToRotate.push(boneName);
1299
+ rotationsToApply.push(boneFrameA.rotation);
1300
+ boneNamesToMove.push(boneName);
1301
+ translationsToApply.push(boneFrameA.translation);
1302
+ }
1303
+ else {
1304
+ // Interpolate between two keyframes
1305
+ const timeA = keyFrames[upperBoundIndexMinusOne].time;
1306
+ const boneFrameB = keyFrames[upperBoundIndex].boneFrame;
1307
+ const gradient = (clampedFrameTime - timeA) / (timeB - timeA);
1308
+ // Interpolate rotation using Bezier
1309
+ const interp = boneFrameB.interpolation;
1310
+ const rotWeight = bezierInterpolate(interp[0] / 127, // x1
1311
+ interp[1] / 127, // x2
1312
+ interp[2] / 127, // y1
1313
+ interp[3] / 127, // y2
1314
+ gradient);
1315
+ const interpolatedRotation = Quat.slerp(boneFrameA.rotation, boneFrameB.rotation, rotWeight);
1316
+ // Interpolate translation using Bezier (separate curves for X, Y, Z)
1317
+ // VMD interpolation layout (from reference, 4x4 grid, row-major):
1318
+ // Row 0: X_x1, Y_x1, phy1, phy2,
1319
+ // Row 1: X_y1, Y_y1, Z_y1, R_y1,
1320
+ // Row 2: X_x2, Y_x2, Z_x2, R_x2,
1321
+ // Row 3: X_y2, Y_y2, Z_y2, R_y2,
1322
+ // Row 4: Y_x1, Z_x1, R_x1, X_y1,
1323
+ // Row 5: Y_y1, Z_y1, R_y1, X_x2,
1324
+ // Row 6: Y_x2, Z_x2, R_x2, X_y2,
1325
+ // Row 7: Y_y2, Z_y2, R_y2, 00,
1326
+ // Row 8: Z_x1, R_x1, X_y1, Y_y1,
1327
+ // Row 9: Z_y1, R_y1, X_x2, Y_x2,
1328
+ // Row 10: Z_x2, R_x2, X_y2, Y_y2,
1329
+ // Row 11: Z_y2, R_y2, 00, 00,
1330
+ // Row 12: R_x1, X_y1, Y_y1, Z_y1,
1331
+ // Row 13: R_y1, X_x2, Y_x2, Z_x2,
1332
+ // Row 14: R_x2, X_y2, Y_y2, Z_y2,
1333
+ // Row 15: R_y2, 00, 00, 00
1334
+ // For rotation: R_x1=16, R_y1=20, R_x2=24, R_y2=28
1335
+ // For position X: X_x1=0, X_y1=4, X_x2=8, X_y2=12
1336
+ // For position Y: Y_x1=16, Y_y1=20, Y_x2=24, Y_y2=28
1337
+ // For position Z: Z_x1=32, Z_y1=36, Z_x2=40, Z_y2=44
1338
+ const xWeight = bezierInterpolate(interp[0] / 127, // X_x1
1339
+ interp[8] / 127, // X_x2
1340
+ interp[4] / 127, // X_y1
1341
+ interp[12] / 127, // X_y2
1342
+ gradient);
1343
+ const yWeight = bezierInterpolate(interp[16] / 127, // Y_x1
1344
+ interp[24] / 127, // Y_x2
1345
+ interp[20] / 127, // Y_y1
1346
+ interp[28] / 127, // Y_y2
1347
+ gradient);
1348
+ const zWeight = bezierInterpolate(interp[32] / 127, // Z_x1
1349
+ interp[40] / 127, // Z_x2
1350
+ interp[36] / 127, // Z_y1
1351
+ interp[44] / 127, // Z_y2
1352
+ gradient);
1353
+ const interpolatedTranslation = new Vec3(boneFrameA.translation.x + (boneFrameB.translation.x - boneFrameA.translation.x) * xWeight, boneFrameA.translation.y + (boneFrameB.translation.y - boneFrameA.translation.y) * yWeight, boneFrameA.translation.z + (boneFrameB.translation.z - boneFrameA.translation.z) * zWeight);
1354
+ boneNamesToRotate.push(boneName);
1355
+ rotationsToApply.push(interpolatedRotation);
1356
+ boneNamesToMove.push(boneName);
1357
+ translationsToApply.push(interpolatedTranslation);
1318
1358
  }
1319
- if (breathingBoneNames.length > 0) {
1320
- this.rotateBones(breathingBoneNames, breathingQuats, halfCycleMs);
1359
+ }
1360
+ // Helper to find upper bound index for morph frames
1361
+ const upperBoundMorphIndex = (time, keyFrames) => {
1362
+ let left = 0;
1363
+ let right = keyFrames.length;
1364
+ while (left < right) {
1365
+ const mid = Math.floor((left + right) / 2);
1366
+ if (keyFrames[mid].time <= time) {
1367
+ left = mid + 1;
1368
+ }
1369
+ else {
1370
+ right = mid;
1371
+ }
1321
1372
  }
1322
- this.breathingTimeout = window.setTimeout(() => animate(!isInhale), halfCycleMs);
1373
+ return left;
1323
1374
  };
1324
- // Start breathing from exhale position (closer to base) to minimize initial movement
1325
- animate(false);
1375
+ // Process each morph track
1376
+ for (const [morphName, keyFrames] of this.morphTracks.entries()) {
1377
+ if (keyFrames.length === 0)
1378
+ continue;
1379
+ // Clamp frame time to track range
1380
+ const startTime = keyFrames[0].time;
1381
+ const endTime = keyFrames[keyFrames.length - 1].time;
1382
+ const clampedFrameTime = Math.max(startTime, Math.min(endTime, frameTime));
1383
+ const upperBoundIndex = upperBoundMorphIndex(clampedFrameTime, keyFrames);
1384
+ const upperBoundIndexMinusOne = upperBoundIndex - 1;
1385
+ if (upperBoundIndexMinusOne < 0)
1386
+ continue;
1387
+ const timeB = keyFrames[upperBoundIndex]?.time;
1388
+ const morphFrameA = keyFrames[upperBoundIndexMinusOne].morphFrame;
1389
+ if (timeB === undefined) {
1390
+ // Last keyframe or beyond - use the last keyframe value
1391
+ morphNamesToSet.push(morphName);
1392
+ morphWeightsToSet.push(morphFrameA.weight);
1393
+ }
1394
+ else {
1395
+ // Linear interpolation between two keyframes
1396
+ const timeA = keyFrames[upperBoundIndexMinusOne].time;
1397
+ const morphFrameB = keyFrames[upperBoundIndex].morphFrame;
1398
+ const gradient = (clampedFrameTime - timeA) / (timeB - timeA);
1399
+ const interpolatedWeight = morphFrameA.weight + (morphFrameB.weight - morphFrameA.weight) * gradient;
1400
+ morphNamesToSet.push(morphName);
1401
+ morphWeightsToSet.push(interpolatedWeight);
1402
+ }
1403
+ }
1404
+ // Apply all rotations, translations, and morphs at once (no tweening - direct application)
1405
+ if (boneNamesToRotate.length > 0) {
1406
+ this.rotateBones(boneNamesToRotate, rotationsToApply, 0);
1407
+ }
1408
+ if (boneNamesToMove.length > 0) {
1409
+ this.moveBones(boneNamesToMove, translationsToApply, 0);
1410
+ }
1411
+ if (morphNamesToSet.length > 0) {
1412
+ for (let i = 0; i < morphNamesToSet.length; i++) {
1413
+ this.setMorphWeight(morphNamesToSet[i], morphWeightsToSet[i], 0);
1414
+ }
1415
+ }
1326
1416
  }
1327
1417
  getStats() {
1328
1418
  return { ...this.stats };
@@ -1348,7 +1438,6 @@ export class Engine {
1348
1438
  dispose() {
1349
1439
  this.stopRenderLoop();
1350
1440
  this.stopAnimation();
1351
- this.stopBreathing();
1352
1441
  if (this.camera)
1353
1442
  this.camera.detachControl();
1354
1443
  if (this.resizeObserver) {
@@ -1369,6 +1458,10 @@ export class Engine {
1369
1458
  rotateBones(bones, rotations, durationMs) {
1370
1459
  this.currentModel?.rotateBones(bones, rotations, durationMs);
1371
1460
  }
1461
+ // moveBones now takes relative translations (VMD-style) by default
1462
+ moveBones(bones, relativeTranslations, durationMs) {
1463
+ this.currentModel?.moveBones(bones, relativeTranslations, durationMs);
1464
+ }
1372
1465
  setMorphWeight(name, weight, durationMs) {
1373
1466
  if (!this.currentModel)
1374
1467
  return;
@@ -1745,6 +1838,22 @@ export class Engine {
1745
1838
  this.lastFrameTime = currentTime;
1746
1839
  this.updateCameraUniforms();
1747
1840
  this.updateRenderTarget();
1841
+ // Animate VMD animation if playing
1842
+ if (this.playingAnimation && this.currentModel && this.animationDuration > 0) {
1843
+ const elapsedSeconds = (currentTime - this.animationStartTime) / 1000;
1844
+ if (elapsedSeconds >= this.animationDuration) {
1845
+ // Animation has ended, stop it
1846
+ this.stopAnimation();
1847
+ }
1848
+ else {
1849
+ const frameTime = elapsedSeconds;
1850
+ this.animate(frameTime);
1851
+ }
1852
+ }
1853
+ else if (this.playingAnimation && this.animationDuration <= 0) {
1854
+ // Animation has no duration or invalid, stop it immediately
1855
+ this.stopAnimation();
1856
+ }
1748
1857
  // Update model pose first (this may update morph weights via tweens)
1749
1858
  // We need to do this before creating the encoder to ensure vertex buffer is ready
1750
1859
  if (this.currentModel) {
@@ -1761,9 +1870,10 @@ export class Engine {
1761
1870
  // Use single encoder for both compute and render (reduces sync points)
1762
1871
  const encoder = this.device.createCommandEncoder();
1763
1872
  this.updateModelPose(deltaTime, encoder);
1764
- // Hide model if animation is loaded but not playing yet (prevents A-pose flash)
1765
- // Still update physics and poses, just don't render visually
1766
- if (this.hasAnimation && !this.playingAnimation) {
1873
+ // Hide model if animation is loaded but hasn't started playing yet (prevents A-pose flash)
1874
+ // Once animation has played (even if it stopped), continue rendering normally
1875
+ // Still update physics and poses, just don't render visually before first play
1876
+ if (this.hasAnimation && !this.playingAnimation && this.animationStartTime === 0) {
1767
1877
  // Submit encoder to ensure matrices are uploaded and physics initializes
1768
1878
  this.device.queue.submit([encoder.finish()]);
1769
1879
  return;
@@ -1964,7 +2074,6 @@ Engine.DEFAULT_BLOOM_INTENSITY = 0.12;
1964
2074
  Engine.DEFAULT_RIM_LIGHT_INTENSITY = 0.45;
1965
2075
  Engine.DEFAULT_CAMERA_DISTANCE = 26.6;
1966
2076
  Engine.DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0);
1967
- Engine.HAIR_OVER_EYES_ALPHA = 0.5;
1968
2077
  Engine.TRANSPARENCY_EPSILON = 0.001;
1969
2078
  Engine.STATS_FPS_UPDATE_INTERVAL_MS = 1000;
1970
2079
  Engine.STATS_FRAME_TIME_ROUNDING = 100;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * IK Solver implementation
3
+ * Based on reference from babylon-mmd and Saba MMD library
4
+ * https://github.com/benikabocha/saba/blob/master/src/Saba/Model/MMD/MMDIkSolver.cpp
5
+ */
6
+ import { Bone, IKSolver, IKChainInfo } from "./model";
7
+ /**
8
+ * Solve IK chains for a model
9
+ */
10
+ export declare class IKSolverSystem {
11
+ private static readonly EPSILON;
12
+ private static readonly THRESHOLD;
13
+ /**
14
+ * Solve all IK chains
15
+ */
16
+ static solve(ikSolvers: IKSolver[], bones: Bone[], localRotations: Float32Array, localTranslations: Float32Array, worldMatrices: Float32Array, ikChainInfo: IKChainInfo[], usePhysics?: boolean): void;
17
+ private static solveIK;
18
+ private static solveChain;
19
+ private static limitAngle;
20
+ private static getWorldTranslation;
21
+ private static getParentWorldRotationMatrix;
22
+ private static transformNormal;
23
+ private static updateWorldMatrixWithIK;
24
+ private static updateWorldMatrix;
25
+ }
26
+ //# sourceMappingURL=ik-solver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ik-solver.d.ts","sourceRoot":"","sources":["../src/ik-solver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,IAAI,EAAU,QAAQ,EAAE,WAAW,EAAiC,MAAM,SAAS,CAAA;AAoE5F;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAS;IACxC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAuB;IAExD;;OAEG;WACW,KAAK,CACjB,SAAS,EAAE,QAAQ,EAAE,EACrB,KAAK,EAAE,IAAI,EAAE,EACb,cAAc,EAAE,YAAY,EAC5B,iBAAiB,EAAE,YAAY,EAC/B,aAAa,EAAE,YAAY,EAC3B,WAAW,EAAE,WAAW,EAAE,EAC1B,UAAU,GAAE,OAAe,GAC1B,IAAI;IASP,OAAO,CAAC,MAAM,CAAC,OAAO;IAgGtB,OAAO,CAAC,MAAM,CAAC,UAAU;IAoKzB,OAAO,CAAC,MAAM,CAAC,UAAU;IAYzB,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAKlC,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAe3C,OAAO,CAAC,MAAM,CAAC,eAAe;IAS9B,OAAO,CAAC,MAAM,CAAC,uBAAuB;IA8CtC,OAAO,CAAC,MAAM,CAAC,iBAAiB;CAsCjC"}