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/README.md +66 -66
- package/dist/audio.d.ts +29 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +116 -0
- package/dist/engine.js +4 -4
- package/dist/particles.d.ts +67 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +266 -0
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +4 -4
- package/src/ik-solver.ts +411 -411
- package/src/math.ts +584 -584
- package/src/physics.ts +742 -742
- package/src/vmd-loader.ts +276 -276
- package/dist/engine_ts.d.ts +0 -143
- package/dist/engine_ts.d.ts.map +0 -1
- package/dist/engine_ts.js +0 -1575
- package/dist/player.d.ts +0 -64
- package/dist/player.d.ts.map +0 -1
- package/dist/player.js +0 -220
- package/src/player.ts +0 -290
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
|
+
}
|