reze-engine 0.3.5 → 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 +10 -26
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +261 -670
- 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/ik-solver.d.ts +6 -0
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +98 -101
- package/dist/math.d.ts +0 -5
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +0 -55
- package/dist/model.d.ts +59 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +415 -141
- package/dist/player.d.ts +6 -20
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +88 -191
- package/package.json +1 -1
- package/src/engine.ts +299 -729
- package/src/ik-solver.ts +106 -124
- package/src/math.ts +0 -74
- package/src/model.ts +516 -186
- package/src/player.ts +115 -210
package/src/model.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3, easeInOut } from "./math"
|
|
2
|
-
import { Rigidbody, Joint } from "./physics"
|
|
1
|
+
import { Mat4, Quat, Vec3, easeInOut, bezierInterpolate } from "./math"
|
|
2
|
+
import { Rigidbody, Joint, Physics } from "./physics"
|
|
3
3
|
import { IKSolverSystem } from "./ik-solver"
|
|
4
|
+
import { VMDKeyFrame, VMDLoader, BoneFrame, MorphFrame } from "./vmd-loader"
|
|
4
5
|
|
|
5
6
|
const VERTEX_STRIDE = 8
|
|
6
7
|
|
|
@@ -119,31 +120,29 @@ export interface MorphRuntime {
|
|
|
119
120
|
weights: Float32Array // One weight per morph (0.0 to 1.0)
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
startTimeMs: Float32Array // one float per bone (ms)
|
|
146
|
-
durationMs: Float32Array // one float per bone (ms)
|
|
123
|
+
// Tween state - combines rotation, translation, and morph tweens
|
|
124
|
+
// All tweens share the same time reference to avoid conflicts
|
|
125
|
+
interface TweenState {
|
|
126
|
+
// Bone rotation tweens
|
|
127
|
+
rotActive: Uint8Array // 0/1 per bone
|
|
128
|
+
rotStartQuat: Float32Array // quat per bone (x,y,z,w)
|
|
129
|
+
rotTargetQuat: Float32Array // quat per bone (x,y,z,w)
|
|
130
|
+
rotStartTimeMs: Float32Array // one float per bone (ms)
|
|
131
|
+
rotDurationMs: Float32Array // one float per bone (ms)
|
|
132
|
+
|
|
133
|
+
// Bone translation tweens
|
|
134
|
+
transActive: Uint8Array // 0/1 per bone
|
|
135
|
+
transStartVec: Float32Array // vec3 per bone (x,y,z)
|
|
136
|
+
transTargetVec: Float32Array // vec3 per bone (x,y,z)
|
|
137
|
+
transStartTimeMs: Float32Array // one float per bone (ms)
|
|
138
|
+
transDurationMs: Float32Array // one float per bone (ms)
|
|
139
|
+
|
|
140
|
+
// Morph weight tweens
|
|
141
|
+
morphActive: Uint8Array // 0/1 per morph
|
|
142
|
+
morphStartWeight: Float32Array // one float per morph
|
|
143
|
+
morphTargetWeight: Float32Array // one float per morph
|
|
144
|
+
morphStartTimeMs: Float32Array // one float per morph (ms)
|
|
145
|
+
morphDurationMs: Float32Array // one float per morph (ms)
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
export class Model {
|
|
@@ -169,14 +168,29 @@ export class Model {
|
|
|
169
168
|
|
|
170
169
|
// Runtime morph state
|
|
171
170
|
private runtimeMorph!: MorphRuntime
|
|
171
|
+
private morphsDirty: boolean = false // Flag indicating if morphs need to be applied
|
|
172
172
|
|
|
173
173
|
// Cached identity matrices to avoid allocations in computeWorldMatrices
|
|
174
174
|
private cachedIdentityMat1 = Mat4.identity()
|
|
175
175
|
private cachedIdentityMat2 = Mat4.identity()
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
private
|
|
179
|
-
|
|
177
|
+
// Cached skin matrices array to avoid allocations in getSkinMatrices
|
|
178
|
+
private cachedSkinMatrices?: Float32Array
|
|
179
|
+
|
|
180
|
+
private tweenState!: TweenState
|
|
181
|
+
private tweenTimeMs: number = 0 // Time tracking for tweens (milliseconds)
|
|
182
|
+
|
|
183
|
+
// Animation runtime
|
|
184
|
+
private animationData: VMDKeyFrame[] | null = null
|
|
185
|
+
private boneTracks: Map<string, Array<{ boneFrame: BoneFrame; time: number }>> = new Map()
|
|
186
|
+
private morphTracks: Map<string, Array<{ morphFrame: MorphFrame; time: number }>> = new Map()
|
|
187
|
+
private animationDuration: number = 0
|
|
188
|
+
private isPlaying: boolean = false
|
|
189
|
+
private isPaused: boolean = false
|
|
190
|
+
private animationTime: number = 0 // Current time in animation (seconds)
|
|
191
|
+
|
|
192
|
+
// Physics runtime
|
|
193
|
+
private physics: Physics | null = null
|
|
180
194
|
|
|
181
195
|
constructor(
|
|
182
196
|
vertexData: Float32Array<ArrayBuffer>,
|
|
@@ -207,11 +221,17 @@ export class Model {
|
|
|
207
221
|
}
|
|
208
222
|
|
|
209
223
|
this.initializeRuntimeSkeleton()
|
|
210
|
-
this.initializeRotTweenBuffers()
|
|
211
|
-
this.initializeTransTweenBuffers()
|
|
212
224
|
this.initializeRuntimeMorph()
|
|
213
|
-
this.
|
|
225
|
+
this.initializeTweenBuffers()
|
|
214
226
|
this.applyMorphs() // Apply initial morphs (all weights are 0, so no change)
|
|
227
|
+
|
|
228
|
+
// Initialize physics if rigidbodies exist
|
|
229
|
+
if (rigidbodies.length > 0) {
|
|
230
|
+
this.physics = new Physics(rigidbodies, joints)
|
|
231
|
+
console.log(`[Model] Physics initialized with ${rigidbodies.length} rigidbodies`)
|
|
232
|
+
} else {
|
|
233
|
+
console.log("[Model] No rigidbodies found, physics disabled")
|
|
234
|
+
}
|
|
215
235
|
}
|
|
216
236
|
|
|
217
237
|
private initializeRuntimeSkeleton(): void {
|
|
@@ -278,36 +298,31 @@ export class Model {
|
|
|
278
298
|
this.runtimeSkeleton.ikSolvers = ikSolvers
|
|
279
299
|
}
|
|
280
300
|
|
|
281
|
-
private
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
active: new Uint8Array(n),
|
|
285
|
-
startQuat: new Float32Array(n * 4),
|
|
286
|
-
targetQuat: new Float32Array(n * 4),
|
|
287
|
-
startTimeMs: new Float32Array(n),
|
|
288
|
-
durationMs: new Float32Array(n),
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private initializeTransTweenBuffers(): void {
|
|
293
|
-
const n = this.skeleton.bones.length
|
|
294
|
-
this.transTweenState = {
|
|
295
|
-
active: new Uint8Array(n),
|
|
296
|
-
startVec: new Float32Array(n * 3),
|
|
297
|
-
targetVec: new Float32Array(n * 3),
|
|
298
|
-
startTimeMs: new Float32Array(n),
|
|
299
|
-
durationMs: new Float32Array(n),
|
|
300
|
-
}
|
|
301
|
-
}
|
|
301
|
+
private initializeTweenBuffers(): void {
|
|
302
|
+
const boneCount = this.skeleton.bones.length
|
|
303
|
+
const morphCount = this.morphing.morphs.length
|
|
302
304
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
305
|
+
this.tweenState = {
|
|
306
|
+
// Bone rotation tweens
|
|
307
|
+
rotActive: new Uint8Array(boneCount),
|
|
308
|
+
rotStartQuat: new Float32Array(boneCount * 4),
|
|
309
|
+
rotTargetQuat: new Float32Array(boneCount * 4),
|
|
310
|
+
rotStartTimeMs: new Float32Array(boneCount),
|
|
311
|
+
rotDurationMs: new Float32Array(boneCount),
|
|
312
|
+
|
|
313
|
+
// Bone translation tweens
|
|
314
|
+
transActive: new Uint8Array(boneCount),
|
|
315
|
+
transStartVec: new Float32Array(boneCount * 3),
|
|
316
|
+
transTargetVec: new Float32Array(boneCount * 3),
|
|
317
|
+
transStartTimeMs: new Float32Array(boneCount),
|
|
318
|
+
transDurationMs: new Float32Array(boneCount),
|
|
319
|
+
|
|
320
|
+
// Morph weight tweens
|
|
321
|
+
morphActive: new Uint8Array(morphCount),
|
|
322
|
+
morphStartWeight: new Float32Array(morphCount),
|
|
323
|
+
morphTargetWeight: new Float32Array(morphCount),
|
|
324
|
+
morphStartTimeMs: new Float32Array(morphCount),
|
|
325
|
+
morphDurationMs: new Float32Array(morphCount),
|
|
311
326
|
}
|
|
312
327
|
}
|
|
313
328
|
|
|
@@ -322,32 +337,37 @@ export class Model {
|
|
|
322
337
|
}
|
|
323
338
|
}
|
|
324
339
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
340
|
+
// Tween update - processes all tweens together with a single time reference
|
|
341
|
+
// This avoids conflicts and ensures consistent timing across all tween types
|
|
342
|
+
// Returns true if morph weights changed (needed for vertex buffer updates)
|
|
343
|
+
private updateTweens(): boolean {
|
|
344
|
+
const state = this.tweenState
|
|
345
|
+
const now = this.tweenTimeMs // Single time reference for all tweens
|
|
346
|
+
let morphChanged = false
|
|
347
|
+
|
|
348
|
+
// Update bone rotation tweens
|
|
328
349
|
const rotations = this.runtimeSkeleton.localRotations
|
|
329
350
|
const boneCount = this.skeleton.bones.length
|
|
330
|
-
|
|
331
351
|
for (let i = 0; i < boneCount; i++) {
|
|
332
|
-
if (state.
|
|
352
|
+
if (state.rotActive[i] !== 1) continue
|
|
333
353
|
|
|
334
|
-
const startMs = state.
|
|
335
|
-
const durMs = Math.max(1, state.
|
|
354
|
+
const startMs = state.rotStartTimeMs[i]
|
|
355
|
+
const durMs = Math.max(1, state.rotDurationMs[i])
|
|
336
356
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
337
357
|
const e = easeInOut(t)
|
|
338
358
|
|
|
339
359
|
const qi = i * 4
|
|
340
360
|
const startQuat = new Quat(
|
|
341
|
-
state.
|
|
342
|
-
state.
|
|
343
|
-
state.
|
|
344
|
-
state.
|
|
361
|
+
state.rotStartQuat[qi],
|
|
362
|
+
state.rotStartQuat[qi + 1],
|
|
363
|
+
state.rotStartQuat[qi + 2],
|
|
364
|
+
state.rotStartQuat[qi + 3]
|
|
345
365
|
)
|
|
346
366
|
const targetQuat = new Quat(
|
|
347
|
-
state.
|
|
348
|
-
state.
|
|
349
|
-
state.
|
|
350
|
-
state.
|
|
367
|
+
state.rotTargetQuat[qi],
|
|
368
|
+
state.rotTargetQuat[qi + 1],
|
|
369
|
+
state.rotTargetQuat[qi + 2],
|
|
370
|
+
state.rotTargetQuat[qi + 3]
|
|
351
371
|
)
|
|
352
372
|
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
353
373
|
|
|
@@ -356,87 +376,81 @@ export class Model {
|
|
|
356
376
|
rotations[qi + 2] = result.z
|
|
357
377
|
rotations[qi + 3] = result.w
|
|
358
378
|
|
|
359
|
-
if (t >= 1)
|
|
379
|
+
if (t >= 1) {
|
|
380
|
+
state.rotActive[i] = 0
|
|
381
|
+
}
|
|
360
382
|
}
|
|
361
|
-
}
|
|
362
383
|
|
|
363
|
-
|
|
364
|
-
const state = this.transTweenState
|
|
365
|
-
const now = performance.now()
|
|
384
|
+
// Update bone translation tweens
|
|
366
385
|
const translations = this.runtimeSkeleton.localTranslations
|
|
367
|
-
const boneCount = this.skeleton.bones.length
|
|
368
|
-
|
|
369
386
|
for (let i = 0; i < boneCount; i++) {
|
|
370
|
-
if (state.
|
|
387
|
+
if (state.transActive[i] !== 1) continue
|
|
371
388
|
|
|
372
|
-
const startMs = state.
|
|
373
|
-
const durMs = Math.max(1, state.
|
|
389
|
+
const startMs = state.transStartTimeMs[i]
|
|
390
|
+
const durMs = Math.max(1, state.transDurationMs[i])
|
|
374
391
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
375
392
|
const e = easeInOut(t)
|
|
376
393
|
|
|
377
394
|
const ti = i * 3
|
|
378
|
-
translations[ti] = state.
|
|
379
|
-
translations[ti + 1] =
|
|
380
|
-
|
|
395
|
+
translations[ti] = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
|
|
396
|
+
translations[ti + 1] =
|
|
397
|
+
state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e
|
|
398
|
+
translations[ti + 2] =
|
|
399
|
+
state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e
|
|
381
400
|
|
|
382
|
-
if (t >= 1)
|
|
401
|
+
if (t >= 1) {
|
|
402
|
+
state.transActive[i] = 0
|
|
403
|
+
}
|
|
383
404
|
}
|
|
384
|
-
}
|
|
385
405
|
|
|
386
|
-
|
|
387
|
-
const state = this.morphTweenState
|
|
388
|
-
const now = performance.now()
|
|
406
|
+
// Update morph weight tweens
|
|
389
407
|
const weights = this.runtimeMorph.weights
|
|
390
408
|
const morphCount = this.morphing.morphs.length
|
|
391
|
-
let hasActiveTweens = false
|
|
392
|
-
|
|
393
409
|
for (let i = 0; i < morphCount; i++) {
|
|
394
|
-
if (state.
|
|
410
|
+
if (state.morphActive[i] !== 1) continue
|
|
395
411
|
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
const durMs = Math.max(1, state.durationMs[i])
|
|
412
|
+
const startMs = state.morphStartTimeMs[i]
|
|
413
|
+
const durMs = Math.max(1, state.morphDurationMs[i])
|
|
399
414
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
400
415
|
const e = easeInOut(t)
|
|
401
416
|
|
|
402
|
-
|
|
417
|
+
const oldWeight = weights[i]
|
|
418
|
+
weights[i] = state.morphStartWeight[i] + (state.morphTargetWeight[i] - state.morphStartWeight[i]) * e
|
|
419
|
+
|
|
420
|
+
// Check if weight actually changed (accounting for floating point precision)
|
|
421
|
+
if (Math.abs(weights[i] - oldWeight) > 1e-6) {
|
|
422
|
+
morphChanged = true
|
|
423
|
+
}
|
|
403
424
|
|
|
404
425
|
if (t >= 1) {
|
|
405
|
-
weights[i] = state.
|
|
406
|
-
state.
|
|
426
|
+
weights[i] = state.morphTargetWeight[i]
|
|
427
|
+
state.morphActive[i] = 0
|
|
428
|
+
// Check if final weight is different from old weight
|
|
429
|
+
if (Math.abs(weights[i] - oldWeight) > 1e-6) {
|
|
430
|
+
morphChanged = true
|
|
431
|
+
}
|
|
407
432
|
}
|
|
408
433
|
}
|
|
409
434
|
|
|
410
|
-
return
|
|
435
|
+
return morphChanged
|
|
411
436
|
}
|
|
412
437
|
|
|
413
|
-
// Get interleaved vertex data for GPU upload
|
|
414
|
-
// Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
|
|
415
438
|
getVertices(): Float32Array<ArrayBuffer> {
|
|
416
439
|
return this.vertexData
|
|
417
440
|
}
|
|
418
441
|
|
|
419
|
-
// Get texture information
|
|
420
442
|
getTextures(): Texture[] {
|
|
421
443
|
return this.textures
|
|
422
444
|
}
|
|
423
445
|
|
|
424
|
-
// Get material information
|
|
425
446
|
getMaterials(): Material[] {
|
|
426
447
|
return this.materials
|
|
427
448
|
}
|
|
428
449
|
|
|
429
|
-
// Get vertex count
|
|
430
|
-
getVertexCount(): number {
|
|
431
|
-
return this.vertexCount
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Get index data for GPU upload
|
|
435
450
|
getIndices(): Uint32Array<ArrayBuffer> {
|
|
436
451
|
return this.indexData
|
|
437
452
|
}
|
|
438
453
|
|
|
439
|
-
// Accessors for skeleton/skinning
|
|
440
454
|
getSkeleton(): Skeleton {
|
|
441
455
|
return this.skeleton
|
|
442
456
|
}
|
|
@@ -445,7 +459,6 @@ export class Model {
|
|
|
445
459
|
return this.skinning
|
|
446
460
|
}
|
|
447
461
|
|
|
448
|
-
// Accessors for physics data
|
|
449
462
|
getRigidbodies(): Rigidbody[] {
|
|
450
463
|
return this.rigidbodies
|
|
451
464
|
}
|
|
@@ -454,7 +467,6 @@ export class Model {
|
|
|
454
467
|
return this.joints
|
|
455
468
|
}
|
|
456
469
|
|
|
457
|
-
// Accessors for morphing
|
|
458
470
|
getMorphing(): Morphing {
|
|
459
471
|
return this.morphing
|
|
460
472
|
}
|
|
@@ -465,14 +477,10 @@ export class Model {
|
|
|
465
477
|
|
|
466
478
|
// ------- Bone helpers (public API) -------
|
|
467
479
|
|
|
468
|
-
getBoneNames(): string[] {
|
|
469
|
-
return this.skeleton.bones.map((b) => b.name)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
480
|
rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
|
|
473
|
-
const state = this.
|
|
481
|
+
const state = this.tweenState
|
|
474
482
|
const normalized = quats.map((q) => q.normalize())
|
|
475
|
-
const now =
|
|
483
|
+
const now = this.tweenTimeMs
|
|
476
484
|
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
477
485
|
|
|
478
486
|
for (let i = 0; i < names.length; i++) {
|
|
@@ -489,7 +497,7 @@ export class Model {
|
|
|
489
497
|
rotations[qi + 1] = ty
|
|
490
498
|
rotations[qi + 2] = tz
|
|
491
499
|
rotations[qi + 3] = tw
|
|
492
|
-
state.
|
|
500
|
+
state.rotActive[idx] = 0
|
|
493
501
|
continue
|
|
494
502
|
}
|
|
495
503
|
|
|
@@ -498,22 +506,22 @@ export class Model {
|
|
|
498
506
|
let sz = rotations[qi + 2]
|
|
499
507
|
let sw = rotations[qi + 3]
|
|
500
508
|
|
|
501
|
-
if (state.
|
|
502
|
-
const startMs = state.
|
|
503
|
-
const prevDur = Math.max(1, state.
|
|
509
|
+
if (state.rotActive[idx] === 1) {
|
|
510
|
+
const startMs = state.rotStartTimeMs[idx]
|
|
511
|
+
const prevDur = Math.max(1, state.rotDurationMs[idx])
|
|
504
512
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
505
513
|
const e = easeInOut(t)
|
|
506
514
|
const startQuat = new Quat(
|
|
507
|
-
state.
|
|
508
|
-
state.
|
|
509
|
-
state.
|
|
510
|
-
state.
|
|
515
|
+
state.rotStartQuat[qi],
|
|
516
|
+
state.rotStartQuat[qi + 1],
|
|
517
|
+
state.rotStartQuat[qi + 2],
|
|
518
|
+
state.rotStartQuat[qi + 3]
|
|
511
519
|
)
|
|
512
520
|
const targetQuat = new Quat(
|
|
513
|
-
state.
|
|
514
|
-
state.
|
|
515
|
-
state.
|
|
516
|
-
state.
|
|
521
|
+
state.rotTargetQuat[qi],
|
|
522
|
+
state.rotTargetQuat[qi + 1],
|
|
523
|
+
state.rotTargetQuat[qi + 2],
|
|
524
|
+
state.rotTargetQuat[qi + 3]
|
|
517
525
|
)
|
|
518
526
|
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
519
527
|
sx = result.x
|
|
@@ -522,25 +530,25 @@ export class Model {
|
|
|
522
530
|
sw = result.w
|
|
523
531
|
}
|
|
524
532
|
|
|
525
|
-
state.
|
|
526
|
-
state.
|
|
527
|
-
state.
|
|
528
|
-
state.
|
|
529
|
-
state.
|
|
530
|
-
state.
|
|
531
|
-
state.
|
|
532
|
-
state.
|
|
533
|
-
state.
|
|
534
|
-
state.
|
|
535
|
-
state.
|
|
533
|
+
state.rotStartQuat[qi] = sx
|
|
534
|
+
state.rotStartQuat[qi + 1] = sy
|
|
535
|
+
state.rotStartQuat[qi + 2] = sz
|
|
536
|
+
state.rotStartQuat[qi + 3] = sw
|
|
537
|
+
state.rotTargetQuat[qi] = tx
|
|
538
|
+
state.rotTargetQuat[qi + 1] = ty
|
|
539
|
+
state.rotTargetQuat[qi + 2] = tz
|
|
540
|
+
state.rotTargetQuat[qi + 3] = tw
|
|
541
|
+
state.rotStartTimeMs[idx] = now
|
|
542
|
+
state.rotDurationMs[idx] = dur
|
|
543
|
+
state.rotActive[idx] = 1
|
|
536
544
|
}
|
|
537
545
|
}
|
|
538
546
|
|
|
539
547
|
// Move bones using VMD-style relative translations (relative to bind pose world position)
|
|
540
548
|
// This is the default behavior for VMD animations
|
|
541
549
|
moveBones(names: string[], relativeTranslations: Vec3[], durationMs?: number): void {
|
|
542
|
-
const state = this.
|
|
543
|
-
const now =
|
|
550
|
+
const state = this.tweenState
|
|
551
|
+
const now = this.tweenTimeMs
|
|
544
552
|
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
545
553
|
const localRot = this.runtimeSkeleton.localRotations
|
|
546
554
|
|
|
@@ -608,7 +616,7 @@ export class Model {
|
|
|
608
616
|
translations[ti] = tx
|
|
609
617
|
translations[ti + 1] = ty
|
|
610
618
|
translations[ti + 2] = tz
|
|
611
|
-
state.
|
|
619
|
+
state.transActive[idx] = 0
|
|
612
620
|
continue
|
|
613
621
|
}
|
|
614
622
|
|
|
@@ -616,25 +624,25 @@ export class Model {
|
|
|
616
624
|
let sy = translations[ti + 1]
|
|
617
625
|
let sz = translations[ti + 2]
|
|
618
626
|
|
|
619
|
-
if (state.
|
|
620
|
-
const startMs = state.
|
|
621
|
-
const prevDur = Math.max(1, state.
|
|
627
|
+
if (state.transActive[idx] === 1) {
|
|
628
|
+
const startMs = state.transStartTimeMs[idx]
|
|
629
|
+
const prevDur = Math.max(1, state.transDurationMs[idx])
|
|
622
630
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
623
631
|
const e = easeInOut(t)
|
|
624
|
-
sx = state.
|
|
625
|
-
sy = state.
|
|
626
|
-
sz = state.
|
|
632
|
+
sx = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
|
|
633
|
+
sy = state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e
|
|
634
|
+
sz = state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e
|
|
627
635
|
}
|
|
628
636
|
|
|
629
|
-
state.
|
|
630
|
-
state.
|
|
631
|
-
state.
|
|
632
|
-
state.
|
|
633
|
-
state.
|
|
634
|
-
state.
|
|
635
|
-
state.
|
|
636
|
-
state.
|
|
637
|
-
state.
|
|
637
|
+
state.transStartVec[ti] = sx
|
|
638
|
+
state.transStartVec[ti + 1] = sy
|
|
639
|
+
state.transStartVec[ti + 2] = sz
|
|
640
|
+
state.transTargetVec[ti] = tx
|
|
641
|
+
state.transTargetVec[ti + 1] = ty
|
|
642
|
+
state.transTargetVec[ti + 2] = tz
|
|
643
|
+
state.transStartTimeMs[idx] = now
|
|
644
|
+
state.transDurationMs[idx] = dur
|
|
645
|
+
state.transActive[idx] = 1
|
|
638
646
|
}
|
|
639
647
|
}
|
|
640
648
|
|
|
@@ -646,8 +654,28 @@ export class Model {
|
|
|
646
654
|
return this.skeleton.inverseBindMatrices
|
|
647
655
|
}
|
|
648
656
|
|
|
649
|
-
|
|
650
|
-
|
|
657
|
+
getSkinMatrices(): Float32Array {
|
|
658
|
+
const boneCount = this.skeleton.bones.length
|
|
659
|
+
const worldMats = this.runtimeSkeleton.worldMatrices
|
|
660
|
+
const invBindMats = this.skeleton.inverseBindMatrices
|
|
661
|
+
|
|
662
|
+
// Initialize cached array if needed or if bone count changed
|
|
663
|
+
if (!this.cachedSkinMatrices || this.cachedSkinMatrices.length !== boneCount * 16) {
|
|
664
|
+
this.cachedSkinMatrices = new Float32Array(boneCount * 16)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const skinMatrices = this.cachedSkinMatrices
|
|
668
|
+
|
|
669
|
+
// Compute skin matrices: skinMatrix = worldMatrix × inverseBindMatrix
|
|
670
|
+
// Use Mat4.multiplyArrays to avoid creating Mat4 objects
|
|
671
|
+
for (let i = 0; i < boneCount; i++) {
|
|
672
|
+
const worldOffset = i * 16
|
|
673
|
+
const invBindOffset = i * 16
|
|
674
|
+
const skinOffset = i * 16
|
|
675
|
+
Mat4.multiplyArrays(worldMats, worldOffset, invBindMats, invBindOffset, skinMatrices, skinOffset)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return skinMatrices
|
|
651
679
|
}
|
|
652
680
|
|
|
653
681
|
setMorphWeight(name: string, weight: number, durationMs?: number): void {
|
|
@@ -660,30 +688,30 @@ export class Model {
|
|
|
660
688
|
if (dur === 0) {
|
|
661
689
|
// Instant change
|
|
662
690
|
this.runtimeMorph.weights[idx] = clampedWeight
|
|
663
|
-
this.
|
|
691
|
+
this.tweenState.morphActive[idx] = 0
|
|
664
692
|
this.applyMorphs()
|
|
665
693
|
return
|
|
666
694
|
}
|
|
667
695
|
|
|
668
696
|
// Animated change
|
|
669
|
-
const state = this.
|
|
670
|
-
const now =
|
|
697
|
+
const state = this.tweenState
|
|
698
|
+
const now = this.tweenTimeMs
|
|
671
699
|
|
|
672
700
|
// If already tweening, start from current interpolated value
|
|
673
701
|
let startWeight = this.runtimeMorph.weights[idx]
|
|
674
|
-
if (state.
|
|
675
|
-
const startMs = state.
|
|
676
|
-
const prevDur = Math.max(1, state.
|
|
702
|
+
if (state.morphActive[idx] === 1) {
|
|
703
|
+
const startMs = state.morphStartTimeMs[idx]
|
|
704
|
+
const prevDur = Math.max(1, state.morphDurationMs[idx])
|
|
677
705
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
678
706
|
const e = easeInOut(t)
|
|
679
|
-
startWeight = state.
|
|
707
|
+
startWeight = state.morphStartWeight[idx] + (state.morphTargetWeight[idx] - state.morphStartWeight[idx]) * e
|
|
680
708
|
}
|
|
681
709
|
|
|
682
|
-
state.
|
|
683
|
-
state.
|
|
684
|
-
state.
|
|
685
|
-
state.
|
|
686
|
-
state.
|
|
710
|
+
state.morphStartWeight[idx] = startWeight
|
|
711
|
+
state.morphTargetWeight[idx] = clampedWeight
|
|
712
|
+
state.morphStartTimeMs[idx] = now
|
|
713
|
+
state.morphDurationMs[idx] = dur
|
|
714
|
+
state.morphActive[idx] = 1
|
|
687
715
|
|
|
688
716
|
// Immediately apply morphs with current weight
|
|
689
717
|
this.runtimeMorph.weights[idx] = startWeight
|
|
@@ -755,15 +783,313 @@ export class Model {
|
|
|
755
783
|
}
|
|
756
784
|
}
|
|
757
785
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
786
|
+
/**
|
|
787
|
+
* Load VMD animation file
|
|
788
|
+
*/
|
|
789
|
+
async loadVmd(vmdUrl: string): Promise<void> {
|
|
790
|
+
this.animationData = await VMDLoader.load(vmdUrl)
|
|
791
|
+
this.processFrames()
|
|
792
|
+
// Apply initial pose at time 0
|
|
793
|
+
this.animationTime = 0
|
|
794
|
+
this.getPoseAtTime(0)
|
|
795
|
+
|
|
796
|
+
// Apply morphs if animation changed them
|
|
797
|
+
if (this.morphsDirty) {
|
|
763
798
|
this.applyMorphs()
|
|
799
|
+
this.morphsDirty = false
|
|
764
800
|
}
|
|
765
801
|
|
|
766
|
-
// Compute
|
|
802
|
+
// Compute world matrices after applying initial pose
|
|
803
|
+
this.computeWorldMatrices()
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Process frames into tracks
|
|
808
|
+
*/
|
|
809
|
+
private processFrames(): void {
|
|
810
|
+
if (!this.animationData) return
|
|
811
|
+
|
|
812
|
+
// Helper to group frames by name and sort by time
|
|
813
|
+
const groupFrames = <T>(
|
|
814
|
+
items: Array<{ item: T; name: string; time: number }>
|
|
815
|
+
): Map<string, Array<{ item: T; time: number }>> => {
|
|
816
|
+
const tracks = new Map<string, Array<{ item: T; time: number }>>()
|
|
817
|
+
for (const { item, name, time } of items) {
|
|
818
|
+
if (!tracks.has(name)) tracks.set(name, [])
|
|
819
|
+
tracks.get(name)!.push({ item, time })
|
|
820
|
+
}
|
|
821
|
+
for (const keyFrames of tracks.values()) {
|
|
822
|
+
keyFrames.sort((a, b) => a.time - b.time)
|
|
823
|
+
}
|
|
824
|
+
return tracks
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Collect all bone and morph frames
|
|
828
|
+
const boneItems: Array<{ item: BoneFrame; name: string; time: number }> = []
|
|
829
|
+
const morphItems: Array<{ item: MorphFrame; name: string; time: number }> = []
|
|
830
|
+
|
|
831
|
+
for (const keyFrame of this.animationData) {
|
|
832
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
833
|
+
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time })
|
|
834
|
+
}
|
|
835
|
+
for (const morphFrame of keyFrame.morphFrames) {
|
|
836
|
+
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time })
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Transform to expected format
|
|
841
|
+
this.boneTracks = new Map()
|
|
842
|
+
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
843
|
+
this.boneTracks.set(
|
|
844
|
+
name,
|
|
845
|
+
frames.map((f) => ({ boneFrame: f.item, time: f.time }))
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
this.morphTracks = new Map()
|
|
850
|
+
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
851
|
+
this.morphTracks.set(
|
|
852
|
+
name,
|
|
853
|
+
frames.map((f) => ({ morphFrame: f.item, time: f.time }))
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Calculate duration from all tracks
|
|
858
|
+
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
|
|
859
|
+
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
860
|
+
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0
|
|
861
|
+
return Math.max(max, lastTime)
|
|
862
|
+
}, 0)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Start or resume playback
|
|
867
|
+
*/
|
|
868
|
+
playAnimation(): void {
|
|
869
|
+
if (!this.animationData) {
|
|
870
|
+
console.warn("[Model] Cannot play animation: no animation data loaded")
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
this.isPaused = false
|
|
875
|
+
this.isPlaying = true
|
|
876
|
+
|
|
877
|
+
// Apply initial pose at current animation time
|
|
878
|
+
this.getPoseAtTime(this.animationTime)
|
|
879
|
+
|
|
880
|
+
// Apply morphs if animation changed them
|
|
881
|
+
if (this.morphsDirty) {
|
|
882
|
+
this.applyMorphs()
|
|
883
|
+
this.morphsDirty = false
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Compute world matrices after applying pose
|
|
887
|
+
this.computeWorldMatrices()
|
|
888
|
+
|
|
889
|
+
// Reset physics when starting animation (prevents instability from sudden pose changes)
|
|
890
|
+
if (this.physics) {
|
|
891
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Pause playback
|
|
897
|
+
*/
|
|
898
|
+
pauseAnimation(): void {
|
|
899
|
+
if (!this.isPlaying || this.isPaused) return
|
|
900
|
+
this.isPaused = true
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Stop playback and reset to beginning
|
|
905
|
+
*/
|
|
906
|
+
stopAnimation(): void {
|
|
907
|
+
this.isPlaying = false
|
|
908
|
+
this.isPaused = false
|
|
909
|
+
this.animationTime = 0
|
|
910
|
+
|
|
911
|
+
// Reset physics state when stopping animation (prevents instability from sudden pose changes)
|
|
912
|
+
if (this.physics) {
|
|
913
|
+
this.computeWorldMatrices()
|
|
914
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Seek to specific time
|
|
920
|
+
* Immediately applies pose at the seeked time
|
|
921
|
+
*/
|
|
922
|
+
seekAnimation(time: number): void {
|
|
923
|
+
if (!this.animationData) return
|
|
924
|
+
|
|
925
|
+
const clampedTime = Math.max(0, Math.min(time, this.animationDuration))
|
|
926
|
+
this.animationTime = clampedTime
|
|
927
|
+
// Immediately apply pose at seeked time
|
|
928
|
+
this.getPoseAtTime(clampedTime)
|
|
929
|
+
|
|
930
|
+
// Apply morphs if animation changed them
|
|
931
|
+
if (this.morphsDirty) {
|
|
932
|
+
this.applyMorphs()
|
|
933
|
+
this.morphsDirty = false
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Compute world matrices after applying pose
|
|
937
|
+
this.computeWorldMatrices()
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Get current animation progress
|
|
942
|
+
*/
|
|
943
|
+
getAnimationProgress(): { current: number; duration: number; percentage: number } {
|
|
944
|
+
const duration = this.animationDuration
|
|
945
|
+
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0
|
|
946
|
+
return {
|
|
947
|
+
current: this.animationTime,
|
|
948
|
+
duration,
|
|
949
|
+
percentage,
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Get pose at specific time (internal helper)
|
|
955
|
+
*/
|
|
956
|
+
private getPoseAtTime(time: number): void {
|
|
957
|
+
if (!this.animationData) return
|
|
958
|
+
|
|
959
|
+
// Helper for binary search upper bound
|
|
960
|
+
const upperBound = <T extends { time: number }>(time: number, keyFrames: T[]): number => {
|
|
961
|
+
let left = 0,
|
|
962
|
+
right = keyFrames.length
|
|
963
|
+
while (left < right) {
|
|
964
|
+
const mid = Math.floor((left + right) / 2)
|
|
965
|
+
if (keyFrames[mid].time <= time) left = mid + 1
|
|
966
|
+
else right = mid
|
|
967
|
+
}
|
|
968
|
+
return left
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Process bone tracks
|
|
972
|
+
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
973
|
+
if (keyFrames.length === 0) continue
|
|
974
|
+
|
|
975
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
976
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
977
|
+
if (idx < 0) continue
|
|
978
|
+
|
|
979
|
+
const frameA = keyFrames[idx].boneFrame
|
|
980
|
+
const frameB = keyFrames[idx + 1]?.boneFrame
|
|
981
|
+
|
|
982
|
+
const boneIdx = this.runtimeSkeleton.nameIndex[boneName]
|
|
983
|
+
if (boneIdx === undefined) continue
|
|
984
|
+
|
|
985
|
+
if (!frameB) {
|
|
986
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4] = frameA.rotation.x
|
|
987
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = frameA.rotation.y
|
|
988
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = frameA.rotation.z
|
|
989
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = frameA.rotation.w
|
|
990
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3] = frameA.translation.x
|
|
991
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = frameA.translation.y
|
|
992
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = frameA.translation.z
|
|
993
|
+
} else {
|
|
994
|
+
const timeA = keyFrames[idx].time
|
|
995
|
+
const timeB = keyFrames[idx + 1].time
|
|
996
|
+
const gradient = (clampedTime - timeA) / (timeB - timeA)
|
|
997
|
+
const interp = frameB.interpolation
|
|
998
|
+
|
|
999
|
+
// Interpolate rotation using SLERP with bezier
|
|
1000
|
+
const rotT = bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient)
|
|
1001
|
+
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT)
|
|
1002
|
+
|
|
1003
|
+
// Interpolate translation using bezier for each component
|
|
1004
|
+
const getWeight = (offset: number) =>
|
|
1005
|
+
bezierInterpolate(
|
|
1006
|
+
interp[offset] / 127,
|
|
1007
|
+
interp[offset + 8] / 127,
|
|
1008
|
+
interp[offset + 4] / 127,
|
|
1009
|
+
interp[offset + 12] / 127,
|
|
1010
|
+
gradient
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
const lerp = (a: number, b: number, w: number) => a + (b - a) * w
|
|
1014
|
+
const translation = new Vec3(
|
|
1015
|
+
lerp(frameA.translation.x, frameB.translation.x, getWeight(0)),
|
|
1016
|
+
lerp(frameA.translation.y, frameB.translation.y, getWeight(16)),
|
|
1017
|
+
lerp(frameA.translation.z, frameB.translation.z, getWeight(32))
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4] = rotation.x
|
|
1021
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = rotation.y
|
|
1022
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = rotation.z
|
|
1023
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = rotation.w
|
|
1024
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3] = translation.x
|
|
1025
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = translation.y
|
|
1026
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = translation.z
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Process morph tracks
|
|
1031
|
+
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
1032
|
+
if (keyFrames.length === 0) continue
|
|
1033
|
+
|
|
1034
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
1035
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
1036
|
+
if (idx < 0) continue
|
|
1037
|
+
|
|
1038
|
+
const frameA = keyFrames[idx].morphFrame
|
|
1039
|
+
const frameB = keyFrames[idx + 1]?.morphFrame
|
|
1040
|
+
|
|
1041
|
+
const morphIdx = this.runtimeMorph.nameIndex[morphName]
|
|
1042
|
+
if (morphIdx === undefined) continue
|
|
1043
|
+
|
|
1044
|
+
const weight = frameB
|
|
1045
|
+
? frameA.weight +
|
|
1046
|
+
(frameB.weight - frameA.weight) *
|
|
1047
|
+
((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
|
|
1048
|
+
: frameA.weight
|
|
1049
|
+
|
|
1050
|
+
this.runtimeMorph.weights[morphIdx] = weight
|
|
1051
|
+
this.morphsDirty = true // Mark as dirty when animation sets morph weights
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Updates the model pose by recomputing all matrices.
|
|
1057
|
+
* If animation is playing, applies animation pose first.
|
|
1058
|
+
* deltaTime: Time elapsed since last update in seconds
|
|
1059
|
+
* Returns true if vertices were modified (morphs changed)
|
|
1060
|
+
*/
|
|
1061
|
+
update(deltaTime: number): boolean {
|
|
1062
|
+
// Update tween time (in milliseconds)
|
|
1063
|
+
this.tweenTimeMs += deltaTime * 1000
|
|
1064
|
+
|
|
1065
|
+
// Update all active tweens (rotations, translations, morphs)
|
|
1066
|
+
const tweensChangedMorphs = this.updateTweens()
|
|
1067
|
+
|
|
1068
|
+
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
1069
|
+
if (this.animationData) {
|
|
1070
|
+
if (this.isPlaying && !this.isPaused) {
|
|
1071
|
+
this.animationTime += deltaTime
|
|
1072
|
+
|
|
1073
|
+
if (this.animationTime >= this.animationDuration) {
|
|
1074
|
+
this.animationTime = this.animationDuration
|
|
1075
|
+
this.pauseAnimation() // Auto-pause at end
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
this.getPoseAtTime(this.animationTime)
|
|
1079
|
+
} else if (this.isPaused || (!this.isPlaying && this.animationTime >= 0)) {
|
|
1080
|
+
// Apply pose at paused time or if we have a seeked time but not playing
|
|
1081
|
+
this.getPoseAtTime(this.animationTime)
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Apply morphs if tweens changed morphs or animation changed morphs
|
|
1086
|
+
const verticesChanged = this.morphsDirty || tweensChangedMorphs
|
|
1087
|
+
if (this.morphsDirty || tweensChangedMorphs) {
|
|
1088
|
+
this.applyMorphs()
|
|
1089
|
+
this.morphsDirty = false
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Compute world matrices (needed for IK solving to read bone positions)
|
|
767
1093
|
this.computeWorldMatrices()
|
|
768
1094
|
|
|
769
1095
|
// Solve IK chains (modifies localRotations with final IK rotations)
|
|
@@ -772,7 +1098,11 @@ export class Model {
|
|
|
772
1098
|
// Recompute world matrices with final IK rotations applied to localRotations
|
|
773
1099
|
this.computeWorldMatrices()
|
|
774
1100
|
|
|
775
|
-
|
|
1101
|
+
if (this.physics) {
|
|
1102
|
+
this.physics.step(deltaTime, this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return verticesChanged
|
|
776
1106
|
}
|
|
777
1107
|
|
|
778
1108
|
private solveIKChains(): void {
|