reze-engine 0.3.4 → 0.3.6

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/ik-solver.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Mat4, Quat, Vec3 } from "./math"
8
- import { Bone, IKLink, IKSolver, IKChainInfo, EulerRotationOrder, SolveAxis } from "./model"
8
+ import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
9
9
 
10
10
  const enum InternalEulerRotationOrder {
11
11
  YXZ = 0,
@@ -89,13 +89,9 @@ export class IKSolverSystem {
89
89
  localRotations: Float32Array,
90
90
  localTranslations: Float32Array,
91
91
  worldMatrices: Float32Array,
92
- ikChainInfo: IKChainInfo[],
93
- usePhysics: boolean = false
92
+ ikChainInfo: IKChainInfo[]
94
93
  ): void {
95
94
  for (const solver of ikSolvers) {
96
- if (usePhysics && solver.canSkipWhenPhysicsEnabled) {
97
- continue
98
- }
99
95
  this.solveIK(solver, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
100
96
  }
101
97
  }
@@ -121,11 +117,7 @@ export class IKSolverSystem {
121
117
  }
122
118
  }
123
119
 
124
- // Get IK bone and target positions
125
- const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
126
- const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
127
-
128
- if (ikPosition.subtract(targetPosition).length() < this.EPSILON) return
120
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
129
121
 
130
122
  // Build IK chains
131
123
  const chains: IKChain[] = []
@@ -135,15 +127,11 @@ export class IKSolverSystem {
135
127
 
136
128
  // Update chain bones and target bone world matrices (initial state, no IK yet)
137
129
  for (let i = chains.length - 1; i >= 0; i--) {
138
- this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices)
130
+ this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
139
131
  }
140
- this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices)
141
-
142
- // Re-read positions after initial update
143
- const updatedIkPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
144
- const updatedTargetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
132
+ this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
145
133
 
146
- if (updatedIkPosition.subtract(updatedTargetPosition).length() < this.EPSILON) return
134
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
147
135
 
148
136
  // Solve iteratively
149
137
  const iteration = Math.min(solver.iterationCount, 256)
@@ -169,29 +157,17 @@ export class IKSolverSystem {
169
157
  }
170
158
  }
171
159
 
172
- // Re-read positions after this iteration
173
- const currentIkPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
174
- const currentTargetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
175
- const distance = currentIkPosition.subtract(currentTargetPosition).length()
176
- if (distance < this.EPSILON) break
160
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) break
177
161
  }
178
162
 
179
163
  // Apply IK rotations to local rotations
180
164
  for (const link of solver.links) {
181
165
  const chainInfo = ikChainInfo[link.boneIndex]
182
- if (chainInfo && chainInfo.ikRotation) {
166
+ if (chainInfo?.ikRotation) {
183
167
  const qi = link.boneIndex * 4
184
- const localRot = new Quat(
185
- localRotations[qi],
186
- localRotations[qi + 1],
187
- localRotations[qi + 2],
188
- localRotations[qi + 3]
189
- )
168
+ const localRot = this.getQuatFromArray(localRotations, qi)
190
169
  const finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
191
- localRotations[qi] = finalRot.x
192
- localRotations[qi + 1] = finalRot.y
193
- localRotations[qi + 2] = finalRot.z
194
- localRotations[qi + 3] = finalRot.w
170
+ this.setQuatToArray(localRotations, qi, finalRot)
195
171
  }
196
172
  }
197
173
  }
@@ -231,25 +207,20 @@ export class IKSolverSystem {
231
207
  finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
232
208
  break
233
209
  }
234
- case InternalSolveAxis.X: {
235
- const m = parentWorldRotMatrix.values
236
- const axisX = new Vec3(m[0], m[1], m[2])
237
- const dot = chainRotationAxis.dot(axisX)
238
- finalRotationAxis = new Vec3(dot >= 0 ? 1 : -1, 0, 0)
239
- break
240
- }
241
- case InternalSolveAxis.Y: {
242
- const m = parentWorldRotMatrix.values
243
- const axisY = new Vec3(m[4], m[5], m[6])
244
- const dot = chainRotationAxis.dot(axisY)
245
- finalRotationAxis = new Vec3(0, dot >= 0 ? 1 : -1, 0)
246
- break
247
- }
210
+ case InternalSolveAxis.X:
211
+ case InternalSolveAxis.Y:
248
212
  case InternalSolveAxis.Z: {
249
213
  const m = parentWorldRotMatrix.values
250
- const axisZ = new Vec3(m[8], m[9], m[10])
251
- const dot = chainRotationAxis.dot(axisZ)
252
- finalRotationAxis = new Vec3(0, 0, dot >= 0 ? 1 : -1)
214
+ const axisOffset = (chain.solveAxis - InternalSolveAxis.X) * 4
215
+ const axis = new Vec3(m[axisOffset], m[axisOffset + 1], m[axisOffset + 2])
216
+ const dot = chainRotationAxis.dot(axis)
217
+ const sign = dot >= 0 ? 1 : -1
218
+ finalRotationAxis =
219
+ chain.solveAxis === InternalSolveAxis.X
220
+ ? new Vec3(sign, 0, 0)
221
+ : chain.solveAxis === InternalSolveAxis.Y
222
+ ? new Vec3(0, sign, 0)
223
+ : new Vec3(0, 0, sign)
253
224
  break
254
225
  }
255
226
  default:
@@ -271,93 +242,25 @@ export class IKSolverSystem {
271
242
  chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
272
243
 
273
244
  // Apply angle constraints if present
274
- if (chain.minimumAngle !== null && chain.maximumAngle !== null) {
245
+ if (chain.minimumAngle && chain.maximumAngle) {
275
246
  const qi = chainBoneIndex * 4
276
- const localRot = new Quat(
277
- localRotations[qi],
278
- localRotations[qi + 1],
279
- localRotations[qi + 2],
280
- localRotations[qi + 3]
281
- )
247
+ const localRot = this.getQuatFromArray(localRotations, qi)
282
248
  chainInfo.localRotation = localRot.clone()
283
249
 
284
250
  const combinedRot = chainInfo.ikRotation.multiply(localRot)
285
- const rotMatrix = Mat4.fromQuat(combinedRot.x, combinedRot.y, combinedRot.z, combinedRot.w)
286
- const m = rotMatrix.values
287
-
288
- let rX: number, rY: number, rZ: number
289
-
290
- switch (chain.rotationOrder) {
291
- case InternalEulerRotationOrder.YXZ: {
292
- rX = Math.asin(-m[9]) // m32
293
- if (Math.abs(rX) > this.THRESHOLD) {
294
- rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
295
- }
296
- let cosX = Math.cos(rX)
297
- if (cosX !== 0) cosX = 1 / cosX
298
- rY = Math.atan2(m[8] * cosX, m[10] * cosX) // m31, m33
299
- rZ = Math.atan2(m[1] * cosX, m[5] * cosX) // m12, m22
300
-
301
- rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
302
- rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
303
- rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
304
-
305
- chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(0, 1, 0), rY)
306
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(1, 0, 0), rX))
307
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ))
308
- break
309
- }
310
- case InternalEulerRotationOrder.ZYX: {
311
- rY = Math.asin(-m[2]) // m13
312
- if (Math.abs(rY) > this.THRESHOLD) {
313
- rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
314
- }
315
- let cosY = Math.cos(rY)
316
- if (cosY !== 0) cosY = 1 / cosY
317
- rX = Math.atan2(m[6] * cosY, m[10] * cosY) // m23, m33
318
- rZ = Math.atan2(m[1] * cosY, m[0] * cosY) // m12, m11
319
-
320
- rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
321
- rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
322
- rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
323
-
324
- chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ)
325
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 1, 0), rY))
326
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(1, 0, 0), rX))
327
- break
328
- }
329
- case InternalEulerRotationOrder.XZY: {
330
- rZ = Math.asin(-m[4]) // m21
331
- if (Math.abs(rZ) > this.THRESHOLD) {
332
- rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
333
- }
334
- let cosZ = Math.cos(rZ)
335
- if (cosZ !== 0) cosZ = 1 / cosZ
336
- rX = Math.atan2(m[6] * cosZ, m[5] * cosZ) // m23, m22
337
- rY = Math.atan2(m[8] * cosZ, m[0] * cosZ) // m31, m11
338
-
339
- rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
340
- rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
341
- rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
342
-
343
- chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(1, 0, 0), rX)
344
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ))
345
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 1, 0), rY))
346
- break
347
- }
348
- }
349
-
350
- const invertedLocalRotation = localRot.conjugate().normalize()
351
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(invertedLocalRotation)
251
+ const euler = this.extractEulerAngles(combinedRot, chain.rotationOrder)
252
+ const limited = this.limitEulerAngles(euler, chain.minimumAngle, chain.maximumAngle, useAxis)
253
+ chainInfo.ikRotation = this.reconstructQuatFromEuler(limited, chain.rotationOrder)
254
+ chainInfo.ikRotation = chainInfo.ikRotation.multiply(localRot.conjugate().normalize())
352
255
  }
353
256
  }
354
257
 
355
258
  // Update world matrices for affected bones (using IK-modified rotations)
356
259
  for (let i = chainIndex; i >= 0; i--) {
357
260
  const link = solver.links[i]
358
- this.updateWorldMatrixWithIK(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
261
+ this.updateWorldMatrix(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
359
262
  }
360
- this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices)
263
+ this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
361
264
  }
362
265
 
363
266
  private static limitAngle(angle: number, min: number, max: number, useAxis: boolean): number {
@@ -372,11 +275,92 @@ export class IKSolverSystem {
372
275
  }
373
276
  }
374
277
 
278
+ private static getDistance(boneIndex1: number, boneIndex2: number, worldMatrices: Float32Array): number {
279
+ const pos1 = this.getWorldTranslation(boneIndex1, worldMatrices)
280
+ const pos2 = this.getWorldTranslation(boneIndex2, worldMatrices)
281
+ return pos1.subtract(pos2).length()
282
+ }
283
+
375
284
  private static getWorldTranslation(boneIndex: number, worldMatrices: Float32Array): Vec3 {
376
285
  const offset = boneIndex * 16
377
286
  return new Vec3(worldMatrices[offset + 12], worldMatrices[offset + 13], worldMatrices[offset + 14])
378
287
  }
379
288
 
289
+ private static getQuatFromArray(array: Float32Array, offset: number): Quat {
290
+ return new Quat(array[offset], array[offset + 1], array[offset + 2], array[offset + 3])
291
+ }
292
+
293
+ private static setQuatToArray(array: Float32Array, offset: number, quat: Quat): void {
294
+ array[offset] = quat.x
295
+ array[offset + 1] = quat.y
296
+ array[offset + 2] = quat.z
297
+ array[offset + 3] = quat.w
298
+ }
299
+
300
+ private static extractEulerAngles(quat: Quat, order: InternalEulerRotationOrder): Vec3 {
301
+ const rotMatrix = Mat4.fromQuat(quat.x, quat.y, quat.z, quat.w)
302
+ const m = rotMatrix.values
303
+
304
+ switch (order) {
305
+ case InternalEulerRotationOrder.YXZ: {
306
+ let rX = Math.asin(-m[9])
307
+ if (Math.abs(rX) > this.THRESHOLD) rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
308
+ let cosX = Math.cos(rX)
309
+ if (cosX !== 0) cosX = 1 / cosX
310
+ const rY = Math.atan2(m[8] * cosX, m[10] * cosX)
311
+ const rZ = Math.atan2(m[1] * cosX, m[5] * cosX)
312
+ return new Vec3(rX, rY, rZ)
313
+ }
314
+ case InternalEulerRotationOrder.ZYX: {
315
+ let rY = Math.asin(-m[2])
316
+ if (Math.abs(rY) > this.THRESHOLD) rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
317
+ let cosY = Math.cos(rY)
318
+ if (cosY !== 0) cosY = 1 / cosY
319
+ const rX = Math.atan2(m[6] * cosY, m[10] * cosY)
320
+ const rZ = Math.atan2(m[1] * cosY, m[0] * cosY)
321
+ return new Vec3(rX, rY, rZ)
322
+ }
323
+ case InternalEulerRotationOrder.XZY: {
324
+ let rZ = Math.asin(-m[4])
325
+ if (Math.abs(rZ) > this.THRESHOLD) rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
326
+ let cosZ = Math.cos(rZ)
327
+ if (cosZ !== 0) cosZ = 1 / cosZ
328
+ const rX = Math.atan2(m[6] * cosZ, m[5] * cosZ)
329
+ const rY = Math.atan2(m[8] * cosZ, m[0] * cosZ)
330
+ return new Vec3(rX, rY, rZ)
331
+ }
332
+ }
333
+ }
334
+
335
+ private static limitEulerAngles(euler: Vec3, min: Vec3, max: Vec3, useAxis: boolean): Vec3 {
336
+ return new Vec3(
337
+ this.limitAngle(euler.x, min.x, max.x, useAxis),
338
+ this.limitAngle(euler.y, min.y, max.y, useAxis),
339
+ this.limitAngle(euler.z, min.z, max.z, useAxis)
340
+ )
341
+ }
342
+
343
+ private static reconstructQuatFromEuler(euler: Vec3, order: InternalEulerRotationOrder): Quat {
344
+ const axes = [
345
+ [new Vec3(1, 0, 0), new Vec3(0, 1, 0), new Vec3(0, 0, 1)],
346
+ [new Vec3(0, 0, 1), new Vec3(0, 1, 0), new Vec3(1, 0, 0)],
347
+ [new Vec3(0, 1, 0), new Vec3(1, 0, 0), new Vec3(0, 0, 1)],
348
+ ]
349
+
350
+ const [axis1, axis2, axis3] = axes[order]
351
+ const [angle1, angle2, angle3] =
352
+ order === InternalEulerRotationOrder.YXZ
353
+ ? [euler.y, euler.x, euler.z]
354
+ : order === InternalEulerRotationOrder.ZYX
355
+ ? [euler.z, euler.y, euler.x]
356
+ : [euler.x, euler.z, euler.y]
357
+
358
+ let result = Quat.fromAxisAngle(axis1, angle1)
359
+ result = result.multiply(Quat.fromAxisAngle(axis2, angle2))
360
+ result = result.multiply(Quat.fromAxisAngle(axis3, angle3))
361
+ return result
362
+ }
363
+
380
364
  private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Float32Array): Mat4 {
381
365
  const bone = bones[boneIndex]
382
366
  if (bone.parentIndex >= 0) {
@@ -401,29 +385,27 @@ export class IKSolverSystem {
401
385
  )
402
386
  }
403
387
 
404
- private static updateWorldMatrixWithIK(
388
+ private static updateWorldMatrix(
405
389
  boneIndex: number,
406
390
  bones: Bone[],
407
391
  localRotations: Float32Array,
408
392
  localTranslations: Float32Array,
409
393
  worldMatrices: Float32Array,
410
- ikChainInfo: IKChainInfo[]
394
+ ikChainInfo?: IKChainInfo[]
411
395
  ): void {
412
396
  const bone = bones[boneIndex]
413
397
  const qi = boneIndex * 4
414
398
  const ti = boneIndex * 3
415
399
 
416
- // Use IK-modified rotation if available
417
- const localRot = new Quat(
418
- localRotations[qi],
419
- localRotations[qi + 1],
420
- localRotations[qi + 2],
421
- localRotations[qi + 3]
422
- )
423
- const chainInfo = ikChainInfo[boneIndex]
400
+ const localRot = this.getQuatFromArray(localRotations, qi)
401
+
402
+ // Apply IK rotation if available
424
403
  let finalRot = localRot
425
- if (chainInfo && chainInfo.ikRotation) {
426
- finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
404
+ if (ikChainInfo) {
405
+ const chainInfo = ikChainInfo[boneIndex]
406
+ if (chainInfo && chainInfo.ikRotation) {
407
+ finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
408
+ }
427
409
  }
428
410
  const rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w)
429
411
 
@@ -446,43 +428,4 @@ export class IKSolverSystem {
446
428
  worldMatrices.subarray(worldOffset, worldOffset + 16).set(localM.values)
447
429
  }
448
430
  }
449
-
450
- private static updateWorldMatrix(
451
- boneIndex: number,
452
- bones: Bone[],
453
- localRotations: Float32Array,
454
- localTranslations: Float32Array,
455
- worldMatrices: Float32Array
456
- ): void {
457
- const bone = bones[boneIndex]
458
- const qi = boneIndex * 4
459
- const ti = boneIndex * 3
460
-
461
- const localRot = new Quat(
462
- localRotations[qi],
463
- localRotations[qi + 1],
464
- localRotations[qi + 2],
465
- localRotations[qi + 3]
466
- )
467
- const rotateM = Mat4.fromQuat(localRot.x, localRot.y, localRot.z, localRot.w)
468
-
469
- const localTx = localTranslations[ti]
470
- const localTy = localTranslations[ti + 1]
471
- const localTz = localTranslations[ti + 2]
472
-
473
- const localM = Mat4.identity()
474
- .translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
475
- .multiply(rotateM)
476
- .translateInPlace(localTx, localTy, localTz)
477
-
478
- const worldOffset = boneIndex * 16
479
- if (bone.parentIndex >= 0) {
480
- const parentOffset = bone.parentIndex * 16
481
- const parentMat = new Mat4(worldMatrices.subarray(parentOffset, parentOffset + 16))
482
- const worldMat = parentMat.multiply(localM)
483
- worldMatrices.subarray(worldOffset, worldOffset + 16).set(worldMat.values)
484
- } else {
485
- worldMatrices.subarray(worldOffset, worldOffset + 16).set(localM.values)
486
- }
487
- }
488
431
  }
package/src/math.ts CHANGED
@@ -26,10 +26,6 @@ export class Vec3 {
26
26
  return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
27
27
  }
28
28
 
29
- lengthSquared(): number {
30
- return this.x * this.x + this.y * this.y + this.z * this.z
31
- }
32
-
33
29
  normalize(): Vec3 {
34
30
  const len = this.length()
35
31
  if (len === 0) return new Vec3(0, 0, 0)
@@ -51,10 +47,6 @@ export class Vec3 {
51
47
  scale(scalar: number): Vec3 {
52
48
  return new Vec3(this.x * scalar, this.y * scalar, this.z * scalar)
53
49
  }
54
-
55
- clone(): Vec3 {
56
- return new Vec3(this.x, this.y, this.z)
57
- }
58
50
  }
59
51
 
60
52
  export class Quat {
@@ -103,55 +95,6 @@ export class Quat {
103
95
  return new Quat(this.x / len, this.y / len, this.z / len, this.w / len)
104
96
  }
105
97
 
106
- // Rotate a vector by this quaternion: result = q * v * q^-1
107
- rotateVec(v: Vec3): Vec3 {
108
- // Treat v as pure quaternion (x, y, z, 0)
109
- const qx = this.x,
110
- qy = this.y,
111
- qz = this.z,
112
- qw = this.w
113
- const vx = v.x,
114
- vy = v.y,
115
- vz = v.z
116
-
117
- // t = 2 * cross(q.xyz, v)
118
- const tx = 2 * (qy * vz - qz * vy)
119
- const ty = 2 * (qz * vx - qx * vz)
120
- const tz = 2 * (qx * vy - qy * vx)
121
-
122
- // result = v + q.w * t + cross(q.xyz, t)
123
- return new Vec3(
124
- vx + qw * tx + (qy * tz - qz * ty),
125
- vy + qw * ty + (qz * tx - qx * tz),
126
- vz + qw * tz + (qx * ty - qy * tx)
127
- )
128
- }
129
-
130
- // Rotate a vector by this quaternion (Babylon.js style naming)
131
- rotate(v: Vec3): Vec3 {
132
- const qv = new Vec3(this.x, this.y, this.z)
133
- const uv = qv.cross(v)
134
- const uuv = qv.cross(uv)
135
- return v.add(uv.scale(2 * this.w)).add(uuv.scale(2))
136
- }
137
-
138
- // Static method: create quaternion that rotates from one direction to another
139
- static fromTo(from: Vec3, to: Vec3): Quat {
140
- const dot = from.dot(to)
141
- if (dot > 0.999999) return new Quat(0, 0, 0, 1) // Already aligned
142
- if (dot < -0.999999) {
143
- // 180 degrees
144
- let axis = from.cross(new Vec3(1, 0, 0))
145
- if (axis.length() < 0.001) axis = from.cross(new Vec3(0, 1, 0))
146
- return new Quat(axis.x, axis.y, axis.z, 0).normalize()
147
- }
148
-
149
- const axis = from.cross(to)
150
- const w = Math.sqrt((1 + dot) * 2)
151
- const invW = 1 / w
152
- return new Quat(axis.x * invW, axis.y * invW, axis.z * invW, w * 0.5).normalize()
153
- }
154
-
155
98
  // Static method: create quaternion from rotation axis and angle
156
99
  static fromAxisAngle(axis: Vec3, angle: number): Quat {
157
100
  const normalizedAxis = axis.normalize()
@@ -217,31 +160,6 @@ export class Quat {
217
160
 
218
161
  return new Quat(x, y, z, w).normalize()
219
162
  }
220
-
221
- // Convert quaternion to Euler angles (ZXY order, inverse of fromEuler)
222
- toEuler(): Vec3 {
223
- const qx = this.x
224
- const qy = this.y
225
- const qz = this.z
226
- const qw = this.w
227
-
228
- // ZXY order (left-handed)
229
- // Roll (X): rotation around X axis
230
- const sinr_cosp = 2 * (qw * qx + qy * qz)
231
- const cosr_cosp = 1 - 2 * (qx * qx + qy * qy)
232
- const rotX = Math.atan2(sinr_cosp, cosr_cosp)
233
-
234
- // Pitch (Y): rotation around Y axis
235
- const sinp = 2 * (qw * qy - qz * qx)
236
- const rotY = Math.abs(sinp) >= 1 ? (sinp >= 0 ? Math.PI / 2 : -Math.PI / 2) : Math.asin(sinp)
237
-
238
- // Yaw (Z): rotation around Z axis
239
- const siny_cosp = 2 * (qw * qz + qx * qy)
240
- const cosy_cosp = 1 - 2 * (qy * qy + qz * qz)
241
- const rotZ = Math.atan2(siny_cosp, cosy_cosp)
242
-
243
- return new Vec3(rotX, rotY, rotZ)
244
- }
245
163
  }
246
164
 
247
165
  export class Mat4 {
@@ -557,3 +475,46 @@ export class Mat4 {
557
475
  return new Mat4(out)
558
476
  }
559
477
  }
478
+
479
+ /**
480
+ * Bezier interpolation function
481
+ * @param x1 First control point X (0-127, normalized to 0-1)
482
+ * @param x2 Second control point X (0-127, normalized to 0-1)
483
+ * @param y1 First control point Y (0-127, normalized to 0-1)
484
+ * @param y2 Second control point Y (0-127, normalized to 0-1)
485
+ * @param t Interpolation parameter (0-1)
486
+ * @returns Interpolated value (0-1)
487
+ */
488
+ export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
489
+ // Clamp t to [0, 1]
490
+ t = Math.max(0, Math.min(1, t))
491
+
492
+ // Binary search for the t value that gives us the desired x
493
+ // 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
494
+ let start = 0
495
+ let end = 1
496
+ let mid = 0.5
497
+
498
+ // Iterate until we find the t value that gives us the desired x
499
+ for (let i = 0; i < 15; i++) {
500
+ // Evaluate Bezier curve at mid point
501
+ const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
502
+
503
+ if (Math.abs(x - t) < 0.0001) {
504
+ break
505
+ }
506
+
507
+ if (x < t) {
508
+ start = mid
509
+ } else {
510
+ end = mid
511
+ }
512
+
513
+ mid = (start + end) / 2
514
+ }
515
+
516
+ // Now evaluate the y value at this t
517
+ const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
518
+
519
+ return y
520
+ }