reze-engine 0.3.9 → 0.3.11

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/src/model.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Mat4, Quat, Vec3, easeInOut, bezierInterpolate } from "./math"
1
+ import { Mat4, Quat, Vec3, bezierInterpolate } from "./math"
2
2
  import { Rigidbody, Joint, Physics } from "./physics"
3
3
  import { IKSolverSystem } from "./ik-solver"
4
4
  import { VMDKeyFrame, VMDLoader, BoneFrame, MorphFrame } from "./vmd-loader"
@@ -107,9 +107,9 @@ export interface Morphing {
107
107
  // Runtime skeleton pose state (updated each frame)
108
108
  export interface SkeletonRuntime {
109
109
  nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
110
- localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
111
- localTranslations: Float32Array // vec3 per bone length = boneCount*3
112
- worldMatrices: Float32Array // mat4 per bone length = boneCount*16
110
+ localRotations: Quat[] // quat per bone
111
+ localTranslations: Vec3[] // vec3 per bone
112
+ worldMatrices: Mat4[] // mat4 per bone
113
113
  ikChainInfo?: IKChainInfo[] // IK chain info per bone (only for IK chain bones)
114
114
  ikSolvers?: IKSolver[] // All IK solvers in the model
115
115
  }
@@ -125,15 +125,15 @@ export interface MorphRuntime {
125
125
  interface TweenState {
126
126
  // Bone rotation tweens
127
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)
128
+ rotStartQuat: Quat[]
129
+ rotTargetQuat: Quat[]
130
130
  rotStartTimeMs: Float32Array // one float per bone (ms)
131
131
  rotDurationMs: Float32Array // one float per bone (ms)
132
132
 
133
133
  // Bone translation tweens
134
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)
135
+ transStartVec: Vec3[] // vec3 per bone (x,y,z)
136
+ transTargetVec: Vec3[] // vec3 per bone (x,y,z)
137
137
  transStartTimeMs: Float32Array // one float per bone (ms)
138
138
  transDurationMs: Float32Array // one float per bone (ms)
139
139
 
@@ -175,7 +175,7 @@ export class Model {
175
175
  private cachedIdentityMat2 = Mat4.identity()
176
176
 
177
177
  // Cached skin matrices array to avoid allocations in getSkinMatrices
178
- private cachedSkinMatrices?: Float32Array
178
+ private skinMatricesArray?: Float32Array
179
179
 
180
180
  private tweenState!: TweenState
181
181
  private tweenTimeMs: number = 0 // Time tracking for tweens (milliseconds)
@@ -241,27 +241,26 @@ export class Model {
241
241
  private initializeRuntimeSkeleton(): void {
242
242
  const boneCount = this.skeleton.bones.length
243
243
 
244
+ // Pre-allocate object arrays for skeletal pose
245
+ const localRotations: Quat[] = new Array(boneCount)
246
+ const localTranslations: Vec3[] = new Array(boneCount)
247
+ const worldMatrices: Mat4[] = new Array(boneCount)
248
+ for (let i = 0; i < boneCount; i++) {
249
+ localRotations[i] = Quat.identity()
250
+ localTranslations[i] = Vec3.zeros()
251
+ worldMatrices[i] = Mat4.identity()
252
+ }
253
+
244
254
  this.runtimeSkeleton = {
245
- localRotations: new Float32Array(boneCount * 4),
246
- localTranslations: new Float32Array(boneCount * 3),
247
- worldMatrices: new Float32Array(boneCount * 16),
255
+ localRotations,
256
+ localTranslations,
257
+ worldMatrices,
248
258
  nameIndex: this.skeleton.bones.reduce((acc, bone, index) => {
249
259
  acc[bone.name] = index
250
260
  return acc
251
261
  }, {} as Record<string, number>),
252
262
  }
253
263
 
254
- const rotations = this.runtimeSkeleton.localRotations
255
- for (let i = 0; i < this.skeleton.bones.length; i++) {
256
- const qi = i * 4
257
- if (rotations[qi + 3] === 0) {
258
- rotations[qi] = 0
259
- rotations[qi + 1] = 0
260
- rotations[qi + 2] = 0
261
- rotations[qi + 3] = 1
262
- }
263
- }
264
-
265
264
  // Initialize IK runtime state
266
265
  this.initializeIKRuntime()
267
266
  }
@@ -274,8 +273,8 @@ export class Model {
274
273
  const ikChainInfo: IKChainInfo[] = new Array(boneCount)
275
274
  for (let i = 0; i < boneCount; i++) {
276
275
  ikChainInfo[i] = {
277
- ikRotation: new Quat(0, 0, 0, 1),
278
- localRotation: new Quat(0, 0, 0, 1),
276
+ ikRotation: Quat.identity(),
277
+ localRotation: Quat.identity(),
279
278
  }
280
279
  }
281
280
 
@@ -306,18 +305,30 @@ export class Model {
306
305
  const boneCount = this.skeleton.bones.length
307
306
  const morphCount = this.morphing.morphs.length
308
307
 
308
+ // Pre-allocate Quat and Vec3 arrays to avoid reallocation during tweens
309
+ const rotStartQuat: Quat[] = new Array(boneCount)
310
+ const rotTargetQuat: Quat[] = new Array(boneCount)
311
+ const transStartVec: Vec3[] = new Array(boneCount)
312
+ const transTargetVec: Vec3[] = new Array(boneCount)
313
+ for (let i = 0; i < boneCount; i++) {
314
+ rotStartQuat[i] = Quat.identity()
315
+ rotTargetQuat[i] = Quat.identity()
316
+ transStartVec[i] = Vec3.zeros()
317
+ transTargetVec[i] = Vec3.zeros()
318
+ }
319
+
309
320
  this.tweenState = {
310
321
  // Bone rotation tweens
311
322
  rotActive: new Uint8Array(boneCount),
312
- rotStartQuat: new Float32Array(boneCount * 4),
313
- rotTargetQuat: new Float32Array(boneCount * 4),
323
+ rotStartQuat,
324
+ rotTargetQuat,
314
325
  rotStartTimeMs: new Float32Array(boneCount),
315
326
  rotDurationMs: new Float32Array(boneCount),
316
327
 
317
328
  // Bone translation tweens
318
329
  transActive: new Uint8Array(boneCount),
319
- transStartVec: new Float32Array(boneCount * 3),
320
- transTargetVec: new Float32Array(boneCount * 3),
330
+ transStartVec,
331
+ transTargetVec,
321
332
  transStartTimeMs: new Float32Array(boneCount),
322
333
  transDurationMs: new Float32Array(boneCount),
323
334
 
@@ -358,27 +369,10 @@ export class Model {
358
369
  const startMs = state.rotStartTimeMs[i]
359
370
  const durMs = Math.max(1, state.rotDurationMs[i])
360
371
  const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
361
- const e = easeInOut(t)
362
-
363
- const qi = i * 4
364
- const startQuat = new Quat(
365
- state.rotStartQuat[qi],
366
- state.rotStartQuat[qi + 1],
367
- state.rotStartQuat[qi + 2],
368
- state.rotStartQuat[qi + 3]
369
- )
370
- const targetQuat = new Quat(
371
- state.rotTargetQuat[qi],
372
- state.rotTargetQuat[qi + 1],
373
- state.rotTargetQuat[qi + 2],
374
- state.rotTargetQuat[qi + 3]
375
- )
376
- const result = Quat.slerp(startQuat, targetQuat, e)
372
+ const e = t // Linear interpolation
377
373
 
378
- rotations[qi] = result.x
379
- rotations[qi + 1] = result.y
380
- rotations[qi + 2] = result.z
381
- rotations[qi + 3] = result.w
374
+ const result = Quat.slerp(state.rotStartQuat[i], state.rotTargetQuat[i], e)
375
+ rotations[i].set(result)
382
376
 
383
377
  if (t >= 1) {
384
378
  state.rotActive[i] = 0
@@ -393,14 +387,13 @@ export class Model {
393
387
  const startMs = state.transStartTimeMs[i]
394
388
  const durMs = Math.max(1, state.transDurationMs[i])
395
389
  const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
396
- const e = easeInOut(t)
390
+ const e = t // Linear interpolation
397
391
 
398
- const ti = i * 3
399
- translations[ti] = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
400
- translations[ti + 1] =
401
- state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e
402
- translations[ti + 2] =
403
- state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e
392
+ const startVec = state.transStartVec[i]
393
+ const targetVec = state.transTargetVec[i]
394
+ translations[i].x = startVec.x + (targetVec.x - startVec.x) * e
395
+ translations[i].y = startVec.y + (targetVec.y - startVec.y) * e
396
+ translations[i].z = startVec.z + (targetVec.z - startVec.z) * e
404
397
 
405
398
  if (t >= 1) {
406
399
  state.transActive[i] = 0
@@ -416,7 +409,7 @@ export class Model {
416
409
  const startMs = state.morphStartTimeMs[i]
417
410
  const durMs = Math.max(1, state.morphDurationMs[i])
418
411
  const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
419
- const e = easeInOut(t)
412
+ const e = t // Linear interpolation
420
413
 
421
414
  const oldWeight = weights[i]
422
415
  weights[i] = state.morphStartWeight[i] + (state.morphTargetWeight[i] - state.morphStartWeight[i]) * e
@@ -483,7 +476,8 @@ export class Model {
483
476
 
484
477
  rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
485
478
  const state = this.tweenState
486
- const normalized = quats.map((q) => q.normalize())
479
+ // Clone and normalize to avoid mutating input
480
+ quats.forEach((q) => q.normalize())
487
481
  const now = this.tweenTimeMs
488
482
  const dur = durationMs && durationMs > 0 ? durationMs : 0
489
483
 
@@ -492,56 +486,38 @@ export class Model {
492
486
  const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
493
487
  if (idx < 0 || idx >= this.skeleton.bones.length) continue
494
488
 
495
- const qi = idx * 4
496
489
  const rotations = this.runtimeSkeleton.localRotations
497
- const [tx, ty, tz, tw] = normalized[i].toArray()
490
+ const targetNorm = quats[i]
498
491
 
499
492
  if (dur === 0) {
500
- rotations[qi] = tx
501
- rotations[qi + 1] = ty
502
- rotations[qi + 2] = tz
503
- rotations[qi + 3] = tw
493
+ rotations[idx].set(targetNorm)
504
494
  state.rotActive[idx] = 0
505
495
  continue
506
496
  }
507
497
 
508
- let sx = rotations[qi]
509
- let sy = rotations[qi + 1]
510
- let sz = rotations[qi + 2]
511
- let sw = rotations[qi + 3]
498
+ const currentRot = rotations[idx]
499
+ let sx = currentRot.x
500
+ let sy = currentRot.y
501
+ let sz = currentRot.z
502
+ let sw = currentRot.w
512
503
 
513
504
  if (state.rotActive[idx] === 1) {
514
505
  const startMs = state.rotStartTimeMs[idx]
515
506
  const prevDur = Math.max(1, state.rotDurationMs[idx])
516
507
  const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
517
- const e = easeInOut(t)
518
- const startQuat = new Quat(
519
- state.rotStartQuat[qi],
520
- state.rotStartQuat[qi + 1],
521
- state.rotStartQuat[qi + 2],
522
- state.rotStartQuat[qi + 3]
523
- )
524
- const targetQuat = new Quat(
525
- state.rotTargetQuat[qi],
526
- state.rotTargetQuat[qi + 1],
527
- state.rotTargetQuat[qi + 2],
528
- state.rotTargetQuat[qi + 3]
529
- )
530
- const result = Quat.slerp(startQuat, targetQuat, e)
508
+ const e = t // Linear interpolation
509
+ const result = Quat.slerp(state.rotStartQuat[idx], state.rotTargetQuat[idx], e)
531
510
  sx = result.x
532
511
  sy = result.y
533
512
  sz = result.z
534
513
  sw = result.w
535
514
  }
536
515
 
537
- state.rotStartQuat[qi] = sx
538
- state.rotStartQuat[qi + 1] = sy
539
- state.rotStartQuat[qi + 2] = sz
540
- state.rotStartQuat[qi + 3] = sw
541
- state.rotTargetQuat[qi] = tx
542
- state.rotTargetQuat[qi + 1] = ty
543
- state.rotTargetQuat[qi + 2] = tz
544
- state.rotTargetQuat[qi + 3] = tw
516
+ state.rotStartQuat[idx].x = sx
517
+ state.rotStartQuat[idx].y = sy
518
+ state.rotStartQuat[idx].z = sz
519
+ state.rotStartQuat[idx].w = sw
520
+ state.rotTargetQuat[idx].set(targetNorm)
545
521
  state.rotStartTimeMs[idx] = now
546
522
  state.rotDurationMs[idx] = dur
547
523
  state.rotActive[idx] = 1
@@ -554,7 +530,6 @@ export class Model {
554
530
  const state = this.tweenState
555
531
  const now = this.tweenTimeMs
556
532
  const dur = durationMs && durationMs > 0 ? durationMs : 0
557
- const localRot = this.runtimeSkeleton.localRotations
558
533
 
559
534
  // Compute bind pose world positions for all bones
560
535
  const skeleton = this.skeleton
@@ -575,9 +550,8 @@ export class Model {
575
550
  if (idx < 0 || idx >= this.skeleton.bones.length) continue
576
551
 
577
552
  const bone = this.skeleton.bones[idx]
578
- const ti = idx * 3
579
- const qi = idx * 4
580
553
  const translations = this.runtimeSkeleton.localTranslations
554
+ const localRot = this.runtimeSkeleton.localRotations
581
555
  const vmdRelativeTranslation = relativeTranslations[i]
582
556
 
583
557
  // VMD translation is relative to bind pose world position
@@ -591,7 +565,7 @@ export class Model {
591
565
  if (bone.parentIndex >= 0) {
592
566
  parentBindPoseWorldPos = computeBindPoseWorldPosition(bone.parentIndex)
593
567
  } else {
594
- parentBindPoseWorldPos = new Vec3(0, 0, 0)
568
+ parentBindPoseWorldPos = Vec3.zeros()
595
569
  }
596
570
 
597
571
  // Transform target world position to parent's local space
@@ -604,8 +578,9 @@ export class Model {
604
578
  )
605
579
 
606
580
  // Apply inverse rotation to get local translation
607
- const localRotation = new Quat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
608
- const invRotation = localRotation.conjugate().normalize()
581
+ const localRotation = localRot[idx]
582
+ // Clone to avoid mutating, then conjugate and normalize
583
+ const invRotation = localRotation.clone().conjugate().normalize()
609
584
  const rotationMat = Mat4.fromQuat(invRotation.x, invRotation.y, invRotation.z, invRotation.w)
610
585
  const rm = rotationMat.values
611
586
  const localTranslation = new Vec3(
@@ -617,33 +592,36 @@ export class Model {
617
592
  const [tx, ty, tz] = [localTranslation.x, localTranslation.y, localTranslation.z]
618
593
 
619
594
  if (dur === 0) {
620
- translations[ti] = tx
621
- translations[ti + 1] = ty
622
- translations[ti + 2] = tz
595
+ translations[idx].x = tx
596
+ translations[idx].y = ty
597
+ translations[idx].z = tz
623
598
  state.transActive[idx] = 0
624
599
  continue
625
600
  }
626
601
 
627
- let sx = translations[ti]
628
- let sy = translations[ti + 1]
629
- let sz = translations[ti + 2]
602
+ const currentTrans = translations[idx]
603
+ let sx = currentTrans.x
604
+ let sy = currentTrans.y
605
+ let sz = currentTrans.z
630
606
 
631
607
  if (state.transActive[idx] === 1) {
632
608
  const startMs = state.transStartTimeMs[idx]
633
609
  const prevDur = Math.max(1, state.transDurationMs[idx])
634
610
  const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
635
- const e = easeInOut(t)
636
- sx = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
637
- sy = state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e
638
- sz = state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e
611
+ const e = t // Linear interpolation
612
+ const startVec = state.transStartVec[idx]
613
+ const targetVec = state.transTargetVec[idx]
614
+ sx = startVec.x + (targetVec.x - startVec.x) * e
615
+ sy = startVec.y + (targetVec.y - startVec.y) * e
616
+ sz = startVec.z + (targetVec.z - startVec.z) * e
639
617
  }
640
618
 
641
- state.transStartVec[ti] = sx
642
- state.transStartVec[ti + 1] = sy
643
- state.transStartVec[ti + 2] = sz
644
- state.transTargetVec[ti] = tx
645
- state.transTargetVec[ti + 1] = ty
646
- state.transTargetVec[ti + 2] = tz
619
+ state.transStartVec[idx].x = sx
620
+ state.transStartVec[idx].y = sy
621
+ state.transStartVec[idx].z = sz
622
+ state.transTargetVec[idx].x = tx
623
+ state.transTargetVec[idx].y = ty
624
+ state.transTargetVec[idx].z = tz
647
625
  state.transStartTimeMs[idx] = now
648
626
  state.transDurationMs[idx] = dur
649
627
  state.transActive[idx] = 1
@@ -651,7 +629,14 @@ export class Model {
651
629
  }
652
630
 
653
631
  getBoneWorldMatrices(): Float32Array {
654
- return this.runtimeSkeleton.worldMatrices
632
+ // Convert Mat4[] to Float32Array for WebGPU compatibility
633
+ const boneCount = this.skeleton.bones.length
634
+ const worldMats = this.runtimeSkeleton.worldMatrices
635
+ const result = new Float32Array(boneCount * 16)
636
+ for (let i = 0; i < boneCount; i++) {
637
+ result.set(worldMats[i].values, i * 16)
638
+ }
639
+ return result
655
640
  }
656
641
 
657
642
  getBoneInverseBindMatrices(): Float32Array {
@@ -664,19 +649,18 @@ export class Model {
664
649
  const invBindMats = this.skeleton.inverseBindMatrices
665
650
 
666
651
  // Initialize cached array if needed or if bone count changed
667
- if (!this.cachedSkinMatrices || this.cachedSkinMatrices.length !== boneCount * 16) {
668
- this.cachedSkinMatrices = new Float32Array(boneCount * 16)
652
+ if (!this.skinMatricesArray || this.skinMatricesArray.length !== boneCount * 16) {
653
+ this.skinMatricesArray = new Float32Array(boneCount * 16)
669
654
  }
670
655
 
671
- const skinMatrices = this.cachedSkinMatrices
656
+ const skinMatrices = this.skinMatricesArray
672
657
 
673
658
  // Compute skin matrices: skinMatrix = worldMatrix × inverseBindMatrix
674
- // Use Mat4.multiplyArrays to avoid creating Mat4 objects
675
659
  for (let i = 0; i < boneCount; i++) {
676
- const worldOffset = i * 16
660
+ const worldMat = worldMats[i]
677
661
  const invBindOffset = i * 16
678
662
  const skinOffset = i * 16
679
- Mat4.multiplyArrays(worldMats, worldOffset, invBindMats, invBindOffset, skinMatrices, skinOffset)
663
+ Mat4.multiplyArrays(worldMat.values, 0, invBindMats, invBindOffset, skinMatrices, skinOffset)
680
664
  }
681
665
 
682
666
  return skinMatrices
@@ -707,7 +691,7 @@ export class Model {
707
691
  const startMs = state.morphStartTimeMs[idx]
708
692
  const prevDur = Math.max(1, state.morphDurationMs[idx])
709
693
  const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
710
- const e = easeInOut(t)
694
+ const e = t // Linear interpolation
711
695
  startWeight = state.morphStartWeight[idx] + (state.morphTargetWeight[idx] - state.morphStartWeight[idx]) * e
712
696
  }
713
697
 
@@ -973,18 +957,13 @@ export class Model {
973
957
  const boneIdx = this.runtimeSkeleton.nameIndex[boneName]
974
958
  if (boneIdx === undefined) continue
975
959
 
976
- const rotOffset = boneIdx * 4
977
- const transOffset = boneIdx * 3
960
+ const localRot = this.runtimeSkeleton.localRotations[boneIdx]
961
+ const localTrans = this.runtimeSkeleton.localTranslations[boneIdx]
978
962
 
979
963
  if (!frameB) {
980
964
  // No interpolation needed - direct assignment
981
- this.runtimeSkeleton.localRotations[rotOffset] = frameA.rotation.x
982
- this.runtimeSkeleton.localRotations[rotOffset + 1] = frameA.rotation.y
983
- this.runtimeSkeleton.localRotations[rotOffset + 2] = frameA.rotation.z
984
- this.runtimeSkeleton.localRotations[rotOffset + 3] = frameA.rotation.w
985
- this.runtimeSkeleton.localTranslations[transOffset] = frameA.translation.x
986
- this.runtimeSkeleton.localTranslations[transOffset + 1] = frameA.translation.y
987
- this.runtimeSkeleton.localTranslations[transOffset + 2] = frameA.translation.z
965
+ localRot.set(frameA.rotation)
966
+ localTrans.set(frameA.translation)
988
967
  } else {
989
968
  const timeA = keyFrames[idx].time
990
969
  const timeB = keyFrames[idx + 1].time
@@ -1001,7 +980,7 @@ export class Model {
1001
980
  gradient
1002
981
  )
1003
982
 
1004
- // Use Quat.slerp but extract components directly to avoid object allocation
983
+ // Use Quat.slerp to interpolate rotation
1005
984
  const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT)
1006
985
 
1007
986
  // Interpolate translation using bezier for each component
@@ -1019,17 +998,11 @@ export class Model {
1019
998
  const tyWeight = getWeight(16)
1020
999
  const tzWeight = getWeight(32)
1021
1000
 
1022
- // Direct array writes instead of Vec3 allocation
1023
- this.runtimeSkeleton.localRotations[rotOffset] = rotation.x
1024
- this.runtimeSkeleton.localRotations[rotOffset + 1] = rotation.y
1025
- this.runtimeSkeleton.localRotations[rotOffset + 2] = rotation.z
1026
- this.runtimeSkeleton.localRotations[rotOffset + 3] = rotation.w
1027
- this.runtimeSkeleton.localTranslations[transOffset] =
1028
- frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight
1029
- this.runtimeSkeleton.localTranslations[transOffset + 1] =
1030
- frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight
1031
- this.runtimeSkeleton.localTranslations[transOffset + 2] =
1032
- frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight
1001
+ // Direct property writes to avoid object allocation
1002
+ localRot.set(rotation)
1003
+ localTrans.x = frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight
1004
+ localTrans.y = frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight
1005
+ localTrans.z = frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight
1033
1006
  }
1034
1007
  }
1035
1008
 
@@ -1137,7 +1110,7 @@ export class Model {
1137
1110
  const bones = this.skeleton.bones
1138
1111
  const localRot = this.runtimeSkeleton.localRotations
1139
1112
  const localTrans = this.runtimeSkeleton.localTranslations
1140
- const worldBuf = this.runtimeSkeleton.worldMatrices
1113
+ const worldMats = this.runtimeSkeleton.worldMatrices
1141
1114
  const boneCount = bones.length
1142
1115
 
1143
1116
  if (boneCount === 0) return
@@ -1153,8 +1126,8 @@ export class Model {
1153
1126
  console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
1154
1127
  }
1155
1128
 
1156
- const qi = i * 4
1157
- let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
1129
+ const boneRot = localRot[i]
1130
+ let rotateM = Mat4.fromQuat(boneRot.x, boneRot.y, boneRot.z, boneRot.w)
1158
1131
  let addLocalTx = 0,
1159
1132
  addLocalTy = 0,
1160
1133
  addLocalTz = 0
@@ -1169,14 +1142,12 @@ export class Model {
1169
1142
  const hasRatio = Math.abs(ratio) > 1e-6
1170
1143
 
1171
1144
  if (hasRatio) {
1172
- const apQi = appendParentIdx * 4
1173
- const apTi = appendParentIdx * 3
1174
-
1175
1145
  if (b.appendRotate) {
1176
- let ax = localRot[apQi]
1177
- let ay = localRot[apQi + 1]
1178
- let az = localRot[apQi + 2]
1179
- const aw = localRot[apQi + 3]
1146
+ const appendRot = localRot[appendParentIdx]
1147
+ let ax = appendRot.x
1148
+ let ay = appendRot.y
1149
+ let az = appendRot.z
1150
+ const aw = appendRot.w
1180
1151
  const absRatio = ratio < 0 ? -ratio : ratio
1181
1152
  if (ratio < 0) {
1182
1153
  ax = -ax
@@ -1184,40 +1155,40 @@ export class Model {
1184
1155
  az = -az
1185
1156
  }
1186
1157
  const appendQuat = new Quat(ax, ay, az, aw)
1187
- const result = Quat.slerp(new Quat(0, 0, 0, 1), appendQuat, absRatio)
1158
+ const result = Quat.slerp(Quat.identity(), appendQuat, absRatio)
1188
1159
  rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
1189
1160
  }
1190
1161
 
1191
1162
  if (b.appendMove) {
1163
+ const appendTrans = localTrans[appendParentIdx]
1192
1164
  const appendRatio = b.appendRatio ?? 1
1193
- addLocalTx = localTrans[apTi] * appendRatio
1194
- addLocalTy = localTrans[apTi + 1] * appendRatio
1195
- addLocalTz = localTrans[apTi + 2] * appendRatio
1165
+ addLocalTx = appendTrans.x * appendRatio
1166
+ addLocalTy = appendTrans.y * appendRatio
1167
+ addLocalTz = appendTrans.z * appendRatio
1196
1168
  }
1197
1169
  }
1198
1170
  }
1199
1171
 
1200
1172
  // Build local matrix: identity + bind translation, then rotation, then local translation, then append translation
1201
- const ti = i * 3
1202
- const localTx = localTrans[ti] + addLocalTx
1203
- const localTy = localTrans[ti + 1] + addLocalTy
1204
- const localTz = localTrans[ti + 2] + addLocalTz
1173
+ const boneTrans = localTrans[i]
1174
+ const localTx = boneTrans.x + addLocalTx
1175
+ const localTy = boneTrans.y + addLocalTy
1176
+ const localTz = boneTrans.z + addLocalTz
1205
1177
  this.cachedIdentityMat1
1206
1178
  .setIdentity()
1207
1179
  .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
1208
1180
  this.cachedIdentityMat2.setIdentity().translateInPlace(localTx, localTy, localTz)
1209
1181
  const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
1210
1182
 
1211
- const worldOffset = i * 16
1183
+ const worldMat = worldMats[i]
1212
1184
  if (b.parentIndex >= 0) {
1213
1185
  const p = b.parentIndex
1214
1186
  if (!computed[p]) computeWorld(p)
1215
- const parentOffset = p * 16
1216
- // Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
1217
- Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
1218
- worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
1187
+ const parentMat = worldMats[p]
1188
+ // Multiply parent world matrix by local matrix
1189
+ Mat4.multiplyArrays(parentMat.values, 0, localM.values, 0, worldMat.values, 0)
1219
1190
  } else {
1220
- worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
1191
+ worldMat.values.set(localM.values)
1221
1192
  }
1222
1193
  computed[i] = true
1223
1194
  }
package/src/physics.ts CHANGED
@@ -460,12 +460,12 @@ export class Physics {
460
460
  // Reset physics state (reposition bodies, clear velocities)
461
461
  // Following babylon-mmd pattern: initialize all rigid body positions from current bone poses
462
462
  // Call this when starting a new animation to prevent physics instability from sudden pose changes
463
- reset(boneWorldMatrices: Float32Array, boneInverseBindMatrices: Float32Array): void {
463
+ reset(boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void {
464
464
  if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
465
465
  return
466
466
  }
467
467
 
468
- const boneCount = boneWorldMatrices.length / 16
468
+ const boneCount = boneWorldMatrices.length
469
469
  const Ammo = this.ammo
470
470
 
471
471
  // Ensure body offsets are computed
@@ -482,10 +482,9 @@ export class Physics {
482
482
  if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount) continue
483
483
 
484
484
  const boneIdx = rb.boneIndex
485
- const worldMatIdx = boneIdx * 16
486
485
 
487
486
  // Get bone world matrix
488
- const boneWorldMat = new Mat4(boneWorldMatrices.subarray(worldMatIdx, worldMatIdx + 16))
487
+ const boneWorldMat = boneWorldMatrices[boneIdx]
489
488
 
490
489
  // Compute body world matrix: bodyWorld = boneWorld × bodyOffsetMatrix
491
490
  // (like babylon-mmd: bodyWorldMatrix = bodyOffsetMatrix.multiplyToRef(bodyWorldMatrix))
@@ -531,13 +530,13 @@ export class Physics {
531
530
 
532
531
  // Syncs bones to rigidbodies, simulates dynamics, solves constraints
533
532
  // Modifies boneWorldMatrices in-place for dynamic rigidbodies that drive bones
534
- step(dt: number, boneWorldMatrices: Float32Array, boneInverseBindMatrices: Float32Array): void {
533
+ step(dt: number, boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void {
535
534
  // Wait for Ammo to initialize
536
535
  if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
537
536
  return
538
537
  }
539
538
 
540
- const boneCount = boneWorldMatrices.length / 16
539
+ const boneCount = boneWorldMatrices.length
541
540
 
542
541
  if (this.firstFrame) {
543
542
  if (!this.rigidbodiesInitialized) {
@@ -596,7 +595,7 @@ export class Physics {
596
595
  }
597
596
 
598
597
  // Position bodies based on current bone transforms (called on first frame only)
599
- private positionBodiesFromBones(boneWorldMatrices: Float32Array, boneCount: number): void {
598
+ private positionBodiesFromBones(boneWorldMatrices: Mat4[], boneCount: number): void {
600
599
  if (!this.ammo || !this.dynamicsWorld) return
601
600
 
602
601
  const Ammo = this.ammo
@@ -607,9 +606,7 @@ export class Physics {
607
606
  if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount) continue
608
607
 
609
608
  const boneIdx = rb.boneIndex
610
- const worldMatIdx = boneIdx * 16
611
-
612
- const boneWorldMat = new Mat4(boneWorldMatrices.subarray(worldMatIdx, worldMatIdx + 16))
609
+ const boneWorldMat = boneWorldMatrices[boneIdx]
613
610
 
614
611
  // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
615
612
  const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse()
@@ -646,11 +643,7 @@ export class Physics {
646
643
  }
647
644
 
648
645
  // Sync Static (FollowBone) and Kinematic rigidbodies to follow bone transforms
649
- private syncFromBones(
650
- boneWorldMatrices: Float32Array,
651
- boneInverseBindMatrices: Float32Array,
652
- boneCount: number
653
- ): void {
646
+ private syncFromBones(boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array, boneCount: number): void {
654
647
  if (!this.ammo || !this.dynamicsWorld) return
655
648
 
656
649
  const Ammo = this.ammo
@@ -667,9 +660,7 @@ export class Physics {
667
660
  rb.boneIndex < boneCount
668
661
  ) {
669
662
  const boneIdx = rb.boneIndex
670
- const worldMatIdx = boneIdx * 16
671
-
672
- const boneWorldMat = new Mat4(boneWorldMatrices.subarray(worldMatIdx, worldMatIdx + 16))
663
+ const boneWorldMat = boneWorldMatrices[boneIdx]
673
664
 
674
665
  // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
675
666
  const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse()
@@ -713,7 +704,7 @@ export class Physics {
713
704
 
714
705
  // Apply dynamic rigidbody world transforms to bone world matrices in-place
715
706
  private applyAmmoRigidbodiesToBones(
716
- boneWorldMatrices: Float32Array,
707
+ boneWorldMatrices: Mat4[],
717
708
  boneInverseBindMatrices: Float32Array,
718
709
  boneCount: number
719
710
  ): void {
@@ -727,7 +718,6 @@ export class Physics {
727
718
  // Only dynamic rigidbodies drive bones (Static/Kinematic follow bones)
728
719
  if (rb.type === RigidbodyType.Dynamic && rb.boneIndex >= 0 && rb.boneIndex < boneCount) {
729
720
  const boneIdx = rb.boneIndex
730
- const worldMatIdx = boneIdx * 16
731
721
 
732
722
  const transform = ammoBody.getWorldTransform()
733
723
  const origin = transform.getOrigin()
@@ -742,7 +732,7 @@ export class Physics {
742
732
 
743
733
  const values = boneWorldMat.values
744
734
  if (!isNaN(values[0]) && !isNaN(values[15]) && Math.abs(values[0]) < 1e6 && Math.abs(values[15]) < 1e6) {
745
- boneWorldMatrices.set(values, worldMatIdx)
735
+ boneWorldMatrices[boneIdx].values.set(values)
746
736
  } else {
747
737
  console.warn(`[Physics] Invalid bone world matrix for rigidbody ${i} (${rb.name}), skipping update`)
748
738
  }