reze-engine 0.13.0 → 0.13.2
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 +9 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +113 -52
- 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 +124 -60
- 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"
|
|
@@ -167,6 +167,27 @@ export class Model {
|
|
|
167
167
|
this._name = value
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// Root transform public API. Instant setters — no tween baked in; wrap in
|
|
171
|
+
// your own lerp if you need smoothing. Changes are applied on the next
|
|
172
|
+
// getSkinMatrices() call (once per frame during rendering).
|
|
173
|
+
get position(): Vec3 {
|
|
174
|
+
return this._position
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get rotation(): Quat {
|
|
178
|
+
return this._rotation
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setPosition(position: Vec3): void {
|
|
182
|
+
this._position.set(position)
|
|
183
|
+
this.rootMatrixDirty = true
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setRotation(rotation: Quat): void {
|
|
187
|
+
this._rotation.set(rotation)
|
|
188
|
+
this.rootMatrixDirty = true
|
|
189
|
+
}
|
|
190
|
+
|
|
170
191
|
private vertexData: Float32Array<ArrayBuffer>
|
|
171
192
|
private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
|
|
172
193
|
private vertexCount: number
|
|
@@ -191,9 +212,15 @@ export class Model {
|
|
|
191
212
|
private runtimeMorph!: MorphRuntime
|
|
192
213
|
private morphsDirty: boolean = false // Flag indicating if morphs need to be applied
|
|
193
214
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
215
|
+
// Root transform — model's placement in world space, independent of bones.
|
|
216
|
+
// Folded into skin matrices (see getSkinMatrices) so every pass (main VS,
|
|
217
|
+
// shadow VS, any future skinned pass) sees it without per-shader plumbing.
|
|
218
|
+
// Skip-when-identity flag avoids the extra mat mul per bone when unused.
|
|
219
|
+
private _position: Vec3 = Vec3.zeros()
|
|
220
|
+
private _rotation: Quat = Quat.identity()
|
|
221
|
+
private rootMatrixValues: Float32Array = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])
|
|
222
|
+
private rootMatrixDirty: boolean = false
|
|
223
|
+
private rootIsIdentity: boolean = true
|
|
197
224
|
|
|
198
225
|
// Cached skin matrices array to avoid allocations in getSkinMatrices
|
|
199
226
|
private skinMatricesArray?: Float32Array
|
|
@@ -687,12 +714,32 @@ export class Model {
|
|
|
687
714
|
|
|
688
715
|
const skinMatrices = this.skinMatricesArray
|
|
689
716
|
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
717
|
+
// Rebuild root matrix + cache identity-shortcut flag only when pos/rot changed.
|
|
718
|
+
if (this.rootMatrixDirty) {
|
|
719
|
+
const p = this._position, r = this._rotation
|
|
720
|
+
Mat4.fromPositionRotationInto(p.x, p.y, p.z, r.x, r.y, r.z, r.w, this.rootMatrixValues)
|
|
721
|
+
this.rootIsIdentity =
|
|
722
|
+
p.x === 0 && p.y === 0 && p.z === 0 &&
|
|
723
|
+
r.x === 0 && r.y === 0 && r.z === 0 && r.w === 1
|
|
724
|
+
this.rootMatrixDirty = false
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (this.rootIsIdentity) {
|
|
728
|
+
// skinMatrix = worldMatrix × inverseBindMatrix
|
|
729
|
+
for (let i = 0; i < boneCount; i++) {
|
|
730
|
+
const off = i * 16
|
|
731
|
+
Mat4.multiplyArrays(worldMats[i].values, 0, invBindMats, off, skinMatrices, off)
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
// skinMatrix = rootMatrix × worldMatrix × inverseBindMatrix
|
|
735
|
+
// Two-mul path. scratchMat4Values[1] — [0] is owned by computeWorldMatrices.
|
|
736
|
+
const rootVals = this.rootMatrixValues
|
|
737
|
+
const tmp = scratchMat4Values[1]
|
|
738
|
+
for (let i = 0; i < boneCount; i++) {
|
|
739
|
+
const off = i * 16
|
|
740
|
+
Mat4.multiplyArrays(rootVals, 0, worldMats[i].values, 0, tmp, 0)
|
|
741
|
+
Mat4.multiplyArrays(tmp, 0, invBindMats, off, skinMatrices, off)
|
|
742
|
+
}
|
|
696
743
|
}
|
|
697
744
|
|
|
698
745
|
return skinMatrices
|
|
@@ -1205,17 +1252,24 @@ export class Model {
|
|
|
1205
1252
|
}
|
|
1206
1253
|
|
|
1207
1254
|
// Get base rotation
|
|
1208
|
-
|
|
1255
|
+
const baseRot = localRot[boneIndex]
|
|
1256
|
+
let fx = baseRot.x, fy = baseRot.y, fz = baseRot.z, fw = baseRot.w
|
|
1209
1257
|
|
|
1210
|
-
// Apply IK rotation if requested
|
|
1258
|
+
// Apply IK rotation if requested: finalRot = ik * base, then normalize
|
|
1211
1259
|
if (applyIK && ikChainInfo) {
|
|
1212
1260
|
const chainInfo = ikChainInfo[boneIndex]
|
|
1213
1261
|
if (chainInfo?.ikRotation) {
|
|
1214
|
-
|
|
1262
|
+
const ik = chainInfo.ikRotation
|
|
1263
|
+
const nx = ik.w * fx + ik.x * fw + ik.y * fz - ik.z * fy
|
|
1264
|
+
const ny = ik.w * fy - ik.x * fz + ik.y * fw + ik.z * fx
|
|
1265
|
+
const nz = ik.w * fz + ik.x * fy - ik.y * fx + ik.z * fw
|
|
1266
|
+
const nw = ik.w * fw - ik.x * fx - ik.y * fy - ik.z * fz
|
|
1267
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz + nw * nw)
|
|
1268
|
+
const inv = len > 0 ? 1 / len : 0
|
|
1269
|
+
fx = nx * inv; fy = ny * inv; fz = nz * inv; fw = nw * inv
|
|
1215
1270
|
}
|
|
1216
1271
|
}
|
|
1217
1272
|
|
|
1218
|
-
let rotateM = Mat4.fromQuat(boneRot.x, boneRot.y, boneRot.z, boneRot.w)
|
|
1219
1273
|
let addLocalTx = 0, addLocalTy = 0, addLocalTz = 0
|
|
1220
1274
|
|
|
1221
1275
|
// Handle append transformations (same logic as computeWorldMatrices)
|
|
@@ -1231,26 +1285,29 @@ export class Model {
|
|
|
1231
1285
|
|
|
1232
1286
|
if (hasRatio) {
|
|
1233
1287
|
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
|
|
1288
|
+
// Recurse first (may touch scratch); all scratch use below happens after it unwinds
|
|
1238
1289
|
if (appendParentIdx >= 0) {
|
|
1239
|
-
// Compute append parent's world matrix for dependency order, but use base rotation for append
|
|
1240
1290
|
this.computeSingleBoneWorldMatrix(appendParentIdx, applyIK)
|
|
1241
1291
|
}
|
|
1242
1292
|
|
|
1243
|
-
|
|
1244
|
-
let appendRot = localRot[appendParentIdx]
|
|
1245
|
-
|
|
1293
|
+
const appendRot = localRot[appendParentIdx]
|
|
1246
1294
|
let ax = appendRot.x, ay = appendRot.y, az = appendRot.z
|
|
1247
1295
|
const aw = appendRot.w
|
|
1248
1296
|
const absRatio = ratio < 0 ? -ratio : ratio
|
|
1249
1297
|
if (ratio < 0) { ax = -ax; ay = -ay; az = -az }
|
|
1250
1298
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1299
|
+
// slerp(identity, appendQuat, absRatio) into scratchQuat[1]
|
|
1300
|
+
scratchQuat[0].setXYZW(ax, ay, az, aw)
|
|
1301
|
+
scratchQuat[2].setIdentity()
|
|
1302
|
+
Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
|
|
1303
|
+
|
|
1304
|
+
// finalRot = slerpResult * finalRot (rotation composition as quat mul)
|
|
1305
|
+
const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
|
|
1306
|
+
const nx = sw * fx + sx * fw + sy * fz - sz * fy
|
|
1307
|
+
const ny = sw * fy - sx * fz + sy * fw + sz * fx
|
|
1308
|
+
const nz = sw * fz + sx * fy - sy * fx + sz * fw
|
|
1309
|
+
const nw = sw * fw - sx * fx - sy * fy - sz * fz
|
|
1310
|
+
fx = nx; fy = ny; fz = nz; fw = nw
|
|
1254
1311
|
}
|
|
1255
1312
|
|
|
1256
1313
|
if (b.appendMove) {
|
|
@@ -1267,18 +1324,21 @@ export class Model {
|
|
|
1267
1324
|
const localTy = boneTrans.y + addLocalTy
|
|
1268
1325
|
const localTz = boneTrans.z + addLocalTz
|
|
1269
1326
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1327
|
+
// Fused local transform: T_bind · R(finalRot) · T_local → scratchMat4Values[0]
|
|
1328
|
+
const localMVals = scratchMat4Values[0]
|
|
1329
|
+
Mat4.localTransformInto(
|
|
1330
|
+
b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2],
|
|
1331
|
+
fx, fy, fz, fw,
|
|
1332
|
+
localTx, localTy, localTz,
|
|
1333
|
+
localMVals
|
|
1334
|
+
)
|
|
1275
1335
|
|
|
1276
1336
|
const worldMat = worldMats[boneIndex]
|
|
1277
1337
|
if (b.parentIndex >= 0) {
|
|
1278
1338
|
const parentMat = worldMats[b.parentIndex]
|
|
1279
|
-
Mat4.multiplyArrays(parentMat.values, 0,
|
|
1339
|
+
Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
|
|
1280
1340
|
} else {
|
|
1281
|
-
worldMat.values.set(
|
|
1341
|
+
worldMat.values.set(localMVals)
|
|
1282
1342
|
}
|
|
1283
1343
|
}
|
|
1284
1344
|
|
|
@@ -1302,13 +1362,15 @@ export class Model {
|
|
|
1302
1362
|
console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
|
|
1303
1363
|
}
|
|
1304
1364
|
|
|
1365
|
+
// Ensure parent is computed FIRST, before we touch any scratch buffers.
|
|
1366
|
+
// Recursion may itself use scratchMat4Values[0] / scratchQuat; doing it up
|
|
1367
|
+
// front keeps the current frame's scratch slots untouched when we use them below.
|
|
1368
|
+
if (b.parentIndex >= 0 && !computed[b.parentIndex]) computeWorld(b.parentIndex)
|
|
1369
|
+
|
|
1305
1370
|
const boneRot = localRot[i]
|
|
1306
|
-
let
|
|
1307
|
-
let addLocalTx = 0,
|
|
1308
|
-
addLocalTy = 0,
|
|
1309
|
-
addLocalTz = 0
|
|
1371
|
+
let fx = boneRot.x, fy = boneRot.y, fz = boneRot.z, fw = boneRot.w
|
|
1372
|
+
let addLocalTx = 0, addLocalTy = 0, addLocalTz = 0
|
|
1310
1373
|
|
|
1311
|
-
// Optimized append rotation check - only check necessary conditions
|
|
1312
1374
|
const appendParentIdx = b.appendParentIndex
|
|
1313
1375
|
const hasAppend =
|
|
1314
1376
|
b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
|
|
@@ -1320,19 +1382,22 @@ export class Model {
|
|
|
1320
1382
|
if (hasRatio) {
|
|
1321
1383
|
if (b.appendRotate) {
|
|
1322
1384
|
const appendRot = localRot[appendParentIdx]
|
|
1323
|
-
let ax = appendRot.x
|
|
1324
|
-
let ay = appendRot.y
|
|
1325
|
-
let az = appendRot.z
|
|
1385
|
+
let ax = appendRot.x, ay = appendRot.y, az = appendRot.z
|
|
1326
1386
|
const aw = appendRot.w
|
|
1327
1387
|
const absRatio = ratio < 0 ? -ratio : ratio
|
|
1328
|
-
if (ratio < 0) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1388
|
+
if (ratio < 0) { ax = -ax; ay = -ay; az = -az }
|
|
1389
|
+
|
|
1390
|
+
scratchQuat[0].setXYZW(ax, ay, az, aw)
|
|
1391
|
+
scratchQuat[2].setIdentity()
|
|
1392
|
+
Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
|
|
1393
|
+
|
|
1394
|
+
// finalRot = slerpResult * finalRot (quat mul)
|
|
1395
|
+
const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
|
|
1396
|
+
const nx = sw * fx + sx * fw + sy * fz - sz * fy
|
|
1397
|
+
const ny = sw * fy - sx * fz + sy * fw + sz * fx
|
|
1398
|
+
const nz = sw * fz + sx * fy - sy * fx + sz * fw
|
|
1399
|
+
const nw = sw * fw - sx * fx - sy * fy - sz * fz
|
|
1400
|
+
fx = nx; fy = ny; fz = nz; fw = nw
|
|
1336
1401
|
}
|
|
1337
1402
|
|
|
1338
1403
|
if (b.appendMove) {
|
|
@@ -1345,26 +1410,25 @@ export class Model {
|
|
|
1345
1410
|
}
|
|
1346
1411
|
}
|
|
1347
1412
|
|
|
1348
|
-
// Build local matrix: identity + bind translation, then rotation, then local translation, then append translation
|
|
1349
1413
|
const boneTrans = localTrans[i]
|
|
1350
1414
|
const localTx = boneTrans.x + addLocalTx
|
|
1351
1415
|
const localTy = boneTrans.y + addLocalTy
|
|
1352
1416
|
const localTz = boneTrans.z + addLocalTz
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1417
|
+
|
|
1418
|
+
const localMVals = scratchMat4Values[0]
|
|
1419
|
+
Mat4.localTransformInto(
|
|
1420
|
+
b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2],
|
|
1421
|
+
fx, fy, fz, fw,
|
|
1422
|
+
localTx, localTy, localTz,
|
|
1423
|
+
localMVals
|
|
1424
|
+
)
|
|
1358
1425
|
|
|
1359
1426
|
const worldMat = worldMats[i]
|
|
1360
1427
|
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)
|
|
1428
|
+
const parentMat = worldMats[b.parentIndex]
|
|
1429
|
+
Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
|
|
1366
1430
|
} else {
|
|
1367
|
-
worldMat.values.set(
|
|
1431
|
+
worldMat.values.set(localMVals)
|
|
1368
1432
|
}
|
|
1369
1433
|
computed[i] = true
|
|
1370
1434
|
}
|