reze-engine 0.12.3 → 0.13.1

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/math.ts CHANGED
@@ -69,6 +69,54 @@ export class Vec3 {
69
69
  this.z = other.z
70
70
  return this
71
71
  }
72
+
73
+ setXYZ(x: number, y: number, z: number): Vec3 {
74
+ this.x = x
75
+ this.y = y
76
+ this.z = z
77
+ return this
78
+ }
79
+
80
+ // out = a - b (no allocation)
81
+ static subtractInto(a: Vec3, b: Vec3, out: Vec3): Vec3 {
82
+ out.x = a.x - b.x
83
+ out.y = a.y - b.y
84
+ out.z = a.z - b.z
85
+ return out
86
+ }
87
+
88
+ // out = a × b (no allocation). Safe when out === a or out === b.
89
+ static crossInto(a: Vec3, b: Vec3, out: Vec3): Vec3 {
90
+ const ax = a.x, ay = a.y, az = a.z
91
+ const bx = b.x, by = b.y, bz = b.z
92
+ out.x = ay * bz - az * by
93
+ out.y = az * bx - ax * bz
94
+ out.z = ax * by - ay * bx
95
+ return out
96
+ }
97
+
98
+ // Read translation from Mat4 values array (column-major) into out.
99
+ static setFromMat4Translation(m: Float32Array, out: Vec3): Vec3 {
100
+ out.x = m[12]
101
+ out.y = m[13]
102
+ out.z = m[14]
103
+ return out
104
+ }
105
+
106
+ // Transform normal by the upper-left 3x3 of a Mat4 (column-major) into out.
107
+ // Safe when out === normal.
108
+ static transformMat4RotationInto(normal: Vec3, m: Float32Array, out: Vec3): Vec3 {
109
+ const nx = normal.x, ny = normal.y, nz = normal.z
110
+ out.x = m[0] * nx + m[4] * ny + m[8] * nz
111
+ out.y = m[1] * nx + m[5] * ny + m[9] * nz
112
+ out.z = m[2] * nx + m[6] * ny + m[10] * nz
113
+ return out
114
+ }
115
+
116
+ // In-place normalize returning length squared info via Vec3. Alias for normalize() but explicit.
117
+ normalizeInPlace(): Vec3 {
118
+ return this.normalize()
119
+ }
72
120
  }
73
121
 
74
122
  export class Quat {
@@ -204,6 +252,71 @@ export class Quat {
204
252
  return new Quat(s0 * a.x + s1 * bx, s0 * a.y + s1 * by, s0 * a.z + s1 * bz, s0 * a.w + s1 * bw)
205
253
  }
206
254
 
255
+ // out = a * b (quaternion multiplication, rotation composition).
256
+ // Safe when out === a or out === b.
257
+ static multiplyInto(a: Quat, b: Quat, out: Quat): Quat {
258
+ const ax = a.x, ay = a.y, az = a.z, aw = a.w
259
+ const bx = b.x, by = b.y, bz = b.z, bw = b.w
260
+ out.x = aw * bx + ax * bw + ay * bz - az * by
261
+ out.y = aw * by - ax * bz + ay * bw + az * bx
262
+ out.z = aw * bz + ax * by - ay * bx + az * bw
263
+ out.w = aw * bw - ax * bx - ay * by - az * bz
264
+ return out
265
+ }
266
+
267
+ // out = quat from axis (unnormalized) and angle.
268
+ static fromAxisAngleInto(ax: number, ay: number, az: number, angle: number, out: Quat): Quat {
269
+ const len = Math.sqrt(ax * ax + ay * ay + az * az)
270
+ const invLen = len > 0 ? 1 / len : 0
271
+ const nx = ax * invLen, ny = ay * invLen, nz = az * invLen
272
+ const half = angle * 0.5
273
+ const s = Math.sin(half), c = Math.cos(half)
274
+ out.x = nx * s
275
+ out.y = ny * s
276
+ out.z = nz * s
277
+ out.w = c
278
+ return out
279
+ }
280
+
281
+ // out = slerp(a, b, t). Safe when out === a or out === b.
282
+ static slerpInto(a: Quat, b: Quat, t: number, out: Quat): Quat {
283
+ let cos = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
284
+ let bx = b.x, by = b.y, bz = b.z, bw = b.w
285
+ if (cos < 0) {
286
+ cos = -cos
287
+ bx = -bx; by = -by; bz = -bz; bw = -bw
288
+ }
289
+ if (cos > 0.9995) {
290
+ const x = a.x + t * (bx - a.x)
291
+ const y = a.y + t * (by - a.y)
292
+ const z = a.z + t * (bz - a.z)
293
+ const w = a.w + t * (bw - a.w)
294
+ const invLen = 1 / Math.hypot(x, y, z, w)
295
+ out.x = x * invLen; out.y = y * invLen; out.z = z * invLen; out.w = w * invLen
296
+ return out
297
+ }
298
+ const theta0 = Math.acos(cos)
299
+ const sinTheta0 = Math.sin(theta0)
300
+ const theta = theta0 * t
301
+ const s0 = Math.sin(theta0 - theta) / sinTheta0
302
+ const s1 = Math.sin(theta) / sinTheta0
303
+ out.x = s0 * a.x + s1 * bx
304
+ out.y = s0 * a.y + s1 * by
305
+ out.z = s0 * a.z + s1 * bz
306
+ out.w = s0 * a.w + s1 * bw
307
+ return out
308
+ }
309
+
310
+ setXYZW(x: number, y: number, z: number, w: number): Quat {
311
+ this.x = x; this.y = y; this.z = z; this.w = w
312
+ return this
313
+ }
314
+
315
+ setIdentity(): Quat {
316
+ this.x = 0; this.y = 0; this.z = 0; this.w = 1
317
+ return this
318
+ }
319
+
207
320
  // Convert Euler angles to quaternion (ZXY order, left-handed, PMX format)
208
321
  static fromEuler(rotX: number, rotY: number, rotZ: number): Quat {
209
322
  const cx = Math.cos(rotX * 0.5)
@@ -371,6 +484,116 @@ export class Mat4 {
371
484
  return new Mat4(this.values.slice())
372
485
  }
373
486
 
487
+ // Write rotation matrix from quaternion into existing Float32Array (column-major).
488
+ static fromQuatInto(x: number, y: number, z: number, w: number, out: Float32Array, offset: number = 0): void {
489
+ const x2 = x + x, y2 = y + y, z2 = z + z
490
+ const xx = x * x2, xy = x * y2, xz = x * z2
491
+ const yy = y * y2, yz = y * z2, zz = z * z2
492
+ const wx = w * x2, wy = w * y2, wz = w * z2
493
+ out[offset + 0] = 1 - (yy + zz)
494
+ out[offset + 1] = xy + wz
495
+ out[offset + 2] = xz - wy
496
+ out[offset + 3] = 0
497
+ out[offset + 4] = xy - wz
498
+ out[offset + 5] = 1 - (xx + zz)
499
+ out[offset + 6] = yz + wx
500
+ out[offset + 7] = 0
501
+ out[offset + 8] = xz + wy
502
+ out[offset + 9] = yz - wx
503
+ out[offset + 10] = 1 - (xx + yy)
504
+ out[offset + 11] = 0
505
+ out[offset + 12] = 0
506
+ out[offset + 13] = 0
507
+ out[offset + 14] = 0
508
+ out[offset + 15] = 1
509
+ }
510
+
511
+ // Fused local transform: out = T(bindT) · R(quat) · T(localT).
512
+ // Result translation = bindT + R * localT; rotation column block = R.
513
+ // Column-major. Zero allocations.
514
+ static localTransformInto(
515
+ bx: number, by: number, bz: number,
516
+ qx: number, qy: number, qz: number, qw: number,
517
+ lx: number, ly: number, lz: number,
518
+ out: Float32Array
519
+ ): void {
520
+ const x2 = qx + qx, y2 = qy + qy, z2 = qz + qz
521
+ const xx = qx * x2, xy = qx * y2, xz = qx * z2
522
+ const yy = qy * y2, yz = qy * z2, zz = qz * z2
523
+ const wx = qw * x2, wy = qw * y2, wz = qw * z2
524
+ const m00 = 1 - (yy + zz), m01 = xy + wz, m02 = xz - wy
525
+ const m10 = xy - wz, m11 = 1 - (xx + zz), m12 = yz + wx
526
+ const m20 = xz + wy, m21 = yz - wx, m22 = 1 - (xx + yy)
527
+ out[0] = m00; out[1] = m01; out[2] = m02; out[3] = 0
528
+ out[4] = m10; out[5] = m11; out[6] = m12; out[7] = 0
529
+ out[8] = m20; out[9] = m21; out[10] = m22; out[11] = 0
530
+ out[12] = bx + m00 * lx + m10 * ly + m20 * lz
531
+ out[13] = by + m01 * lx + m11 * ly + m21 * lz
532
+ out[14] = bz + m02 * lx + m12 * ly + m22 * lz
533
+ out[15] = 1
534
+ }
535
+
536
+ // Write position+rotation transform into existing Float32Array.
537
+ static fromPositionRotationInto(
538
+ px: number, py: number, pz: number,
539
+ qx: number, qy: number, qz: number, qw: number,
540
+ out: Float32Array
541
+ ): void {
542
+ Mat4.fromQuatInto(qx, qy, qz, qw, out, 0)
543
+ out[12] = px
544
+ out[13] = py
545
+ out[14] = pz
546
+ }
547
+
548
+ // In-place 4x4 inverse into out array. Returns true on success, false if singular (out untouched).
549
+ static inverseInto(m: Float32Array, out: Float32Array): boolean {
550
+ const a00 = m[0], a01 = m[1], a02 = m[2], a03 = m[3]
551
+ const a10 = m[4], a11 = m[5], a12 = m[6], a13 = m[7]
552
+ const a20 = m[8], a21 = m[9], a22 = m[10], a23 = m[11]
553
+ const a30 = m[12], a31 = m[13], a32 = m[14], a33 = m[15]
554
+ const b00 = a00 * a11 - a01 * a10
555
+ const b01 = a00 * a12 - a02 * a10
556
+ const b02 = a00 * a13 - a03 * a10
557
+ const b03 = a01 * a12 - a02 * a11
558
+ const b04 = a01 * a13 - a03 * a11
559
+ const b05 = a02 * a13 - a03 * a12
560
+ const b06 = a20 * a31 - a21 * a30
561
+ const b07 = a20 * a32 - a22 * a30
562
+ const b08 = a20 * a33 - a23 * a30
563
+ const b09 = a21 * a32 - a22 * a31
564
+ const b10 = a21 * a33 - a23 * a31
565
+ const b11 = a22 * a33 - a23 * a32
566
+ let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06
567
+ if (Math.abs(det) < 1e-10) return false
568
+ det = 1.0 / det
569
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det
570
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det
571
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det
572
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det
573
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det
574
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det
575
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det
576
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det
577
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det
578
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det
579
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det
580
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det
581
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det
582
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det
583
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det
584
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det
585
+ return true
586
+ }
587
+
588
+ // Copy only the rotation (upper-left 3x3) of src into dst, zero out translation, identity w.
589
+ // Column-major in both.
590
+ static copyRotationInto(src: Float32Array, dst: Float32Array): void {
591
+ dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = 0
592
+ dst[4] = src[4]; dst[5] = src[5]; dst[6] = src[6]; dst[7] = 0
593
+ dst[8] = src[8]; dst[9] = src[9]; dst[10] = src[10]; dst[11] = 0
594
+ dst[12] = 0; dst[13] = 0; dst[14] = 0; dst[15] = 1
595
+ }
596
+
374
597
  static fromQuat(x: number, y: number, z: number, w: number): Mat4 {
375
598
  // Column-major rotation matrix from quaternion (matches glMatrix/WGSL)
376
599
  const out = new Float32Array(16)
@@ -424,6 +647,46 @@ export class Mat4 {
424
647
  return Mat4.toQuatFromArray(this.values, 0)
425
648
  }
426
649
 
650
+ // Extract quaternion from matrix array into an existing Quat (no allocation).
651
+ static toQuatFromArrayInto(m: Float32Array, offset: number, out: Quat): Quat {
652
+ const m00 = m[offset + 0], m01 = m[offset + 4], m02 = m[offset + 8]
653
+ const m10 = m[offset + 1], m11 = m[offset + 5], m12 = m[offset + 9]
654
+ const m20 = m[offset + 2], m21 = m[offset + 6], m22 = m[offset + 10]
655
+ const trace = m00 + m11 + m22
656
+ let x = 0, y = 0, z = 0, w = 1
657
+ if (trace > 0) {
658
+ const s = Math.sqrt(trace + 1.0) * 2
659
+ w = 0.25 * s
660
+ x = (m21 - m12) / s
661
+ y = (m02 - m20) / s
662
+ z = (m10 - m01) / s
663
+ } else if (m00 > m11 && m00 > m22) {
664
+ const s = Math.sqrt(1.0 + m00 - m11 - m22) * 2
665
+ w = (m21 - m12) / s
666
+ x = 0.25 * s
667
+ y = (m01 + m10) / s
668
+ z = (m02 + m20) / s
669
+ } else if (m11 > m22) {
670
+ const s = Math.sqrt(1.0 + m11 - m00 - m22) * 2
671
+ w = (m02 - m20) / s
672
+ x = (m01 + m10) / s
673
+ y = 0.25 * s
674
+ z = (m12 + m21) / s
675
+ } else {
676
+ const s = Math.sqrt(1.0 + m22 - m00 - m11) * 2
677
+ w = (m10 - m01) / s
678
+ x = (m02 + m20) / s
679
+ y = (m12 + m21) / s
680
+ z = 0.25 * s
681
+ }
682
+ const invLen = 1 / Math.hypot(x, y, z, w)
683
+ out.x = x * invLen
684
+ out.y = y * invLen
685
+ out.z = z * invLen
686
+ out.w = w * invLen
687
+ return out
688
+ }
689
+
427
690
  // Static method to extract quaternion from matrix array (avoids creating Mat4 object)
428
691
  static toQuatFromArray(m: Float32Array, offset: number): Quat {
429
692
  const m00 = m[offset + 0],
@@ -567,3 +830,31 @@ export class Mat4 {
567
830
  }
568
831
  }
569
832
 
833
+ // Preallocated scratch instances for hot paths. Each subsystem should use its own
834
+ // slot to avoid cross-call stomping. Bump the count if more call sites need scratch.
835
+ export const scratchMat4Values: Float32Array[] = [
836
+ new Float32Array(16),
837
+ new Float32Array(16),
838
+ new Float32Array(16),
839
+ new Float32Array(16),
840
+ new Float32Array(16),
841
+ new Float32Array(16),
842
+ ]
843
+
844
+ export const scratchVec3: Vec3[] = [
845
+ new Vec3(0, 0, 0),
846
+ new Vec3(0, 0, 0),
847
+ new Vec3(0, 0, 0),
848
+ new Vec3(0, 0, 0),
849
+ new Vec3(0, 0, 0),
850
+ new Vec3(0, 0, 0),
851
+ new Vec3(0, 0, 0),
852
+ new Vec3(0, 0, 0),
853
+ ]
854
+
855
+ export const scratchQuat: Quat[] = [
856
+ new Quat(0, 0, 0, 1),
857
+ new Quat(0, 0, 0, 1),
858
+ new Quat(0, 0, 0, 1),
859
+ new Quat(0, 0, 0, 1),
860
+ ]
package/src/model.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Mat4, Quat, Vec3 } from "./math"
1
+ import { Mat4, Quat, Vec3, scratchMat4Values, scratchQuat } from "./math"
2
2
  import { Engine } from "./engine"
3
3
  import { joinAssetPath, type AssetReader } from "./asset-reader"
4
4
  import { Rigidbody, Joint } from "./physics"
@@ -191,10 +191,6 @@ export class Model {
191
191
  private runtimeMorph!: MorphRuntime
192
192
  private morphsDirty: boolean = false // Flag indicating if morphs need to be applied
193
193
 
194
- // Cached identity matrices to avoid allocations in computeWorldMatrices
195
- private cachedIdentityMat1 = Mat4.identity()
196
- private cachedIdentityMat2 = Mat4.identity()
197
-
198
194
  // Cached skin matrices array to avoid allocations in getSkinMatrices
199
195
  private skinMatricesArray?: Float32Array
200
196
 
@@ -1205,17 +1201,24 @@ export class Model {
1205
1201
  }
1206
1202
 
1207
1203
  // Get base rotation
1208
- let boneRot = localRot[boneIndex]
1204
+ const baseRot = localRot[boneIndex]
1205
+ let fx = baseRot.x, fy = baseRot.y, fz = baseRot.z, fw = baseRot.w
1209
1206
 
1210
- // Apply IK rotation if requested
1207
+ // Apply IK rotation if requested: finalRot = ik * base, then normalize
1211
1208
  if (applyIK && ikChainInfo) {
1212
1209
  const chainInfo = ikChainInfo[boneIndex]
1213
1210
  if (chainInfo?.ikRotation) {
1214
- boneRot = chainInfo.ikRotation.multiply(boneRot).normalize()
1211
+ const ik = chainInfo.ikRotation
1212
+ const nx = ik.w * fx + ik.x * fw + ik.y * fz - ik.z * fy
1213
+ const ny = ik.w * fy - ik.x * fz + ik.y * fw + ik.z * fx
1214
+ const nz = ik.w * fz + ik.x * fy - ik.y * fx + ik.z * fw
1215
+ const nw = ik.w * fw - ik.x * fx - ik.y * fy - ik.z * fz
1216
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz + nw * nw)
1217
+ const inv = len > 0 ? 1 / len : 0
1218
+ fx = nx * inv; fy = ny * inv; fz = nz * inv; fw = nw * inv
1215
1219
  }
1216
1220
  }
1217
1221
 
1218
- let rotateM = Mat4.fromQuat(boneRot.x, boneRot.y, boneRot.z, boneRot.w)
1219
1222
  let addLocalTx = 0, addLocalTy = 0, addLocalTz = 0
1220
1223
 
1221
1224
  // Handle append transformations (same logic as computeWorldMatrices)
@@ -1231,26 +1234,29 @@ export class Model {
1231
1234
 
1232
1235
  if (hasRatio) {
1233
1236
  if (b.appendRotate) {
1234
- // Get append parent's rotation
1235
- // During IK solving, use only base local rotation (not IK rotations) to avoid
1236
- // conflicts with IK rotations that are still being computed incrementally
1237
- // IK rotations will be applied to localRotations after IK solving completes
1237
+ // Recurse first (may touch scratch); all scratch use below happens after it unwinds
1238
1238
  if (appendParentIdx >= 0) {
1239
- // Compute append parent's world matrix for dependency order, but use base rotation for append
1240
1239
  this.computeSingleBoneWorldMatrix(appendParentIdx, applyIK)
1241
1240
  }
1242
1241
 
1243
- // Use append parent's base local rotation only (IK rotations are applied after solving)
1244
- let appendRot = localRot[appendParentIdx]
1245
-
1242
+ const appendRot = localRot[appendParentIdx]
1246
1243
  let ax = appendRot.x, ay = appendRot.y, az = appendRot.z
1247
1244
  const aw = appendRot.w
1248
1245
  const absRatio = ratio < 0 ? -ratio : ratio
1249
1246
  if (ratio < 0) { ax = -ax; ay = -ay; az = -az }
1250
1247
 
1251
- const appendQuat = new Quat(ax, ay, az, aw)
1252
- const result = Quat.slerp(Quat.identity(), appendQuat, absRatio)
1253
- rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
1248
+ // slerp(identity, appendQuat, absRatio) into scratchQuat[1]
1249
+ scratchQuat[0].setXYZW(ax, ay, az, aw)
1250
+ scratchQuat[2].setIdentity()
1251
+ Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
1252
+
1253
+ // finalRot = slerpResult * finalRot (rotation composition as quat mul)
1254
+ const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
1255
+ const nx = sw * fx + sx * fw + sy * fz - sz * fy
1256
+ const ny = sw * fy - sx * fz + sy * fw + sz * fx
1257
+ const nz = sw * fz + sx * fy - sy * fx + sz * fw
1258
+ const nw = sw * fw - sx * fx - sy * fy - sz * fz
1259
+ fx = nx; fy = ny; fz = nz; fw = nw
1254
1260
  }
1255
1261
 
1256
1262
  if (b.appendMove) {
@@ -1267,18 +1273,21 @@ export class Model {
1267
1273
  const localTy = boneTrans.y + addLocalTy
1268
1274
  const localTz = boneTrans.z + addLocalTz
1269
1275
 
1270
- this.cachedIdentityMat1
1271
- .setIdentity()
1272
- .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
1273
- this.cachedIdentityMat2.setIdentity().translateInPlace(localTx, localTy, localTz)
1274
- const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
1276
+ // Fused local transform: T_bind · R(finalRot) · T_local → scratchMat4Values[0]
1277
+ const localMVals = scratchMat4Values[0]
1278
+ Mat4.localTransformInto(
1279
+ b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2],
1280
+ fx, fy, fz, fw,
1281
+ localTx, localTy, localTz,
1282
+ localMVals
1283
+ )
1275
1284
 
1276
1285
  const worldMat = worldMats[boneIndex]
1277
1286
  if (b.parentIndex >= 0) {
1278
1287
  const parentMat = worldMats[b.parentIndex]
1279
- Mat4.multiplyArrays(parentMat.values, 0, localM.values, 0, worldMat.values, 0)
1288
+ Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
1280
1289
  } else {
1281
- worldMat.values.set(localM.values)
1290
+ worldMat.values.set(localMVals)
1282
1291
  }
1283
1292
  }
1284
1293
 
@@ -1302,13 +1311,15 @@ export class Model {
1302
1311
  console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
1303
1312
  }
1304
1313
 
1314
+ // Ensure parent is computed FIRST, before we touch any scratch buffers.
1315
+ // Recursion may itself use scratchMat4Values[0] / scratchQuat; doing it up
1316
+ // front keeps the current frame's scratch slots untouched when we use them below.
1317
+ if (b.parentIndex >= 0 && !computed[b.parentIndex]) computeWorld(b.parentIndex)
1318
+
1305
1319
  const boneRot = localRot[i]
1306
- let rotateM = Mat4.fromQuat(boneRot.x, boneRot.y, boneRot.z, boneRot.w)
1307
- let addLocalTx = 0,
1308
- addLocalTy = 0,
1309
- addLocalTz = 0
1320
+ let fx = boneRot.x, fy = boneRot.y, fz = boneRot.z, fw = boneRot.w
1321
+ let addLocalTx = 0, addLocalTy = 0, addLocalTz = 0
1310
1322
 
1311
- // Optimized append rotation check - only check necessary conditions
1312
1323
  const appendParentIdx = b.appendParentIndex
1313
1324
  const hasAppend =
1314
1325
  b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
@@ -1320,19 +1331,22 @@ export class Model {
1320
1331
  if (hasRatio) {
1321
1332
  if (b.appendRotate) {
1322
1333
  const appendRot = localRot[appendParentIdx]
1323
- let ax = appendRot.x
1324
- let ay = appendRot.y
1325
- let az = appendRot.z
1334
+ let ax = appendRot.x, ay = appendRot.y, az = appendRot.z
1326
1335
  const aw = appendRot.w
1327
1336
  const absRatio = ratio < 0 ? -ratio : ratio
1328
- if (ratio < 0) {
1329
- ax = -ax
1330
- ay = -ay
1331
- az = -az
1332
- }
1333
- const appendQuat = new Quat(ax, ay, az, aw)
1334
- const result = Quat.slerp(Quat.identity(), appendQuat, absRatio)
1335
- rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
1337
+ if (ratio < 0) { ax = -ax; ay = -ay; az = -az }
1338
+
1339
+ scratchQuat[0].setXYZW(ax, ay, az, aw)
1340
+ scratchQuat[2].setIdentity()
1341
+ Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
1342
+
1343
+ // finalRot = slerpResult * finalRot (quat mul)
1344
+ const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
1345
+ const nx = sw * fx + sx * fw + sy * fz - sz * fy
1346
+ const ny = sw * fy - sx * fz + sy * fw + sz * fx
1347
+ const nz = sw * fz + sx * fy - sy * fx + sz * fw
1348
+ const nw = sw * fw - sx * fx - sy * fy - sz * fz
1349
+ fx = nx; fy = ny; fz = nz; fw = nw
1336
1350
  }
1337
1351
 
1338
1352
  if (b.appendMove) {
@@ -1345,26 +1359,25 @@ export class Model {
1345
1359
  }
1346
1360
  }
1347
1361
 
1348
- // Build local matrix: identity + bind translation, then rotation, then local translation, then append translation
1349
1362
  const boneTrans = localTrans[i]
1350
1363
  const localTx = boneTrans.x + addLocalTx
1351
1364
  const localTy = boneTrans.y + addLocalTy
1352
1365
  const localTz = boneTrans.z + addLocalTz
1353
- this.cachedIdentityMat1
1354
- .setIdentity()
1355
- .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
1356
- this.cachedIdentityMat2.setIdentity().translateInPlace(localTx, localTy, localTz)
1357
- const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
1366
+
1367
+ const localMVals = scratchMat4Values[0]
1368
+ Mat4.localTransformInto(
1369
+ b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2],
1370
+ fx, fy, fz, fw,
1371
+ localTx, localTy, localTz,
1372
+ localMVals
1373
+ )
1358
1374
 
1359
1375
  const worldMat = worldMats[i]
1360
1376
  if (b.parentIndex >= 0) {
1361
- const p = b.parentIndex
1362
- if (!computed[p]) computeWorld(p)
1363
- const parentMat = worldMats[p]
1364
- // Multiply parent world matrix by local matrix
1365
- Mat4.multiplyArrays(parentMat.values, 0, localM.values, 0, worldMat.values, 0)
1377
+ const parentMat = worldMats[b.parentIndex]
1378
+ Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
1366
1379
  } else {
1367
- worldMat.values.set(localM.values)
1380
+ worldMat.values.set(localMVals)
1368
1381
  }
1369
1382
  computed[i] = true
1370
1383
  }
package/src/physics.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import { Quat, Vec3, Mat4 } from "./math"
2
+
3
+ // Physics-local scratch pool for per-frame sync (syncFromBones, applyAmmoRigidbodiesToBones).
4
+ // Each method uses only these slots and completes synchronously before the next is called.
5
+ const _physMat: Float32Array[] = [
6
+ new Float32Array(16), new Float32Array(16), new Float32Array(16),
7
+ ]
8
+ const _physQuat = new Quat(0, 0, 0, 1)
2
9
  import { loadAmmo } from "./ammo-loader"
3
10
  import type { AmmoInstance } from "@fred3d/ammo"
4
11
 
@@ -500,11 +507,11 @@ export class Physics {
500
507
  }
501
508
 
502
509
  // Step order: 1) Sync Static/Kinematic from bones, 2) Step physics, 3) Apply dynamic to bones
503
- this.syncFromBones(boneWorldMatrices, boneInverseBindMatrices, boneCount)
510
+ this.syncFromBones(boneWorldMatrices, boneCount)
504
511
 
505
512
  this.stepAmmoPhysics(dt)
506
513
 
507
- this.applyAmmoRigidbodiesToBones(boneWorldMatrices, boneInverseBindMatrices, boneCount)
514
+ this.applyAmmoRigidbodiesToBones(boneWorldMatrices, boneCount)
508
515
  }
509
516
 
510
517
  // Compute bodyOffsetMatrixInverse for all rigidbodies (called once during initialization)
@@ -583,7 +590,7 @@ export class Physics {
583
590
  }
584
591
 
585
592
  // Sync Static (FollowBone) and Kinematic rigidbodies to follow bone transforms
586
- private syncFromBones(boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array, boneCount: number): void {
593
+ private syncFromBones(boneWorldMatrices: Mat4[], boneCount: number): void {
587
594
  if (!this.ammo || !this.dynamicsWorld) return
588
595
 
589
596
  const Ammo = this.ammo
@@ -602,16 +609,18 @@ export class Physics {
602
609
  const boneIdx = rb.boneIndex
603
610
  const boneWorldMat = boneWorldMatrices[boneIdx]
604
611
 
605
- // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
606
- const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse()
607
- const nodeWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix)
612
+ // Lazy-cache bodyOffsetMatrix on first hit (cold path).
613
+ if (!rb.bodyOffsetMatrix) rb.bodyOffsetMatrix = rb.bodyOffsetMatrixInverse.inverse()
608
614
 
609
- const worldPos = nodeWorldMatrix.getPosition()
610
- const worldRot = nodeWorldMatrix.toQuat()
615
+ // nodeWorld = boneWorld × bodyOffsetMatrix → _physMat[0]
616
+ Mat4.multiplyArrays(boneWorldMat.values, 0, rb.bodyOffsetMatrix.values, 0, _physMat[0], 0)
617
+ const nodeVals = _physMat[0]
618
+ const wx = nodeVals[12], wy = nodeVals[13], wz = nodeVals[14]
619
+ Mat4.toQuatFromArrayInto(nodeVals, 0, _physQuat)
611
620
 
612
621
  const transform = new Ammo.btTransform()
613
- const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z)
614
- const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w)
622
+ const pos = new Ammo.btVector3(wx, wy, wz)
623
+ const quat = new Ammo.btQuaternion(_physQuat.x, _physQuat.y, _physQuat.z, _physQuat.w)
615
624
 
616
625
  transform.setOrigin(pos)
617
626
  transform.setRotation(quat)
@@ -643,11 +652,7 @@ export class Physics {
643
652
  }
644
653
 
645
654
  // Apply dynamic rigidbody world transforms to bone world matrices in-place
646
- private applyAmmoRigidbodiesToBones(
647
- boneWorldMatrices: Mat4[],
648
- boneInverseBindMatrices: Float32Array,
649
- boneCount: number
650
- ): void {
655
+ private applyAmmoRigidbodiesToBones(boneWorldMatrices: Mat4[], boneCount: number): void {
651
656
  if (!this.ammo || !this.dynamicsWorld) return
652
657
 
653
658
  for (let i = 0; i < this.rigidbodies.length; i++) {
@@ -663,16 +668,19 @@ export class Physics {
663
668
  const origin = transform.getOrigin()
664
669
  const rotation = transform.getRotation()
665
670
 
666
- const nodePos = new Vec3(origin.x(), origin.y(), origin.z())
667
- const nodeRot = new Quat(rotation.x(), rotation.y(), rotation.z(), rotation.w())
668
- const nodeWorldMatrix = Mat4.fromPositionRotation(nodePos, nodeRot)
671
+ // nodeWorldMatrix _physMat[0] (from ammo position/rotation directly)
672
+ Mat4.fromPositionRotationInto(
673
+ origin.x(), origin.y(), origin.z(),
674
+ rotation.x(), rotation.y(), rotation.z(), rotation.w(),
675
+ _physMat[0]
676
+ )
669
677
 
670
- // boneWorld = nodeWorld × bodyOffsetMatrixInverse (not bodyOffsetMatrixInverse × nodeWorld)
671
- const boneWorldMat = nodeWorldMatrix.multiply(rb.bodyOffsetMatrixInverse)
678
+ // boneWorld = nodeWorld × bodyOffsetMatrixInverse _physMat[1]
679
+ const boneVals = _physMat[1]
680
+ Mat4.multiplyArrays(_physMat[0], 0, rb.bodyOffsetMatrixInverse.values, 0, boneVals, 0)
672
681
 
673
- const values = boneWorldMat.values
674
- if (!isNaN(values[0]) && !isNaN(values[15]) && Math.abs(values[0]) < 1e6 && Math.abs(values[15]) < 1e6) {
675
- boneWorldMatrices[boneIdx].values.set(values)
682
+ if (!isNaN(boneVals[0]) && !isNaN(boneVals[15]) && Math.abs(boneVals[0]) < 1e6 && Math.abs(boneVals[15]) < 1e6) {
683
+ boneWorldMatrices[boneIdx].values.set(boneVals)
676
684
  } else {
677
685
  console.warn(`[Physics] Invalid bone world matrix for rigidbody ${i} (${rb.name}), skipping update`)
678
686
  }