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/README.md +32 -29
- package/dist/ik-solver.d.ts +5 -6
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +163 -98
- package/dist/math.d.ts +20 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +300 -0
- package/dist/model.d.ts +0 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +61 -46
- package/dist/physics.d.ts.map +1 -1
- package/dist/physics.js +27 -19
- package/package.json +1 -1
- package/src/ik-solver.ts +164 -111
- package/src/math.ts +291 -0
- package/src/model.ts +68 -55
- package/src/physics.ts +31 -23
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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,
|
|
1288
|
+
Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
|
|
1280
1289
|
} else {
|
|
1281
|
-
worldMat.values.set(
|
|
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
|
|
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
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
|
1362
|
-
|
|
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(
|
|
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,
|
|
510
|
+
this.syncFromBones(boneWorldMatrices, boneCount)
|
|
504
511
|
|
|
505
512
|
this.stepAmmoPhysics(dt)
|
|
506
513
|
|
|
507
|
-
this.applyAmmoRigidbodiesToBones(boneWorldMatrices,
|
|
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[],
|
|
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
|
-
//
|
|
606
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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(
|
|
614
|
-
const quat = new Ammo.btQuaternion(
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
|
671
|
-
const
|
|
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
|
-
|
|
674
|
-
|
|
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
|
}
|