reze-engine 0.3.11 → 0.3.12

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
@@ -1,411 +1,411 @@
1
- /**
2
- * IK Solver implementation
3
- * Based on reference from babylon-mmd and Saba MMD library
4
- * https://github.com/benikabocha/saba/blob/master/src/Saba/Model/MMD/MMDIkSolver.cpp
5
- */
6
-
7
- import { Mat4, Quat, Vec3 } from "./math"
8
- import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
9
-
10
- const enum InternalEulerRotationOrder {
11
- YXZ = 0,
12
- ZYX = 1,
13
- XZY = 2,
14
- }
15
-
16
- const enum InternalSolveAxis {
17
- None = 0,
18
- Fixed = 1,
19
- X = 2,
20
- Y = 3,
21
- Z = 4,
22
- }
23
-
24
- class IKChain {
25
- public readonly boneIndex: number
26
- public readonly minimumAngle: Vec3 | null
27
- public readonly maximumAngle: Vec3 | null
28
- public readonly rotationOrder: InternalEulerRotationOrder
29
- public readonly solveAxis: InternalSolveAxis
30
-
31
- public constructor(boneIndex: number, link: IKLink) {
32
- this.boneIndex = boneIndex
33
-
34
- if (link.hasLimit && link.minAngle && link.maxAngle) {
35
- // Normalize min/max angles
36
- const minX = Math.min(link.minAngle.x, link.maxAngle.x)
37
- const minY = Math.min(link.minAngle.y, link.maxAngle.y)
38
- const minZ = Math.min(link.minAngle.z, link.maxAngle.z)
39
- const maxX = Math.max(link.minAngle.x, link.maxAngle.x)
40
- const maxY = Math.max(link.minAngle.y, link.maxAngle.y)
41
- const maxZ = Math.max(link.minAngle.z, link.maxAngle.z)
42
- this.minimumAngle = new Vec3(minX, minY, minZ)
43
- this.maximumAngle = new Vec3(maxX, maxY, maxZ)
44
-
45
- // Determine rotation order based on constraint ranges
46
- const halfPi = Math.PI * 0.5
47
- if (-halfPi < minX && maxX < halfPi) {
48
- this.rotationOrder = InternalEulerRotationOrder.YXZ
49
- } else if (-halfPi < minY && maxY < halfPi) {
50
- this.rotationOrder = InternalEulerRotationOrder.ZYX
51
- } else {
52
- this.rotationOrder = InternalEulerRotationOrder.XZY
53
- }
54
-
55
- // Determine solve axis optimization
56
- if (minX === 0 && maxX === 0 && minY === 0 && maxY === 0 && minZ === 0 && maxZ === 0) {
57
- this.solveAxis = InternalSolveAxis.Fixed
58
- } else if (minY === 0 && maxY === 0 && minZ === 0 && maxZ === 0) {
59
- this.solveAxis = InternalSolveAxis.X
60
- } else if (minX === 0 && maxX === 0 && minZ === 0 && maxZ === 0) {
61
- this.solveAxis = InternalSolveAxis.Y
62
- } else if (minX === 0 && maxX === 0 && minY === 0 && maxY === 0) {
63
- this.solveAxis = InternalSolveAxis.Z
64
- } else {
65
- this.solveAxis = InternalSolveAxis.None
66
- }
67
- } else {
68
- this.minimumAngle = null
69
- this.maximumAngle = null
70
- this.rotationOrder = InternalEulerRotationOrder.XZY // not used
71
- this.solveAxis = InternalSolveAxis.None
72
- }
73
- }
74
- }
75
-
76
- /**
77
- * Solve IK chains for a model
78
- */
79
- export class IKSolverSystem {
80
- private static readonly EPSILON = 1.0e-8
81
- private static readonly THRESHOLD = (88 * Math.PI) / 180
82
-
83
- /**
84
- * Solve all IK chains
85
- */
86
- public static solve(
87
- ikSolvers: IKSolver[],
88
- bones: Bone[],
89
- localRotations: Quat[],
90
- localTranslations: Vec3[],
91
- worldMatrices: Mat4[],
92
- ikChainInfo: IKChainInfo[]
93
- ): void {
94
- for (const solver of ikSolvers) {
95
- this.solveIK(solver, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
96
- }
97
- }
98
-
99
- private static solveIK(
100
- solver: IKSolver,
101
- bones: Bone[],
102
- localRotations: Quat[],
103
- localTranslations: Vec3[],
104
- worldMatrices: Mat4[],
105
- ikChainInfo: IKChainInfo[]
106
- ): void {
107
- if (solver.links.length === 0) return
108
-
109
- const ikBoneIndex = solver.ikBoneIndex
110
- const targetBoneIndex = solver.targetBoneIndex
111
-
112
- // Reset IK rotations
113
- for (const link of solver.links) {
114
- const chainInfo = ikChainInfo[link.boneIndex]
115
- if (chainInfo) {
116
- chainInfo.ikRotation = new Quat(0, 0, 0, 1)
117
- }
118
- }
119
-
120
- if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
121
-
122
- // Build IK chains
123
- const chains: IKChain[] = []
124
- for (const link of solver.links) {
125
- chains.push(new IKChain(link.boneIndex, link))
126
- }
127
-
128
- // Update chain bones and target bone world matrices (initial state, no IK yet)
129
- for (let i = chains.length - 1; i >= 0; i--) {
130
- this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
131
- }
132
- this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
133
-
134
- if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
135
-
136
- // Solve iteratively
137
- const iteration = Math.min(solver.iterationCount, 256)
138
- const halfIteration = iteration >> 1
139
-
140
- for (let i = 0; i < iteration; i++) {
141
- for (let chainIndex = 0; chainIndex < chains.length; chainIndex++) {
142
- const chain = chains[chainIndex]
143
- if (chain.solveAxis !== InternalSolveAxis.Fixed) {
144
- this.solveChain(
145
- chain,
146
- chainIndex,
147
- solver,
148
- ikBoneIndex,
149
- targetBoneIndex,
150
- bones,
151
- localRotations,
152
- localTranslations,
153
- worldMatrices,
154
- ikChainInfo,
155
- i < halfIteration
156
- )
157
- }
158
- }
159
-
160
- if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) break
161
- }
162
-
163
- // Apply IK rotations to local rotations
164
- for (const link of solver.links) {
165
- const chainInfo = ikChainInfo[link.boneIndex]
166
- if (chainInfo?.ikRotation) {
167
- const localRot = localRotations[link.boneIndex]
168
- const finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
169
- localRot.set(finalRot)
170
- }
171
- }
172
- }
173
-
174
- private static solveChain(
175
- chain: IKChain,
176
- chainIndex: number,
177
- solver: IKSolver,
178
- ikBoneIndex: number,
179
- targetBoneIndex: number,
180
- bones: Bone[],
181
- localRotations: Quat[],
182
- localTranslations: Vec3[],
183
- worldMatrices: Mat4[],
184
- ikChainInfo: IKChainInfo[],
185
- useAxis: boolean
186
- ): void {
187
- const chainBoneIndex = chain.boneIndex
188
- const chainPosition = this.getWorldTranslation(chainBoneIndex, worldMatrices)
189
- const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
190
- const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
191
-
192
- const chainTargetVector = chainPosition.subtract(targetPosition).normalize()
193
- const chainIkVector = chainPosition.subtract(ikPosition).normalize()
194
-
195
- const chainRotationAxis = chainTargetVector.cross(chainIkVector)
196
- if (chainRotationAxis.length() < this.EPSILON) return
197
-
198
- // Get parent's world rotation matrix (translation removed)
199
- const parentWorldRotMatrix = this.getParentWorldRotationMatrix(chainBoneIndex, bones, worldMatrices)
200
-
201
- let finalRotationAxis: Vec3
202
- if (chain.minimumAngle !== null && useAxis) {
203
- switch (chain.solveAxis) {
204
- case InternalSolveAxis.None: {
205
- const invParentRot = parentWorldRotMatrix.inverse()
206
- finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
207
- break
208
- }
209
- case InternalSolveAxis.X:
210
- case InternalSolveAxis.Y:
211
- case InternalSolveAxis.Z: {
212
- const m = parentWorldRotMatrix.values
213
- const axisOffset = (chain.solveAxis - InternalSolveAxis.X) * 4
214
- const axis = new Vec3(m[axisOffset], m[axisOffset + 1], m[axisOffset + 2])
215
- const dot = chainRotationAxis.dot(axis)
216
- const sign = dot >= 0 ? 1 : -1
217
- finalRotationAxis =
218
- chain.solveAxis === InternalSolveAxis.X
219
- ? new Vec3(sign, 0, 0)
220
- : chain.solveAxis === InternalSolveAxis.Y
221
- ? new Vec3(0, sign, 0)
222
- : new Vec3(0, 0, sign)
223
- break
224
- }
225
- default:
226
- finalRotationAxis = chainRotationAxis
227
- }
228
- } else {
229
- const invParentRot = parentWorldRotMatrix.inverse()
230
- finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
231
- }
232
-
233
- let dot = chainTargetVector.dot(chainIkVector)
234
- dot = Math.max(-1.0, Math.min(1.0, dot))
235
-
236
- const angle = Math.min(solver.limitAngle * (chainIndex + 1), Math.acos(dot))
237
- const ikRotation = Quat.fromAxisAngle(finalRotationAxis, angle)
238
-
239
- const chainInfo = ikChainInfo[chainBoneIndex]
240
- if (chainInfo) {
241
- chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
242
-
243
- // Apply angle constraints if present
244
- if (chain.minimumAngle && chain.maximumAngle) {
245
- const localRot = localRotations[chainBoneIndex]
246
- chainInfo.localRotation = localRot.clone()
247
-
248
- const combinedRot = chainInfo.ikRotation.multiply(localRot)
249
- const euler = this.extractEulerAngles(combinedRot, chain.rotationOrder)
250
- const limited = this.limitEulerAngles(euler, chain.minimumAngle, chain.maximumAngle, useAxis)
251
- chainInfo.ikRotation = this.reconstructQuatFromEuler(limited, chain.rotationOrder)
252
- // Clone localRot to avoid mutating, then conjugate and normalize
253
- chainInfo.ikRotation = chainInfo.ikRotation.multiply(localRot.clone().conjugate().normalize())
254
- }
255
- }
256
-
257
- // Update world matrices for affected bones (using IK-modified rotations)
258
- for (let i = chainIndex; i >= 0; i--) {
259
- const link = solver.links[i]
260
- this.updateWorldMatrix(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
261
- }
262
- this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
263
- }
264
-
265
- private static limitAngle(angle: number, min: number, max: number, useAxis: boolean): number {
266
- if (angle < min) {
267
- const diff = 2 * min - angle
268
- return diff <= max && useAxis ? diff : min
269
- } else if (angle > max) {
270
- const diff = 2 * max - angle
271
- return diff >= min && useAxis ? diff : max
272
- } else {
273
- return angle
274
- }
275
- }
276
-
277
- private static getDistance(boneIndex1: number, boneIndex2: number, worldMatrices: Mat4[]): number {
278
- const pos1 = this.getWorldTranslation(boneIndex1, worldMatrices)
279
- const pos2 = this.getWorldTranslation(boneIndex2, worldMatrices)
280
- return pos1.subtract(pos2).length()
281
- }
282
-
283
- private static getWorldTranslation(boneIndex: number, worldMatrices: Mat4[]): Vec3 {
284
- const mat = worldMatrices[boneIndex]
285
- return new Vec3(mat.values[12], mat.values[13], mat.values[14])
286
- }
287
-
288
- private static extractEulerAngles(quat: Quat, order: InternalEulerRotationOrder): Vec3 {
289
- const rotMatrix = Mat4.fromQuat(quat.x, quat.y, quat.z, quat.w)
290
- const m = rotMatrix.values
291
-
292
- switch (order) {
293
- case InternalEulerRotationOrder.YXZ: {
294
- let rX = Math.asin(-m[9])
295
- if (Math.abs(rX) > this.THRESHOLD) rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
296
- let cosX = Math.cos(rX)
297
- if (cosX !== 0) cosX = 1 / cosX
298
- const rY = Math.atan2(m[8] * cosX, m[10] * cosX)
299
- const rZ = Math.atan2(m[1] * cosX, m[5] * cosX)
300
- return new Vec3(rX, rY, rZ)
301
- }
302
- case InternalEulerRotationOrder.ZYX: {
303
- let rY = Math.asin(-m[2])
304
- if (Math.abs(rY) > this.THRESHOLD) rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
305
- let cosY = Math.cos(rY)
306
- if (cosY !== 0) cosY = 1 / cosY
307
- const rX = Math.atan2(m[6] * cosY, m[10] * cosY)
308
- const rZ = Math.atan2(m[1] * cosY, m[0] * cosY)
309
- return new Vec3(rX, rY, rZ)
310
- }
311
- case InternalEulerRotationOrder.XZY: {
312
- let rZ = Math.asin(-m[4])
313
- if (Math.abs(rZ) > this.THRESHOLD) rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
314
- let cosZ = Math.cos(rZ)
315
- if (cosZ !== 0) cosZ = 1 / cosZ
316
- const rX = Math.atan2(m[6] * cosZ, m[5] * cosZ)
317
- const rY = Math.atan2(m[8] * cosZ, m[0] * cosZ)
318
- return new Vec3(rX, rY, rZ)
319
- }
320
- }
321
- }
322
-
323
- private static limitEulerAngles(euler: Vec3, min: Vec3, max: Vec3, useAxis: boolean): Vec3 {
324
- return new Vec3(
325
- this.limitAngle(euler.x, min.x, max.x, useAxis),
326
- this.limitAngle(euler.y, min.y, max.y, useAxis),
327
- this.limitAngle(euler.z, min.z, max.z, useAxis)
328
- )
329
- }
330
-
331
- private static reconstructQuatFromEuler(euler: Vec3, order: InternalEulerRotationOrder): Quat {
332
- const axes = [
333
- [new Vec3(1, 0, 0), new Vec3(0, 1, 0), new Vec3(0, 0, 1)],
334
- [new Vec3(0, 0, 1), new Vec3(0, 1, 0), new Vec3(1, 0, 0)],
335
- [new Vec3(0, 1, 0), new Vec3(1, 0, 0), new Vec3(0, 0, 1)],
336
- ]
337
-
338
- const [axis1, axis2, axis3] = axes[order]
339
- const [angle1, angle2, angle3] =
340
- order === InternalEulerRotationOrder.YXZ
341
- ? [euler.y, euler.x, euler.z]
342
- : order === InternalEulerRotationOrder.ZYX
343
- ? [euler.z, euler.y, euler.x]
344
- : [euler.x, euler.z, euler.y]
345
-
346
- let result = Quat.fromAxisAngle(axis1, angle1)
347
- result = result.multiply(Quat.fromAxisAngle(axis2, angle2))
348
- result = result.multiply(Quat.fromAxisAngle(axis3, angle3))
349
- return result
350
- }
351
-
352
- private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Mat4[]): Mat4 {
353
- const bone = bones[boneIndex]
354
- if (bone.parentIndex >= 0) {
355
- const parentMat = worldMatrices[bone.parentIndex]
356
- // Remove translation
357
- const rotMat = Mat4.identity()
358
- const m = parentMat.values
359
- 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])
360
- return rotMat
361
- } else {
362
- return Mat4.identity()
363
- }
364
- }
365
-
366
- private static transformNormal(normal: Vec3, matrix: Mat4): Vec3 {
367
- const m = matrix.values
368
- return new Vec3(
369
- m[0] * normal.x + m[4] * normal.y + m[8] * normal.z,
370
- m[1] * normal.x + m[5] * normal.y + m[9] * normal.z,
371
- m[2] * normal.x + m[6] * normal.y + m[10] * normal.z
372
- )
373
- }
374
-
375
- private static updateWorldMatrix(
376
- boneIndex: number,
377
- bones: Bone[],
378
- localRotations: Quat[],
379
- localTranslations: Vec3[],
380
- worldMatrices: Mat4[],
381
- ikChainInfo?: IKChainInfo[]
382
- ): void {
383
- const bone = bones[boneIndex]
384
- const localRot = localRotations[boneIndex]
385
- const localTrans = localTranslations[boneIndex]
386
-
387
- // Apply IK rotation if available
388
- let finalRot = localRot
389
- if (ikChainInfo) {
390
- const chainInfo = ikChainInfo[boneIndex]
391
- if (chainInfo && chainInfo.ikRotation) {
392
- finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
393
- }
394
- }
395
- const rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w)
396
-
397
- const localM = Mat4.identity()
398
- .translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
399
- .multiply(rotateM)
400
- .translateInPlace(localTrans.x, localTrans.y, localTrans.z)
401
-
402
- const worldMat = worldMatrices[boneIndex]
403
- if (bone.parentIndex >= 0) {
404
- const parentMat = worldMatrices[bone.parentIndex]
405
- const result = parentMat.multiply(localM)
406
- worldMat.values.set(result.values)
407
- } else {
408
- worldMat.values.set(localM.values)
409
- }
410
- }
411
- }
1
+ /**
2
+ * IK Solver implementation
3
+ * Based on reference from babylon-mmd and Saba MMD library
4
+ * https://github.com/benikabocha/saba/blob/master/src/Saba/Model/MMD/MMDIkSolver.cpp
5
+ */
6
+
7
+ import { Mat4, Quat, Vec3 } from "./math"
8
+ import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
9
+
10
+ const enum InternalEulerRotationOrder {
11
+ YXZ = 0,
12
+ ZYX = 1,
13
+ XZY = 2,
14
+ }
15
+
16
+ const enum InternalSolveAxis {
17
+ None = 0,
18
+ Fixed = 1,
19
+ X = 2,
20
+ Y = 3,
21
+ Z = 4,
22
+ }
23
+
24
+ class IKChain {
25
+ public readonly boneIndex: number
26
+ public readonly minimumAngle: Vec3 | null
27
+ public readonly maximumAngle: Vec3 | null
28
+ public readonly rotationOrder: InternalEulerRotationOrder
29
+ public readonly solveAxis: InternalSolveAxis
30
+
31
+ public constructor(boneIndex: number, link: IKLink) {
32
+ this.boneIndex = boneIndex
33
+
34
+ if (link.hasLimit && link.minAngle && link.maxAngle) {
35
+ // Normalize min/max angles
36
+ const minX = Math.min(link.minAngle.x, link.maxAngle.x)
37
+ const minY = Math.min(link.minAngle.y, link.maxAngle.y)
38
+ const minZ = Math.min(link.minAngle.z, link.maxAngle.z)
39
+ const maxX = Math.max(link.minAngle.x, link.maxAngle.x)
40
+ const maxY = Math.max(link.minAngle.y, link.maxAngle.y)
41
+ const maxZ = Math.max(link.minAngle.z, link.maxAngle.z)
42
+ this.minimumAngle = new Vec3(minX, minY, minZ)
43
+ this.maximumAngle = new Vec3(maxX, maxY, maxZ)
44
+
45
+ // Determine rotation order based on constraint ranges
46
+ const halfPi = Math.PI * 0.5
47
+ if (-halfPi < minX && maxX < halfPi) {
48
+ this.rotationOrder = InternalEulerRotationOrder.YXZ
49
+ } else if (-halfPi < minY && maxY < halfPi) {
50
+ this.rotationOrder = InternalEulerRotationOrder.ZYX
51
+ } else {
52
+ this.rotationOrder = InternalEulerRotationOrder.XZY
53
+ }
54
+
55
+ // Determine solve axis optimization
56
+ if (minX === 0 && maxX === 0 && minY === 0 && maxY === 0 && minZ === 0 && maxZ === 0) {
57
+ this.solveAxis = InternalSolveAxis.Fixed
58
+ } else if (minY === 0 && maxY === 0 && minZ === 0 && maxZ === 0) {
59
+ this.solveAxis = InternalSolveAxis.X
60
+ } else if (minX === 0 && maxX === 0 && minZ === 0 && maxZ === 0) {
61
+ this.solveAxis = InternalSolveAxis.Y
62
+ } else if (minX === 0 && maxX === 0 && minY === 0 && maxY === 0) {
63
+ this.solveAxis = InternalSolveAxis.Z
64
+ } else {
65
+ this.solveAxis = InternalSolveAxis.None
66
+ }
67
+ } else {
68
+ this.minimumAngle = null
69
+ this.maximumAngle = null
70
+ this.rotationOrder = InternalEulerRotationOrder.XZY // not used
71
+ this.solveAxis = InternalSolveAxis.None
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Solve IK chains for a model
78
+ */
79
+ export class IKSolverSystem {
80
+ private static readonly EPSILON = 1.0e-8
81
+ private static readonly THRESHOLD = (88 * Math.PI) / 180
82
+
83
+ /**
84
+ * Solve all IK chains
85
+ */
86
+ public static solve(
87
+ ikSolvers: IKSolver[],
88
+ bones: Bone[],
89
+ localRotations: Quat[],
90
+ localTranslations: Vec3[],
91
+ worldMatrices: Mat4[],
92
+ ikChainInfo: IKChainInfo[]
93
+ ): void {
94
+ for (const solver of ikSolvers) {
95
+ this.solveIK(solver, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
96
+ }
97
+ }
98
+
99
+ private static solveIK(
100
+ solver: IKSolver,
101
+ bones: Bone[],
102
+ localRotations: Quat[],
103
+ localTranslations: Vec3[],
104
+ worldMatrices: Mat4[],
105
+ ikChainInfo: IKChainInfo[]
106
+ ): void {
107
+ if (solver.links.length === 0) return
108
+
109
+ const ikBoneIndex = solver.ikBoneIndex
110
+ const targetBoneIndex = solver.targetBoneIndex
111
+
112
+ // Reset IK rotations
113
+ for (const link of solver.links) {
114
+ const chainInfo = ikChainInfo[link.boneIndex]
115
+ if (chainInfo) {
116
+ chainInfo.ikRotation = new Quat(0, 0, 0, 1)
117
+ }
118
+ }
119
+
120
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
121
+
122
+ // Build IK chains
123
+ const chains: IKChain[] = []
124
+ for (const link of solver.links) {
125
+ chains.push(new IKChain(link.boneIndex, link))
126
+ }
127
+
128
+ // Update chain bones and target bone world matrices (initial state, no IK yet)
129
+ for (let i = chains.length - 1; i >= 0; i--) {
130
+ this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
131
+ }
132
+ this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
133
+
134
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
135
+
136
+ // Solve iteratively
137
+ const iteration = Math.min(solver.iterationCount, 256)
138
+ const halfIteration = iteration >> 1
139
+
140
+ for (let i = 0; i < iteration; i++) {
141
+ for (let chainIndex = 0; chainIndex < chains.length; chainIndex++) {
142
+ const chain = chains[chainIndex]
143
+ if (chain.solveAxis !== InternalSolveAxis.Fixed) {
144
+ this.solveChain(
145
+ chain,
146
+ chainIndex,
147
+ solver,
148
+ ikBoneIndex,
149
+ targetBoneIndex,
150
+ bones,
151
+ localRotations,
152
+ localTranslations,
153
+ worldMatrices,
154
+ ikChainInfo,
155
+ i < halfIteration
156
+ )
157
+ }
158
+ }
159
+
160
+ if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) break
161
+ }
162
+
163
+ // Apply IK rotations to local rotations
164
+ for (const link of solver.links) {
165
+ const chainInfo = ikChainInfo[link.boneIndex]
166
+ if (chainInfo?.ikRotation) {
167
+ const localRot = localRotations[link.boneIndex]
168
+ const finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
169
+ localRot.set(finalRot)
170
+ }
171
+ }
172
+ }
173
+
174
+ private static solveChain(
175
+ chain: IKChain,
176
+ chainIndex: number,
177
+ solver: IKSolver,
178
+ ikBoneIndex: number,
179
+ targetBoneIndex: number,
180
+ bones: Bone[],
181
+ localRotations: Quat[],
182
+ localTranslations: Vec3[],
183
+ worldMatrices: Mat4[],
184
+ ikChainInfo: IKChainInfo[],
185
+ useAxis: boolean
186
+ ): void {
187
+ const chainBoneIndex = chain.boneIndex
188
+ const chainPosition = this.getWorldTranslation(chainBoneIndex, worldMatrices)
189
+ const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
190
+ const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
191
+
192
+ const chainTargetVector = chainPosition.subtract(targetPosition).normalize()
193
+ const chainIkVector = chainPosition.subtract(ikPosition).normalize()
194
+
195
+ const chainRotationAxis = chainTargetVector.cross(chainIkVector)
196
+ if (chainRotationAxis.length() < this.EPSILON) return
197
+
198
+ // Get parent's world rotation matrix (translation removed)
199
+ const parentWorldRotMatrix = this.getParentWorldRotationMatrix(chainBoneIndex, bones, worldMatrices)
200
+
201
+ let finalRotationAxis: Vec3
202
+ if (chain.minimumAngle !== null && useAxis) {
203
+ switch (chain.solveAxis) {
204
+ case InternalSolveAxis.None: {
205
+ const invParentRot = parentWorldRotMatrix.inverse()
206
+ finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
207
+ break
208
+ }
209
+ case InternalSolveAxis.X:
210
+ case InternalSolveAxis.Y:
211
+ case InternalSolveAxis.Z: {
212
+ const m = parentWorldRotMatrix.values
213
+ const axisOffset = (chain.solveAxis - InternalSolveAxis.X) * 4
214
+ const axis = new Vec3(m[axisOffset], m[axisOffset + 1], m[axisOffset + 2])
215
+ const dot = chainRotationAxis.dot(axis)
216
+ const sign = dot >= 0 ? 1 : -1
217
+ finalRotationAxis =
218
+ chain.solveAxis === InternalSolveAxis.X
219
+ ? new Vec3(sign, 0, 0)
220
+ : chain.solveAxis === InternalSolveAxis.Y
221
+ ? new Vec3(0, sign, 0)
222
+ : new Vec3(0, 0, sign)
223
+ break
224
+ }
225
+ default:
226
+ finalRotationAxis = chainRotationAxis
227
+ }
228
+ } else {
229
+ const invParentRot = parentWorldRotMatrix.inverse()
230
+ finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
231
+ }
232
+
233
+ let dot = chainTargetVector.dot(chainIkVector)
234
+ dot = Math.max(-1.0, Math.min(1.0, dot))
235
+
236
+ const angle = Math.min(solver.limitAngle * (chainIndex + 1), Math.acos(dot))
237
+ const ikRotation = Quat.fromAxisAngle(finalRotationAxis, angle)
238
+
239
+ const chainInfo = ikChainInfo[chainBoneIndex]
240
+ if (chainInfo) {
241
+ chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
242
+
243
+ // Apply angle constraints if present
244
+ if (chain.minimumAngle && chain.maximumAngle) {
245
+ const localRot = localRotations[chainBoneIndex]
246
+ chainInfo.localRotation = localRot.clone()
247
+
248
+ const combinedRot = chainInfo.ikRotation.multiply(localRot)
249
+ const euler = this.extractEulerAngles(combinedRot, chain.rotationOrder)
250
+ const limited = this.limitEulerAngles(euler, chain.minimumAngle, chain.maximumAngle, useAxis)
251
+ chainInfo.ikRotation = this.reconstructQuatFromEuler(limited, chain.rotationOrder)
252
+ // Clone localRot to avoid mutating, then conjugate and normalize
253
+ chainInfo.ikRotation = chainInfo.ikRotation.multiply(localRot.clone().conjugate().normalize())
254
+ }
255
+ }
256
+
257
+ // Update world matrices for affected bones (using IK-modified rotations)
258
+ for (let i = chainIndex; i >= 0; i--) {
259
+ const link = solver.links[i]
260
+ this.updateWorldMatrix(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
261
+ }
262
+ this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
263
+ }
264
+
265
+ private static limitAngle(angle: number, min: number, max: number, useAxis: boolean): number {
266
+ if (angle < min) {
267
+ const diff = 2 * min - angle
268
+ return diff <= max && useAxis ? diff : min
269
+ } else if (angle > max) {
270
+ const diff = 2 * max - angle
271
+ return diff >= min && useAxis ? diff : max
272
+ } else {
273
+ return angle
274
+ }
275
+ }
276
+
277
+ private static getDistance(boneIndex1: number, boneIndex2: number, worldMatrices: Mat4[]): number {
278
+ const pos1 = this.getWorldTranslation(boneIndex1, worldMatrices)
279
+ const pos2 = this.getWorldTranslation(boneIndex2, worldMatrices)
280
+ return pos1.subtract(pos2).length()
281
+ }
282
+
283
+ private static getWorldTranslation(boneIndex: number, worldMatrices: Mat4[]): Vec3 {
284
+ const mat = worldMatrices[boneIndex]
285
+ return new Vec3(mat.values[12], mat.values[13], mat.values[14])
286
+ }
287
+
288
+ private static extractEulerAngles(quat: Quat, order: InternalEulerRotationOrder): Vec3 {
289
+ const rotMatrix = Mat4.fromQuat(quat.x, quat.y, quat.z, quat.w)
290
+ const m = rotMatrix.values
291
+
292
+ switch (order) {
293
+ case InternalEulerRotationOrder.YXZ: {
294
+ let rX = Math.asin(-m[9])
295
+ if (Math.abs(rX) > this.THRESHOLD) rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
296
+ let cosX = Math.cos(rX)
297
+ if (cosX !== 0) cosX = 1 / cosX
298
+ const rY = Math.atan2(m[8] * cosX, m[10] * cosX)
299
+ const rZ = Math.atan2(m[1] * cosX, m[5] * cosX)
300
+ return new Vec3(rX, rY, rZ)
301
+ }
302
+ case InternalEulerRotationOrder.ZYX: {
303
+ let rY = Math.asin(-m[2])
304
+ if (Math.abs(rY) > this.THRESHOLD) rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
305
+ let cosY = Math.cos(rY)
306
+ if (cosY !== 0) cosY = 1 / cosY
307
+ const rX = Math.atan2(m[6] * cosY, m[10] * cosY)
308
+ const rZ = Math.atan2(m[1] * cosY, m[0] * cosY)
309
+ return new Vec3(rX, rY, rZ)
310
+ }
311
+ case InternalEulerRotationOrder.XZY: {
312
+ let rZ = Math.asin(-m[4])
313
+ if (Math.abs(rZ) > this.THRESHOLD) rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
314
+ let cosZ = Math.cos(rZ)
315
+ if (cosZ !== 0) cosZ = 1 / cosZ
316
+ const rX = Math.atan2(m[6] * cosZ, m[5] * cosZ)
317
+ const rY = Math.atan2(m[8] * cosZ, m[0] * cosZ)
318
+ return new Vec3(rX, rY, rZ)
319
+ }
320
+ }
321
+ }
322
+
323
+ private static limitEulerAngles(euler: Vec3, min: Vec3, max: Vec3, useAxis: boolean): Vec3 {
324
+ return new Vec3(
325
+ this.limitAngle(euler.x, min.x, max.x, useAxis),
326
+ this.limitAngle(euler.y, min.y, max.y, useAxis),
327
+ this.limitAngle(euler.z, min.z, max.z, useAxis)
328
+ )
329
+ }
330
+
331
+ private static reconstructQuatFromEuler(euler: Vec3, order: InternalEulerRotationOrder): Quat {
332
+ const axes = [
333
+ [new Vec3(1, 0, 0), new Vec3(0, 1, 0), new Vec3(0, 0, 1)],
334
+ [new Vec3(0, 0, 1), new Vec3(0, 1, 0), new Vec3(1, 0, 0)],
335
+ [new Vec3(0, 1, 0), new Vec3(1, 0, 0), new Vec3(0, 0, 1)],
336
+ ]
337
+
338
+ const [axis1, axis2, axis3] = axes[order]
339
+ const [angle1, angle2, angle3] =
340
+ order === InternalEulerRotationOrder.YXZ
341
+ ? [euler.y, euler.x, euler.z]
342
+ : order === InternalEulerRotationOrder.ZYX
343
+ ? [euler.z, euler.y, euler.x]
344
+ : [euler.x, euler.z, euler.y]
345
+
346
+ let result = Quat.fromAxisAngle(axis1, angle1)
347
+ result = result.multiply(Quat.fromAxisAngle(axis2, angle2))
348
+ result = result.multiply(Quat.fromAxisAngle(axis3, angle3))
349
+ return result
350
+ }
351
+
352
+ private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Mat4[]): Mat4 {
353
+ const bone = bones[boneIndex]
354
+ if (bone.parentIndex >= 0) {
355
+ const parentMat = worldMatrices[bone.parentIndex]
356
+ // Remove translation
357
+ const rotMat = Mat4.identity()
358
+ const m = parentMat.values
359
+ 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])
360
+ return rotMat
361
+ } else {
362
+ return Mat4.identity()
363
+ }
364
+ }
365
+
366
+ private static transformNormal(normal: Vec3, matrix: Mat4): Vec3 {
367
+ const m = matrix.values
368
+ return new Vec3(
369
+ m[0] * normal.x + m[4] * normal.y + m[8] * normal.z,
370
+ m[1] * normal.x + m[5] * normal.y + m[9] * normal.z,
371
+ m[2] * normal.x + m[6] * normal.y + m[10] * normal.z
372
+ )
373
+ }
374
+
375
+ private static updateWorldMatrix(
376
+ boneIndex: number,
377
+ bones: Bone[],
378
+ localRotations: Quat[],
379
+ localTranslations: Vec3[],
380
+ worldMatrices: Mat4[],
381
+ ikChainInfo?: IKChainInfo[]
382
+ ): void {
383
+ const bone = bones[boneIndex]
384
+ const localRot = localRotations[boneIndex]
385
+ const localTrans = localTranslations[boneIndex]
386
+
387
+ // Apply IK rotation if available
388
+ let finalRot = localRot
389
+ if (ikChainInfo) {
390
+ const chainInfo = ikChainInfo[boneIndex]
391
+ if (chainInfo && chainInfo.ikRotation) {
392
+ finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
393
+ }
394
+ }
395
+ const rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w)
396
+
397
+ const localM = Mat4.identity()
398
+ .translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
399
+ .multiply(rotateM)
400
+ .translateInPlace(localTrans.x, localTrans.y, localTrans.z)
401
+
402
+ const worldMat = worldMatrices[boneIndex]
403
+ if (bone.parentIndex >= 0) {
404
+ const parentMat = worldMatrices[bone.parentIndex]
405
+ const result = parentMat.multiply(localM)
406
+ worldMat.values.set(result.values)
407
+ } else {
408
+ worldMat.values.set(localM.values)
409
+ }
410
+ }
411
+ }