reze-engine 0.3.6 → 0.3.7
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 +0 -1
- package/dist/engine.d.ts +8 -25
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +235 -588
- package/dist/engine_r.d.ts +132 -0
- package/dist/engine_r.d.ts.map +1 -0
- package/dist/engine_r.js +1489 -0
- package/dist/engine_ts.d.ts +143 -0
- package/dist/engine_ts.d.ts.map +1 -0
- package/dist/engine_ts.js +1575 -0
- package/dist/model.d.ts +59 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +416 -112
- package/package.json +1 -1
- package/src/engine.ts +266 -649
- package/src/model.ts +518 -156
- package/dist/ik.d.ts +0 -32
- package/dist/ik.d.ts.map +0 -1
- package/dist/ik.js +0 -337
- package/dist/pool-scene.d.ts +0 -52
- package/dist/pool-scene.d.ts.map +0 -1
- package/dist/pool-scene.js +0 -1122
- package/dist/pool.d.ts +0 -38
- package/dist/pool.d.ts.map +0 -1
- package/dist/pool.js +0 -422
package/dist/model.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3, easeInOut } from "./math";
|
|
1
|
+
import { Mat4, Quat, Vec3, easeInOut, bezierInterpolate } from "./math";
|
|
2
|
+
import { Physics } from "./physics";
|
|
2
3
|
import { IKSolverSystem } from "./ik-solver";
|
|
4
|
+
import { VMDLoader } from "./vmd-loader";
|
|
3
5
|
const VERTEX_STRIDE = 8;
|
|
4
6
|
export class Model {
|
|
5
7
|
constructor(vertexData, indexData, textures, materials, skeleton, skinning, morphing, rigidbodies = [], joints = []) {
|
|
@@ -8,9 +10,21 @@ export class Model {
|
|
|
8
10
|
// Physics data from PMX
|
|
9
11
|
this.rigidbodies = [];
|
|
10
12
|
this.joints = [];
|
|
13
|
+
this.morphsDirty = false; // Flag indicating if morphs need to be applied
|
|
11
14
|
// Cached identity matrices to avoid allocations in computeWorldMatrices
|
|
12
15
|
this.cachedIdentityMat1 = Mat4.identity();
|
|
13
16
|
this.cachedIdentityMat2 = Mat4.identity();
|
|
17
|
+
this.tweenTimeMs = 0; // Time tracking for tweens (milliseconds)
|
|
18
|
+
// Animation runtime
|
|
19
|
+
this.animationData = null;
|
|
20
|
+
this.boneTracks = new Map();
|
|
21
|
+
this.morphTracks = new Map();
|
|
22
|
+
this.animationDuration = 0;
|
|
23
|
+
this.isPlaying = false;
|
|
24
|
+
this.isPaused = false;
|
|
25
|
+
this.animationTime = 0; // Current time in animation (seconds)
|
|
26
|
+
// Physics runtime
|
|
27
|
+
this.physics = null;
|
|
14
28
|
// Store base vertex data (original positions before morphing)
|
|
15
29
|
this.baseVertexData = new Float32Array(vertexData);
|
|
16
30
|
this.vertexData = vertexData;
|
|
@@ -27,11 +41,17 @@ export class Model {
|
|
|
27
41
|
throw new Error("Model has no bones");
|
|
28
42
|
}
|
|
29
43
|
this.initializeRuntimeSkeleton();
|
|
30
|
-
this.initializeRotTweenBuffers();
|
|
31
|
-
this.initializeTransTweenBuffers();
|
|
32
44
|
this.initializeRuntimeMorph();
|
|
33
|
-
this.
|
|
45
|
+
this.initializeTweenBuffers();
|
|
34
46
|
this.applyMorphs(); // Apply initial morphs (all weights are 0, so no change)
|
|
47
|
+
// Initialize physics if rigidbodies exist
|
|
48
|
+
if (rigidbodies.length > 0) {
|
|
49
|
+
this.physics = new Physics(rigidbodies, joints);
|
|
50
|
+
console.log(`[Model] Physics initialized with ${rigidbodies.length} rigidbodies`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log("[Model] No rigidbodies found, physics disabled");
|
|
54
|
+
}
|
|
35
55
|
}
|
|
36
56
|
initializeRuntimeSkeleton() {
|
|
37
57
|
const boneCount = this.skeleton.bones.length;
|
|
@@ -88,22 +108,28 @@ export class Model {
|
|
|
88
108
|
this.runtimeSkeleton.ikChainInfo = ikChainInfo;
|
|
89
109
|
this.runtimeSkeleton.ikSolvers = ikSolvers;
|
|
90
110
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
initializeTweenBuffers() {
|
|
112
|
+
const boneCount = this.skeleton.bones.length;
|
|
113
|
+
const morphCount = this.morphing.morphs.length;
|
|
114
|
+
this.tweenState = {
|
|
115
|
+
// Bone rotation tweens
|
|
116
|
+
rotActive: new Uint8Array(boneCount),
|
|
117
|
+
rotStartQuat: new Float32Array(boneCount * 4),
|
|
118
|
+
rotTargetQuat: new Float32Array(boneCount * 4),
|
|
119
|
+
rotStartTimeMs: new Float32Array(boneCount),
|
|
120
|
+
rotDurationMs: new Float32Array(boneCount),
|
|
121
|
+
// Bone translation tweens
|
|
122
|
+
transActive: new Uint8Array(boneCount),
|
|
123
|
+
transStartVec: new Float32Array(boneCount * 3),
|
|
124
|
+
transTargetVec: new Float32Array(boneCount * 3),
|
|
125
|
+
transStartTimeMs: new Float32Array(boneCount),
|
|
126
|
+
transDurationMs: new Float32Array(boneCount),
|
|
127
|
+
// Morph weight tweens
|
|
128
|
+
morphActive: new Uint8Array(morphCount),
|
|
129
|
+
morphStartWeight: new Float32Array(morphCount),
|
|
130
|
+
morphTargetWeight: new Float32Array(morphCount),
|
|
131
|
+
morphStartTimeMs: new Float32Array(morphCount),
|
|
132
|
+
morphDurationMs: new Float32Array(morphCount),
|
|
107
133
|
};
|
|
108
134
|
}
|
|
109
135
|
initializeRuntimeMorph() {
|
|
@@ -116,71 +142,80 @@ export class Model {
|
|
|
116
142
|
weights: new Float32Array(morphCount),
|
|
117
143
|
};
|
|
118
144
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
// Tween update - processes all tweens together with a single time reference
|
|
146
|
+
// This avoids conflicts and ensures consistent timing across all tween types
|
|
147
|
+
// Returns true if morph weights changed (needed for vertex buffer updates)
|
|
148
|
+
updateTweens() {
|
|
149
|
+
const state = this.tweenState;
|
|
150
|
+
const now = this.tweenTimeMs; // Single time reference for all tweens
|
|
151
|
+
let morphChanged = false;
|
|
152
|
+
// Update bone rotation tweens
|
|
122
153
|
const rotations = this.runtimeSkeleton.localRotations;
|
|
123
154
|
const boneCount = this.skeleton.bones.length;
|
|
124
155
|
for (let i = 0; i < boneCount; i++) {
|
|
125
|
-
if (state.
|
|
156
|
+
if (state.rotActive[i] !== 1)
|
|
126
157
|
continue;
|
|
127
|
-
const startMs = state.
|
|
128
|
-
const durMs = Math.max(1, state.
|
|
158
|
+
const startMs = state.rotStartTimeMs[i];
|
|
159
|
+
const durMs = Math.max(1, state.rotDurationMs[i]);
|
|
129
160
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs));
|
|
130
161
|
const e = easeInOut(t);
|
|
131
162
|
const qi = i * 4;
|
|
132
|
-
const startQuat = new Quat(state.
|
|
133
|
-
const targetQuat = new Quat(state.
|
|
163
|
+
const startQuat = new Quat(state.rotStartQuat[qi], state.rotStartQuat[qi + 1], state.rotStartQuat[qi + 2], state.rotStartQuat[qi + 3]);
|
|
164
|
+
const targetQuat = new Quat(state.rotTargetQuat[qi], state.rotTargetQuat[qi + 1], state.rotTargetQuat[qi + 2], state.rotTargetQuat[qi + 3]);
|
|
134
165
|
const result = Quat.slerp(startQuat, targetQuat, e);
|
|
135
166
|
rotations[qi] = result.x;
|
|
136
167
|
rotations[qi + 1] = result.y;
|
|
137
168
|
rotations[qi + 2] = result.z;
|
|
138
169
|
rotations[qi + 3] = result.w;
|
|
139
|
-
if (t >= 1)
|
|
140
|
-
state.
|
|
170
|
+
if (t >= 1) {
|
|
171
|
+
state.rotActive[i] = 0;
|
|
172
|
+
}
|
|
141
173
|
}
|
|
142
|
-
|
|
143
|
-
updateTranslationTweens() {
|
|
144
|
-
const state = this.transTweenState;
|
|
145
|
-
const now = performance.now();
|
|
174
|
+
// Update bone translation tweens
|
|
146
175
|
const translations = this.runtimeSkeleton.localTranslations;
|
|
147
|
-
const boneCount = this.skeleton.bones.length;
|
|
148
176
|
for (let i = 0; i < boneCount; i++) {
|
|
149
|
-
if (state.
|
|
177
|
+
if (state.transActive[i] !== 1)
|
|
150
178
|
continue;
|
|
151
|
-
const startMs = state.
|
|
152
|
-
const durMs = Math.max(1, state.
|
|
179
|
+
const startMs = state.transStartTimeMs[i];
|
|
180
|
+
const durMs = Math.max(1, state.transDurationMs[i]);
|
|
153
181
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs));
|
|
154
182
|
const e = easeInOut(t);
|
|
155
183
|
const ti = i * 3;
|
|
156
|
-
translations[ti] = state.
|
|
157
|
-
translations[ti + 1] =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
state.
|
|
184
|
+
translations[ti] = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e;
|
|
185
|
+
translations[ti + 1] =
|
|
186
|
+
state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e;
|
|
187
|
+
translations[ti + 2] =
|
|
188
|
+
state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e;
|
|
189
|
+
if (t >= 1) {
|
|
190
|
+
state.transActive[i] = 0;
|
|
191
|
+
}
|
|
161
192
|
}
|
|
162
|
-
|
|
163
|
-
updateMorphWeightTweens() {
|
|
164
|
-
const state = this.morphTweenState;
|
|
165
|
-
const now = performance.now();
|
|
193
|
+
// Update morph weight tweens
|
|
166
194
|
const weights = this.runtimeMorph.weights;
|
|
167
195
|
const morphCount = this.morphing.morphs.length;
|
|
168
|
-
let hasActiveTweens = false;
|
|
169
196
|
for (let i = 0; i < morphCount; i++) {
|
|
170
|
-
if (state.
|
|
197
|
+
if (state.morphActive[i] !== 1)
|
|
171
198
|
continue;
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const durMs = Math.max(1, state.durationMs[i]);
|
|
199
|
+
const startMs = state.morphStartTimeMs[i];
|
|
200
|
+
const durMs = Math.max(1, state.morphDurationMs[i]);
|
|
175
201
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs));
|
|
176
202
|
const e = easeInOut(t);
|
|
177
|
-
|
|
203
|
+
const oldWeight = weights[i];
|
|
204
|
+
weights[i] = state.morphStartWeight[i] + (state.morphTargetWeight[i] - state.morphStartWeight[i]) * e;
|
|
205
|
+
// Check if weight actually changed (accounting for floating point precision)
|
|
206
|
+
if (Math.abs(weights[i] - oldWeight) > 1e-6) {
|
|
207
|
+
morphChanged = true;
|
|
208
|
+
}
|
|
178
209
|
if (t >= 1) {
|
|
179
|
-
weights[i] = state.
|
|
180
|
-
state.
|
|
210
|
+
weights[i] = state.morphTargetWeight[i];
|
|
211
|
+
state.morphActive[i] = 0;
|
|
212
|
+
// Check if final weight is different from old weight
|
|
213
|
+
if (Math.abs(weights[i] - oldWeight) > 1e-6) {
|
|
214
|
+
morphChanged = true;
|
|
215
|
+
}
|
|
181
216
|
}
|
|
182
217
|
}
|
|
183
|
-
return
|
|
218
|
+
return morphChanged;
|
|
184
219
|
}
|
|
185
220
|
getVertices() {
|
|
186
221
|
return this.vertexData;
|
|
@@ -214,9 +249,9 @@ export class Model {
|
|
|
214
249
|
}
|
|
215
250
|
// ------- Bone helpers (public API) -------
|
|
216
251
|
rotateBones(names, quats, durationMs) {
|
|
217
|
-
const state = this.
|
|
252
|
+
const state = this.tweenState;
|
|
218
253
|
const normalized = quats.map((q) => q.normalize());
|
|
219
|
-
const now =
|
|
254
|
+
const now = this.tweenTimeMs;
|
|
220
255
|
const dur = durationMs && durationMs > 0 ? durationMs : 0;
|
|
221
256
|
for (let i = 0; i < names.length; i++) {
|
|
222
257
|
const name = names[i];
|
|
@@ -231,44 +266,44 @@ export class Model {
|
|
|
231
266
|
rotations[qi + 1] = ty;
|
|
232
267
|
rotations[qi + 2] = tz;
|
|
233
268
|
rotations[qi + 3] = tw;
|
|
234
|
-
state.
|
|
269
|
+
state.rotActive[idx] = 0;
|
|
235
270
|
continue;
|
|
236
271
|
}
|
|
237
272
|
let sx = rotations[qi];
|
|
238
273
|
let sy = rotations[qi + 1];
|
|
239
274
|
let sz = rotations[qi + 2];
|
|
240
275
|
let sw = rotations[qi + 3];
|
|
241
|
-
if (state.
|
|
242
|
-
const startMs = state.
|
|
243
|
-
const prevDur = Math.max(1, state.
|
|
276
|
+
if (state.rotActive[idx] === 1) {
|
|
277
|
+
const startMs = state.rotStartTimeMs[idx];
|
|
278
|
+
const prevDur = Math.max(1, state.rotDurationMs[idx]);
|
|
244
279
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur));
|
|
245
280
|
const e = easeInOut(t);
|
|
246
|
-
const startQuat = new Quat(state.
|
|
247
|
-
const targetQuat = new Quat(state.
|
|
281
|
+
const startQuat = new Quat(state.rotStartQuat[qi], state.rotStartQuat[qi + 1], state.rotStartQuat[qi + 2], state.rotStartQuat[qi + 3]);
|
|
282
|
+
const targetQuat = new Quat(state.rotTargetQuat[qi], state.rotTargetQuat[qi + 1], state.rotTargetQuat[qi + 2], state.rotTargetQuat[qi + 3]);
|
|
248
283
|
const result = Quat.slerp(startQuat, targetQuat, e);
|
|
249
284
|
sx = result.x;
|
|
250
285
|
sy = result.y;
|
|
251
286
|
sz = result.z;
|
|
252
287
|
sw = result.w;
|
|
253
288
|
}
|
|
254
|
-
state.
|
|
255
|
-
state.
|
|
256
|
-
state.
|
|
257
|
-
state.
|
|
258
|
-
state.
|
|
259
|
-
state.
|
|
260
|
-
state.
|
|
261
|
-
state.
|
|
262
|
-
state.
|
|
263
|
-
state.
|
|
264
|
-
state.
|
|
289
|
+
state.rotStartQuat[qi] = sx;
|
|
290
|
+
state.rotStartQuat[qi + 1] = sy;
|
|
291
|
+
state.rotStartQuat[qi + 2] = sz;
|
|
292
|
+
state.rotStartQuat[qi + 3] = sw;
|
|
293
|
+
state.rotTargetQuat[qi] = tx;
|
|
294
|
+
state.rotTargetQuat[qi + 1] = ty;
|
|
295
|
+
state.rotTargetQuat[qi + 2] = tz;
|
|
296
|
+
state.rotTargetQuat[qi + 3] = tw;
|
|
297
|
+
state.rotStartTimeMs[idx] = now;
|
|
298
|
+
state.rotDurationMs[idx] = dur;
|
|
299
|
+
state.rotActive[idx] = 1;
|
|
265
300
|
}
|
|
266
301
|
}
|
|
267
302
|
// Move bones using VMD-style relative translations (relative to bind pose world position)
|
|
268
303
|
// This is the default behavior for VMD animations
|
|
269
304
|
moveBones(names, relativeTranslations, durationMs) {
|
|
270
|
-
const state = this.
|
|
271
|
-
const now =
|
|
305
|
+
const state = this.tweenState;
|
|
306
|
+
const now = this.tweenTimeMs;
|
|
272
307
|
const dur = durationMs && durationMs > 0 ? durationMs : 0;
|
|
273
308
|
const localRot = this.runtimeSkeleton.localRotations;
|
|
274
309
|
// Compute bind pose world positions for all bones
|
|
@@ -323,30 +358,30 @@ export class Model {
|
|
|
323
358
|
translations[ti] = tx;
|
|
324
359
|
translations[ti + 1] = ty;
|
|
325
360
|
translations[ti + 2] = tz;
|
|
326
|
-
state.
|
|
361
|
+
state.transActive[idx] = 0;
|
|
327
362
|
continue;
|
|
328
363
|
}
|
|
329
364
|
let sx = translations[ti];
|
|
330
365
|
let sy = translations[ti + 1];
|
|
331
366
|
let sz = translations[ti + 2];
|
|
332
|
-
if (state.
|
|
333
|
-
const startMs = state.
|
|
334
|
-
const prevDur = Math.max(1, state.
|
|
367
|
+
if (state.transActive[idx] === 1) {
|
|
368
|
+
const startMs = state.transStartTimeMs[idx];
|
|
369
|
+
const prevDur = Math.max(1, state.transDurationMs[idx]);
|
|
335
370
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur));
|
|
336
371
|
const e = easeInOut(t);
|
|
337
|
-
sx = state.
|
|
338
|
-
sy = state.
|
|
339
|
-
sz = state.
|
|
372
|
+
sx = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e;
|
|
373
|
+
sy = state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e;
|
|
374
|
+
sz = state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e;
|
|
340
375
|
}
|
|
341
|
-
state.
|
|
342
|
-
state.
|
|
343
|
-
state.
|
|
344
|
-
state.
|
|
345
|
-
state.
|
|
346
|
-
state.
|
|
347
|
-
state.
|
|
348
|
-
state.
|
|
349
|
-
state.
|
|
376
|
+
state.transStartVec[ti] = sx;
|
|
377
|
+
state.transStartVec[ti + 1] = sy;
|
|
378
|
+
state.transStartVec[ti + 2] = sz;
|
|
379
|
+
state.transTargetVec[ti] = tx;
|
|
380
|
+
state.transTargetVec[ti + 1] = ty;
|
|
381
|
+
state.transTargetVec[ti + 2] = tz;
|
|
382
|
+
state.transStartTimeMs[idx] = now;
|
|
383
|
+
state.transDurationMs[idx] = dur;
|
|
384
|
+
state.transActive[idx] = 1;
|
|
350
385
|
}
|
|
351
386
|
}
|
|
352
387
|
getBoneWorldMatrices() {
|
|
@@ -355,6 +390,25 @@ export class Model {
|
|
|
355
390
|
getBoneInverseBindMatrices() {
|
|
356
391
|
return this.skeleton.inverseBindMatrices;
|
|
357
392
|
}
|
|
393
|
+
getSkinMatrices() {
|
|
394
|
+
const boneCount = this.skeleton.bones.length;
|
|
395
|
+
const worldMats = this.runtimeSkeleton.worldMatrices;
|
|
396
|
+
const invBindMats = this.skeleton.inverseBindMatrices;
|
|
397
|
+
// Initialize cached array if needed or if bone count changed
|
|
398
|
+
if (!this.cachedSkinMatrices || this.cachedSkinMatrices.length !== boneCount * 16) {
|
|
399
|
+
this.cachedSkinMatrices = new Float32Array(boneCount * 16);
|
|
400
|
+
}
|
|
401
|
+
const skinMatrices = this.cachedSkinMatrices;
|
|
402
|
+
// Compute skin matrices: skinMatrix = worldMatrix × inverseBindMatrix
|
|
403
|
+
// Use Mat4.multiplyArrays to avoid creating Mat4 objects
|
|
404
|
+
for (let i = 0; i < boneCount; i++) {
|
|
405
|
+
const worldOffset = i * 16;
|
|
406
|
+
const invBindOffset = i * 16;
|
|
407
|
+
const skinOffset = i * 16;
|
|
408
|
+
Mat4.multiplyArrays(worldMats, worldOffset, invBindMats, invBindOffset, skinMatrices, skinOffset);
|
|
409
|
+
}
|
|
410
|
+
return skinMatrices;
|
|
411
|
+
}
|
|
358
412
|
setMorphWeight(name, weight, durationMs) {
|
|
359
413
|
const idx = this.runtimeMorph.nameIndex[name] ?? -1;
|
|
360
414
|
if (idx < 0 || idx >= this.runtimeMorph.weights.length)
|
|
@@ -364,27 +418,27 @@ export class Model {
|
|
|
364
418
|
if (dur === 0) {
|
|
365
419
|
// Instant change
|
|
366
420
|
this.runtimeMorph.weights[idx] = clampedWeight;
|
|
367
|
-
this.
|
|
421
|
+
this.tweenState.morphActive[idx] = 0;
|
|
368
422
|
this.applyMorphs();
|
|
369
423
|
return;
|
|
370
424
|
}
|
|
371
425
|
// Animated change
|
|
372
|
-
const state = this.
|
|
373
|
-
const now =
|
|
426
|
+
const state = this.tweenState;
|
|
427
|
+
const now = this.tweenTimeMs;
|
|
374
428
|
// If already tweening, start from current interpolated value
|
|
375
429
|
let startWeight = this.runtimeMorph.weights[idx];
|
|
376
|
-
if (state.
|
|
377
|
-
const startMs = state.
|
|
378
|
-
const prevDur = Math.max(1, state.
|
|
430
|
+
if (state.morphActive[idx] === 1) {
|
|
431
|
+
const startMs = state.morphStartTimeMs[idx];
|
|
432
|
+
const prevDur = Math.max(1, state.morphDurationMs[idx]);
|
|
379
433
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur));
|
|
380
434
|
const e = easeInOut(t);
|
|
381
|
-
startWeight = state.
|
|
435
|
+
startWeight = state.morphStartWeight[idx] + (state.morphTargetWeight[idx] - state.morphStartWeight[idx]) * e;
|
|
382
436
|
}
|
|
383
|
-
state.
|
|
384
|
-
state.
|
|
385
|
-
state.
|
|
386
|
-
state.
|
|
387
|
-
state.
|
|
437
|
+
state.morphStartWeight[idx] = startWeight;
|
|
438
|
+
state.morphTargetWeight[idx] = clampedWeight;
|
|
439
|
+
state.morphStartTimeMs[idx] = now;
|
|
440
|
+
state.morphDurationMs[idx] = dur;
|
|
441
|
+
state.morphActive[idx] = 1;
|
|
388
442
|
// Immediately apply morphs with current weight
|
|
389
443
|
this.runtimeMorph.weights[idx] = startWeight;
|
|
390
444
|
this.applyMorphs();
|
|
@@ -446,20 +500,270 @@ export class Model {
|
|
|
446
500
|
}
|
|
447
501
|
}
|
|
448
502
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Load VMD animation file
|
|
505
|
+
*/
|
|
506
|
+
async loadVmd(vmdUrl) {
|
|
507
|
+
this.animationData = await VMDLoader.load(vmdUrl);
|
|
508
|
+
this.processFrames();
|
|
509
|
+
// Apply initial pose at time 0
|
|
510
|
+
this.animationTime = 0;
|
|
511
|
+
this.getPoseAtTime(0);
|
|
512
|
+
// Apply morphs if animation changed them
|
|
513
|
+
if (this.morphsDirty) {
|
|
514
|
+
this.applyMorphs();
|
|
515
|
+
this.morphsDirty = false;
|
|
516
|
+
}
|
|
517
|
+
// Compute world matrices after applying initial pose
|
|
518
|
+
this.computeWorldMatrices();
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Process frames into tracks
|
|
522
|
+
*/
|
|
523
|
+
processFrames() {
|
|
524
|
+
if (!this.animationData)
|
|
525
|
+
return;
|
|
526
|
+
// Helper to group frames by name and sort by time
|
|
527
|
+
const groupFrames = (items) => {
|
|
528
|
+
const tracks = new Map();
|
|
529
|
+
for (const { item, name, time } of items) {
|
|
530
|
+
if (!tracks.has(name))
|
|
531
|
+
tracks.set(name, []);
|
|
532
|
+
tracks.get(name).push({ item, time });
|
|
533
|
+
}
|
|
534
|
+
for (const keyFrames of tracks.values()) {
|
|
535
|
+
keyFrames.sort((a, b) => a.time - b.time);
|
|
536
|
+
}
|
|
537
|
+
return tracks;
|
|
538
|
+
};
|
|
539
|
+
// Collect all bone and morph frames
|
|
540
|
+
const boneItems = [];
|
|
541
|
+
const morphItems = [];
|
|
542
|
+
for (const keyFrame of this.animationData) {
|
|
543
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
544
|
+
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time });
|
|
545
|
+
}
|
|
546
|
+
for (const morphFrame of keyFrame.morphFrames) {
|
|
547
|
+
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Transform to expected format
|
|
551
|
+
this.boneTracks = new Map();
|
|
552
|
+
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
553
|
+
this.boneTracks.set(name, frames.map((f) => ({ boneFrame: f.item, time: f.time })));
|
|
554
|
+
}
|
|
555
|
+
this.morphTracks = new Map();
|
|
556
|
+
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
557
|
+
this.morphTracks.set(name, frames.map((f) => ({ morphFrame: f.item, time: f.time })));
|
|
558
|
+
}
|
|
559
|
+
// Calculate duration from all tracks
|
|
560
|
+
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()];
|
|
561
|
+
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
562
|
+
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0;
|
|
563
|
+
return Math.max(max, lastTime);
|
|
564
|
+
}, 0);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Start or resume playback
|
|
568
|
+
*/
|
|
569
|
+
playAnimation() {
|
|
570
|
+
if (!this.animationData) {
|
|
571
|
+
console.warn("[Model] Cannot play animation: no animation data loaded");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this.isPaused = false;
|
|
575
|
+
this.isPlaying = true;
|
|
576
|
+
// Apply initial pose at current animation time
|
|
577
|
+
this.getPoseAtTime(this.animationTime);
|
|
578
|
+
// Apply morphs if animation changed them
|
|
579
|
+
if (this.morphsDirty) {
|
|
580
|
+
this.applyMorphs();
|
|
581
|
+
this.morphsDirty = false;
|
|
582
|
+
}
|
|
583
|
+
// Compute world matrices after applying pose
|
|
584
|
+
this.computeWorldMatrices();
|
|
585
|
+
// Reset physics when starting animation (prevents instability from sudden pose changes)
|
|
586
|
+
if (this.physics) {
|
|
587
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Pause playback
|
|
592
|
+
*/
|
|
593
|
+
pauseAnimation() {
|
|
594
|
+
if (!this.isPlaying || this.isPaused)
|
|
595
|
+
return;
|
|
596
|
+
this.isPaused = true;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Stop playback and reset to beginning
|
|
600
|
+
*/
|
|
601
|
+
stopAnimation() {
|
|
602
|
+
this.isPlaying = false;
|
|
603
|
+
this.isPaused = false;
|
|
604
|
+
this.animationTime = 0;
|
|
605
|
+
// Reset physics state when stopping animation (prevents instability from sudden pose changes)
|
|
606
|
+
if (this.physics) {
|
|
607
|
+
this.computeWorldMatrices();
|
|
608
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Seek to specific time
|
|
613
|
+
* Immediately applies pose at the seeked time
|
|
614
|
+
*/
|
|
615
|
+
seekAnimation(time) {
|
|
616
|
+
if (!this.animationData)
|
|
617
|
+
return;
|
|
618
|
+
const clampedTime = Math.max(0, Math.min(time, this.animationDuration));
|
|
619
|
+
this.animationTime = clampedTime;
|
|
620
|
+
// Immediately apply pose at seeked time
|
|
621
|
+
this.getPoseAtTime(clampedTime);
|
|
622
|
+
// Apply morphs if animation changed them
|
|
623
|
+
if (this.morphsDirty) {
|
|
624
|
+
this.applyMorphs();
|
|
625
|
+
this.morphsDirty = false;
|
|
626
|
+
}
|
|
627
|
+
// Compute world matrices after applying pose
|
|
628
|
+
this.computeWorldMatrices();
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get current animation progress
|
|
632
|
+
*/
|
|
633
|
+
getAnimationProgress() {
|
|
634
|
+
const duration = this.animationDuration;
|
|
635
|
+
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0;
|
|
636
|
+
return {
|
|
637
|
+
current: this.animationTime,
|
|
638
|
+
duration,
|
|
639
|
+
percentage,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Get pose at specific time (internal helper)
|
|
644
|
+
*/
|
|
645
|
+
getPoseAtTime(time) {
|
|
646
|
+
if (!this.animationData)
|
|
647
|
+
return;
|
|
648
|
+
// Helper for binary search upper bound
|
|
649
|
+
const upperBound = (time, keyFrames) => {
|
|
650
|
+
let left = 0, right = keyFrames.length;
|
|
651
|
+
while (left < right) {
|
|
652
|
+
const mid = Math.floor((left + right) / 2);
|
|
653
|
+
if (keyFrames[mid].time <= time)
|
|
654
|
+
left = mid + 1;
|
|
655
|
+
else
|
|
656
|
+
right = mid;
|
|
657
|
+
}
|
|
658
|
+
return left;
|
|
659
|
+
};
|
|
660
|
+
// Process bone tracks
|
|
661
|
+
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
662
|
+
if (keyFrames.length === 0)
|
|
663
|
+
continue;
|
|
664
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time));
|
|
665
|
+
const idx = upperBound(clampedTime, keyFrames) - 1;
|
|
666
|
+
if (idx < 0)
|
|
667
|
+
continue;
|
|
668
|
+
const frameA = keyFrames[idx].boneFrame;
|
|
669
|
+
const frameB = keyFrames[idx + 1]?.boneFrame;
|
|
670
|
+
const boneIdx = this.runtimeSkeleton.nameIndex[boneName];
|
|
671
|
+
if (boneIdx === undefined)
|
|
672
|
+
continue;
|
|
673
|
+
if (!frameB) {
|
|
674
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4] = frameA.rotation.x;
|
|
675
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = frameA.rotation.y;
|
|
676
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = frameA.rotation.z;
|
|
677
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = frameA.rotation.w;
|
|
678
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3] = frameA.translation.x;
|
|
679
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = frameA.translation.y;
|
|
680
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = frameA.translation.z;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
const timeA = keyFrames[idx].time;
|
|
684
|
+
const timeB = keyFrames[idx + 1].time;
|
|
685
|
+
const gradient = (clampedTime - timeA) / (timeB - timeA);
|
|
686
|
+
const interp = frameB.interpolation;
|
|
687
|
+
// Interpolate rotation using SLERP with bezier
|
|
688
|
+
const rotT = bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient);
|
|
689
|
+
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT);
|
|
690
|
+
// Interpolate translation using bezier for each component
|
|
691
|
+
const getWeight = (offset) => bezierInterpolate(interp[offset] / 127, interp[offset + 8] / 127, interp[offset + 4] / 127, interp[offset + 12] / 127, gradient);
|
|
692
|
+
const lerp = (a, b, w) => a + (b - a) * w;
|
|
693
|
+
const translation = new Vec3(lerp(frameA.translation.x, frameB.translation.x, getWeight(0)), lerp(frameA.translation.y, frameB.translation.y, getWeight(16)), lerp(frameA.translation.z, frameB.translation.z, getWeight(32)));
|
|
694
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4] = rotation.x;
|
|
695
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = rotation.y;
|
|
696
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = rotation.z;
|
|
697
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = rotation.w;
|
|
698
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3] = translation.x;
|
|
699
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = translation.y;
|
|
700
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = translation.z;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Process morph tracks
|
|
704
|
+
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
705
|
+
if (keyFrames.length === 0)
|
|
706
|
+
continue;
|
|
707
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time));
|
|
708
|
+
const idx = upperBound(clampedTime, keyFrames) - 1;
|
|
709
|
+
if (idx < 0)
|
|
710
|
+
continue;
|
|
711
|
+
const frameA = keyFrames[idx].morphFrame;
|
|
712
|
+
const frameB = keyFrames[idx + 1]?.morphFrame;
|
|
713
|
+
const morphIdx = this.runtimeMorph.nameIndex[morphName];
|
|
714
|
+
if (morphIdx === undefined)
|
|
715
|
+
continue;
|
|
716
|
+
const weight = frameB
|
|
717
|
+
? frameA.weight +
|
|
718
|
+
(frameB.weight - frameA.weight) *
|
|
719
|
+
((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
|
|
720
|
+
: frameA.weight;
|
|
721
|
+
this.runtimeMorph.weights[morphIdx] = weight;
|
|
722
|
+
this.morphsDirty = true; // Mark as dirty when animation sets morph weights
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Updates the model pose by recomputing all matrices.
|
|
727
|
+
* If animation is playing, applies animation pose first.
|
|
728
|
+
* deltaTime: Time elapsed since last update in seconds
|
|
729
|
+
* Returns true if vertices were modified (morphs changed)
|
|
730
|
+
*/
|
|
731
|
+
update(deltaTime) {
|
|
732
|
+
// Update tween time (in milliseconds)
|
|
733
|
+
this.tweenTimeMs += deltaTime * 1000;
|
|
734
|
+
// Update all active tweens (rotations, translations, morphs)
|
|
735
|
+
const tweensChangedMorphs = this.updateTweens();
|
|
736
|
+
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
737
|
+
if (this.animationData) {
|
|
738
|
+
if (this.isPlaying && !this.isPaused) {
|
|
739
|
+
this.animationTime += deltaTime;
|
|
740
|
+
if (this.animationTime >= this.animationDuration) {
|
|
741
|
+
this.animationTime = this.animationDuration;
|
|
742
|
+
this.pauseAnimation(); // Auto-pause at end
|
|
743
|
+
}
|
|
744
|
+
this.getPoseAtTime(this.animationTime);
|
|
745
|
+
}
|
|
746
|
+
else if (this.isPaused || (!this.isPlaying && this.animationTime >= 0)) {
|
|
747
|
+
// Apply pose at paused time or if we have a seeked time but not playing
|
|
748
|
+
this.getPoseAtTime(this.animationTime);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// Apply morphs if tweens changed morphs or animation changed morphs
|
|
752
|
+
const verticesChanged = this.morphsDirty || tweensChangedMorphs;
|
|
753
|
+
if (this.morphsDirty || tweensChangedMorphs) {
|
|
454
754
|
this.applyMorphs();
|
|
755
|
+
this.morphsDirty = false;
|
|
455
756
|
}
|
|
456
|
-
// Compute
|
|
757
|
+
// Compute world matrices (needed for IK solving to read bone positions)
|
|
457
758
|
this.computeWorldMatrices();
|
|
458
759
|
// Solve IK chains (modifies localRotations with final IK rotations)
|
|
459
760
|
this.solveIKChains();
|
|
460
761
|
// Recompute world matrices with final IK rotations applied to localRotations
|
|
461
762
|
this.computeWorldMatrices();
|
|
462
|
-
|
|
763
|
+
if (this.physics) {
|
|
764
|
+
this.physics.step(deltaTime, this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices);
|
|
765
|
+
}
|
|
766
|
+
return verticesChanged;
|
|
463
767
|
}
|
|
464
768
|
solveIKChains() {
|
|
465
769
|
const ikSolvers = this.runtimeSkeleton.ikSolvers;
|