reze-engine 0.3.12 → 0.3.14

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