reze-engine 0.3.5 → 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/engine.ts CHANGED
@@ -135,7 +135,6 @@ export class Engine {
135
135
 
136
136
  private player: Player = new Player()
137
137
  private hasAnimation = false // Set to true when loadAnimation is called
138
- private animationStartTime: number = 0 // Track when animation first started (for A-pose prevention)
139
138
 
140
139
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
141
140
  this.canvas = canvas
@@ -1317,8 +1316,8 @@ export class Engine {
1317
1316
  public playAnimation() {
1318
1317
  if (!this.hasAnimation || !this.currentModel) return
1319
1318
 
1320
- const wasPaused = this.player.isPausedState()
1321
- const wasPlaying = this.player.isPlayingState()
1319
+ const wasPaused = this.player.isPausedState
1320
+ const wasPlaying = this.player.isPlayingState
1322
1321
 
1323
1322
  // Only reset pose and physics if starting from beginning (not resuming)
1324
1323
  if (!wasPlaying && !wasPaused) {
@@ -1365,9 +1364,6 @@ export class Engine {
1365
1364
 
1366
1365
  // Start playback (or resume if paused)
1367
1366
  this.player.play()
1368
- if (this.animationStartTime === 0) {
1369
- this.animationStartTime = performance.now()
1370
- }
1371
1367
  }
1372
1368
 
1373
1369
  public stopAnimation() {
@@ -1659,23 +1655,7 @@ export class Engine {
1659
1655
  const materialAlpha = mat.diffuse[3]
1660
1656
  const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON
1661
1657
 
1662
- // Create material uniform data
1663
- const materialUniformData = new Float32Array(8)
1664
- materialUniformData[0] = materialAlpha
1665
- materialUniformData[1] = 1.0 // alphaMultiplier: 1.0 for non-hair materials
1666
- materialUniformData[2] = this.rimLightIntensity
1667
- materialUniformData[3] = 0.0 // _padding1
1668
- materialUniformData[4] = 1.0 // rimColor.r
1669
- materialUniformData[5] = 1.0 // rimColor.g
1670
- materialUniformData[6] = 1.0 // rimColor.b
1671
- materialUniformData[7] = 0.0 // isOverEyes
1672
-
1673
- const materialUniformBuffer = this.device.createBuffer({
1674
- label: `material uniform: ${mat.name}`,
1675
- size: materialUniformData.byteLength,
1676
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1677
- })
1678
- this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
1658
+ const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0)
1679
1659
 
1680
1660
  // Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
1681
1661
  const bindGroup = this.device.createBindGroup({
@@ -1691,33 +1671,22 @@ export class Engine {
1691
1671
  ],
1692
1672
  })
1693
1673
 
1694
- if (mat.isEye) {
1674
+ const addDrawCall = (draws: DrawCall[]) => {
1695
1675
  if (indexCount > 0) {
1696
- this.eyeDraws.push({
1697
- count: indexCount,
1698
- firstIndex: currentIndexOffset,
1699
- bindGroup,
1700
- })
1676
+ draws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup })
1701
1677
  }
1678
+ }
1679
+
1680
+ if (mat.isEye) {
1681
+ addDrawCall(this.eyeDraws)
1702
1682
  } else if (mat.isHair) {
1703
1683
  // Hair materials: create separate bind groups for over-eyes vs over-non-eyes
1704
1684
  const createHairBindGroup = (isOverEyes: boolean) => {
1705
- const uniformData = new Float32Array(8)
1706
- uniformData[0] = materialAlpha
1707
- uniformData[1] = 1.0 // alphaMultiplier (shader adjusts based on isOverEyes)
1708
- uniformData[2] = this.rimLightIntensity
1709
- uniformData[3] = 0.0 // _padding1
1710
- uniformData[4] = 1.0 // rimColor.rgb
1711
- uniformData[5] = 1.0
1712
- uniformData[6] = 1.0
1713
- uniformData[7] = isOverEyes ? 1.0 : 0.0 // isOverEyes
1714
-
1715
- const buffer = this.device.createBuffer({
1716
- label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
1717
- size: uniformData.byteLength,
1718
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1719
- })
1720
- this.device.queue.writeBuffer(buffer, 0, uniformData)
1685
+ const buffer = this.createMaterialUniformBuffer(
1686
+ `${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
1687
+ materialAlpha,
1688
+ isOverEyes ? 1.0 : 0.0
1689
+ )
1721
1690
 
1722
1691
  return this.device.createBindGroup({
1723
1692
  label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
@@ -1742,7 +1711,6 @@ export class Engine {
1742
1711
  firstIndex: currentIndexOffset,
1743
1712
  bindGroup: bindGroupOverEyes,
1744
1713
  })
1745
-
1746
1714
  this.hairDrawsOverNonEyes.push({
1747
1715
  count: indexCount,
1748
1716
  firstIndex: currentIndexOffset,
@@ -1750,41 +1718,27 @@ export class Engine {
1750
1718
  })
1751
1719
  }
1752
1720
  } else if (isTransparent) {
1753
- if (indexCount > 0) {
1754
- this.transparentDraws.push({
1755
- count: indexCount,
1756
- firstIndex: currentIndexOffset,
1757
- bindGroup,
1758
- })
1759
- }
1721
+ addDrawCall(this.transparentDraws)
1760
1722
  } else {
1761
- if (indexCount > 0) {
1762
- this.opaqueDraws.push({
1763
- count: indexCount,
1764
- firstIndex: currentIndexOffset,
1765
- bindGroup,
1766
- })
1767
- }
1723
+ addDrawCall(this.opaqueDraws)
1768
1724
  }
1769
1725
 
1770
1726
  // Edge flag is at bit 4 (0x10) in PMX format
1771
1727
  if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
1772
- const materialUniformData = new Float32Array(8)
1773
- materialUniformData[0] = mat.edgeColor[0] // edgeColor.r
1774
- materialUniformData[1] = mat.edgeColor[1] // edgeColor.g
1775
- materialUniformData[2] = mat.edgeColor[2] // edgeColor.b
1776
- materialUniformData[3] = mat.edgeColor[3] // edgeColor.a
1777
- materialUniformData[4] = mat.edgeSize
1778
- materialUniformData[5] = 0.0 // isOverEyes
1779
- materialUniformData[6] = 0.0
1780
- materialUniformData[7] = 0.0
1781
-
1782
- const materialUniformBuffer = this.device.createBuffer({
1783
- label: `outline material uniform: ${mat.name}`,
1784
- size: materialUniformData.byteLength,
1785
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1786
- })
1787
- this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
1728
+ const materialUniformData = new Float32Array([
1729
+ mat.edgeColor[0],
1730
+ mat.edgeColor[1],
1731
+ mat.edgeColor[2],
1732
+ mat.edgeColor[3],
1733
+ mat.edgeSize,
1734
+ 0,
1735
+ 0,
1736
+ 0,
1737
+ ])
1738
+ const materialUniformBuffer = this.createUniformBuffer(
1739
+ `outline material uniform: ${mat.name}`,
1740
+ materialUniformData
1741
+ )
1788
1742
 
1789
1743
  const outlineBindGroup = this.device.createBindGroup({
1790
1744
  label: `outline bind group: ${mat.name}`,
@@ -1797,31 +1751,14 @@ export class Engine {
1797
1751
  })
1798
1752
 
1799
1753
  if (indexCount > 0) {
1800
- if (mat.isEye) {
1801
- this.eyeOutlineDraws.push({
1802
- count: indexCount,
1803
- firstIndex: currentIndexOffset,
1804
- bindGroup: outlineBindGroup,
1805
- })
1806
- } else if (mat.isHair) {
1807
- this.hairOutlineDraws.push({
1808
- count: indexCount,
1809
- firstIndex: currentIndexOffset,
1810
- bindGroup: outlineBindGroup,
1811
- })
1812
- } else if (isTransparent) {
1813
- this.transparentOutlineDraws.push({
1814
- count: indexCount,
1815
- firstIndex: currentIndexOffset,
1816
- bindGroup: outlineBindGroup,
1817
- })
1818
- } else {
1819
- this.opaqueOutlineDraws.push({
1820
- count: indexCount,
1821
- firstIndex: currentIndexOffset,
1822
- bindGroup: outlineBindGroup,
1823
- })
1824
- }
1754
+ const outlineDraws = mat.isEye
1755
+ ? this.eyeOutlineDraws
1756
+ : mat.isHair
1757
+ ? this.hairOutlineDraws
1758
+ : isTransparent
1759
+ ? this.transparentOutlineDraws
1760
+ : this.opaqueOutlineDraws
1761
+ outlineDraws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup })
1825
1762
  }
1826
1763
  }
1827
1764
 
@@ -1829,6 +1766,22 @@ export class Engine {
1829
1766
  }
1830
1767
  }
1831
1768
 
1769
+ private createMaterialUniformBuffer(label: string, alpha: number, isOverEyes: number): GPUBuffer {
1770
+ const data = new Float32Array(8)
1771
+ data.set([alpha, 1.0, this.rimLightIntensity, 0.0, 1.0, 1.0, 1.0, isOverEyes])
1772
+ return this.createUniformBuffer(`material uniform: ${label}`, data)
1773
+ }
1774
+
1775
+ private createUniformBuffer(label: string, data: Float32Array | Uint32Array): GPUBuffer {
1776
+ const buffer = this.device.createBuffer({
1777
+ label,
1778
+ size: data.byteLength,
1779
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1780
+ })
1781
+ this.device.queue.writeBuffer(buffer, 0, data as ArrayBufferView<ArrayBuffer>)
1782
+ return buffer
1783
+ }
1784
+
1832
1785
  private async createTextureFromPath(path: string): Promise<GPUTexture | null> {
1833
1786
  const cached = this.textureCache.get(path)
1834
1787
  if (cached) {
package/src/ik-solver.ts CHANGED
@@ -117,11 +117,7 @@ export class IKSolverSystem {
117
117
  }
118
118
  }
119
119
 
120
- // Get IK bone and target positions
121
- const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
122
- const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
123
-
124
- if (ikPosition.subtract(targetPosition).length() < this.EPSILON) return
120
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
125
121
 
126
122
  // Build IK chains
127
123
  const chains: IKChain[] = []
@@ -135,11 +131,7 @@ export class IKSolverSystem {
135
131
  }
136
132
  this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
137
133
 
138
- // Re-read positions after initial update
139
- const updatedIkPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
140
- const updatedTargetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
141
-
142
- if (updatedIkPosition.subtract(updatedTargetPosition).length() < this.EPSILON) return
134
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
143
135
 
144
136
  // Solve iteratively
145
137
  const iteration = Math.min(solver.iterationCount, 256)
@@ -165,29 +157,17 @@ export class IKSolverSystem {
165
157
  }
166
158
  }
167
159
 
168
- // Re-read positions after this iteration
169
- const currentIkPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
170
- const currentTargetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
171
- const distance = currentIkPosition.subtract(currentTargetPosition).length()
172
- if (distance < this.EPSILON) break
160
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) break
173
161
  }
174
162
 
175
163
  // Apply IK rotations to local rotations
176
164
  for (const link of solver.links) {
177
165
  const chainInfo = ikChainInfo[link.boneIndex]
178
- if (chainInfo && chainInfo.ikRotation) {
166
+ if (chainInfo?.ikRotation) {
179
167
  const qi = link.boneIndex * 4
180
- const localRot = new Quat(
181
- localRotations[qi],
182
- localRotations[qi + 1],
183
- localRotations[qi + 2],
184
- localRotations[qi + 3]
185
- )
168
+ const localRot = this.getQuatFromArray(localRotations, qi)
186
169
  const finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
187
- localRotations[qi] = finalRot.x
188
- localRotations[qi + 1] = finalRot.y
189
- localRotations[qi + 2] = finalRot.z
190
- localRotations[qi + 3] = finalRot.w
170
+ this.setQuatToArray(localRotations, qi, finalRot)
191
171
  }
192
172
  }
193
173
  }
@@ -227,25 +207,20 @@ export class IKSolverSystem {
227
207
  finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
228
208
  break
229
209
  }
230
- case InternalSolveAxis.X: {
231
- const m = parentWorldRotMatrix.values
232
- const axisX = new Vec3(m[0], m[1], m[2])
233
- const dot = chainRotationAxis.dot(axisX)
234
- finalRotationAxis = new Vec3(dot >= 0 ? 1 : -1, 0, 0)
235
- break
236
- }
237
- case InternalSolveAxis.Y: {
238
- const m = parentWorldRotMatrix.values
239
- const axisY = new Vec3(m[4], m[5], m[6])
240
- const dot = chainRotationAxis.dot(axisY)
241
- finalRotationAxis = new Vec3(0, dot >= 0 ? 1 : -1, 0)
242
- break
243
- }
210
+ case InternalSolveAxis.X:
211
+ case InternalSolveAxis.Y:
244
212
  case InternalSolveAxis.Z: {
245
213
  const m = parentWorldRotMatrix.values
246
- const axisZ = new Vec3(m[8], m[9], m[10])
247
- const dot = chainRotationAxis.dot(axisZ)
248
- 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)
249
224
  break
250
225
  }
251
226
  default:
@@ -267,84 +242,16 @@ export class IKSolverSystem {
267
242
  chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
268
243
 
269
244
  // Apply angle constraints if present
270
- if (chain.minimumAngle !== null && chain.maximumAngle !== null) {
245
+ if (chain.minimumAngle && chain.maximumAngle) {
271
246
  const qi = chainBoneIndex * 4
272
- const localRot = new Quat(
273
- localRotations[qi],
274
- localRotations[qi + 1],
275
- localRotations[qi + 2],
276
- localRotations[qi + 3]
277
- )
247
+ const localRot = this.getQuatFromArray(localRotations, qi)
278
248
  chainInfo.localRotation = localRot.clone()
279
249
 
280
250
  const combinedRot = chainInfo.ikRotation.multiply(localRot)
281
- const rotMatrix = Mat4.fromQuat(combinedRot.x, combinedRot.y, combinedRot.z, combinedRot.w)
282
- const m = rotMatrix.values
283
-
284
- let rX: number, rY: number, rZ: number
285
-
286
- switch (chain.rotationOrder) {
287
- case InternalEulerRotationOrder.YXZ: {
288
- rX = Math.asin(-m[9]) // m32
289
- if (Math.abs(rX) > this.THRESHOLD) {
290
- rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
291
- }
292
- let cosX = Math.cos(rX)
293
- if (cosX !== 0) cosX = 1 / cosX
294
- rY = Math.atan2(m[8] * cosX, m[10] * cosX) // m31, m33
295
- rZ = Math.atan2(m[1] * cosX, m[5] * cosX) // m12, m22
296
-
297
- rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
298
- rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
299
- rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
300
-
301
- chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(0, 1, 0), rY)
302
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(1, 0, 0), rX))
303
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ))
304
- break
305
- }
306
- case InternalEulerRotationOrder.ZYX: {
307
- rY = Math.asin(-m[2]) // m13
308
- if (Math.abs(rY) > this.THRESHOLD) {
309
- rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
310
- }
311
- let cosY = Math.cos(rY)
312
- if (cosY !== 0) cosY = 1 / cosY
313
- rX = Math.atan2(m[6] * cosY, m[10] * cosY) // m23, m33
314
- rZ = Math.atan2(m[1] * cosY, m[0] * cosY) // m12, m11
315
-
316
- rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
317
- rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
318
- rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
319
-
320
- chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ)
321
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 1, 0), rY))
322
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(1, 0, 0), rX))
323
- break
324
- }
325
- case InternalEulerRotationOrder.XZY: {
326
- rZ = Math.asin(-m[4]) // m21
327
- if (Math.abs(rZ) > this.THRESHOLD) {
328
- rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
329
- }
330
- let cosZ = Math.cos(rZ)
331
- if (cosZ !== 0) cosZ = 1 / cosZ
332
- rX = Math.atan2(m[6] * cosZ, m[5] * cosZ) // m23, m22
333
- rY = Math.atan2(m[8] * cosZ, m[0] * cosZ) // m31, m11
334
-
335
- rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
336
- rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
337
- rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
338
-
339
- chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(1, 0, 0), rX)
340
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ))
341
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 1, 0), rY))
342
- break
343
- }
344
- }
345
-
346
- const invertedLocalRotation = localRot.conjugate().normalize()
347
- 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())
348
255
  }
349
256
  }
350
257
 
@@ -368,11 +275,92 @@ export class IKSolverSystem {
368
275
  }
369
276
  }
370
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
+
371
284
  private static getWorldTranslation(boneIndex: number, worldMatrices: Float32Array): Vec3 {
372
285
  const offset = boneIndex * 16
373
286
  return new Vec3(worldMatrices[offset + 12], worldMatrices[offset + 13], worldMatrices[offset + 14])
374
287
  }
375
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
+
376
364
  private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Float32Array): Mat4 {
377
365
  const bone = bones[boneIndex]
378
366
  if (bone.parentIndex >= 0) {
@@ -409,13 +397,7 @@ export class IKSolverSystem {
409
397
  const qi = boneIndex * 4
410
398
  const ti = boneIndex * 3
411
399
 
412
- // Get local rotation
413
- const localRot = new Quat(
414
- localRotations[qi],
415
- localRotations[qi + 1],
416
- localRotations[qi + 2],
417
- localRotations[qi + 3]
418
- )
400
+ const localRot = this.getQuatFromArray(localRotations, qi)
419
401
 
420
402
  // Apply IK rotation if available
421
403
  let finalRot = localRot
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,47 +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
- // Static method: create quaternion that rotates from one direction to another
131
- static fromTo(from: Vec3, to: Vec3): Quat {
132
- const dot = from.dot(to)
133
- if (dot > 0.999999) return new Quat(0, 0, 0, 1) // Already aligned
134
- if (dot < -0.999999) {
135
- // 180 degrees
136
- let axis = from.cross(new Vec3(1, 0, 0))
137
- if (axis.length() < 0.001) axis = from.cross(new Vec3(0, 1, 0))
138
- return new Quat(axis.x, axis.y, axis.z, 0).normalize()
139
- }
140
-
141
- const axis = from.cross(to)
142
- const w = Math.sqrt((1 + dot) * 2)
143
- const invW = 1 / w
144
- return new Quat(axis.x * invW, axis.y * invW, axis.z * invW, w * 0.5).normalize()
145
- }
146
-
147
98
  // Static method: create quaternion from rotation axis and angle
148
99
  static fromAxisAngle(axis: Vec3, angle: number): Quat {
149
100
  const normalizedAxis = axis.normalize()
@@ -209,31 +160,6 @@ export class Quat {
209
160
 
210
161
  return new Quat(x, y, z, w).normalize()
211
162
  }
212
-
213
- // Convert quaternion to Euler angles (ZXY order, inverse of fromEuler)
214
- toEuler(): Vec3 {
215
- const qx = this.x
216
- const qy = this.y
217
- const qz = this.z
218
- const qw = this.w
219
-
220
- // ZXY order (left-handed)
221
- // Roll (X): rotation around X axis
222
- const sinr_cosp = 2 * (qw * qx + qy * qz)
223
- const cosr_cosp = 1 - 2 * (qx * qx + qy * qy)
224
- const rotX = Math.atan2(sinr_cosp, cosr_cosp)
225
-
226
- // Pitch (Y): rotation around Y axis
227
- const sinp = 2 * (qw * qy - qz * qx)
228
- const rotY = Math.abs(sinp) >= 1 ? (sinp >= 0 ? Math.PI / 2 : -Math.PI / 2) : Math.asin(sinp)
229
-
230
- // Yaw (Z): rotation around Z axis
231
- const siny_cosp = 2 * (qw * qz + qx * qy)
232
- const cosy_cosp = 1 - 2 * (qy * qy + qz * qz)
233
- const rotZ = Math.atan2(siny_cosp, cosy_cosp)
234
-
235
- return new Vec3(rotX, rotY, rotZ)
236
- }
237
163
  }
238
164
 
239
165
  export class Mat4 {