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/README.md +67 -66
- package/dist/bezier-interpolate.d.ts +15 -0
- package/dist/bezier-interpolate.d.ts.map +1 -0
- package/dist/bezier-interpolate.js +40 -0
- package/dist/engine.d.ts +10 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +284 -144
- package/dist/ik-solver.d.ts +26 -0
- package/dist/ik-solver.d.ts.map +1 -0
- package/dist/ik-solver.js +372 -0
- package/dist/math.d.ts +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +8 -0
- package/dist/model.d.ts +82 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +357 -4
- package/dist/pmx-loader.d.ts +3 -1
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +218 -130
- package/dist/vmd-loader.d.ts +11 -1
- package/dist/vmd-loader.d.ts.map +1 -1
- package/dist/vmd-loader.js +91 -15
- package/package.json +1 -1
- package/src/bezier-interpolate.ts +47 -0
- package/src/camera.ts +358 -358
- package/src/engine.ts +308 -165
- package/src/ik-solver.ts +488 -0
- package/src/math.ts +555 -546
- package/src/model.ts +930 -421
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1173 -1054
- package/src/vmd-loader.ts +276 -179
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.
|
|
55
|
-
this.
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1161
|
-
if (!boneKeyFramesByBone.has(
|
|
1162
|
-
boneKeyFramesByBone.set(
|
|
1149
|
+
for (const { boneFrame, time } of allBoneKeyFrames) {
|
|
1150
|
+
if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
|
|
1151
|
+
boneKeyFramesByBone.set(boneFrame.boneName, []);
|
|
1163
1152
|
}
|
|
1164
|
-
boneKeyFramesByBone.get(
|
|
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
|
-
|
|
1170
|
-
const
|
|
1171
|
-
for (const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
//
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1373
|
+
return left;
|
|
1322
1374
|
};
|
|
1323
|
-
//
|
|
1324
|
-
|
|
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
|
|
1735
|
-
//
|
|
1736
|
-
|
|
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
|
-
|
|
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"}
|