reze-engine 0.3.0 → 0.3.2
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.d.ts +9 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +96 -281
- package/dist/player.d.ts +100 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +409 -0
- package/package.json +1 -1
- package/src/engine.ts +111 -302
- package/src/player.ts +490 -0
package/dist/engine.d.ts
CHANGED
|
@@ -100,14 +100,9 @@ export declare class Engine {
|
|
|
100
100
|
private stats;
|
|
101
101
|
private animationFrameId;
|
|
102
102
|
private renderLoopCallback;
|
|
103
|
-
private
|
|
104
|
-
private animationTimeouts;
|
|
103
|
+
private player;
|
|
105
104
|
private hasAnimation;
|
|
106
|
-
private playingAnimation;
|
|
107
105
|
private animationStartTime;
|
|
108
|
-
private animationDuration;
|
|
109
|
-
private boneTracks;
|
|
110
|
-
private morphTracks;
|
|
111
106
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions);
|
|
112
107
|
init(): Promise<void>;
|
|
113
108
|
private createPipelines;
|
|
@@ -119,10 +114,16 @@ export declare class Engine {
|
|
|
119
114
|
private setupCamera;
|
|
120
115
|
private setupLighting;
|
|
121
116
|
private setAmbientColor;
|
|
122
|
-
loadAnimation(url: string): Promise<void>;
|
|
117
|
+
loadAnimation(url: string, audioUrl?: string): Promise<void>;
|
|
123
118
|
playAnimation(): void;
|
|
124
119
|
stopAnimation(): void;
|
|
125
|
-
|
|
120
|
+
pauseAnimation(): void;
|
|
121
|
+
seekAnimation(time: number): void;
|
|
122
|
+
getAnimationProgress(): import("./player").AnimationProgress;
|
|
123
|
+
/**
|
|
124
|
+
* Apply animation pose to model
|
|
125
|
+
*/
|
|
126
|
+
private applyPose;
|
|
126
127
|
getStats(): EngineStats;
|
|
127
128
|
runRenderLoop(callback?: () => void): void;
|
|
128
129
|
stopRenderLoop(): void;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { bezierInterpolate } from "./bezier-interpolate";
|
|
2
1
|
import { Camera } from "./camera";
|
|
3
2
|
import { Quat, Vec3 } from "./math";
|
|
4
3
|
import { PmxLoader } from "./pmx-loader";
|
|
5
4
|
import { Physics } from "./physics";
|
|
6
|
-
import {
|
|
5
|
+
import { Player } from "./player";
|
|
7
6
|
export class Engine {
|
|
8
7
|
constructor(canvas, options) {
|
|
9
8
|
this.cameraMatrixData = new Float32Array(36);
|
|
@@ -49,14 +48,9 @@ export class Engine {
|
|
|
49
48
|
};
|
|
50
49
|
this.animationFrameId = null;
|
|
51
50
|
this.renderLoopCallback = null;
|
|
52
|
-
this.
|
|
53
|
-
this.animationTimeouts = [];
|
|
51
|
+
this.player = new Player();
|
|
54
52
|
this.hasAnimation = false; // Set to true when loadAnimation is called
|
|
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();
|
|
53
|
+
this.animationStartTime = 0; // Track when animation first started (for A-pose prevention)
|
|
60
54
|
this.canvas = canvas;
|
|
61
55
|
if (options) {
|
|
62
56
|
this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
|
|
@@ -1125,96 +1119,56 @@ export class Engine {
|
|
|
1125
1119
|
this.lightData[2] = color.z;
|
|
1126
1120
|
this.lightData[3] = 0.0; // Padding for vec3f alignment
|
|
1127
1121
|
}
|
|
1128
|
-
async loadAnimation(url) {
|
|
1129
|
-
|
|
1130
|
-
this.animationFrames = frames;
|
|
1122
|
+
async loadAnimation(url, audioUrl) {
|
|
1123
|
+
await this.player.loadVmd(url, audioUrl);
|
|
1131
1124
|
this.hasAnimation = true;
|
|
1132
|
-
|
|
1133
|
-
playAnimation() {
|
|
1134
|
-
if (this.animationFrames.length === 0)
|
|
1135
|
-
return;
|
|
1136
|
-
this.stopAnimation();
|
|
1137
|
-
this.playingAnimation = true;
|
|
1138
|
-
// Process bone frames
|
|
1139
|
-
const allBoneKeyFrames = [];
|
|
1140
|
-
for (const keyFrame of this.animationFrames) {
|
|
1141
|
-
for (const boneFrame of keyFrame.boneFrames) {
|
|
1142
|
-
allBoneKeyFrames.push({
|
|
1143
|
-
boneFrame,
|
|
1144
|
-
time: keyFrame.time,
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
const boneKeyFramesByBone = new Map();
|
|
1149
|
-
for (const { boneFrame, time } of allBoneKeyFrames) {
|
|
1150
|
-
if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
|
|
1151
|
-
boneKeyFramesByBone.set(boneFrame.boneName, []);
|
|
1152
|
-
}
|
|
1153
|
-
boneKeyFramesByBone.get(boneFrame.boneName).push({ boneFrame, time });
|
|
1154
|
-
}
|
|
1155
|
-
for (const keyFrames of boneKeyFramesByBone.values()) {
|
|
1156
|
-
keyFrames.sort((a, b) => a.time - b.time);
|
|
1157
|
-
}
|
|
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,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
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
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
this.animationDuration = maxFrameTime > 0 ? maxFrameTime : 0;
|
|
1200
|
-
this.animationStartTime = performance.now();
|
|
1201
|
-
// Initialize bones and morphs to time 0 pose
|
|
1125
|
+
// Show first frame (time 0) immediately
|
|
1202
1126
|
if (this.currentModel) {
|
|
1127
|
+
const initialPose = this.player.getPoseAtTime(0);
|
|
1128
|
+
this.applyPose(initialPose);
|
|
1129
|
+
// Reset bones without time 0 keyframes
|
|
1203
1130
|
const skeleton = this.currentModel.getSkeleton();
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
for (const
|
|
1207
|
-
if (
|
|
1208
|
-
|
|
1209
|
-
this.rotateBones([boneName], [boneFrame.rotation], 0);
|
|
1210
|
-
this.moveBones([boneName], [boneFrame.translation], 0);
|
|
1211
|
-
bonesWithTime0.add(boneName);
|
|
1131
|
+
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1132
|
+
const bonesToReset = [];
|
|
1133
|
+
for (const bone of skeleton.bones) {
|
|
1134
|
+
if (!bonesWithPose.has(bone.name)) {
|
|
1135
|
+
bonesToReset.push(bone.name);
|
|
1212
1136
|
}
|
|
1213
1137
|
}
|
|
1138
|
+
if (bonesToReset.length > 0) {
|
|
1139
|
+
const identityQuat = new Quat(0, 0, 0, 1);
|
|
1140
|
+
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1141
|
+
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1142
|
+
}
|
|
1143
|
+
// Update model pose and physics
|
|
1144
|
+
this.currentModel.evaluatePose();
|
|
1145
|
+
if (this.physics) {
|
|
1146
|
+
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1147
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1148
|
+
// Upload matrices immediately
|
|
1149
|
+
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1150
|
+
const encoder = this.device.createCommandEncoder();
|
|
1151
|
+
this.computeSkinMatrices(encoder);
|
|
1152
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
playAnimation() {
|
|
1157
|
+
if (!this.hasAnimation || !this.currentModel)
|
|
1158
|
+
return;
|
|
1159
|
+
const wasPaused = this.player.isPausedState();
|
|
1160
|
+
const wasPlaying = this.player.isPlayingState();
|
|
1161
|
+
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1162
|
+
if (!wasPlaying && !wasPaused) {
|
|
1163
|
+
// Get initial pose at time 0
|
|
1164
|
+
const initialPose = this.player.getPoseAtTime(0);
|
|
1165
|
+
this.applyPose(initialPose);
|
|
1214
1166
|
// Reset bones without time 0 keyframes
|
|
1167
|
+
const skeleton = this.currentModel.getSkeleton();
|
|
1168
|
+
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1215
1169
|
const bonesToReset = [];
|
|
1216
1170
|
for (const bone of skeleton.bones) {
|
|
1217
|
-
if (!
|
|
1171
|
+
if (!bonesWithPose.has(bone.name)) {
|
|
1218
1172
|
bonesToReset.push(bone.name);
|
|
1219
1173
|
}
|
|
1220
1174
|
}
|
|
@@ -1223,13 +1177,6 @@ export class Engine {
|
|
|
1223
1177
|
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1224
1178
|
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1225
1179
|
}
|
|
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
|
-
}
|
|
1233
1180
|
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1234
1181
|
if (this.physics) {
|
|
1235
1182
|
this.currentModel.evaluatePose();
|
|
@@ -1242,176 +1189,61 @@ export class Engine {
|
|
|
1242
1189
|
this.device.queue.submit([encoder.finish()]);
|
|
1243
1190
|
}
|
|
1244
1191
|
}
|
|
1192
|
+
// Start playback (or resume if paused)
|
|
1193
|
+
this.player.play();
|
|
1194
|
+
if (this.animationStartTime === 0) {
|
|
1195
|
+
this.animationStartTime = performance.now();
|
|
1196
|
+
}
|
|
1245
1197
|
}
|
|
1246
1198
|
stopAnimation() {
|
|
1247
|
-
|
|
1248
|
-
|
|
1199
|
+
this.player.stop();
|
|
1200
|
+
}
|
|
1201
|
+
pauseAnimation() {
|
|
1202
|
+
this.player.pause();
|
|
1203
|
+
}
|
|
1204
|
+
seekAnimation(time) {
|
|
1205
|
+
if (!this.currentModel || !this.hasAnimation)
|
|
1206
|
+
return;
|
|
1207
|
+
this.player.seek(time);
|
|
1208
|
+
// Immediately apply pose at seeked time
|
|
1209
|
+
const pose = this.player.getPoseAtTime(time);
|
|
1210
|
+
this.applyPose(pose);
|
|
1211
|
+
// Update model pose and physics
|
|
1212
|
+
this.currentModel.evaluatePose();
|
|
1213
|
+
if (this.physics) {
|
|
1214
|
+
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1215
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1216
|
+
// Upload matrices immediately
|
|
1217
|
+
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1218
|
+
const encoder = this.device.createCommandEncoder();
|
|
1219
|
+
this.computeSkinMatrices(encoder);
|
|
1220
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1249
1221
|
}
|
|
1250
|
-
this.animationTimeouts = [];
|
|
1251
|
-
this.playingAnimation = false;
|
|
1252
|
-
this.boneTracks.clear();
|
|
1253
|
-
this.morphTracks.clear();
|
|
1254
1222
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1223
|
+
getAnimationProgress() {
|
|
1224
|
+
return this.player.getProgress();
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Apply animation pose to model
|
|
1228
|
+
*/
|
|
1229
|
+
applyPose(pose) {
|
|
1259
1230
|
if (!this.currentModel)
|
|
1260
1231
|
return;
|
|
1261
|
-
//
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
}
|
|
1273
|
-
}
|
|
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);
|
|
1358
|
-
}
|
|
1232
|
+
// Apply bone rotations
|
|
1233
|
+
if (pose.boneRotations.size > 0) {
|
|
1234
|
+
const boneNames = Array.from(pose.boneRotations.keys());
|
|
1235
|
+
const rotations = Array.from(pose.boneRotations.values());
|
|
1236
|
+
this.rotateBones(boneNames, rotations, 0);
|
|
1359
1237
|
}
|
|
1360
|
-
//
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return left;
|
|
1374
|
-
};
|
|
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);
|
|
1238
|
+
// Apply bone translations
|
|
1239
|
+
if (pose.boneTranslations.size > 0) {
|
|
1240
|
+
const boneNames = Array.from(pose.boneTranslations.keys());
|
|
1241
|
+
const translations = Array.from(pose.boneTranslations.values());
|
|
1242
|
+
this.moveBones(boneNames, translations, 0);
|
|
1407
1243
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
if (morphNamesToSet.length > 0) {
|
|
1412
|
-
for (let i = 0; i < morphNamesToSet.length; i++) {
|
|
1413
|
-
this.setMorphWeight(morphNamesToSet[i], morphWeightsToSet[i], 0);
|
|
1414
|
-
}
|
|
1244
|
+
// Apply morph weights
|
|
1245
|
+
for (const [morphName, weight] of pose.morphWeights.entries()) {
|
|
1246
|
+
this.setMorphWeight(morphName, weight, 0);
|
|
1415
1247
|
}
|
|
1416
1248
|
}
|
|
1417
1249
|
getStats() {
|
|
@@ -1839,20 +1671,11 @@ export class Engine {
|
|
|
1839
1671
|
this.updateCameraUniforms();
|
|
1840
1672
|
this.updateRenderTarget();
|
|
1841
1673
|
// Animate VMD animation if playing
|
|
1842
|
-
if (this.
|
|
1843
|
-
const
|
|
1844
|
-
if (
|
|
1845
|
-
|
|
1846
|
-
this.stopAnimation();
|
|
1674
|
+
if (this.hasAnimation && this.currentModel) {
|
|
1675
|
+
const pose = this.player.update(currentTime);
|
|
1676
|
+
if (pose) {
|
|
1677
|
+
this.applyPose(pose);
|
|
1847
1678
|
}
|
|
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
1679
|
}
|
|
1857
1680
|
// Update model pose first (this may update morph weights via tweens)
|
|
1858
1681
|
// We need to do this before creating the encoder to ensure vertex buffer is ready
|
|
@@ -1870,14 +1693,6 @@ export class Engine {
|
|
|
1870
1693
|
// Use single encoder for both compute and render (reduces sync points)
|
|
1871
1694
|
const encoder = this.device.createCommandEncoder();
|
|
1872
1695
|
this.updateModelPose(deltaTime, encoder);
|
|
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) {
|
|
1877
|
-
// Submit encoder to ensure matrices are uploaded and physics initializes
|
|
1878
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
1696
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1882
1697
|
if (this.currentModel) {
|
|
1883
1698
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
package/dist/player.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Quat, Vec3 } from "./math";
|
|
2
|
+
export interface AnimationPose {
|
|
3
|
+
boneRotations: Map<string, Quat>;
|
|
4
|
+
boneTranslations: Map<string, Vec3>;
|
|
5
|
+
morphWeights: Map<string, number>;
|
|
6
|
+
}
|
|
7
|
+
export interface AnimationProgress {
|
|
8
|
+
current: number;
|
|
9
|
+
duration: number;
|
|
10
|
+
percentage: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class Player {
|
|
13
|
+
private frames;
|
|
14
|
+
private boneTracks;
|
|
15
|
+
private morphTracks;
|
|
16
|
+
private duration;
|
|
17
|
+
private isPlaying;
|
|
18
|
+
private isPaused;
|
|
19
|
+
private currentTime;
|
|
20
|
+
private startTime;
|
|
21
|
+
private pausedTime;
|
|
22
|
+
private pauseStartTime;
|
|
23
|
+
private audioElement?;
|
|
24
|
+
private audioUrl?;
|
|
25
|
+
private audioLoaded;
|
|
26
|
+
/**
|
|
27
|
+
* Load VMD animation file and optionally audio
|
|
28
|
+
*/
|
|
29
|
+
loadVmd(vmdUrl: string, audioUrl?: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Load audio file
|
|
32
|
+
*/
|
|
33
|
+
loadAudio(url: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Process frames into tracks
|
|
36
|
+
*/
|
|
37
|
+
private processFrames;
|
|
38
|
+
/**
|
|
39
|
+
* Start or resume playback
|
|
40
|
+
*/
|
|
41
|
+
play(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Pause playback
|
|
44
|
+
*/
|
|
45
|
+
pause(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Stop playback and reset to beginning
|
|
48
|
+
*/
|
|
49
|
+
stop(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Seek to specific time
|
|
52
|
+
*/
|
|
53
|
+
seek(time: number): void;
|
|
54
|
+
/**
|
|
55
|
+
* Update playback and return current pose
|
|
56
|
+
* Returns null if not playing, but returns current pose if paused
|
|
57
|
+
*/
|
|
58
|
+
update(currentRealTime: number): AnimationPose | null;
|
|
59
|
+
/**
|
|
60
|
+
* Get pose at specific time (pure function)
|
|
61
|
+
*/
|
|
62
|
+
getPoseAtTime(time: number): AnimationPose;
|
|
63
|
+
/**
|
|
64
|
+
* Get current playback progress
|
|
65
|
+
*/
|
|
66
|
+
getProgress(): AnimationProgress;
|
|
67
|
+
/**
|
|
68
|
+
* Get current time
|
|
69
|
+
*/
|
|
70
|
+
getCurrentTime(): number;
|
|
71
|
+
/**
|
|
72
|
+
* Get animation duration
|
|
73
|
+
*/
|
|
74
|
+
getDuration(): number;
|
|
75
|
+
/**
|
|
76
|
+
* Check if playing
|
|
77
|
+
*/
|
|
78
|
+
isPlayingState(): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Check if paused
|
|
81
|
+
*/
|
|
82
|
+
isPausedState(): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Check if has audio
|
|
85
|
+
*/
|
|
86
|
+
hasAudio(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Set audio volume (0.0 to 1.0)
|
|
89
|
+
*/
|
|
90
|
+
setVolume(volume: number): void;
|
|
91
|
+
/**
|
|
92
|
+
* Mute audio
|
|
93
|
+
*/
|
|
94
|
+
mute(): void;
|
|
95
|
+
/**
|
|
96
|
+
* Unmute audio
|
|
97
|
+
*/
|
|
98
|
+
unmute(): void;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=player.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../src/player.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAGnC,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAChC,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACnC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,MAAM;IAEjB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAwE;IAC1F,OAAO,CAAC,WAAW,CAA0E;IAC7F,OAAO,CAAC,QAAQ,CAAY;IAG5B,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,WAAW,CAAY;IAG/B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,cAAc,CAAY;IAGlC,OAAO,CAAC,YAAY,CAAC,CAAkB;IACvC,OAAO,CAAC,QAAQ,CAAC,CAAQ;IACzB,OAAO,CAAC,WAAW,CAAiB;IAEpC;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/D;;OAEG;IACG,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB3C;;OAEG;IACH,OAAO,CAAC,aAAa;IAwErB;;OAEG;IACH,IAAI,IAAI,IAAI;IAyBZ;;OAEG;IACH,KAAK,IAAI,IAAI;IAYb;;OAEG;IACH,IAAI,IAAI,IAAI;IAcZ;;OAEG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAgBxB;;;OAGG;IACH,MAAM,CAAC,eAAe,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAiCrD;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IA+I1C;;OAEG;IACH,WAAW,IAAI,iBAAiB;IAQhC;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;OAEG;IACH,cAAc,IAAI,OAAO;IAIzB;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM/B;;OAEG;IACH,IAAI,IAAI,IAAI;IAMZ;;OAEG;IACH,MAAM,IAAI,IAAI;CAKf"}
|