reze-engine 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/physics.js CHANGED
@@ -1,4 +1,10 @@
1
1
  import { Quat, Vec3, Mat4 } from "./math";
2
+ // Physics-local scratch pool for per-frame sync (syncFromBones, applyAmmoRigidbodiesToBones).
3
+ // Each method uses only these slots and completes synchronously before the next is called.
4
+ const _physMat = [
5
+ new Float32Array(16), new Float32Array(16), new Float32Array(16),
6
+ ];
7
+ const _physQuat = new Quat(0, 0, 0, 1);
2
8
  import { loadAmmo } from "./ammo-loader";
3
9
  export var RigidbodyShape;
4
10
  (function (RigidbodyShape) {
@@ -358,9 +364,9 @@ export class Physics {
358
364
  this.firstFrame = false;
359
365
  }
360
366
  // Step order: 1) Sync Static/Kinematic from bones, 2) Step physics, 3) Apply dynamic to bones
361
- this.syncFromBones(boneWorldMatrices, boneInverseBindMatrices, boneCount);
367
+ this.syncFromBones(boneWorldMatrices, boneCount);
362
368
  this.stepAmmoPhysics(dt);
363
- this.applyAmmoRigidbodiesToBones(boneWorldMatrices, boneInverseBindMatrices, boneCount);
369
+ this.applyAmmoRigidbodiesToBones(boneWorldMatrices, boneCount);
364
370
  }
365
371
  // Compute bodyOffsetMatrixInverse for all rigidbodies (called once during initialization)
366
372
  computeBodyOffsets(boneInverseBindMatrices, boneCount) {
@@ -425,7 +431,7 @@ export class Physics {
425
431
  }
426
432
  }
427
433
  // Sync Static (FollowBone) and Kinematic rigidbodies to follow bone transforms
428
- syncFromBones(boneWorldMatrices, boneInverseBindMatrices, boneCount) {
434
+ syncFromBones(boneWorldMatrices, boneCount) {
429
435
  if (!this.ammo || !this.dynamicsWorld)
430
436
  return;
431
437
  const Ammo = this.ammo;
@@ -440,14 +446,17 @@ export class Physics {
440
446
  rb.boneIndex < boneCount) {
441
447
  const boneIdx = rb.boneIndex;
442
448
  const boneWorldMat = boneWorldMatrices[boneIdx];
443
- // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
444
- const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse();
445
- const nodeWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix);
446
- const worldPos = nodeWorldMatrix.getPosition();
447
- const worldRot = nodeWorldMatrix.toQuat();
449
+ // Lazy-cache bodyOffsetMatrix on first hit (cold path).
450
+ if (!rb.bodyOffsetMatrix)
451
+ rb.bodyOffsetMatrix = rb.bodyOffsetMatrixInverse.inverse();
452
+ // nodeWorld = boneWorld × bodyOffsetMatrix → _physMat[0]
453
+ Mat4.multiplyArrays(boneWorldMat.values, 0, rb.bodyOffsetMatrix.values, 0, _physMat[0], 0);
454
+ const nodeVals = _physMat[0];
455
+ const wx = nodeVals[12], wy = nodeVals[13], wz = nodeVals[14];
456
+ Mat4.toQuatFromArrayInto(nodeVals, 0, _physQuat);
448
457
  const transform = new Ammo.btTransform();
449
- const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z);
450
- const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w);
458
+ const pos = new Ammo.btVector3(wx, wy, wz);
459
+ const quat = new Ammo.btQuaternion(_physQuat.x, _physQuat.y, _physQuat.z, _physQuat.w);
451
460
  transform.setOrigin(pos);
452
461
  transform.setRotation(quat);
453
462
  ammoBody.setWorldTransform(transform);
@@ -472,7 +481,7 @@ export class Physics {
472
481
  this.dynamicsWorld.stepSimulation(dt, maxSubSteps, fixedTimeStep);
473
482
  }
474
483
  // Apply dynamic rigidbody world transforms to bone world matrices in-place
475
- applyAmmoRigidbodiesToBones(boneWorldMatrices, boneInverseBindMatrices, boneCount) {
484
+ applyAmmoRigidbodiesToBones(boneWorldMatrices, boneCount) {
476
485
  if (!this.ammo || !this.dynamicsWorld)
477
486
  return;
478
487
  for (let i = 0; i < this.rigidbodies.length; i++) {
@@ -486,14 +495,13 @@ export class Physics {
486
495
  const transform = ammoBody.getWorldTransform();
487
496
  const origin = transform.getOrigin();
488
497
  const rotation = transform.getRotation();
489
- const nodePos = new Vec3(origin.x(), origin.y(), origin.z());
490
- const nodeRot = new Quat(rotation.x(), rotation.y(), rotation.z(), rotation.w());
491
- const nodeWorldMatrix = Mat4.fromPositionRotation(nodePos, nodeRot);
492
- // boneWorld = nodeWorld × bodyOffsetMatrixInverse (not bodyOffsetMatrixInverse × nodeWorld)
493
- const boneWorldMat = nodeWorldMatrix.multiply(rb.bodyOffsetMatrixInverse);
494
- const values = boneWorldMat.values;
495
- if (!isNaN(values[0]) && !isNaN(values[15]) && Math.abs(values[0]) < 1e6 && Math.abs(values[15]) < 1e6) {
496
- boneWorldMatrices[boneIdx].values.set(values);
498
+ // nodeWorldMatrix _physMat[0] (from ammo position/rotation directly)
499
+ Mat4.fromPositionRotationInto(origin.x(), origin.y(), origin.z(), rotation.x(), rotation.y(), rotation.z(), rotation.w(), _physMat[0]);
500
+ // boneWorld = nodeWorld × bodyOffsetMatrixInverse → _physMat[1]
501
+ const boneVals = _physMat[1];
502
+ Mat4.multiplyArrays(_physMat[0], 0, rb.bodyOffsetMatrixInverse.values, 0, boneVals, 0);
503
+ if (!isNaN(boneVals[0]) && !isNaN(boneVals[15]) && Math.abs(boneVals[0]) < 1e6 && Math.abs(boneVals[15]) < 1e6) {
504
+ boneWorldMatrices[boneIdx].values.set(boneVals);
497
505
  }
498
506
  else {
499
507
  console.warn(`[Physics] Invalid bone world matrix for rigidbody ${i} (${rb.name}), skipping update`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/ik-solver.ts CHANGED
@@ -72,6 +72,20 @@ class IKChain {
72
72
  }
73
73
  }
74
74
 
75
+ // IK-local scratch pool. Safe because solve() runs synchronously and all scratch
76
+ // use completes before the updateWorldMatrix callback is invoked.
77
+ const _ikVec: Vec3[] = [
78
+ new Vec3(0, 0, 0), new Vec3(0, 0, 0), new Vec3(0, 0, 0), new Vec3(0, 0, 0), new Vec3(0, 0, 0),
79
+ new Vec3(0, 0, 0), new Vec3(0, 0, 0), new Vec3(0, 0, 0), new Vec3(0, 0, 0),
80
+ ]
81
+ const _ikQuat: Quat[] = [
82
+ new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1),
83
+ new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1),
84
+ ]
85
+ const _ikMat: Float32Array[] = [
86
+ new Float32Array(16), new Float32Array(16), new Float32Array(16), new Float32Array(16),
87
+ ]
88
+
75
89
  export class IKSolverSystem {
76
90
  private static readonly EPSILON = 1.0e-8
77
91
  private static readonly THRESHOLD = (88 * Math.PI) / 180
@@ -163,13 +177,13 @@ export class IKSolverSystem {
163
177
  if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) break
164
178
  }
165
179
 
166
- // Apply IK rotations to local rotations
180
+ // Apply IK rotations to local rotations (mutate localRot in place)
167
181
  for (const link of solver.links) {
168
182
  const chainInfo = ikChainInfo[link.boneIndex]
169
183
  if (chainInfo?.ikRotation) {
170
184
  const localRot = localRotations[link.boneIndex]
171
- const finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
172
- localRot.set(finalRot)
185
+ Quat.multiplyInto(chainInfo.ikRotation, localRot, localRot)
186
+ localRot.normalize()
173
187
  }
174
188
  }
175
189
  }
@@ -189,72 +203,95 @@ export class IKSolverSystem {
189
203
  updateWorldMatrix?: UpdateWorldMatrixFn
190
204
  ): void {
191
205
  const chainBoneIndex = chain.boneIndex
192
- const chainPosition = this.getWorldTranslation(chainBoneIndex, worldMatrices)
193
- const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
194
- const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
195
206
 
196
- const chainTargetVector = chainPosition.subtract(targetPosition).normalize()
197
- const chainIkVector = chainPosition.subtract(ikPosition).normalize()
207
+ // scratch layout:
208
+ // _ikVec[0]=chainPos, [1]=ikPos, [2]=targetPos, [3]=chainTargetVec, [4]=chainIkVec,
209
+ // [5]=rotAxis, [6]=finalAxis, [7]=eulerTmp, [8]=limitedEuler
210
+ // _ikMat[0]=parentRot, [1]=invParentRot, [2]=quatToMatTmp
211
+ // _ikQuat[0]=ikRotation, [1]=combinedRot, [2]=localRotConj, [3..5]=axisAngleTmp
212
+
213
+ const chainPos = Vec3.setFromMat4Translation(worldMatrices[chainBoneIndex].values, _ikVec[0])
214
+ const ikPos = Vec3.setFromMat4Translation(worldMatrices[ikBoneIndex].values, _ikVec[1])
215
+ const targetPos = Vec3.setFromMat4Translation(worldMatrices[targetBoneIndex].values, _ikVec[2])
216
+
217
+ const chainTargetVec = Vec3.subtractInto(chainPos, targetPos, _ikVec[3])
218
+ chainTargetVec.normalize()
219
+ const chainIkVec = Vec3.subtractInto(chainPos, ikPos, _ikVec[4])
220
+ chainIkVec.normalize()
198
221
 
199
- const chainRotationAxis = chainTargetVector.cross(chainIkVector)
200
- if (chainRotationAxis.length() < this.EPSILON) return
222
+ const rotAxis = Vec3.crossInto(chainTargetVec, chainIkVec, _ikVec[5])
223
+ if (rotAxis.length() < this.EPSILON) return
201
224
 
202
- // Get parent's world rotation matrix (translation removed)
203
- const parentWorldRotMatrix = this.getParentWorldRotationMatrix(chainBoneIndex, bones, worldMatrices)
225
+ // Parent world rotation matrix (translation removed) into _ikMat[0]
226
+ this.getParentWorldRotationMatrixInto(chainBoneIndex, bones, worldMatrices, _ikMat[0])
204
227
 
205
- let finalRotationAxis: Vec3
228
+ let finalAxis: Vec3
206
229
  if (chain.minimumAngle !== null && useAxis) {
207
230
  switch (chain.solveAxis) {
208
231
  case InternalSolveAxis.None: {
209
- const invParentRot = parentWorldRotMatrix.inverse()
210
- finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
232
+ if (!Mat4.inverseInto(_ikMat[0], _ikMat[1])) {
233
+ finalAxis = rotAxis
234
+ } else {
235
+ finalAxis = Vec3.transformMat4RotationInto(rotAxis, _ikMat[1], _ikVec[6])
236
+ finalAxis.normalize()
237
+ }
211
238
  break
212
239
  }
213
240
  case InternalSolveAxis.X:
214
241
  case InternalSolveAxis.Y:
215
242
  case InternalSolveAxis.Z: {
216
- const m = parentWorldRotMatrix.values
243
+ const m = _ikMat[0]
217
244
  const axisOffset = (chain.solveAxis - InternalSolveAxis.X) * 4
218
- const axis = new Vec3(m[axisOffset], m[axisOffset + 1], m[axisOffset + 2])
219
- const dot = chainRotationAxis.dot(axis)
220
- const sign = dot >= 0 ? 1 : -1
221
- finalRotationAxis =
245
+ const ax = m[axisOffset], ay = m[axisOffset + 1], az = m[axisOffset + 2]
246
+ const dotA = rotAxis.x * ax + rotAxis.y * ay + rotAxis.z * az
247
+ const sign = dotA >= 0 ? 1 : -1
248
+ finalAxis =
222
249
  chain.solveAxis === InternalSolveAxis.X
223
- ? new Vec3(sign, 0, 0)
250
+ ? _ikVec[6].setXYZ(sign, 0, 0)
224
251
  : chain.solveAxis === InternalSolveAxis.Y
225
- ? new Vec3(0, sign, 0)
226
- : new Vec3(0, 0, sign)
252
+ ? _ikVec[6].setXYZ(0, sign, 0)
253
+ : _ikVec[6].setXYZ(0, 0, sign)
227
254
  break
228
255
  }
229
256
  default:
230
- finalRotationAxis = chainRotationAxis
257
+ finalAxis = rotAxis
231
258
  }
232
259
  } else {
233
- const invParentRot = parentWorldRotMatrix.inverse()
234
- finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
260
+ if (!Mat4.inverseInto(_ikMat[0], _ikMat[1])) {
261
+ finalAxis = rotAxis
262
+ } else {
263
+ finalAxis = Vec3.transformMat4RotationInto(rotAxis, _ikMat[1], _ikVec[6])
264
+ finalAxis.normalize()
265
+ }
235
266
  }
236
267
 
237
- let dot = chainTargetVector.dot(chainIkVector)
238
- dot = Math.max(-1.0, Math.min(1.0, dot))
268
+ let dotTI = chainTargetVec.dot(chainIkVec)
269
+ dotTI = Math.max(-1.0, Math.min(1.0, dotTI))
239
270
 
240
- const angle = Math.min(solver.limitAngle * (chainIndex + 1), Math.acos(dot))
241
- const ikRotation = Quat.fromAxisAngle(finalRotationAxis, angle)
271
+ const angle = Math.min(solver.limitAngle * (chainIndex + 1), Math.acos(dotTI))
272
+ const ikRotation = Quat.fromAxisAngleInto(finalAxis.x, finalAxis.y, finalAxis.z, angle, _ikQuat[0])
242
273
 
243
274
  const chainInfo = ikChainInfo[chainBoneIndex]
244
275
  if (chainInfo) {
245
- chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
276
+ // chainInfo.ikRotation = ikRotation * chainInfo.ikRotation (in place)
277
+ Quat.multiplyInto(ikRotation, chainInfo.ikRotation, chainInfo.ikRotation)
246
278
 
247
- // Apply angle constraints if present
248
279
  if (chain.minimumAngle && chain.maximumAngle) {
249
280
  const localRot = localRotations[chainBoneIndex]
250
281
  chainInfo.localRotation = localRot.clone()
251
282
 
252
- const combinedRot = chainInfo.ikRotation.multiply(localRot)
253
- const euler = this.extractEulerAngles(combinedRot, chain.rotationOrder)
254
- const limited = this.limitEulerAngles(euler, chain.minimumAngle, chain.maximumAngle, useAxis)
255
- chainInfo.ikRotation = this.reconstructQuatFromEuler(limited, chain.rotationOrder)
256
- // Clone localRot to avoid mutating, then conjugate and normalize
257
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(localRot.clone().conjugate().normalize())
283
+ // combinedRot = chainInfo.ikRotation * localRot
284
+ Quat.multiplyInto(chainInfo.ikRotation, localRot, _ikQuat[1])
285
+ // extract euler into _ikVec[7]
286
+ this.extractEulerAnglesInto(_ikQuat[1], chain.rotationOrder, _ikVec[7])
287
+ // limit into _ikVec[8]
288
+ this.limitEulerAnglesInto(_ikVec[7], chain.minimumAngle, chain.maximumAngle, useAxis, _ikVec[8])
289
+ // reconstruct quat into chainInfo.ikRotation (uses _ikQuat[3..5] as tmp)
290
+ this.reconstructQuatFromEulerInto(_ikVec[8], chain.rotationOrder, chainInfo.ikRotation)
291
+ // localRot conjugate into _ikQuat[2] (localRot is unit, so conjugate == inverse)
292
+ _ikQuat[2].setXYZW(-localRot.x, -localRot.y, -localRot.z, localRot.w)
293
+ // chainInfo.ikRotation *= localRotConj
294
+ Quat.multiplyInto(chainInfo.ikRotation, _ikQuat[2], chainInfo.ikRotation)
258
295
  }
259
296
  }
260
297
 
@@ -288,19 +325,28 @@ export class IKSolverSystem {
288
325
  }
289
326
 
290
327
  private static getDistance(boneIndex1: number, boneIndex2: number, worldMatrices: Mat4[]): number {
291
- const pos1 = this.getWorldTranslation(boneIndex1, worldMatrices)
292
- const pos2 = this.getWorldTranslation(boneIndex2, worldMatrices)
293
- return pos1.subtract(pos2).length()
328
+ const m1 = worldMatrices[boneIndex1].values
329
+ const m2 = worldMatrices[boneIndex2].values
330
+ const dx = m1[12] - m2[12]
331
+ const dy = m1[13] - m2[13]
332
+ const dz = m1[14] - m2[14]
333
+ return Math.sqrt(dx * dx + dy * dy + dz * dz)
294
334
  }
295
335
 
296
- private static getWorldTranslation(boneIndex: number, worldMatrices: Mat4[]): Vec3 {
297
- const mat = worldMatrices[boneIndex]
298
- return new Vec3(mat.values[12], mat.values[13], mat.values[14])
299
- }
300
-
301
- private static extractEulerAngles(quat: Quat, order: InternalEulerRotationOrder): Vec3 {
302
- const rotMatrix = Mat4.fromQuat(quat.x, quat.y, quat.z, quat.w)
303
- const m = rotMatrix.values
336
+ // Euler axis triples for each rotation order (indexed by order enum).
337
+ // Reused to avoid allocations in reconstructQuatFromEulerInto.
338
+ private static readonly EULER_AXES: readonly [number, number, number][][] = [
339
+ // YXZ: Y, X, Z
340
+ [[0, 1, 0], [1, 0, 0], [0, 0, 1]],
341
+ // ZYX: Z, Y, X
342
+ [[0, 0, 1], [0, 1, 0], [1, 0, 0]],
343
+ // XZY: X, Z, Y
344
+ [[1, 0, 0], [0, 0, 1], [0, 1, 0]],
345
+ ]
346
+
347
+ private static extractEulerAnglesInto(quat: Quat, order: InternalEulerRotationOrder, out: Vec3): void {
348
+ Mat4.fromQuatInto(quat.x, quat.y, quat.z, quat.w, _ikMat[2], 0)
349
+ const m = _ikMat[2]
304
350
 
305
351
  switch (order) {
306
352
  case InternalEulerRotationOrder.YXZ: {
@@ -308,83 +354,83 @@ export class IKSolverSystem {
308
354
  if (Math.abs(rX) > this.THRESHOLD) rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
309
355
  let cosX = Math.cos(rX)
310
356
  if (cosX !== 0) cosX = 1 / cosX
311
- const rY = Math.atan2(m[8] * cosX, m[10] * cosX)
312
- const rZ = Math.atan2(m[1] * cosX, m[5] * cosX)
313
- return new Vec3(rX, rY, rZ)
357
+ out.x = rX
358
+ out.y = Math.atan2(m[8] * cosX, m[10] * cosX)
359
+ out.z = Math.atan2(m[1] * cosX, m[5] * cosX)
360
+ return
314
361
  }
315
362
  case InternalEulerRotationOrder.ZYX: {
316
363
  let rY = Math.asin(-m[2])
317
364
  if (Math.abs(rY) > this.THRESHOLD) rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
318
365
  let cosY = Math.cos(rY)
319
366
  if (cosY !== 0) cosY = 1 / cosY
320
- const rX = Math.atan2(m[6] * cosY, m[10] * cosY)
321
- const rZ = Math.atan2(m[1] * cosY, m[0] * cosY)
322
- return new Vec3(rX, rY, rZ)
367
+ out.x = Math.atan2(m[6] * cosY, m[10] * cosY)
368
+ out.y = rY
369
+ out.z = Math.atan2(m[1] * cosY, m[0] * cosY)
370
+ return
323
371
  }
324
372
  case InternalEulerRotationOrder.XZY: {
325
373
  let rZ = Math.asin(-m[4])
326
374
  if (Math.abs(rZ) > this.THRESHOLD) rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
327
375
  let cosZ = Math.cos(rZ)
328
376
  if (cosZ !== 0) cosZ = 1 / cosZ
329
- const rX = Math.atan2(m[6] * cosZ, m[5] * cosZ)
330
- const rY = Math.atan2(m[8] * cosZ, m[0] * cosZ)
331
- return new Vec3(rX, rY, rZ)
377
+ out.x = Math.atan2(m[6] * cosZ, m[5] * cosZ)
378
+ out.y = Math.atan2(m[8] * cosZ, m[0] * cosZ)
379
+ out.z = rZ
380
+ return
332
381
  }
333
382
  }
334
383
  }
335
384
 
336
- private static limitEulerAngles(euler: Vec3, min: Vec3, max: Vec3, useAxis: boolean): Vec3 {
337
- return new Vec3(
338
- this.limitAngle(euler.x, min.x, max.x, useAxis),
339
- this.limitAngle(euler.y, min.y, max.y, useAxis),
340
- this.limitAngle(euler.z, min.z, max.z, useAxis)
341
- )
385
+ private static limitEulerAnglesInto(euler: Vec3, min: Vec3, max: Vec3, useAxis: boolean, out: Vec3): void {
386
+ out.x = this.limitAngle(euler.x, min.x, max.x, useAxis)
387
+ out.y = this.limitAngle(euler.y, min.y, max.y, useAxis)
388
+ out.z = this.limitAngle(euler.z, min.z, max.z, useAxis)
342
389
  }
343
390
 
344
- private static reconstructQuatFromEuler(euler: Vec3, order: InternalEulerRotationOrder): Quat {
345
- const axes = [
346
- [new Vec3(1, 0, 0), new Vec3(0, 1, 0), new Vec3(0, 0, 1)],
347
- [new Vec3(0, 0, 1), new Vec3(0, 1, 0), new Vec3(1, 0, 0)],
348
- [new Vec3(0, 1, 0), new Vec3(1, 0, 0), new Vec3(0, 0, 1)],
349
- ]
350
-
351
- const [axis1, axis2, axis3] = axes[order]
352
- const [angle1, angle2, angle3] =
353
- order === InternalEulerRotationOrder.YXZ
354
- ? [euler.y, euler.x, euler.z]
355
- : order === InternalEulerRotationOrder.ZYX
356
- ? [euler.z, euler.y, euler.x]
357
- : [euler.x, euler.z, euler.y]
358
-
359
- let result = Quat.fromAxisAngle(axis1, angle1)
360
- result = result.multiply(Quat.fromAxisAngle(axis2, angle2))
361
- result = result.multiply(Quat.fromAxisAngle(axis3, angle3))
362
- return result
391
+ private static reconstructQuatFromEulerInto(euler: Vec3, order: InternalEulerRotationOrder, out: Quat): void {
392
+ const axes = this.EULER_AXES[order]
393
+ const a1 = axes[0], a2 = axes[1], a3 = axes[2]
394
+ const ang1 =
395
+ order === InternalEulerRotationOrder.YXZ ? euler.y
396
+ : order === InternalEulerRotationOrder.ZYX ? euler.z
397
+ : euler.x
398
+ const ang2 =
399
+ order === InternalEulerRotationOrder.YXZ ? euler.x
400
+ : order === InternalEulerRotationOrder.ZYX ? euler.y
401
+ : euler.z
402
+ const ang3 =
403
+ order === InternalEulerRotationOrder.YXZ ? euler.z
404
+ : order === InternalEulerRotationOrder.ZYX ? euler.x
405
+ : euler.y
406
+
407
+ // result = axisAngle(a1, ang1); then *= axisAngle(a2, ang2); then *= axisAngle(a3, ang3)
408
+ Quat.fromAxisAngleInto(a1[0], a1[1], a1[2], ang1, out)
409
+ Quat.fromAxisAngleInto(a2[0], a2[1], a2[2], ang2, _ikQuat[3])
410
+ Quat.multiplyInto(out, _ikQuat[3], out)
411
+ Quat.fromAxisAngleInto(a3[0], a3[1], a3[2], ang3, _ikQuat[3])
412
+ Quat.multiplyInto(out, _ikQuat[3], out)
363
413
  }
364
414
 
365
- private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Mat4[]): Mat4 {
415
+ // Write parent's world rotation (translation stripped) into out Float32Array.
416
+ private static getParentWorldRotationMatrixInto(
417
+ boneIndex: number,
418
+ bones: Bone[],
419
+ worldMatrices: Mat4[],
420
+ out: Float32Array
421
+ ): void {
366
422
  const bone = bones[boneIndex]
367
423
  if (bone.parentIndex >= 0) {
368
- const parentMat = worldMatrices[bone.parentIndex]
369
- // Remove translation
370
- const rotMat = Mat4.identity()
371
- const m = parentMat.values
372
- rotMat.values.set([m[0], m[1], m[2], 0, m[4], m[5], m[6], 0, m[8], m[9], m[10], 0, 0, 0, 0, 1])
373
- return rotMat
424
+ Mat4.copyRotationInto(worldMatrices[bone.parentIndex].values, out)
374
425
  } else {
375
- return Mat4.identity()
426
+ // Identity
427
+ out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0
428
+ out[4] = 0; out[5] = 1; out[6] = 0; out[7] = 0
429
+ out[8] = 0; out[9] = 0; out[10] = 1; out[11] = 0
430
+ out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1
376
431
  }
377
432
  }
378
433
 
379
- private static transformNormal(normal: Vec3, matrix: Mat4): Vec3 {
380
- const m = matrix.values
381
- return new Vec3(
382
- m[0] * normal.x + m[4] * normal.y + m[8] * normal.z,
383
- m[1] * normal.x + m[5] * normal.y + m[9] * normal.z,
384
- m[2] * normal.x + m[6] * normal.y + m[10] * normal.z
385
- )
386
- }
387
-
388
434
  private static updateWorldMatrix(
389
435
  boneIndex: number,
390
436
  bones: Bone[],
@@ -397,28 +443,35 @@ export class IKSolverSystem {
397
443
  const localRot = localRotations[boneIndex]
398
444
  const localTrans = localTranslations[boneIndex]
399
445
 
400
- // Apply IK rotation if available
401
- let finalRot = localRot
446
+ let fx = localRot.x, fy = localRot.y, fz = localRot.z, fw = localRot.w
402
447
  if (ikChainInfo) {
403
448
  const chainInfo = ikChainInfo[boneIndex]
404
449
  if (chainInfo && chainInfo.ikRotation) {
405
- finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
450
+ const ik = chainInfo.ikRotation
451
+ const nx = ik.w * fx + ik.x * fw + ik.y * fz - ik.z * fy
452
+ const ny = ik.w * fy - ik.x * fz + ik.y * fw + ik.z * fx
453
+ const nz = ik.w * fz + ik.x * fy - ik.y * fx + ik.z * fw
454
+ const nw = ik.w * fw - ik.x * fx - ik.y * fy - ik.z * fz
455
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz + nw * nw)
456
+ const inv = len > 0 ? 1 / len : 0
457
+ fx = nx * inv; fy = ny * inv; fz = nz * inv; fw = nw * inv
406
458
  }
407
459
  }
408
- const rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w)
409
460
 
410
- const localM = Mat4.identity()
411
- .translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
412
- .multiply(rotateM)
413
- .translateInPlace(localTrans.x, localTrans.y, localTrans.z)
461
+ const localMVals = _ikMat[3]
462
+ Mat4.localTransformInto(
463
+ bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2],
464
+ fx, fy, fz, fw,
465
+ localTrans.x, localTrans.y, localTrans.z,
466
+ localMVals
467
+ )
414
468
 
415
469
  const worldMat = worldMatrices[boneIndex]
416
470
  if (bone.parentIndex >= 0) {
417
471
  const parentMat = worldMatrices[bone.parentIndex]
418
- const result = parentMat.multiply(localM)
419
- worldMat.values.set(result.values)
472
+ Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
420
473
  } else {
421
- worldMat.values.set(localM.values)
474
+ worldMat.values.set(localMVals)
422
475
  }
423
476
  }
424
477
  }