reze-engine 0.2.18 → 0.3.0

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
@@ -1,546 +1,555 @@
1
- // Easing function: ease-in-out quadratic
2
- export function easeInOut(t: number): number {
3
- return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
4
- }
5
-
6
- export class Vec3 {
7
- x: number
8
- y: number
9
- z: number
10
-
11
- constructor(x: number, y: number, z: number) {
12
- this.x = x
13
- this.y = y
14
- this.z = z
15
- }
16
-
17
- add(other: Vec3): Vec3 {
18
- return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z)
19
- }
20
-
21
- subtract(other: Vec3): Vec3 {
22
- return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z)
23
- }
24
-
25
- length(): number {
26
- return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
27
- }
28
-
29
- normalize(): Vec3 {
30
- const len = this.length()
31
- if (len === 0) return new Vec3(0, 0, 0)
32
- return new Vec3(this.x / len, this.y / len, this.z / len)
33
- }
34
-
35
- cross(other: Vec3): Vec3 {
36
- return new Vec3(
37
- this.y * other.z - this.z * other.y,
38
- this.z * other.x - this.x * other.z,
39
- this.x * other.y - this.y * other.x
40
- )
41
- }
42
-
43
- dot(other: Vec3): number {
44
- return this.x * other.x + this.y * other.y + this.z * other.z
45
- }
46
-
47
- scale(scalar: number): Vec3 {
48
- return new Vec3(this.x * scalar, this.y * scalar, this.z * scalar)
49
- }
50
-
51
- clone(): Vec3 {
52
- return new Vec3(this.x, this.y, this.z)
53
- }
54
- }
55
-
56
- export class Quat {
57
- x: number
58
- y: number
59
- z: number
60
- w: number
61
-
62
- constructor(x: number, y: number, z: number, w: number) {
63
- this.x = x
64
- this.y = y
65
- this.z = z
66
- this.w = w
67
- }
68
-
69
- add(other: Quat): Quat {
70
- return new Quat(this.x + other.x, this.y + other.y, this.z + other.z, this.w + other.w)
71
- }
72
-
73
- clone(): Quat {
74
- return new Quat(this.x, this.y, this.z, this.w)
75
- }
76
-
77
- multiply(other: Quat): Quat {
78
- // Proper quaternion multiplication (not component-wise)
79
- return new Quat(
80
- this.w * other.x + this.x * other.w + this.y * other.z - this.z * other.y,
81
- this.w * other.y - this.x * other.z + this.y * other.w + this.z * other.x,
82
- this.w * other.z + this.x * other.y - this.y * other.x + this.z * other.w,
83
- this.w * other.w - this.x * other.x - this.y * other.y - this.z * other.z
84
- )
85
- }
86
-
87
- conjugate(): Quat {
88
- // Conjugate (inverse for unit quaternions): (x, y, z, w) -> (-x, -y, -z, w)
89
- return new Quat(-this.x, -this.y, -this.z, this.w)
90
- }
91
-
92
- length(): number {
93
- return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w)
94
- }
95
-
96
- normalize(): Quat {
97
- const len = this.length()
98
- if (len === 0) return new Quat(0, 0, 0, 1)
99
- return new Quat(this.x / len, this.y / len, this.z / len, this.w / len)
100
- }
101
-
102
- // Rotate a vector by this quaternion: result = q * v * q^-1
103
- rotateVec(v: Vec3): Vec3 {
104
- // Treat v as pure quaternion (x, y, z, 0)
105
- const qx = this.x,
106
- qy = this.y,
107
- qz = this.z,
108
- qw = this.w
109
- const vx = v.x,
110
- vy = v.y,
111
- vz = v.z
112
-
113
- // t = 2 * cross(q.xyz, v)
114
- const tx = 2 * (qy * vz - qz * vy)
115
- const ty = 2 * (qz * vx - qx * vz)
116
- const tz = 2 * (qx * vy - qy * vx)
117
-
118
- // result = v + q.w * t + cross(q.xyz, t)
119
- return new Vec3(
120
- vx + qw * tx + (qy * tz - qz * ty),
121
- vy + qw * ty + (qz * tx - qx * tz),
122
- vz + qw * tz + (qx * ty - qy * tx)
123
- )
124
- }
125
-
126
- // Rotate a vector by this quaternion (Babylon.js style naming)
127
- rotate(v: Vec3): Vec3 {
128
- const qv = new Vec3(this.x, this.y, this.z)
129
- const uv = qv.cross(v)
130
- const uuv = qv.cross(uv)
131
- return v.add(uv.scale(2 * this.w)).add(uuv.scale(2))
132
- }
133
-
134
- // Static method: create quaternion that rotates from one direction to another
135
- static fromTo(from: Vec3, to: Vec3): Quat {
136
- const dot = from.dot(to)
137
- if (dot > 0.999999) return new Quat(0, 0, 0, 1) // Already aligned
138
- if (dot < -0.999999) {
139
- // 180 degrees
140
- let axis = from.cross(new Vec3(1, 0, 0))
141
- if (axis.length() < 0.001) axis = from.cross(new Vec3(0, 1, 0))
142
- return new Quat(axis.x, axis.y, axis.z, 0).normalize()
143
- }
144
-
145
- const axis = from.cross(to)
146
- const w = Math.sqrt((1 + dot) * 2)
147
- const invW = 1 / w
148
- return new Quat(axis.x * invW, axis.y * invW, axis.z * invW, w * 0.5).normalize()
149
- }
150
-
151
- toArray(): [number, number, number, number] {
152
- return [this.x, this.y, this.z, this.w]
153
- }
154
-
155
- // Spherical linear interpolation between two quaternions
156
- static slerp(a: Quat, b: Quat, t: number): Quat {
157
- let cos = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
158
- let bx = b.x,
159
- by = b.y,
160
- bz = b.z,
161
- bw = b.w
162
-
163
- // If dot product is negative, negate one quaternion to take shorter path
164
- if (cos < 0) {
165
- cos = -cos
166
- bx = -bx
167
- by = -by
168
- bz = -bz
169
- bw = -bw
170
- }
171
-
172
- // If quaternions are very close, use linear interpolation
173
- if (cos > 0.9995) {
174
- const x = a.x + t * (bx - a.x)
175
- const y = a.y + t * (by - a.y)
176
- const z = a.z + t * (bz - a.z)
177
- const w = a.w + t * (bw - a.w)
178
- const invLen = 1 / Math.hypot(x, y, z, w)
179
- return new Quat(x * invLen, y * invLen, z * invLen, w * invLen)
180
- }
181
-
182
- // Standard SLERP
183
- const theta0 = Math.acos(cos)
184
- const sinTheta0 = Math.sin(theta0)
185
- const theta = theta0 * t
186
- const s0 = Math.sin(theta0 - theta) / sinTheta0
187
- const s1 = Math.sin(theta) / sinTheta0
188
- return new Quat(s0 * a.x + s1 * bx, s0 * a.y + s1 * by, s0 * a.z + s1 * bz, s0 * a.w + s1 * bw)
189
- }
190
-
191
- // Convert Euler angles to quaternion (ZXY order, left-handed, PMX format)
192
- static fromEuler(rotX: number, rotY: number, rotZ: number): Quat {
193
- const cx = Math.cos(rotX * 0.5)
194
- const sx = Math.sin(rotX * 0.5)
195
- const cy = Math.cos(rotY * 0.5)
196
- const sy = Math.sin(rotY * 0.5)
197
- const cz = Math.cos(rotZ * 0.5)
198
- const sz = Math.sin(rotZ * 0.5)
199
-
200
- const w = cy * cx * cz + sy * sx * sz
201
- const x = cy * sx * cz + sy * cx * sz
202
- const y = sy * cx * cz - cy * sx * sz
203
- const z = cy * cx * sz - sy * sx * cz
204
-
205
- return new Quat(x, y, z, w).normalize()
206
- }
207
-
208
- // Convert quaternion to Euler angles (ZXY order, inverse of fromEuler)
209
- toEuler(): Vec3 {
210
- const qx = this.x
211
- const qy = this.y
212
- const qz = this.z
213
- const qw = this.w
214
-
215
- // ZXY order (left-handed)
216
- // Roll (X): rotation around X axis
217
- const sinr_cosp = 2 * (qw * qx + qy * qz)
218
- const cosr_cosp = 1 - 2 * (qx * qx + qy * qy)
219
- const rotX = Math.atan2(sinr_cosp, cosr_cosp)
220
-
221
- // Pitch (Y): rotation around Y axis
222
- const sinp = 2 * (qw * qy - qz * qx)
223
- const rotY = Math.abs(sinp) >= 1 ? (sinp >= 0 ? Math.PI / 2 : -Math.PI / 2) : Math.asin(sinp)
224
-
225
- // Yaw (Z): rotation around Z axis
226
- const siny_cosp = 2 * (qw * qz + qx * qy)
227
- const cosy_cosp = 1 - 2 * (qy * qy + qz * qz)
228
- const rotZ = Math.atan2(siny_cosp, cosy_cosp)
229
-
230
- return new Vec3(rotX, rotY, rotZ)
231
- }
232
- }
233
-
234
- export class Mat4 {
235
- values: Float32Array
236
-
237
- constructor(values: Float32Array) {
238
- this.values = values
239
- }
240
-
241
- static identity(): Mat4 {
242
- return new Mat4(new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]))
243
- }
244
-
245
- // Perspective matrix for LEFT-HANDED coordinate system (Z+ forward)
246
- // For left-handed: Z goes from 0 (near) to 1 (far), +Z is forward
247
- static perspective(fov: number, aspect: number, near: number, far: number): Mat4 {
248
- const f = 1.0 / Math.tan(fov / 2)
249
- const rangeInv = 1.0 / (far - near) // Positive for left-handed
250
-
251
- return new Mat4(
252
- new Float32Array([
253
- f / aspect,
254
- 0,
255
- 0,
256
- 0,
257
- 0,
258
- f,
259
- 0,
260
- 0,
261
- 0,
262
- 0,
263
- (far + near) * rangeInv,
264
- 1, // Positive for left-handed (Z+ forward)
265
- 0,
266
- 0,
267
- -near * far * rangeInv * 2, // Negated for left-handed
268
- 0,
269
- ])
270
- )
271
- }
272
-
273
- // LookAt matrix for LEFT-HANDED coordinate system (Z+ forward)
274
- // For left-handed: camera looks along +Z direction
275
- static lookAt(eye: Vec3, target: Vec3, up: Vec3): Mat4 {
276
- // In left-handed: forward = target - eye (Z+ direction)
277
- const forward = target.subtract(eye).normalize()
278
- const right = up.cross(forward).normalize() // X+ is right
279
- const upVec = forward.cross(right).normalize() // Y+ is up
280
-
281
- return new Mat4(
282
- new Float32Array([
283
- right.x,
284
- upVec.x,
285
- forward.x,
286
- 0,
287
- right.y,
288
- upVec.y,
289
- forward.y,
290
- 0,
291
- right.z,
292
- upVec.z,
293
- forward.z,
294
- 0,
295
- -right.dot(eye),
296
- -upVec.dot(eye),
297
- -forward.dot(eye),
298
- 1,
299
- ])
300
- )
301
- }
302
-
303
- multiply(other: Mat4): Mat4 {
304
- // Column-major multiplication (matches WGSL/GLSL convention):
305
- // result = a * b
306
- const out = new Float32Array(16)
307
- const a = this.values
308
- const b = other.values
309
- for (let c = 0; c < 4; c++) {
310
- const b0 = b[c * 4 + 0]
311
- const b1 = b[c * 4 + 1]
312
- const b2 = b[c * 4 + 2]
313
- const b3 = b[c * 4 + 3]
314
- out[c * 4 + 0] = a[0] * b0 + a[4] * b1 + a[8] * b2 + a[12] * b3
315
- out[c * 4 + 1] = a[1] * b0 + a[5] * b1 + a[9] * b2 + a[13] * b3
316
- out[c * 4 + 2] = a[2] * b0 + a[6] * b1 + a[10] * b2 + a[14] * b3
317
- out[c * 4 + 3] = a[3] * b0 + a[7] * b1 + a[11] * b2 + a[15] * b3
318
- }
319
- return new Mat4(out)
320
- }
321
-
322
- // Static method to multiply two matrix array segments directly into output array (no object creation)
323
- // Column-major multiplication: result = a * b
324
- static multiplyArrays(
325
- a: Float32Array,
326
- aOffset: number,
327
- b: Float32Array,
328
- bOffset: number,
329
- out: Float32Array,
330
- outOffset: number
331
- ): void {
332
- for (let c = 0; c < 4; c++) {
333
- const b0 = b[bOffset + c * 4 + 0]
334
- const b1 = b[bOffset + c * 4 + 1]
335
- const b2 = b[bOffset + c * 4 + 2]
336
- const b3 = b[bOffset + c * 4 + 3]
337
- out[outOffset + c * 4 + 0] =
338
- a[aOffset + 0] * b0 + a[aOffset + 4] * b1 + a[aOffset + 8] * b2 + a[aOffset + 12] * b3
339
- out[outOffset + c * 4 + 1] =
340
- a[aOffset + 1] * b0 + a[aOffset + 5] * b1 + a[aOffset + 9] * b2 + a[aOffset + 13] * b3
341
- out[outOffset + c * 4 + 2] =
342
- a[aOffset + 2] * b0 + a[aOffset + 6] * b1 + a[aOffset + 10] * b2 + a[aOffset + 14] * b3
343
- out[outOffset + c * 4 + 3] =
344
- a[aOffset + 3] * b0 + a[aOffset + 7] * b1 + a[aOffset + 11] * b2 + a[aOffset + 15] * b3
345
- }
346
- }
347
-
348
- clone(): Mat4 {
349
- return new Mat4(this.values.slice())
350
- }
351
-
352
- static fromQuat(x: number, y: number, z: number, w: number): Mat4 {
353
- // Column-major rotation matrix from quaternion (matches glMatrix/WGSL)
354
- const out = new Float32Array(16)
355
- const x2 = x + x,
356
- y2 = y + y,
357
- z2 = z + z
358
- const xx = x * x2,
359
- xy = x * y2,
360
- xz = x * z2
361
- const yy = y * y2,
362
- yz = y * z2,
363
- zz = z * z2
364
- const wx = w * x2,
365
- wy = w * y2,
366
- wz = w * z2
367
- out[0] = 1 - (yy + zz)
368
- out[1] = xy + wz
369
- out[2] = xz - wy
370
- out[3] = 0
371
- out[4] = xy - wz
372
- out[5] = 1 - (xx + zz)
373
- out[6] = yz + wx
374
- out[7] = 0
375
- out[8] = xz + wy
376
- out[9] = yz - wx
377
- out[10] = 1 - (xx + yy)
378
- out[11] = 0
379
- out[12] = 0
380
- out[13] = 0
381
- out[14] = 0
382
- out[15] = 1
383
- return new Mat4(out)
384
- }
385
-
386
- // Create transform matrix from position and rotation
387
- static fromPositionRotation(position: Vec3, rotation: Quat): Mat4 {
388
- const rotMat = Mat4.fromQuat(rotation.x, rotation.y, rotation.z, rotation.w)
389
- rotMat.values[12] = position.x
390
- rotMat.values[13] = position.y
391
- rotMat.values[14] = position.z
392
- return rotMat
393
- }
394
-
395
- // Extract position from transform matrix
396
- getPosition(): Vec3 {
397
- return new Vec3(this.values[12], this.values[13], this.values[14])
398
- }
399
-
400
- // Extract quaternion rotation from this matrix (upper-left 3x3 rotation part)
401
- toQuat(): Quat {
402
- return Mat4.toQuatFromArray(this.values, 0)
403
- }
404
-
405
- // Static method to extract quaternion from matrix array (avoids creating Mat4 object)
406
- static toQuatFromArray(m: Float32Array, offset: number): Quat {
407
- const m00 = m[offset + 0],
408
- m01 = m[offset + 4],
409
- m02 = m[offset + 8]
410
- const m10 = m[offset + 1],
411
- m11 = m[offset + 5],
412
- m12 = m[offset + 9]
413
- const m20 = m[offset + 2],
414
- m21 = m[offset + 6],
415
- m22 = m[offset + 10]
416
- const trace = m00 + m11 + m22
417
- let x = 0,
418
- y = 0,
419
- z = 0,
420
- w = 1
421
- if (trace > 0) {
422
- const s = Math.sqrt(trace + 1.0) * 2
423
- w = 0.25 * s
424
- x = (m21 - m12) / s
425
- y = (m02 - m20) / s
426
- z = (m10 - m01) / s
427
- } else if (m00 > m11 && m00 > m22) {
428
- const s = Math.sqrt(1.0 + m00 - m11 - m22) * 2
429
- w = (m21 - m12) / s
430
- x = 0.25 * s
431
- y = (m01 + m10) / s
432
- z = (m02 + m20) / s
433
- } else if (m11 > m22) {
434
- const s = Math.sqrt(1.0 + m11 - m00 - m22) * 2
435
- w = (m02 - m20) / s
436
- x = (m01 + m10) / s
437
- y = 0.25 * s
438
- z = (m12 + m21) / s
439
- } else {
440
- const s = Math.sqrt(1.0 + m22 - m00 - m11) * 2
441
- w = (m10 - m01) / s
442
- x = (m02 + m20) / s
443
- y = (m12 + m21) / s
444
- z = 0.25 * s
445
- }
446
- const invLen = 1 / Math.hypot(x, y, z, w)
447
- return new Quat(x * invLen, y * invLen, z * invLen, w * invLen)
448
- }
449
-
450
- // Reset matrix to identity in place
451
- setIdentity(): this {
452
- const v = this.values
453
- v[0] = 1
454
- v[1] = 0
455
- v[2] = 0
456
- v[3] = 0
457
- v[4] = 0
458
- v[5] = 1
459
- v[6] = 0
460
- v[7] = 0
461
- v[8] = 0
462
- v[9] = 0
463
- v[10] = 1
464
- v[11] = 0
465
- v[12] = 0
466
- v[13] = 0
467
- v[14] = 0
468
- v[15] = 1
469
- return this
470
- }
471
-
472
- translateInPlace(tx: number, ty: number, tz: number): this {
473
- this.values[12] += tx
474
- this.values[13] += ty
475
- this.values[14] += tz
476
- return this
477
- }
478
-
479
- // Full 4x4 matrix inverse using adjugate method
480
- // This works for any invertible matrix, not just orthonormal transforms
481
- // The previous implementation assumed orthonormal rotation matrices, which fails
482
- // when matrices have scaling or are not perfectly orthonormal (e.g., after
483
- // bone hierarchy transformations)
484
- inverse(): Mat4 {
485
- const m = this.values
486
- const out = new Float32Array(16)
487
-
488
- const a00 = m[0],
489
- a01 = m[1],
490
- a02 = m[2],
491
- a03 = m[3]
492
- const a10 = m[4],
493
- a11 = m[5],
494
- a12 = m[6],
495
- a13 = m[7]
496
- const a20 = m[8],
497
- a21 = m[9],
498
- a22 = m[10],
499
- a23 = m[11]
500
- const a30 = m[12],
501
- a31 = m[13],
502
- a32 = m[14],
503
- a33 = m[15]
504
-
505
- const b00 = a00 * a11 - a01 * a10
506
- const b01 = a00 * a12 - a02 * a10
507
- const b02 = a00 * a13 - a03 * a10
508
- const b03 = a01 * a12 - a02 * a11
509
- const b04 = a01 * a13 - a03 * a11
510
- const b05 = a02 * a13 - a03 * a12
511
- const b06 = a20 * a31 - a21 * a30
512
- const b07 = a20 * a32 - a22 * a30
513
- const b08 = a20 * a33 - a23 * a30
514
- const b09 = a21 * a32 - a22 * a31
515
- const b10 = a21 * a33 - a23 * a31
516
- const b11 = a22 * a33 - a23 * a32
517
-
518
- let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06
519
-
520
- if (Math.abs(det) < 1e-10) {
521
- console.warn("Matrix is not invertible (determinant near zero)")
522
- return Mat4.identity()
523
- }
524
-
525
- det = 1.0 / det
526
-
527
- out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det
528
- out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det
529
- out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det
530
- out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det
531
- out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det
532
- out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det
533
- out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det
534
- out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det
535
- out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det
536
- out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det
537
- out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det
538
- out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det
539
- out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det
540
- out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det
541
- out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det
542
- out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det
543
-
544
- return new Mat4(out)
545
- }
546
- }
1
+ // Easing function: ease-in-out quadratic
2
+ export function easeInOut(t: number): number {
3
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
4
+ }
5
+
6
+ export class Vec3 {
7
+ x: number
8
+ y: number
9
+ z: number
10
+
11
+ constructor(x: number, y: number, z: number) {
12
+ this.x = x
13
+ this.y = y
14
+ this.z = z
15
+ }
16
+
17
+ add(other: Vec3): Vec3 {
18
+ return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z)
19
+ }
20
+
21
+ subtract(other: Vec3): Vec3 {
22
+ return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z)
23
+ }
24
+
25
+ length(): number {
26
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
27
+ }
28
+
29
+ normalize(): Vec3 {
30
+ const len = this.length()
31
+ if (len === 0) return new Vec3(0, 0, 0)
32
+ return new Vec3(this.x / len, this.y / len, this.z / len)
33
+ }
34
+
35
+ cross(other: Vec3): Vec3 {
36
+ return new Vec3(
37
+ this.y * other.z - this.z * other.y,
38
+ this.z * other.x - this.x * other.z,
39
+ this.x * other.y - this.y * other.x
40
+ )
41
+ }
42
+
43
+ dot(other: Vec3): number {
44
+ return this.x * other.x + this.y * other.y + this.z * other.z
45
+ }
46
+
47
+ scale(scalar: number): Vec3 {
48
+ return new Vec3(this.x * scalar, this.y * scalar, this.z * scalar)
49
+ }
50
+
51
+ clone(): Vec3 {
52
+ return new Vec3(this.x, this.y, this.z)
53
+ }
54
+ }
55
+
56
+ export class Quat {
57
+ x: number
58
+ y: number
59
+ z: number
60
+ w: number
61
+
62
+ constructor(x: number, y: number, z: number, w: number) {
63
+ this.x = x
64
+ this.y = y
65
+ this.z = z
66
+ this.w = w
67
+ }
68
+
69
+ add(other: Quat): Quat {
70
+ return new Quat(this.x + other.x, this.y + other.y, this.z + other.z, this.w + other.w)
71
+ }
72
+
73
+ clone(): Quat {
74
+ return new Quat(this.x, this.y, this.z, this.w)
75
+ }
76
+
77
+ multiply(other: Quat): Quat {
78
+ // Proper quaternion multiplication (not component-wise)
79
+ return new Quat(
80
+ this.w * other.x + this.x * other.w + this.y * other.z - this.z * other.y,
81
+ this.w * other.y - this.x * other.z + this.y * other.w + this.z * other.x,
82
+ this.w * other.z + this.x * other.y - this.y * other.x + this.z * other.w,
83
+ this.w * other.w - this.x * other.x - this.y * other.y - this.z * other.z
84
+ )
85
+ }
86
+
87
+ conjugate(): Quat {
88
+ // Conjugate (inverse for unit quaternions): (x, y, z, w) -> (-x, -y, -z, w)
89
+ return new Quat(-this.x, -this.y, -this.z, this.w)
90
+ }
91
+
92
+ length(): number {
93
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w)
94
+ }
95
+
96
+ normalize(): Quat {
97
+ const len = this.length()
98
+ if (len === 0) return new Quat(0, 0, 0, 1)
99
+ return new Quat(this.x / len, this.y / len, this.z / len, this.w / len)
100
+ }
101
+
102
+ // Rotate a vector by this quaternion: result = q * v * q^-1
103
+ rotateVec(v: Vec3): Vec3 {
104
+ // Treat v as pure quaternion (x, y, z, 0)
105
+ const qx = this.x,
106
+ qy = this.y,
107
+ qz = this.z,
108
+ qw = this.w
109
+ const vx = v.x,
110
+ vy = v.y,
111
+ vz = v.z
112
+
113
+ // t = 2 * cross(q.xyz, v)
114
+ const tx = 2 * (qy * vz - qz * vy)
115
+ const ty = 2 * (qz * vx - qx * vz)
116
+ const tz = 2 * (qx * vy - qy * vx)
117
+
118
+ // result = v + q.w * t + cross(q.xyz, t)
119
+ return new Vec3(
120
+ vx + qw * tx + (qy * tz - qz * ty),
121
+ vy + qw * ty + (qz * tx - qx * tz),
122
+ vz + qw * tz + (qx * ty - qy * tx)
123
+ )
124
+ }
125
+
126
+ // Rotate a vector by this quaternion (Babylon.js style naming)
127
+ rotate(v: Vec3): Vec3 {
128
+ const qv = new Vec3(this.x, this.y, this.z)
129
+ const uv = qv.cross(v)
130
+ const uuv = qv.cross(uv)
131
+ return v.add(uv.scale(2 * this.w)).add(uuv.scale(2))
132
+ }
133
+
134
+ // Static method: create quaternion that rotates from one direction to another
135
+ static fromTo(from: Vec3, to: Vec3): Quat {
136
+ const dot = from.dot(to)
137
+ if (dot > 0.999999) return new Quat(0, 0, 0, 1) // Already aligned
138
+ if (dot < -0.999999) {
139
+ // 180 degrees
140
+ let axis = from.cross(new Vec3(1, 0, 0))
141
+ if (axis.length() < 0.001) axis = from.cross(new Vec3(0, 1, 0))
142
+ return new Quat(axis.x, axis.y, axis.z, 0).normalize()
143
+ }
144
+
145
+ const axis = from.cross(to)
146
+ const w = Math.sqrt((1 + dot) * 2)
147
+ const invW = 1 / w
148
+ return new Quat(axis.x * invW, axis.y * invW, axis.z * invW, w * 0.5).normalize()
149
+ }
150
+
151
+ // Static method: create quaternion from rotation axis and angle
152
+ static fromAxisAngle(axis: Vec3, angle: number): Quat {
153
+ const normalizedAxis = axis.normalize()
154
+ const halfAngle = angle * 0.5
155
+ const sinHalf = Math.sin(halfAngle)
156
+ const cosHalf = Math.cos(halfAngle)
157
+ return new Quat(normalizedAxis.x * sinHalf, normalizedAxis.y * sinHalf, normalizedAxis.z * sinHalf, cosHalf)
158
+ }
159
+
160
+ toArray(): [number, number, number, number] {
161
+ return [this.x, this.y, this.z, this.w]
162
+ }
163
+
164
+ // Spherical linear interpolation between two quaternions
165
+ static slerp(a: Quat, b: Quat, t: number): Quat {
166
+ let cos = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
167
+ let bx = b.x,
168
+ by = b.y,
169
+ bz = b.z,
170
+ bw = b.w
171
+
172
+ // If dot product is negative, negate one quaternion to take shorter path
173
+ if (cos < 0) {
174
+ cos = -cos
175
+ bx = -bx
176
+ by = -by
177
+ bz = -bz
178
+ bw = -bw
179
+ }
180
+
181
+ // If quaternions are very close, use linear interpolation
182
+ if (cos > 0.9995) {
183
+ const x = a.x + t * (bx - a.x)
184
+ const y = a.y + t * (by - a.y)
185
+ const z = a.z + t * (bz - a.z)
186
+ const w = a.w + t * (bw - a.w)
187
+ const invLen = 1 / Math.hypot(x, y, z, w)
188
+ return new Quat(x * invLen, y * invLen, z * invLen, w * invLen)
189
+ }
190
+
191
+ // Standard SLERP
192
+ const theta0 = Math.acos(cos)
193
+ const sinTheta0 = Math.sin(theta0)
194
+ const theta = theta0 * t
195
+ const s0 = Math.sin(theta0 - theta) / sinTheta0
196
+ const s1 = Math.sin(theta) / sinTheta0
197
+ return new Quat(s0 * a.x + s1 * bx, s0 * a.y + s1 * by, s0 * a.z + s1 * bz, s0 * a.w + s1 * bw)
198
+ }
199
+
200
+ // Convert Euler angles to quaternion (ZXY order, left-handed, PMX format)
201
+ static fromEuler(rotX: number, rotY: number, rotZ: number): Quat {
202
+ const cx = Math.cos(rotX * 0.5)
203
+ const sx = Math.sin(rotX * 0.5)
204
+ const cy = Math.cos(rotY * 0.5)
205
+ const sy = Math.sin(rotY * 0.5)
206
+ const cz = Math.cos(rotZ * 0.5)
207
+ const sz = Math.sin(rotZ * 0.5)
208
+
209
+ const w = cy * cx * cz + sy * sx * sz
210
+ const x = cy * sx * cz + sy * cx * sz
211
+ const y = sy * cx * cz - cy * sx * sz
212
+ const z = cy * cx * sz - sy * sx * cz
213
+
214
+ return new Quat(x, y, z, w).normalize()
215
+ }
216
+
217
+ // Convert quaternion to Euler angles (ZXY order, inverse of fromEuler)
218
+ toEuler(): Vec3 {
219
+ const qx = this.x
220
+ const qy = this.y
221
+ const qz = this.z
222
+ const qw = this.w
223
+
224
+ // ZXY order (left-handed)
225
+ // Roll (X): rotation around X axis
226
+ const sinr_cosp = 2 * (qw * qx + qy * qz)
227
+ const cosr_cosp = 1 - 2 * (qx * qx + qy * qy)
228
+ const rotX = Math.atan2(sinr_cosp, cosr_cosp)
229
+
230
+ // Pitch (Y): rotation around Y axis
231
+ const sinp = 2 * (qw * qy - qz * qx)
232
+ const rotY = Math.abs(sinp) >= 1 ? (sinp >= 0 ? Math.PI / 2 : -Math.PI / 2) : Math.asin(sinp)
233
+
234
+ // Yaw (Z): rotation around Z axis
235
+ const siny_cosp = 2 * (qw * qz + qx * qy)
236
+ const cosy_cosp = 1 - 2 * (qy * qy + qz * qz)
237
+ const rotZ = Math.atan2(siny_cosp, cosy_cosp)
238
+
239
+ return new Vec3(rotX, rotY, rotZ)
240
+ }
241
+ }
242
+
243
+ export class Mat4 {
244
+ values: Float32Array
245
+
246
+ constructor(values: Float32Array) {
247
+ this.values = values
248
+ }
249
+
250
+ static identity(): Mat4 {
251
+ return new Mat4(new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]))
252
+ }
253
+
254
+ // Perspective matrix for LEFT-HANDED coordinate system (Z+ forward)
255
+ // For left-handed: Z goes from 0 (near) to 1 (far), +Z is forward
256
+ static perspective(fov: number, aspect: number, near: number, far: number): Mat4 {
257
+ const f = 1.0 / Math.tan(fov / 2)
258
+ const rangeInv = 1.0 / (far - near) // Positive for left-handed
259
+
260
+ return new Mat4(
261
+ new Float32Array([
262
+ f / aspect,
263
+ 0,
264
+ 0,
265
+ 0,
266
+ 0,
267
+ f,
268
+ 0,
269
+ 0,
270
+ 0,
271
+ 0,
272
+ (far + near) * rangeInv,
273
+ 1, // Positive for left-handed (Z+ forward)
274
+ 0,
275
+ 0,
276
+ -near * far * rangeInv * 2, // Negated for left-handed
277
+ 0,
278
+ ])
279
+ )
280
+ }
281
+
282
+ // LookAt matrix for LEFT-HANDED coordinate system (Z+ forward)
283
+ // For left-handed: camera looks along +Z direction
284
+ static lookAt(eye: Vec3, target: Vec3, up: Vec3): Mat4 {
285
+ // In left-handed: forward = target - eye (Z+ direction)
286
+ const forward = target.subtract(eye).normalize()
287
+ const right = up.cross(forward).normalize() // X+ is right
288
+ const upVec = forward.cross(right).normalize() // Y+ is up
289
+
290
+ return new Mat4(
291
+ new Float32Array([
292
+ right.x,
293
+ upVec.x,
294
+ forward.x,
295
+ 0,
296
+ right.y,
297
+ upVec.y,
298
+ forward.y,
299
+ 0,
300
+ right.z,
301
+ upVec.z,
302
+ forward.z,
303
+ 0,
304
+ -right.dot(eye),
305
+ -upVec.dot(eye),
306
+ -forward.dot(eye),
307
+ 1,
308
+ ])
309
+ )
310
+ }
311
+
312
+ multiply(other: Mat4): Mat4 {
313
+ // Column-major multiplication (matches WGSL/GLSL convention):
314
+ // result = a * b
315
+ const out = new Float32Array(16)
316
+ const a = this.values
317
+ const b = other.values
318
+ for (let c = 0; c < 4; c++) {
319
+ const b0 = b[c * 4 + 0]
320
+ const b1 = b[c * 4 + 1]
321
+ const b2 = b[c * 4 + 2]
322
+ const b3 = b[c * 4 + 3]
323
+ out[c * 4 + 0] = a[0] * b0 + a[4] * b1 + a[8] * b2 + a[12] * b3
324
+ out[c * 4 + 1] = a[1] * b0 + a[5] * b1 + a[9] * b2 + a[13] * b3
325
+ out[c * 4 + 2] = a[2] * b0 + a[6] * b1 + a[10] * b2 + a[14] * b3
326
+ out[c * 4 + 3] = a[3] * b0 + a[7] * b1 + a[11] * b2 + a[15] * b3
327
+ }
328
+ return new Mat4(out)
329
+ }
330
+
331
+ // Static method to multiply two matrix array segments directly into output array (no object creation)
332
+ // Column-major multiplication: result = a * b
333
+ static multiplyArrays(
334
+ a: Float32Array,
335
+ aOffset: number,
336
+ b: Float32Array,
337
+ bOffset: number,
338
+ out: Float32Array,
339
+ outOffset: number
340
+ ): void {
341
+ for (let c = 0; c < 4; c++) {
342
+ const b0 = b[bOffset + c * 4 + 0]
343
+ const b1 = b[bOffset + c * 4 + 1]
344
+ const b2 = b[bOffset + c * 4 + 2]
345
+ const b3 = b[bOffset + c * 4 + 3]
346
+ out[outOffset + c * 4 + 0] =
347
+ a[aOffset + 0] * b0 + a[aOffset + 4] * b1 + a[aOffset + 8] * b2 + a[aOffset + 12] * b3
348
+ out[outOffset + c * 4 + 1] =
349
+ a[aOffset + 1] * b0 + a[aOffset + 5] * b1 + a[aOffset + 9] * b2 + a[aOffset + 13] * b3
350
+ out[outOffset + c * 4 + 2] =
351
+ a[aOffset + 2] * b0 + a[aOffset + 6] * b1 + a[aOffset + 10] * b2 + a[aOffset + 14] * b3
352
+ out[outOffset + c * 4 + 3] =
353
+ a[aOffset + 3] * b0 + a[aOffset + 7] * b1 + a[aOffset + 11] * b2 + a[aOffset + 15] * b3
354
+ }
355
+ }
356
+
357
+ clone(): Mat4 {
358
+ return new Mat4(this.values.slice())
359
+ }
360
+
361
+ static fromQuat(x: number, y: number, z: number, w: number): Mat4 {
362
+ // Column-major rotation matrix from quaternion (matches glMatrix/WGSL)
363
+ const out = new Float32Array(16)
364
+ const x2 = x + x,
365
+ y2 = y + y,
366
+ z2 = z + z
367
+ const xx = x * x2,
368
+ xy = x * y2,
369
+ xz = x * z2
370
+ const yy = y * y2,
371
+ yz = y * z2,
372
+ zz = z * z2
373
+ const wx = w * x2,
374
+ wy = w * y2,
375
+ wz = w * z2
376
+ out[0] = 1 - (yy + zz)
377
+ out[1] = xy + wz
378
+ out[2] = xz - wy
379
+ out[3] = 0
380
+ out[4] = xy - wz
381
+ out[5] = 1 - (xx + zz)
382
+ out[6] = yz + wx
383
+ out[7] = 0
384
+ out[8] = xz + wy
385
+ out[9] = yz - wx
386
+ out[10] = 1 - (xx + yy)
387
+ out[11] = 0
388
+ out[12] = 0
389
+ out[13] = 0
390
+ out[14] = 0
391
+ out[15] = 1
392
+ return new Mat4(out)
393
+ }
394
+
395
+ // Create transform matrix from position and rotation
396
+ static fromPositionRotation(position: Vec3, rotation: Quat): Mat4 {
397
+ const rotMat = Mat4.fromQuat(rotation.x, rotation.y, rotation.z, rotation.w)
398
+ rotMat.values[12] = position.x
399
+ rotMat.values[13] = position.y
400
+ rotMat.values[14] = position.z
401
+ return rotMat
402
+ }
403
+
404
+ // Extract position from transform matrix
405
+ getPosition(): Vec3 {
406
+ return new Vec3(this.values[12], this.values[13], this.values[14])
407
+ }
408
+
409
+ // Extract quaternion rotation from this matrix (upper-left 3x3 rotation part)
410
+ toQuat(): Quat {
411
+ return Mat4.toQuatFromArray(this.values, 0)
412
+ }
413
+
414
+ // Static method to extract quaternion from matrix array (avoids creating Mat4 object)
415
+ static toQuatFromArray(m: Float32Array, offset: number): Quat {
416
+ const m00 = m[offset + 0],
417
+ m01 = m[offset + 4],
418
+ m02 = m[offset + 8]
419
+ const m10 = m[offset + 1],
420
+ m11 = m[offset + 5],
421
+ m12 = m[offset + 9]
422
+ const m20 = m[offset + 2],
423
+ m21 = m[offset + 6],
424
+ m22 = m[offset + 10]
425
+ const trace = m00 + m11 + m22
426
+ let x = 0,
427
+ y = 0,
428
+ z = 0,
429
+ w = 1
430
+ if (trace > 0) {
431
+ const s = Math.sqrt(trace + 1.0) * 2
432
+ w = 0.25 * s
433
+ x = (m21 - m12) / s
434
+ y = (m02 - m20) / s
435
+ z = (m10 - m01) / s
436
+ } else if (m00 > m11 && m00 > m22) {
437
+ const s = Math.sqrt(1.0 + m00 - m11 - m22) * 2
438
+ w = (m21 - m12) / s
439
+ x = 0.25 * s
440
+ y = (m01 + m10) / s
441
+ z = (m02 + m20) / s
442
+ } else if (m11 > m22) {
443
+ const s = Math.sqrt(1.0 + m11 - m00 - m22) * 2
444
+ w = (m02 - m20) / s
445
+ x = (m01 + m10) / s
446
+ y = 0.25 * s
447
+ z = (m12 + m21) / s
448
+ } else {
449
+ const s = Math.sqrt(1.0 + m22 - m00 - m11) * 2
450
+ w = (m10 - m01) / s
451
+ x = (m02 + m20) / s
452
+ y = (m12 + m21) / s
453
+ z = 0.25 * s
454
+ }
455
+ const invLen = 1 / Math.hypot(x, y, z, w)
456
+ return new Quat(x * invLen, y * invLen, z * invLen, w * invLen)
457
+ }
458
+
459
+ // Reset matrix to identity in place
460
+ setIdentity(): this {
461
+ const v = this.values
462
+ v[0] = 1
463
+ v[1] = 0
464
+ v[2] = 0
465
+ v[3] = 0
466
+ v[4] = 0
467
+ v[5] = 1
468
+ v[6] = 0
469
+ v[7] = 0
470
+ v[8] = 0
471
+ v[9] = 0
472
+ v[10] = 1
473
+ v[11] = 0
474
+ v[12] = 0
475
+ v[13] = 0
476
+ v[14] = 0
477
+ v[15] = 1
478
+ return this
479
+ }
480
+
481
+ translateInPlace(tx: number, ty: number, tz: number): this {
482
+ this.values[12] += tx
483
+ this.values[13] += ty
484
+ this.values[14] += tz
485
+ return this
486
+ }
487
+
488
+ // Full 4x4 matrix inverse using adjugate method
489
+ // This works for any invertible matrix, not just orthonormal transforms
490
+ // The previous implementation assumed orthonormal rotation matrices, which fails
491
+ // when matrices have scaling or are not perfectly orthonormal (e.g., after
492
+ // bone hierarchy transformations)
493
+ inverse(): Mat4 {
494
+ const m = this.values
495
+ const out = new Float32Array(16)
496
+
497
+ const a00 = m[0],
498
+ a01 = m[1],
499
+ a02 = m[2],
500
+ a03 = m[3]
501
+ const a10 = m[4],
502
+ a11 = m[5],
503
+ a12 = m[6],
504
+ a13 = m[7]
505
+ const a20 = m[8],
506
+ a21 = m[9],
507
+ a22 = m[10],
508
+ a23 = m[11]
509
+ const a30 = m[12],
510
+ a31 = m[13],
511
+ a32 = m[14],
512
+ a33 = m[15]
513
+
514
+ const b00 = a00 * a11 - a01 * a10
515
+ const b01 = a00 * a12 - a02 * a10
516
+ const b02 = a00 * a13 - a03 * a10
517
+ const b03 = a01 * a12 - a02 * a11
518
+ const b04 = a01 * a13 - a03 * a11
519
+ const b05 = a02 * a13 - a03 * a12
520
+ const b06 = a20 * a31 - a21 * a30
521
+ const b07 = a20 * a32 - a22 * a30
522
+ const b08 = a20 * a33 - a23 * a30
523
+ const b09 = a21 * a32 - a22 * a31
524
+ const b10 = a21 * a33 - a23 * a31
525
+ const b11 = a22 * a33 - a23 * a32
526
+
527
+ let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06
528
+
529
+ if (Math.abs(det) < 1e-10) {
530
+ console.warn("Matrix is not invertible (determinant near zero)")
531
+ return Mat4.identity()
532
+ }
533
+
534
+ det = 1.0 / det
535
+
536
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det
537
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det
538
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det
539
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det
540
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det
541
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det
542
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det
543
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det
544
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det
545
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det
546
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det
547
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det
548
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det
549
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det
550
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det
551
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det
552
+
553
+ return new Mat4(out)
554
+ }
555
+ }