reze-engine 0.2.18 → 0.3.0

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.
@@ -0,0 +1,488 @@
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, EulerRotationOrder, SolveAxis } 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: Float32Array,
90
+ localTranslations: Float32Array,
91
+ worldMatrices: Float32Array,
92
+ ikChainInfo: IKChainInfo[],
93
+ usePhysics: boolean = false
94
+ ): void {
95
+ for (const solver of ikSolvers) {
96
+ if (usePhysics && solver.canSkipWhenPhysicsEnabled) {
97
+ continue
98
+ }
99
+ this.solveIK(solver, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
100
+ }
101
+ }
102
+
103
+ private static solveIK(
104
+ solver: IKSolver,
105
+ bones: Bone[],
106
+ localRotations: Float32Array,
107
+ localTranslations: Float32Array,
108
+ worldMatrices: Float32Array,
109
+ ikChainInfo: IKChainInfo[]
110
+ ): void {
111
+ if (solver.links.length === 0) return
112
+
113
+ const ikBoneIndex = solver.ikBoneIndex
114
+ const targetBoneIndex = solver.targetBoneIndex
115
+
116
+ // Reset IK rotations
117
+ for (const link of solver.links) {
118
+ const chainInfo = ikChainInfo[link.boneIndex]
119
+ if (chainInfo) {
120
+ chainInfo.ikRotation = new Quat(0, 0, 0, 1)
121
+ }
122
+ }
123
+
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
129
+
130
+ // Build IK chains
131
+ const chains: IKChain[] = []
132
+ for (const link of solver.links) {
133
+ chains.push(new IKChain(link.boneIndex, link))
134
+ }
135
+
136
+ // Update chain bones and target bone world matrices (initial state, no IK yet)
137
+ for (let i = chains.length - 1; i >= 0; i--) {
138
+ this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices)
139
+ }
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)
145
+
146
+ if (updatedIkPosition.subtract(updatedTargetPosition).length() < this.EPSILON) return
147
+
148
+ // Solve iteratively
149
+ const iteration = Math.min(solver.iterationCount, 256)
150
+ const halfIteration = iteration >> 1
151
+
152
+ for (let i = 0; i < iteration; i++) {
153
+ for (let chainIndex = 0; chainIndex < chains.length; chainIndex++) {
154
+ const chain = chains[chainIndex]
155
+ if (chain.solveAxis !== InternalSolveAxis.Fixed) {
156
+ this.solveChain(
157
+ chain,
158
+ chainIndex,
159
+ solver,
160
+ ikBoneIndex,
161
+ targetBoneIndex,
162
+ bones,
163
+ localRotations,
164
+ localTranslations,
165
+ worldMatrices,
166
+ ikChainInfo,
167
+ i < halfIteration
168
+ )
169
+ }
170
+ }
171
+
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
177
+ }
178
+
179
+ // Apply IK rotations to local rotations
180
+ for (const link of solver.links) {
181
+ const chainInfo = ikChainInfo[link.boneIndex]
182
+ if (chainInfo && chainInfo.ikRotation) {
183
+ 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
+ )
190
+ 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
195
+ }
196
+ }
197
+ }
198
+
199
+ private static solveChain(
200
+ chain: IKChain,
201
+ chainIndex: number,
202
+ solver: IKSolver,
203
+ ikBoneIndex: number,
204
+ targetBoneIndex: number,
205
+ bones: Bone[],
206
+ localRotations: Float32Array,
207
+ localTranslations: Float32Array,
208
+ worldMatrices: Float32Array,
209
+ ikChainInfo: IKChainInfo[],
210
+ useAxis: boolean
211
+ ): void {
212
+ const chainBoneIndex = chain.boneIndex
213
+ const chainPosition = this.getWorldTranslation(chainBoneIndex, worldMatrices)
214
+ const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
215
+ const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
216
+
217
+ const chainTargetVector = chainPosition.subtract(targetPosition).normalize()
218
+ const chainIkVector = chainPosition.subtract(ikPosition).normalize()
219
+
220
+ const chainRotationAxis = chainTargetVector.cross(chainIkVector)
221
+ if (chainRotationAxis.length() < this.EPSILON) return
222
+
223
+ // Get parent's world rotation matrix (translation removed)
224
+ const parentWorldRotMatrix = this.getParentWorldRotationMatrix(chainBoneIndex, bones, worldMatrices)
225
+
226
+ let finalRotationAxis: Vec3
227
+ if (chain.minimumAngle !== null && useAxis) {
228
+ switch (chain.solveAxis) {
229
+ case InternalSolveAxis.None: {
230
+ const invParentRot = parentWorldRotMatrix.inverse()
231
+ finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
232
+ break
233
+ }
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
+ }
248
+ case InternalSolveAxis.Z: {
249
+ 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)
253
+ break
254
+ }
255
+ default:
256
+ finalRotationAxis = chainRotationAxis
257
+ }
258
+ } else {
259
+ const invParentRot = parentWorldRotMatrix.inverse()
260
+ finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
261
+ }
262
+
263
+ let dot = chainTargetVector.dot(chainIkVector)
264
+ dot = Math.max(-1.0, Math.min(1.0, dot))
265
+
266
+ const angle = Math.min(solver.limitAngle * (chainIndex + 1), Math.acos(dot))
267
+ const ikRotation = Quat.fromAxisAngle(finalRotationAxis, angle)
268
+
269
+ const chainInfo = ikChainInfo[chainBoneIndex]
270
+ if (chainInfo) {
271
+ chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
272
+
273
+ // Apply angle constraints if present
274
+ if (chain.minimumAngle !== null && chain.maximumAngle !== null) {
275
+ 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
+ )
282
+ chainInfo.localRotation = localRot.clone()
283
+
284
+ 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)
352
+ }
353
+ }
354
+
355
+ // Update world matrices for affected bones (using IK-modified rotations)
356
+ for (let i = chainIndex; i >= 0; i--) {
357
+ const link = solver.links[i]
358
+ this.updateWorldMatrixWithIK(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
359
+ }
360
+ this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices)
361
+ }
362
+
363
+ private static limitAngle(angle: number, min: number, max: number, useAxis: boolean): number {
364
+ if (angle < min) {
365
+ const diff = 2 * min - angle
366
+ return diff <= max && useAxis ? diff : min
367
+ } else if (angle > max) {
368
+ const diff = 2 * max - angle
369
+ return diff >= min && useAxis ? diff : max
370
+ } else {
371
+ return angle
372
+ }
373
+ }
374
+
375
+ private static getWorldTranslation(boneIndex: number, worldMatrices: Float32Array): Vec3 {
376
+ const offset = boneIndex * 16
377
+ return new Vec3(worldMatrices[offset + 12], worldMatrices[offset + 13], worldMatrices[offset + 14])
378
+ }
379
+
380
+ private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Float32Array): Mat4 {
381
+ const bone = bones[boneIndex]
382
+ if (bone.parentIndex >= 0) {
383
+ const parentOffset = bone.parentIndex * 16
384
+ const parentMat = new Mat4(worldMatrices.subarray(parentOffset, parentOffset + 16))
385
+ // Remove translation
386
+ const rotMat = Mat4.identity()
387
+ const m = parentMat.values
388
+ 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])
389
+ return rotMat
390
+ } else {
391
+ return Mat4.identity()
392
+ }
393
+ }
394
+
395
+ private static transformNormal(normal: Vec3, matrix: Mat4): Vec3 {
396
+ const m = matrix.values
397
+ return new Vec3(
398
+ m[0] * normal.x + m[4] * normal.y + m[8] * normal.z,
399
+ m[1] * normal.x + m[5] * normal.y + m[9] * normal.z,
400
+ m[2] * normal.x + m[6] * normal.y + m[10] * normal.z
401
+ )
402
+ }
403
+
404
+ private static updateWorldMatrixWithIK(
405
+ boneIndex: number,
406
+ bones: Bone[],
407
+ localRotations: Float32Array,
408
+ localTranslations: Float32Array,
409
+ worldMatrices: Float32Array,
410
+ ikChainInfo: IKChainInfo[]
411
+ ): void {
412
+ const bone = bones[boneIndex]
413
+ const qi = boneIndex * 4
414
+ const ti = boneIndex * 3
415
+
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]
424
+ let finalRot = localRot
425
+ if (chainInfo && chainInfo.ikRotation) {
426
+ finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
427
+ }
428
+ const rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w)
429
+
430
+ const localTx = localTranslations[ti]
431
+ const localTy = localTranslations[ti + 1]
432
+ const localTz = localTranslations[ti + 2]
433
+
434
+ const localM = Mat4.identity()
435
+ .translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
436
+ .multiply(rotateM)
437
+ .translateInPlace(localTx, localTy, localTz)
438
+
439
+ const worldOffset = boneIndex * 16
440
+ if (bone.parentIndex >= 0) {
441
+ const parentOffset = bone.parentIndex * 16
442
+ const parentMat = new Mat4(worldMatrices.subarray(parentOffset, parentOffset + 16))
443
+ const worldMat = parentMat.multiply(localM)
444
+ worldMatrices.subarray(worldOffset, worldOffset + 16).set(worldMat.values)
445
+ } else {
446
+ worldMatrices.subarray(worldOffset, worldOffset + 16).set(localM.values)
447
+ }
448
+ }
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
+ }