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/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 +7 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +252 -143
- 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 +46 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +201 -3
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +57 -36
- 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 +275 -164
- package/src/ik-solver.ts +488 -0
- package/src/math.ts +555 -546
- package/src/model.ts +284 -3
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1173 -1145
- 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";
|
|
@@ -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.
|
|
56
|
-
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();
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1162
|
-
if (!boneKeyFramesByBone.has(
|
|
1163
|
-
boneKeyFramesByBone.set(
|
|
1149
|
+
for (const { boneFrame, time } of allBoneKeyFrames) {
|
|
1150
|
+
if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
|
|
1151
|
+
boneKeyFramesByBone.set(boneFrame.boneName, []);
|
|
1164
1152
|
}
|
|
1165
|
-
boneKeyFramesByBone.get(
|
|
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
|
-
|
|
1171
|
-
const
|
|
1172
|
-
for (const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
//
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1373
|
+
return left;
|
|
1323
1374
|
};
|
|
1324
|
-
//
|
|
1325
|
-
|
|
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
|
|
1765
|
-
//
|
|
1766
|
-
|
|
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"}
|