reze-engine 0.2.18 → 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";
@@ -26,6 +27,7 @@ export class Engine {
26
27
  this.modelDir = "";
27
28
  this.physics = null;
28
29
  this.textureCache = new Map();
30
+ this.vertexBufferNeedsUpdate = false;
29
31
  // Draw lists
30
32
  this.opaqueDraws = [];
31
33
  this.eyeDraws = [];
@@ -51,8 +53,10 @@ export class Engine {
51
53
  this.animationTimeouts = [];
52
54
  this.hasAnimation = false; // Set to true when loadAnimation is called
53
55
  this.playingAnimation = false; // Set to true when playAnimation is called
54
- this.breathingTimeout = null;
55
- 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();
56
60
  this.canvas = canvas;
57
61
  if (options) {
58
62
  this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
@@ -1126,64 +1130,88 @@ export class Engine {
1126
1130
  this.animationFrames = frames;
1127
1131
  this.hasAnimation = true;
1128
1132
  }
1129
- playAnimation(options) {
1133
+ playAnimation() {
1130
1134
  if (this.animationFrames.length === 0)
1131
1135
  return;
1132
1136
  this.stopAnimation();
1133
- this.stopBreathing();
1134
1137
  this.playingAnimation = true;
1135
- // Enable breathing if breathBones is provided
1136
- const enableBreath = options?.breathBones !== undefined && options.breathBones !== null;
1137
- let breathBones = [];
1138
- let breathRotationRanges = undefined;
1139
- if (enableBreath && options.breathBones) {
1140
- if (Array.isArray(options.breathBones)) {
1141
- breathBones = options.breathBones;
1142
- }
1143
- else {
1144
- breathBones = Object.keys(options.breathBones);
1145
- breathRotationRanges = options.breathBones;
1146
- }
1147
- }
1148
- const breathDuration = options?.breathDuration ?? 4000;
1138
+ // Process bone frames
1149
1139
  const allBoneKeyFrames = [];
1150
1140
  for (const keyFrame of this.animationFrames) {
1151
1141
  for (const boneFrame of keyFrame.boneFrames) {
1152
1142
  allBoneKeyFrames.push({
1153
- boneName: boneFrame.boneName,
1143
+ boneFrame,
1154
1144
  time: keyFrame.time,
1155
- rotation: boneFrame.rotation,
1156
1145
  });
1157
1146
  }
1158
1147
  }
1159
1148
  const boneKeyFramesByBone = new Map();
1160
- for (const boneKeyFrame of allBoneKeyFrames) {
1161
- if (!boneKeyFramesByBone.has(boneKeyFrame.boneName)) {
1162
- boneKeyFramesByBone.set(boneKeyFrame.boneName, []);
1149
+ for (const { boneFrame, time } of allBoneKeyFrames) {
1150
+ if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
1151
+ boneKeyFramesByBone.set(boneFrame.boneName, []);
1163
1152
  }
1164
- boneKeyFramesByBone.get(boneKeyFrame.boneName).push(boneKeyFrame);
1153
+ boneKeyFramesByBone.get(boneFrame.boneName).push({ boneFrame, time });
1165
1154
  }
1166
1155
  for (const keyFrames of boneKeyFramesByBone.values()) {
1167
1156
  keyFrames.sort((a, b) => a.time - b.time);
1168
1157
  }
1169
- const time0Rotations = [];
1170
- const bonesWithTime0 = new Set();
1171
- for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
1172
- if (keyFrames.length > 0 && keyFrames[0].time === 0) {
1173
- time0Rotations.push({
1174
- boneName: boneName,
1175
- 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,
1176
1165
  });
1177
- bonesWithTime0.add(boneName);
1178
1166
  }
1179
1167
  }
1180
- if (this.currentModel) {
1181
- if (time0Rotations.length > 0) {
1182
- const boneNames = time0Rotations.map((r) => r.boneName);
1183
- const rotations = time0Rotations.map((r) => r.rotation);
1184
- 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, []);
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
+ }
1185
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) {
1186
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
1187
1215
  const bonesToReset = [];
1188
1216
  for (const bone of skeleton.bones) {
1189
1217
  if (!bonesWithTime0.has(bone.name)) {
@@ -1195,6 +1223,13 @@ export class Engine {
1195
1223
  const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
1196
1224
  this.rotateBones(bonesToReset, identityQuats, 0);
1197
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
+ }
1198
1233
  // Reset physics immediately and upload matrices to prevent A-pose flash
1199
1234
  if (this.physics) {
1200
1235
  this.currentModel.evaluatePose();
@@ -1207,66 +1242,6 @@ export class Engine {
1207
1242
  this.device.queue.submit([encoder.finish()]);
1208
1243
  }
1209
1244
  }
1210
- for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
1211
- for (let i = 0; i < keyFrames.length; i++) {
1212
- const boneKeyFrame = keyFrames[i];
1213
- const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null;
1214
- if (boneKeyFrame.time === 0)
1215
- continue;
1216
- let durationMs = 0;
1217
- if (i === 0) {
1218
- durationMs = boneKeyFrame.time * 1000;
1219
- }
1220
- else if (previousBoneKeyFrame) {
1221
- durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000;
1222
- }
1223
- const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0;
1224
- const delayMs = scheduleTime * 1000;
1225
- if (delayMs <= 0) {
1226
- this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
1227
- }
1228
- else {
1229
- const timeoutId = window.setTimeout(() => {
1230
- this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
1231
- }, delayMs);
1232
- this.animationTimeouts.push(timeoutId);
1233
- }
1234
- }
1235
- }
1236
- // Setup breathing animation if enabled
1237
- if (enableBreath && this.currentModel) {
1238
- // Find the last frame time
1239
- let maxTime = 0;
1240
- for (const keyFrame of this.animationFrames) {
1241
- if (keyFrame.time > maxTime) {
1242
- maxTime = keyFrame.time;
1243
- }
1244
- }
1245
- // Get last frame rotations directly from animation data for breathing bones
1246
- const lastFrameRotations = new Map();
1247
- for (const bone of breathBones) {
1248
- const keyFrames = boneKeyFramesByBone.get(bone);
1249
- if (keyFrames && keyFrames.length > 0) {
1250
- // Find the rotation at the last frame time (closest keyframe <= maxTime)
1251
- let lastRotation = null;
1252
- for (let i = keyFrames.length - 1; i >= 0; i--) {
1253
- if (keyFrames[i].time <= maxTime) {
1254
- lastRotation = keyFrames[i].rotation;
1255
- break;
1256
- }
1257
- }
1258
- if (lastRotation) {
1259
- lastFrameRotations.set(bone, lastRotation);
1260
- }
1261
- }
1262
- }
1263
- // Start breathing after animation completes
1264
- // Use the last frame rotations directly from animation data (no need to capture from model)
1265
- const animationEndTime = maxTime * 1000 + 200; // Small buffer for final tweens to complete
1266
- this.breathingTimeout = window.setTimeout(() => {
1267
- this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration);
1268
- }, animationEndTime);
1269
- }
1270
1245
  }
1271
1246
  stopAnimation() {
1272
1247
  for (const timeoutId of this.animationTimeouts) {
@@ -1274,54 +1249,170 @@ export class Engine {
1274
1249
  }
1275
1250
  this.animationTimeouts = [];
1276
1251
  this.playingAnimation = false;
1252
+ this.boneTracks.clear();
1253
+ this.morphTracks.clear();
1277
1254
  }
1278
- stopBreathing() {
1279
- if (this.breathingTimeout !== null) {
1280
- clearTimeout(this.breathingTimeout);
1281
- this.breathingTimeout = null;
1282
- }
1283
- this.breathingBaseRotations.clear();
1284
- }
1285
- 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) {
1286
1259
  if (!this.currentModel)
1287
1260
  return;
1288
- // Store base rotations directly from last frame of animation data
1289
- // These are the exact rotations from the animation - use them as-is
1290
- for (const bone of bones) {
1291
- const baseRot = baseRotations.get(bone);
1292
- if (baseRot) {
1293
- 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
+ }
1294
1273
  }
1295
- }
1296
- const halfCycleMs = durationMs / 2;
1297
- const defaultRotation = 0.02; // Default rotation range if not specified per bone
1298
- // Start breathing cycle - oscillate around exact base rotation (final pose)
1299
- // Each bone can have its own rotation range, or use default
1300
- const animate = (isInhale) => {
1301
- if (!this.currentModel)
1302
- return;
1303
- const breathingBoneNames = [];
1304
- const breathingQuats = [];
1305
- for (const bone of bones) {
1306
- const baseRot = this.breathingBaseRotations.get(bone);
1307
- if (!baseRot)
1308
- continue;
1309
- // Get rotation range for this bone (per-bone or default)
1310
- const rotation = rotationRanges?.[bone] ?? defaultRotation;
1311
- // Oscillate around base rotation with the bone's rotation range
1312
- // isInhale: base * rotation, exhale: base * (-rotation)
1313
- const oscillationRot = Quat.fromEuler(isInhale ? rotation : -rotation, 0, 0);
1314
- const finalRot = baseRot.multiply(oscillationRot);
1315
- breathingBoneNames.push(bone);
1316
- 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);
1317
1302
  }
1318
- if (breathingBoneNames.length > 0) {
1319
- this.rotateBones(breathingBoneNames, breathingQuats, halfCycleMs);
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);
1358
+ }
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
+ }
1320
1372
  }
1321
- this.breathingTimeout = window.setTimeout(() => animate(!isInhale), halfCycleMs);
1373
+ return left;
1322
1374
  };
1323
- // Start breathing from exhale position (closer to base) to minimize initial movement
1324
- 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
+ }
1325
1416
  }
1326
1417
  getStats() {
1327
1418
  return { ...this.stats };
@@ -1347,7 +1438,6 @@ export class Engine {
1347
1438
  dispose() {
1348
1439
  this.stopRenderLoop();
1349
1440
  this.stopAnimation();
1350
- this.stopBreathing();
1351
1441
  if (this.camera)
1352
1442
  this.camera.detachControl();
1353
1443
  if (this.resizeObserver) {
@@ -1368,6 +1458,26 @@ export class Engine {
1368
1458
  rotateBones(bones, rotations, durationMs) {
1369
1459
  this.currentModel?.rotateBones(bones, rotations, durationMs);
1370
1460
  }
1461
+ // moveBones now takes relative translations (VMD-style) by default
1462
+ moveBones(bones, relativeTranslations, durationMs) {
1463
+ this.currentModel?.moveBones(bones, relativeTranslations, durationMs);
1464
+ }
1465
+ setMorphWeight(name, weight, durationMs) {
1466
+ if (!this.currentModel)
1467
+ return;
1468
+ this.currentModel.setMorphWeight(name, weight, durationMs);
1469
+ if (!durationMs || durationMs === 0) {
1470
+ this.vertexBufferNeedsUpdate = true;
1471
+ }
1472
+ }
1473
+ updateVertexBuffer() {
1474
+ if (!this.currentModel || !this.vertexBuffer)
1475
+ return;
1476
+ const vertices = this.currentModel.getVertices();
1477
+ if (!vertices || vertices.length === 0)
1478
+ return;
1479
+ this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices);
1480
+ }
1371
1481
  // Step 7: Create vertex, index, and joint buffers
1372
1482
  async setupModelBuffers(model) {
1373
1483
  this.currentModel = model;
@@ -1728,12 +1838,42 @@ export class Engine {
1728
1838
  this.lastFrameTime = currentTime;
1729
1839
  this.updateCameraUniforms();
1730
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
+ }
1857
+ // Update model pose first (this may update morph weights via tweens)
1858
+ // We need to do this before creating the encoder to ensure vertex buffer is ready
1859
+ if (this.currentModel) {
1860
+ const hasActiveMorphTweens = this.currentModel.evaluatePose();
1861
+ if (hasActiveMorphTweens) {
1862
+ this.vertexBufferNeedsUpdate = true;
1863
+ }
1864
+ }
1865
+ // Update vertex buffer if morphs changed
1866
+ if (this.vertexBufferNeedsUpdate) {
1867
+ this.updateVertexBuffer();
1868
+ this.vertexBufferNeedsUpdate = false;
1869
+ }
1731
1870
  // Use single encoder for both compute and render (reduces sync points)
1732
1871
  const encoder = this.device.createCommandEncoder();
1733
1872
  this.updateModelPose(deltaTime, encoder);
1734
- // Hide model if animation is loaded but not playing yet (prevents A-pose flash)
1735
- // Still update physics and poses, just don't render visually
1736
- 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) {
1737
1877
  // Submit encoder to ensure matrices are uploaded and physics initializes
1738
1878
  this.device.queue.submit([encoder.finish()]);
1739
1879
  return;
@@ -1877,7 +2017,8 @@ export class Engine {
1877
2017
  }
1878
2018
  }
1879
2019
  updateModelPose(deltaTime, encoder) {
1880
- this.currentModel.evaluatePose();
2020
+ // Note: evaluatePose is called earlier in render() to update vertex buffer before encoder creation
2021
+ // Here we just get the matrices and update physics/compute
1881
2022
  const worldMats = this.currentModel.getBoneWorldMatrices();
1882
2023
  if (this.physics) {
1883
2024
  this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
@@ -1933,7 +2074,6 @@ Engine.DEFAULT_BLOOM_INTENSITY = 0.12;
1933
2074
  Engine.DEFAULT_RIM_LIGHT_INTENSITY = 0.45;
1934
2075
  Engine.DEFAULT_CAMERA_DISTANCE = 26.6;
1935
2076
  Engine.DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0);
1936
- Engine.HAIR_OVER_EYES_ALPHA = 0.5;
1937
2077
  Engine.TRANSPARENCY_EPSILON = 0.001;
1938
2078
  Engine.STATS_FPS_UPDATE_INTERVAL_MS = 1000;
1939
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"}