reze-engine 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ammo-loader.d.ts +0 -1
- package/dist/ammo-loader.d.ts.map +1 -1
- package/dist/ammo-loader.js +0 -3
- package/dist/engine.d.ts +2 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +44 -100
- package/dist/ik-solver.d.ts +7 -2
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +110 -137
- package/dist/math.d.ts +10 -6
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +36 -62
- package/dist/model.d.ts +1 -19
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +24 -93
- package/dist/player.d.ts +6 -20
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +89 -193
- package/package.json +1 -1
- package/src/ammo-loader.ts +0 -4
- package/src/engine.ts +54 -101
- package/src/ik-solver.ts +121 -178
- package/src/math.ts +43 -82
- package/src/model.ts +26 -101
- package/src/player.ts +116 -212
- package/src/bezier-interpolate.ts +0 -47
- package/src/ik.ts +0 -449
package/src/ik-solver.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
8
|
-
import { Bone, IKLink, IKSolver, IKChainInfo
|
|
8
|
+
import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
|
|
9
9
|
|
|
10
10
|
const enum InternalEulerRotationOrder {
|
|
11
11
|
YXZ = 0,
|
|
@@ -89,13 +89,9 @@ export class IKSolverSystem {
|
|
|
89
89
|
localRotations: Float32Array,
|
|
90
90
|
localTranslations: Float32Array,
|
|
91
91
|
worldMatrices: Float32Array,
|
|
92
|
-
ikChainInfo: IKChainInfo[]
|
|
93
|
-
usePhysics: boolean = false
|
|
92
|
+
ikChainInfo: IKChainInfo[]
|
|
94
93
|
): void {
|
|
95
94
|
for (const solver of ikSolvers) {
|
|
96
|
-
if (usePhysics && solver.canSkipWhenPhysicsEnabled) {
|
|
97
|
-
continue
|
|
98
|
-
}
|
|
99
95
|
this.solveIK(solver, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
|
|
100
96
|
}
|
|
101
97
|
}
|
|
@@ -121,11 +117,7 @@ export class IKSolverSystem {
|
|
|
121
117
|
}
|
|
122
118
|
}
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
|
|
126
|
-
const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
|
|
127
|
-
|
|
128
|
-
if (ikPosition.subtract(targetPosition).length() < this.EPSILON) return
|
|
120
|
+
if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
|
|
129
121
|
|
|
130
122
|
// Build IK chains
|
|
131
123
|
const chains: IKChain[] = []
|
|
@@ -135,15 +127,11 @@ export class IKSolverSystem {
|
|
|
135
127
|
|
|
136
128
|
// Update chain bones and target bone world matrices (initial state, no IK yet)
|
|
137
129
|
for (let i = chains.length - 1; i >= 0; i--) {
|
|
138
|
-
this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices)
|
|
130
|
+
this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
|
|
139
131
|
}
|
|
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)
|
|
132
|
+
this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
|
|
145
133
|
|
|
146
|
-
if (
|
|
134
|
+
if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
|
|
147
135
|
|
|
148
136
|
// Solve iteratively
|
|
149
137
|
const iteration = Math.min(solver.iterationCount, 256)
|
|
@@ -169,29 +157,17 @@ export class IKSolverSystem {
|
|
|
169
157
|
}
|
|
170
158
|
}
|
|
171
159
|
|
|
172
|
-
|
|
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
|
|
160
|
+
if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) break
|
|
177
161
|
}
|
|
178
162
|
|
|
179
163
|
// Apply IK rotations to local rotations
|
|
180
164
|
for (const link of solver.links) {
|
|
181
165
|
const chainInfo = ikChainInfo[link.boneIndex]
|
|
182
|
-
if (chainInfo
|
|
166
|
+
if (chainInfo?.ikRotation) {
|
|
183
167
|
const qi = link.boneIndex * 4
|
|
184
|
-
const localRot =
|
|
185
|
-
localRotations[qi],
|
|
186
|
-
localRotations[qi + 1],
|
|
187
|
-
localRotations[qi + 2],
|
|
188
|
-
localRotations[qi + 3]
|
|
189
|
-
)
|
|
168
|
+
const localRot = this.getQuatFromArray(localRotations, qi)
|
|
190
169
|
const finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
|
|
191
|
-
localRotations
|
|
192
|
-
localRotations[qi + 1] = finalRot.y
|
|
193
|
-
localRotations[qi + 2] = finalRot.z
|
|
194
|
-
localRotations[qi + 3] = finalRot.w
|
|
170
|
+
this.setQuatToArray(localRotations, qi, finalRot)
|
|
195
171
|
}
|
|
196
172
|
}
|
|
197
173
|
}
|
|
@@ -231,25 +207,20 @@ export class IKSolverSystem {
|
|
|
231
207
|
finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
|
|
232
208
|
break
|
|
233
209
|
}
|
|
234
|
-
case InternalSolveAxis.X:
|
|
235
|
-
|
|
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
|
-
}
|
|
210
|
+
case InternalSolveAxis.X:
|
|
211
|
+
case InternalSolveAxis.Y:
|
|
248
212
|
case InternalSolveAxis.Z: {
|
|
249
213
|
const m = parentWorldRotMatrix.values
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
214
|
+
const axisOffset = (chain.solveAxis - InternalSolveAxis.X) * 4
|
|
215
|
+
const axis = new Vec3(m[axisOffset], m[axisOffset + 1], m[axisOffset + 2])
|
|
216
|
+
const dot = chainRotationAxis.dot(axis)
|
|
217
|
+
const sign = dot >= 0 ? 1 : -1
|
|
218
|
+
finalRotationAxis =
|
|
219
|
+
chain.solveAxis === InternalSolveAxis.X
|
|
220
|
+
? new Vec3(sign, 0, 0)
|
|
221
|
+
: chain.solveAxis === InternalSolveAxis.Y
|
|
222
|
+
? new Vec3(0, sign, 0)
|
|
223
|
+
: new Vec3(0, 0, sign)
|
|
253
224
|
break
|
|
254
225
|
}
|
|
255
226
|
default:
|
|
@@ -271,93 +242,25 @@ export class IKSolverSystem {
|
|
|
271
242
|
chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
|
|
272
243
|
|
|
273
244
|
// Apply angle constraints if present
|
|
274
|
-
if (chain.minimumAngle
|
|
245
|
+
if (chain.minimumAngle && chain.maximumAngle) {
|
|
275
246
|
const qi = chainBoneIndex * 4
|
|
276
|
-
const localRot =
|
|
277
|
-
localRotations[qi],
|
|
278
|
-
localRotations[qi + 1],
|
|
279
|
-
localRotations[qi + 2],
|
|
280
|
-
localRotations[qi + 3]
|
|
281
|
-
)
|
|
247
|
+
const localRot = this.getQuatFromArray(localRotations, qi)
|
|
282
248
|
chainInfo.localRotation = localRot.clone()
|
|
283
249
|
|
|
284
250
|
const combinedRot = chainInfo.ikRotation.multiply(localRot)
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
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)
|
|
251
|
+
const euler = this.extractEulerAngles(combinedRot, chain.rotationOrder)
|
|
252
|
+
const limited = this.limitEulerAngles(euler, chain.minimumAngle, chain.maximumAngle, useAxis)
|
|
253
|
+
chainInfo.ikRotation = this.reconstructQuatFromEuler(limited, chain.rotationOrder)
|
|
254
|
+
chainInfo.ikRotation = chainInfo.ikRotation.multiply(localRot.conjugate().normalize())
|
|
352
255
|
}
|
|
353
256
|
}
|
|
354
257
|
|
|
355
258
|
// Update world matrices for affected bones (using IK-modified rotations)
|
|
356
259
|
for (let i = chainIndex; i >= 0; i--) {
|
|
357
260
|
const link = solver.links[i]
|
|
358
|
-
this.
|
|
261
|
+
this.updateWorldMatrix(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
|
|
359
262
|
}
|
|
360
|
-
this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices)
|
|
263
|
+
this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
|
|
361
264
|
}
|
|
362
265
|
|
|
363
266
|
private static limitAngle(angle: number, min: number, max: number, useAxis: boolean): number {
|
|
@@ -372,11 +275,92 @@ export class IKSolverSystem {
|
|
|
372
275
|
}
|
|
373
276
|
}
|
|
374
277
|
|
|
278
|
+
private static getDistance(boneIndex1: number, boneIndex2: number, worldMatrices: Float32Array): number {
|
|
279
|
+
const pos1 = this.getWorldTranslation(boneIndex1, worldMatrices)
|
|
280
|
+
const pos2 = this.getWorldTranslation(boneIndex2, worldMatrices)
|
|
281
|
+
return pos1.subtract(pos2).length()
|
|
282
|
+
}
|
|
283
|
+
|
|
375
284
|
private static getWorldTranslation(boneIndex: number, worldMatrices: Float32Array): Vec3 {
|
|
376
285
|
const offset = boneIndex * 16
|
|
377
286
|
return new Vec3(worldMatrices[offset + 12], worldMatrices[offset + 13], worldMatrices[offset + 14])
|
|
378
287
|
}
|
|
379
288
|
|
|
289
|
+
private static getQuatFromArray(array: Float32Array, offset: number): Quat {
|
|
290
|
+
return new Quat(array[offset], array[offset + 1], array[offset + 2], array[offset + 3])
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private static setQuatToArray(array: Float32Array, offset: number, quat: Quat): void {
|
|
294
|
+
array[offset] = quat.x
|
|
295
|
+
array[offset + 1] = quat.y
|
|
296
|
+
array[offset + 2] = quat.z
|
|
297
|
+
array[offset + 3] = quat.w
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private static extractEulerAngles(quat: Quat, order: InternalEulerRotationOrder): Vec3 {
|
|
301
|
+
const rotMatrix = Mat4.fromQuat(quat.x, quat.y, quat.z, quat.w)
|
|
302
|
+
const m = rotMatrix.values
|
|
303
|
+
|
|
304
|
+
switch (order) {
|
|
305
|
+
case InternalEulerRotationOrder.YXZ: {
|
|
306
|
+
let rX = Math.asin(-m[9])
|
|
307
|
+
if (Math.abs(rX) > this.THRESHOLD) rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
|
|
308
|
+
let cosX = Math.cos(rX)
|
|
309
|
+
if (cosX !== 0) cosX = 1 / cosX
|
|
310
|
+
const rY = Math.atan2(m[8] * cosX, m[10] * cosX)
|
|
311
|
+
const rZ = Math.atan2(m[1] * cosX, m[5] * cosX)
|
|
312
|
+
return new Vec3(rX, rY, rZ)
|
|
313
|
+
}
|
|
314
|
+
case InternalEulerRotationOrder.ZYX: {
|
|
315
|
+
let rY = Math.asin(-m[2])
|
|
316
|
+
if (Math.abs(rY) > this.THRESHOLD) rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
|
|
317
|
+
let cosY = Math.cos(rY)
|
|
318
|
+
if (cosY !== 0) cosY = 1 / cosY
|
|
319
|
+
const rX = Math.atan2(m[6] * cosY, m[10] * cosY)
|
|
320
|
+
const rZ = Math.atan2(m[1] * cosY, m[0] * cosY)
|
|
321
|
+
return new Vec3(rX, rY, rZ)
|
|
322
|
+
}
|
|
323
|
+
case InternalEulerRotationOrder.XZY: {
|
|
324
|
+
let rZ = Math.asin(-m[4])
|
|
325
|
+
if (Math.abs(rZ) > this.THRESHOLD) rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
|
|
326
|
+
let cosZ = Math.cos(rZ)
|
|
327
|
+
if (cosZ !== 0) cosZ = 1 / cosZ
|
|
328
|
+
const rX = Math.atan2(m[6] * cosZ, m[5] * cosZ)
|
|
329
|
+
const rY = Math.atan2(m[8] * cosZ, m[0] * cosZ)
|
|
330
|
+
return new Vec3(rX, rY, rZ)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private static limitEulerAngles(euler: Vec3, min: Vec3, max: Vec3, useAxis: boolean): Vec3 {
|
|
336
|
+
return new Vec3(
|
|
337
|
+
this.limitAngle(euler.x, min.x, max.x, useAxis),
|
|
338
|
+
this.limitAngle(euler.y, min.y, max.y, useAxis),
|
|
339
|
+
this.limitAngle(euler.z, min.z, max.z, useAxis)
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private static reconstructQuatFromEuler(euler: Vec3, order: InternalEulerRotationOrder): Quat {
|
|
344
|
+
const axes = [
|
|
345
|
+
[new Vec3(1, 0, 0), new Vec3(0, 1, 0), new Vec3(0, 0, 1)],
|
|
346
|
+
[new Vec3(0, 0, 1), new Vec3(0, 1, 0), new Vec3(1, 0, 0)],
|
|
347
|
+
[new Vec3(0, 1, 0), new Vec3(1, 0, 0), new Vec3(0, 0, 1)],
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
const [axis1, axis2, axis3] = axes[order]
|
|
351
|
+
const [angle1, angle2, angle3] =
|
|
352
|
+
order === InternalEulerRotationOrder.YXZ
|
|
353
|
+
? [euler.y, euler.x, euler.z]
|
|
354
|
+
: order === InternalEulerRotationOrder.ZYX
|
|
355
|
+
? [euler.z, euler.y, euler.x]
|
|
356
|
+
: [euler.x, euler.z, euler.y]
|
|
357
|
+
|
|
358
|
+
let result = Quat.fromAxisAngle(axis1, angle1)
|
|
359
|
+
result = result.multiply(Quat.fromAxisAngle(axis2, angle2))
|
|
360
|
+
result = result.multiply(Quat.fromAxisAngle(axis3, angle3))
|
|
361
|
+
return result
|
|
362
|
+
}
|
|
363
|
+
|
|
380
364
|
private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Float32Array): Mat4 {
|
|
381
365
|
const bone = bones[boneIndex]
|
|
382
366
|
if (bone.parentIndex >= 0) {
|
|
@@ -401,29 +385,27 @@ export class IKSolverSystem {
|
|
|
401
385
|
)
|
|
402
386
|
}
|
|
403
387
|
|
|
404
|
-
private static
|
|
388
|
+
private static updateWorldMatrix(
|
|
405
389
|
boneIndex: number,
|
|
406
390
|
bones: Bone[],
|
|
407
391
|
localRotations: Float32Array,
|
|
408
392
|
localTranslations: Float32Array,
|
|
409
393
|
worldMatrices: Float32Array,
|
|
410
|
-
ikChainInfo
|
|
394
|
+
ikChainInfo?: IKChainInfo[]
|
|
411
395
|
): void {
|
|
412
396
|
const bone = bones[boneIndex]
|
|
413
397
|
const qi = boneIndex * 4
|
|
414
398
|
const ti = boneIndex * 3
|
|
415
399
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
localRotations[qi + 1],
|
|
420
|
-
localRotations[qi + 2],
|
|
421
|
-
localRotations[qi + 3]
|
|
422
|
-
)
|
|
423
|
-
const chainInfo = ikChainInfo[boneIndex]
|
|
400
|
+
const localRot = this.getQuatFromArray(localRotations, qi)
|
|
401
|
+
|
|
402
|
+
// Apply IK rotation if available
|
|
424
403
|
let finalRot = localRot
|
|
425
|
-
if (
|
|
426
|
-
|
|
404
|
+
if (ikChainInfo) {
|
|
405
|
+
const chainInfo = ikChainInfo[boneIndex]
|
|
406
|
+
if (chainInfo && chainInfo.ikRotation) {
|
|
407
|
+
finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
|
|
408
|
+
}
|
|
427
409
|
}
|
|
428
410
|
const rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w)
|
|
429
411
|
|
|
@@ -446,43 +428,4 @@ export class IKSolverSystem {
|
|
|
446
428
|
worldMatrices.subarray(worldOffset, worldOffset + 16).set(localM.values)
|
|
447
429
|
}
|
|
448
430
|
}
|
|
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
431
|
}
|
package/src/math.ts
CHANGED
|
@@ -26,10 +26,6 @@ export class Vec3 {
|
|
|
26
26
|
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
lengthSquared(): number {
|
|
30
|
-
return this.x * this.x + this.y * this.y + this.z * this.z
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
normalize(): Vec3 {
|
|
34
30
|
const len = this.length()
|
|
35
31
|
if (len === 0) return new Vec3(0, 0, 0)
|
|
@@ -51,10 +47,6 @@ export class Vec3 {
|
|
|
51
47
|
scale(scalar: number): Vec3 {
|
|
52
48
|
return new Vec3(this.x * scalar, this.y * scalar, this.z * scalar)
|
|
53
49
|
}
|
|
54
|
-
|
|
55
|
-
clone(): Vec3 {
|
|
56
|
-
return new Vec3(this.x, this.y, this.z)
|
|
57
|
-
}
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
export class Quat {
|
|
@@ -103,55 +95,6 @@ export class Quat {
|
|
|
103
95
|
return new Quat(this.x / len, this.y / len, this.z / len, this.w / len)
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
// Rotate a vector by this quaternion: result = q * v * q^-1
|
|
107
|
-
rotateVec(v: Vec3): Vec3 {
|
|
108
|
-
// Treat v as pure quaternion (x, y, z, 0)
|
|
109
|
-
const qx = this.x,
|
|
110
|
-
qy = this.y,
|
|
111
|
-
qz = this.z,
|
|
112
|
-
qw = this.w
|
|
113
|
-
const vx = v.x,
|
|
114
|
-
vy = v.y,
|
|
115
|
-
vz = v.z
|
|
116
|
-
|
|
117
|
-
// t = 2 * cross(q.xyz, v)
|
|
118
|
-
const tx = 2 * (qy * vz - qz * vy)
|
|
119
|
-
const ty = 2 * (qz * vx - qx * vz)
|
|
120
|
-
const tz = 2 * (qx * vy - qy * vx)
|
|
121
|
-
|
|
122
|
-
// result = v + q.w * t + cross(q.xyz, t)
|
|
123
|
-
return new Vec3(
|
|
124
|
-
vx + qw * tx + (qy * tz - qz * ty),
|
|
125
|
-
vy + qw * ty + (qz * tx - qx * tz),
|
|
126
|
-
vz + qw * tz + (qx * ty - qy * tx)
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Rotate a vector by this quaternion (Babylon.js style naming)
|
|
131
|
-
rotate(v: Vec3): Vec3 {
|
|
132
|
-
const qv = new Vec3(this.x, this.y, this.z)
|
|
133
|
-
const uv = qv.cross(v)
|
|
134
|
-
const uuv = qv.cross(uv)
|
|
135
|
-
return v.add(uv.scale(2 * this.w)).add(uuv.scale(2))
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Static method: create quaternion that rotates from one direction to another
|
|
139
|
-
static fromTo(from: Vec3, to: Vec3): Quat {
|
|
140
|
-
const dot = from.dot(to)
|
|
141
|
-
if (dot > 0.999999) return new Quat(0, 0, 0, 1) // Already aligned
|
|
142
|
-
if (dot < -0.999999) {
|
|
143
|
-
// 180 degrees
|
|
144
|
-
let axis = from.cross(new Vec3(1, 0, 0))
|
|
145
|
-
if (axis.length() < 0.001) axis = from.cross(new Vec3(0, 1, 0))
|
|
146
|
-
return new Quat(axis.x, axis.y, axis.z, 0).normalize()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const axis = from.cross(to)
|
|
150
|
-
const w = Math.sqrt((1 + dot) * 2)
|
|
151
|
-
const invW = 1 / w
|
|
152
|
-
return new Quat(axis.x * invW, axis.y * invW, axis.z * invW, w * 0.5).normalize()
|
|
153
|
-
}
|
|
154
|
-
|
|
155
98
|
// Static method: create quaternion from rotation axis and angle
|
|
156
99
|
static fromAxisAngle(axis: Vec3, angle: number): Quat {
|
|
157
100
|
const normalizedAxis = axis.normalize()
|
|
@@ -217,31 +160,6 @@ export class Quat {
|
|
|
217
160
|
|
|
218
161
|
return new Quat(x, y, z, w).normalize()
|
|
219
162
|
}
|
|
220
|
-
|
|
221
|
-
// Convert quaternion to Euler angles (ZXY order, inverse of fromEuler)
|
|
222
|
-
toEuler(): Vec3 {
|
|
223
|
-
const qx = this.x
|
|
224
|
-
const qy = this.y
|
|
225
|
-
const qz = this.z
|
|
226
|
-
const qw = this.w
|
|
227
|
-
|
|
228
|
-
// ZXY order (left-handed)
|
|
229
|
-
// Roll (X): rotation around X axis
|
|
230
|
-
const sinr_cosp = 2 * (qw * qx + qy * qz)
|
|
231
|
-
const cosr_cosp = 1 - 2 * (qx * qx + qy * qy)
|
|
232
|
-
const rotX = Math.atan2(sinr_cosp, cosr_cosp)
|
|
233
|
-
|
|
234
|
-
// Pitch (Y): rotation around Y axis
|
|
235
|
-
const sinp = 2 * (qw * qy - qz * qx)
|
|
236
|
-
const rotY = Math.abs(sinp) >= 1 ? (sinp >= 0 ? Math.PI / 2 : -Math.PI / 2) : Math.asin(sinp)
|
|
237
|
-
|
|
238
|
-
// Yaw (Z): rotation around Z axis
|
|
239
|
-
const siny_cosp = 2 * (qw * qz + qx * qy)
|
|
240
|
-
const cosy_cosp = 1 - 2 * (qy * qy + qz * qz)
|
|
241
|
-
const rotZ = Math.atan2(siny_cosp, cosy_cosp)
|
|
242
|
-
|
|
243
|
-
return new Vec3(rotX, rotY, rotZ)
|
|
244
|
-
}
|
|
245
163
|
}
|
|
246
164
|
|
|
247
165
|
export class Mat4 {
|
|
@@ -557,3 +475,46 @@ export class Mat4 {
|
|
|
557
475
|
return new Mat4(out)
|
|
558
476
|
}
|
|
559
477
|
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Bezier interpolation function
|
|
481
|
+
* @param x1 First control point X (0-127, normalized to 0-1)
|
|
482
|
+
* @param x2 Second control point X (0-127, normalized to 0-1)
|
|
483
|
+
* @param y1 First control point Y (0-127, normalized to 0-1)
|
|
484
|
+
* @param y2 Second control point Y (0-127, normalized to 0-1)
|
|
485
|
+
* @param t Interpolation parameter (0-1)
|
|
486
|
+
* @returns Interpolated value (0-1)
|
|
487
|
+
*/
|
|
488
|
+
export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
|
|
489
|
+
// Clamp t to [0, 1]
|
|
490
|
+
t = Math.max(0, Math.min(1, t))
|
|
491
|
+
|
|
492
|
+
// Binary search for the t value that gives us the desired x
|
|
493
|
+
// We're solving for t in the Bezier curve: x(t) = 3*(1-t)^2*t*x1 + 3*(1-t)*t^2*x2 + t^3
|
|
494
|
+
let start = 0
|
|
495
|
+
let end = 1
|
|
496
|
+
let mid = 0.5
|
|
497
|
+
|
|
498
|
+
// Iterate until we find the t value that gives us the desired x
|
|
499
|
+
for (let i = 0; i < 15; i++) {
|
|
500
|
+
// Evaluate Bezier curve at mid point
|
|
501
|
+
const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
|
|
502
|
+
|
|
503
|
+
if (Math.abs(x - t) < 0.0001) {
|
|
504
|
+
break
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (x < t) {
|
|
508
|
+
start = mid
|
|
509
|
+
} else {
|
|
510
|
+
end = mid
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
mid = (start + end) / 2
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Now evaluate the y value at this t
|
|
517
|
+
const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
|
|
518
|
+
|
|
519
|
+
return y
|
|
520
|
+
}
|