reze-engine 0.3.5 → 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/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 +6 -0
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +98 -101
- package/dist/ik.d.ts +32 -0
- package/dist/ik.d.ts.map +1 -0
- package/dist/ik.js +337 -0
- package/dist/math.d.ts +0 -5
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +0 -55
- package/dist/model.d.ts +1 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +11 -41
- package/dist/player.d.ts +6 -20
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +88 -191
- package/dist/pool-scene.d.ts +52 -0
- package/dist/pool-scene.d.ts.map +1 -0
- package/dist/pool-scene.js +1122 -0
- package/dist/pool.d.ts +38 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +422 -0
- package/package.json +1 -1
- package/src/engine.ts +54 -101
- package/src/ik-solver.ts +106 -124
- package/src/math.ts +0 -74
- package/src/model.ts +12 -44
- package/src/player.ts +115 -210
package/src/engine.ts
CHANGED
|
@@ -135,7 +135,6 @@ export class Engine {
|
|
|
135
135
|
|
|
136
136
|
private player: Player = new Player()
|
|
137
137
|
private hasAnimation = false // Set to true when loadAnimation is called
|
|
138
|
-
private animationStartTime: number = 0 // Track when animation first started (for A-pose prevention)
|
|
139
138
|
|
|
140
139
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
141
140
|
this.canvas = canvas
|
|
@@ -1317,8 +1316,8 @@ export class Engine {
|
|
|
1317
1316
|
public playAnimation() {
|
|
1318
1317
|
if (!this.hasAnimation || !this.currentModel) return
|
|
1319
1318
|
|
|
1320
|
-
const wasPaused = this.player.isPausedState
|
|
1321
|
-
const wasPlaying = this.player.isPlayingState
|
|
1319
|
+
const wasPaused = this.player.isPausedState
|
|
1320
|
+
const wasPlaying = this.player.isPlayingState
|
|
1322
1321
|
|
|
1323
1322
|
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1324
1323
|
if (!wasPlaying && !wasPaused) {
|
|
@@ -1365,9 +1364,6 @@ export class Engine {
|
|
|
1365
1364
|
|
|
1366
1365
|
// Start playback (or resume if paused)
|
|
1367
1366
|
this.player.play()
|
|
1368
|
-
if (this.animationStartTime === 0) {
|
|
1369
|
-
this.animationStartTime = performance.now()
|
|
1370
|
-
}
|
|
1371
1367
|
}
|
|
1372
1368
|
|
|
1373
1369
|
public stopAnimation() {
|
|
@@ -1659,23 +1655,7 @@ export class Engine {
|
|
|
1659
1655
|
const materialAlpha = mat.diffuse[3]
|
|
1660
1656
|
const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON
|
|
1661
1657
|
|
|
1662
|
-
|
|
1663
|
-
const materialUniformData = new Float32Array(8)
|
|
1664
|
-
materialUniformData[0] = materialAlpha
|
|
1665
|
-
materialUniformData[1] = 1.0 // alphaMultiplier: 1.0 for non-hair materials
|
|
1666
|
-
materialUniformData[2] = this.rimLightIntensity
|
|
1667
|
-
materialUniformData[3] = 0.0 // _padding1
|
|
1668
|
-
materialUniformData[4] = 1.0 // rimColor.r
|
|
1669
|
-
materialUniformData[5] = 1.0 // rimColor.g
|
|
1670
|
-
materialUniformData[6] = 1.0 // rimColor.b
|
|
1671
|
-
materialUniformData[7] = 0.0 // isOverEyes
|
|
1672
|
-
|
|
1673
|
-
const materialUniformBuffer = this.device.createBuffer({
|
|
1674
|
-
label: `material uniform: ${mat.name}`,
|
|
1675
|
-
size: materialUniformData.byteLength,
|
|
1676
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1677
|
-
})
|
|
1678
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1658
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0)
|
|
1679
1659
|
|
|
1680
1660
|
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1681
1661
|
const bindGroup = this.device.createBindGroup({
|
|
@@ -1691,33 +1671,22 @@ export class Engine {
|
|
|
1691
1671
|
],
|
|
1692
1672
|
})
|
|
1693
1673
|
|
|
1694
|
-
|
|
1674
|
+
const addDrawCall = (draws: DrawCall[]) => {
|
|
1695
1675
|
if (indexCount > 0) {
|
|
1696
|
-
|
|
1697
|
-
count: indexCount,
|
|
1698
|
-
firstIndex: currentIndexOffset,
|
|
1699
|
-
bindGroup,
|
|
1700
|
-
})
|
|
1676
|
+
draws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup })
|
|
1701
1677
|
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (mat.isEye) {
|
|
1681
|
+
addDrawCall(this.eyeDraws)
|
|
1702
1682
|
} else if (mat.isHair) {
|
|
1703
1683
|
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
1704
1684
|
const createHairBindGroup = (isOverEyes: boolean) => {
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
uniformData[4] = 1.0 // rimColor.rgb
|
|
1711
|
-
uniformData[5] = 1.0
|
|
1712
|
-
uniformData[6] = 1.0
|
|
1713
|
-
uniformData[7] = isOverEyes ? 1.0 : 0.0 // isOverEyes
|
|
1714
|
-
|
|
1715
|
-
const buffer = this.device.createBuffer({
|
|
1716
|
-
label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
1717
|
-
size: uniformData.byteLength,
|
|
1718
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1719
|
-
})
|
|
1720
|
-
this.device.queue.writeBuffer(buffer, 0, uniformData)
|
|
1685
|
+
const buffer = this.createMaterialUniformBuffer(
|
|
1686
|
+
`${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
1687
|
+
materialAlpha,
|
|
1688
|
+
isOverEyes ? 1.0 : 0.0
|
|
1689
|
+
)
|
|
1721
1690
|
|
|
1722
1691
|
return this.device.createBindGroup({
|
|
1723
1692
|
label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
@@ -1742,7 +1711,6 @@ export class Engine {
|
|
|
1742
1711
|
firstIndex: currentIndexOffset,
|
|
1743
1712
|
bindGroup: bindGroupOverEyes,
|
|
1744
1713
|
})
|
|
1745
|
-
|
|
1746
1714
|
this.hairDrawsOverNonEyes.push({
|
|
1747
1715
|
count: indexCount,
|
|
1748
1716
|
firstIndex: currentIndexOffset,
|
|
@@ -1750,41 +1718,27 @@ export class Engine {
|
|
|
1750
1718
|
})
|
|
1751
1719
|
}
|
|
1752
1720
|
} else if (isTransparent) {
|
|
1753
|
-
|
|
1754
|
-
this.transparentDraws.push({
|
|
1755
|
-
count: indexCount,
|
|
1756
|
-
firstIndex: currentIndexOffset,
|
|
1757
|
-
bindGroup,
|
|
1758
|
-
})
|
|
1759
|
-
}
|
|
1721
|
+
addDrawCall(this.transparentDraws)
|
|
1760
1722
|
} else {
|
|
1761
|
-
|
|
1762
|
-
this.opaqueDraws.push({
|
|
1763
|
-
count: indexCount,
|
|
1764
|
-
firstIndex: currentIndexOffset,
|
|
1765
|
-
bindGroup,
|
|
1766
|
-
})
|
|
1767
|
-
}
|
|
1723
|
+
addDrawCall(this.opaqueDraws)
|
|
1768
1724
|
}
|
|
1769
1725
|
|
|
1770
1726
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1771
1727
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1772
|
-
const materialUniformData = new Float32Array(
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const materialUniformBuffer = this.
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
})
|
|
1787
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1728
|
+
const materialUniformData = new Float32Array([
|
|
1729
|
+
mat.edgeColor[0],
|
|
1730
|
+
mat.edgeColor[1],
|
|
1731
|
+
mat.edgeColor[2],
|
|
1732
|
+
mat.edgeColor[3],
|
|
1733
|
+
mat.edgeSize,
|
|
1734
|
+
0,
|
|
1735
|
+
0,
|
|
1736
|
+
0,
|
|
1737
|
+
])
|
|
1738
|
+
const materialUniformBuffer = this.createUniformBuffer(
|
|
1739
|
+
`outline material uniform: ${mat.name}`,
|
|
1740
|
+
materialUniformData
|
|
1741
|
+
)
|
|
1788
1742
|
|
|
1789
1743
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1790
1744
|
label: `outline bind group: ${mat.name}`,
|
|
@@ -1797,31 +1751,14 @@ export class Engine {
|
|
|
1797
1751
|
})
|
|
1798
1752
|
|
|
1799
1753
|
if (indexCount > 0) {
|
|
1800
|
-
|
|
1801
|
-
this.eyeOutlineDraws
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
count: indexCount,
|
|
1809
|
-
firstIndex: currentIndexOffset,
|
|
1810
|
-
bindGroup: outlineBindGroup,
|
|
1811
|
-
})
|
|
1812
|
-
} else if (isTransparent) {
|
|
1813
|
-
this.transparentOutlineDraws.push({
|
|
1814
|
-
count: indexCount,
|
|
1815
|
-
firstIndex: currentIndexOffset,
|
|
1816
|
-
bindGroup: outlineBindGroup,
|
|
1817
|
-
})
|
|
1818
|
-
} else {
|
|
1819
|
-
this.opaqueOutlineDraws.push({
|
|
1820
|
-
count: indexCount,
|
|
1821
|
-
firstIndex: currentIndexOffset,
|
|
1822
|
-
bindGroup: outlineBindGroup,
|
|
1823
|
-
})
|
|
1824
|
-
}
|
|
1754
|
+
const outlineDraws = mat.isEye
|
|
1755
|
+
? this.eyeOutlineDraws
|
|
1756
|
+
: mat.isHair
|
|
1757
|
+
? this.hairOutlineDraws
|
|
1758
|
+
: isTransparent
|
|
1759
|
+
? this.transparentOutlineDraws
|
|
1760
|
+
: this.opaqueOutlineDraws
|
|
1761
|
+
outlineDraws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup })
|
|
1825
1762
|
}
|
|
1826
1763
|
}
|
|
1827
1764
|
|
|
@@ -1829,6 +1766,22 @@ export class Engine {
|
|
|
1829
1766
|
}
|
|
1830
1767
|
}
|
|
1831
1768
|
|
|
1769
|
+
private createMaterialUniformBuffer(label: string, alpha: number, isOverEyes: number): GPUBuffer {
|
|
1770
|
+
const data = new Float32Array(8)
|
|
1771
|
+
data.set([alpha, 1.0, this.rimLightIntensity, 0.0, 1.0, 1.0, 1.0, isOverEyes])
|
|
1772
|
+
return this.createUniformBuffer(`material uniform: ${label}`, data)
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
private createUniformBuffer(label: string, data: Float32Array | Uint32Array): GPUBuffer {
|
|
1776
|
+
const buffer = this.device.createBuffer({
|
|
1777
|
+
label,
|
|
1778
|
+
size: data.byteLength,
|
|
1779
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1780
|
+
})
|
|
1781
|
+
this.device.queue.writeBuffer(buffer, 0, data as ArrayBufferView<ArrayBuffer>)
|
|
1782
|
+
return buffer
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1832
1785
|
private async createTextureFromPath(path: string): Promise<GPUTexture | null> {
|
|
1833
1786
|
const cached = this.textureCache.get(path)
|
|
1834
1787
|
if (cached) {
|
package/src/ik-solver.ts
CHANGED
|
@@ -117,11 +117,7 @@ export class IKSolverSystem {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
const ikPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
|
|
122
|
-
const targetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
|
|
123
|
-
|
|
124
|
-
if (ikPosition.subtract(targetPosition).length() < this.EPSILON) return
|
|
120
|
+
if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
|
|
125
121
|
|
|
126
122
|
// Build IK chains
|
|
127
123
|
const chains: IKChain[] = []
|
|
@@ -135,11 +131,7 @@ export class IKSolverSystem {
|
|
|
135
131
|
}
|
|
136
132
|
this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
const updatedIkPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
|
|
140
|
-
const updatedTargetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
|
|
141
|
-
|
|
142
|
-
if (updatedIkPosition.subtract(updatedTargetPosition).length() < this.EPSILON) return
|
|
134
|
+
if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) return
|
|
143
135
|
|
|
144
136
|
// Solve iteratively
|
|
145
137
|
const iteration = Math.min(solver.iterationCount, 256)
|
|
@@ -165,29 +157,17 @@ export class IKSolverSystem {
|
|
|
165
157
|
}
|
|
166
158
|
}
|
|
167
159
|
|
|
168
|
-
|
|
169
|
-
const currentIkPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
|
|
170
|
-
const currentTargetPosition = this.getWorldTranslation(targetBoneIndex, worldMatrices)
|
|
171
|
-
const distance = currentIkPosition.subtract(currentTargetPosition).length()
|
|
172
|
-
if (distance < this.EPSILON) break
|
|
160
|
+
if (this.getDistance(ikBoneIndex, targetBoneIndex, worldMatrices) < this.EPSILON) break
|
|
173
161
|
}
|
|
174
162
|
|
|
175
163
|
// Apply IK rotations to local rotations
|
|
176
164
|
for (const link of solver.links) {
|
|
177
165
|
const chainInfo = ikChainInfo[link.boneIndex]
|
|
178
|
-
if (chainInfo
|
|
166
|
+
if (chainInfo?.ikRotation) {
|
|
179
167
|
const qi = link.boneIndex * 4
|
|
180
|
-
const localRot =
|
|
181
|
-
localRotations[qi],
|
|
182
|
-
localRotations[qi + 1],
|
|
183
|
-
localRotations[qi + 2],
|
|
184
|
-
localRotations[qi + 3]
|
|
185
|
-
)
|
|
168
|
+
const localRot = this.getQuatFromArray(localRotations, qi)
|
|
186
169
|
const finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
|
|
187
|
-
localRotations
|
|
188
|
-
localRotations[qi + 1] = finalRot.y
|
|
189
|
-
localRotations[qi + 2] = finalRot.z
|
|
190
|
-
localRotations[qi + 3] = finalRot.w
|
|
170
|
+
this.setQuatToArray(localRotations, qi, finalRot)
|
|
191
171
|
}
|
|
192
172
|
}
|
|
193
173
|
}
|
|
@@ -227,25 +207,20 @@ export class IKSolverSystem {
|
|
|
227
207
|
finalRotationAxis = this.transformNormal(chainRotationAxis, invParentRot).normalize()
|
|
228
208
|
break
|
|
229
209
|
}
|
|
230
|
-
case InternalSolveAxis.X:
|
|
231
|
-
|
|
232
|
-
const axisX = new Vec3(m[0], m[1], m[2])
|
|
233
|
-
const dot = chainRotationAxis.dot(axisX)
|
|
234
|
-
finalRotationAxis = new Vec3(dot >= 0 ? 1 : -1, 0, 0)
|
|
235
|
-
break
|
|
236
|
-
}
|
|
237
|
-
case InternalSolveAxis.Y: {
|
|
238
|
-
const m = parentWorldRotMatrix.values
|
|
239
|
-
const axisY = new Vec3(m[4], m[5], m[6])
|
|
240
|
-
const dot = chainRotationAxis.dot(axisY)
|
|
241
|
-
finalRotationAxis = new Vec3(0, dot >= 0 ? 1 : -1, 0)
|
|
242
|
-
break
|
|
243
|
-
}
|
|
210
|
+
case InternalSolveAxis.X:
|
|
211
|
+
case InternalSolveAxis.Y:
|
|
244
212
|
case InternalSolveAxis.Z: {
|
|
245
213
|
const m = parentWorldRotMatrix.values
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
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)
|
|
249
224
|
break
|
|
250
225
|
}
|
|
251
226
|
default:
|
|
@@ -267,84 +242,16 @@ export class IKSolverSystem {
|
|
|
267
242
|
chainInfo.ikRotation = ikRotation.multiply(chainInfo.ikRotation)
|
|
268
243
|
|
|
269
244
|
// Apply angle constraints if present
|
|
270
|
-
if (chain.minimumAngle
|
|
245
|
+
if (chain.minimumAngle && chain.maximumAngle) {
|
|
271
246
|
const qi = chainBoneIndex * 4
|
|
272
|
-
const localRot =
|
|
273
|
-
localRotations[qi],
|
|
274
|
-
localRotations[qi + 1],
|
|
275
|
-
localRotations[qi + 2],
|
|
276
|
-
localRotations[qi + 3]
|
|
277
|
-
)
|
|
247
|
+
const localRot = this.getQuatFromArray(localRotations, qi)
|
|
278
248
|
chainInfo.localRotation = localRot.clone()
|
|
279
249
|
|
|
280
250
|
const combinedRot = chainInfo.ikRotation.multiply(localRot)
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
switch (chain.rotationOrder) {
|
|
287
|
-
case InternalEulerRotationOrder.YXZ: {
|
|
288
|
-
rX = Math.asin(-m[9]) // m32
|
|
289
|
-
if (Math.abs(rX) > this.THRESHOLD) {
|
|
290
|
-
rX = rX < 0 ? -this.THRESHOLD : this.THRESHOLD
|
|
291
|
-
}
|
|
292
|
-
let cosX = Math.cos(rX)
|
|
293
|
-
if (cosX !== 0) cosX = 1 / cosX
|
|
294
|
-
rY = Math.atan2(m[8] * cosX, m[10] * cosX) // m31, m33
|
|
295
|
-
rZ = Math.atan2(m[1] * cosX, m[5] * cosX) // m12, m22
|
|
296
|
-
|
|
297
|
-
rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
|
|
298
|
-
rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
|
|
299
|
-
rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
|
|
300
|
-
|
|
301
|
-
chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(0, 1, 0), rY)
|
|
302
|
-
chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(1, 0, 0), rX))
|
|
303
|
-
chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ))
|
|
304
|
-
break
|
|
305
|
-
}
|
|
306
|
-
case InternalEulerRotationOrder.ZYX: {
|
|
307
|
-
rY = Math.asin(-m[2]) // m13
|
|
308
|
-
if (Math.abs(rY) > this.THRESHOLD) {
|
|
309
|
-
rY = rY < 0 ? -this.THRESHOLD : this.THRESHOLD
|
|
310
|
-
}
|
|
311
|
-
let cosY = Math.cos(rY)
|
|
312
|
-
if (cosY !== 0) cosY = 1 / cosY
|
|
313
|
-
rX = Math.atan2(m[6] * cosY, m[10] * cosY) // m23, m33
|
|
314
|
-
rZ = Math.atan2(m[1] * cosY, m[0] * cosY) // m12, m11
|
|
315
|
-
|
|
316
|
-
rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
|
|
317
|
-
rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
|
|
318
|
-
rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
|
|
319
|
-
|
|
320
|
-
chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ)
|
|
321
|
-
chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 1, 0), rY))
|
|
322
|
-
chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(1, 0, 0), rX))
|
|
323
|
-
break
|
|
324
|
-
}
|
|
325
|
-
case InternalEulerRotationOrder.XZY: {
|
|
326
|
-
rZ = Math.asin(-m[4]) // m21
|
|
327
|
-
if (Math.abs(rZ) > this.THRESHOLD) {
|
|
328
|
-
rZ = rZ < 0 ? -this.THRESHOLD : this.THRESHOLD
|
|
329
|
-
}
|
|
330
|
-
let cosZ = Math.cos(rZ)
|
|
331
|
-
if (cosZ !== 0) cosZ = 1 / cosZ
|
|
332
|
-
rX = Math.atan2(m[6] * cosZ, m[5] * cosZ) // m23, m22
|
|
333
|
-
rY = Math.atan2(m[8] * cosZ, m[0] * cosZ) // m31, m11
|
|
334
|
-
|
|
335
|
-
rX = this.limitAngle(rX, chain.minimumAngle.x, chain.maximumAngle.x, useAxis)
|
|
336
|
-
rY = this.limitAngle(rY, chain.minimumAngle.y, chain.maximumAngle.y, useAxis)
|
|
337
|
-
rZ = this.limitAngle(rZ, chain.minimumAngle.z, chain.maximumAngle.z, useAxis)
|
|
338
|
-
|
|
339
|
-
chainInfo.ikRotation = Quat.fromAxisAngle(new Vec3(1, 0, 0), rX)
|
|
340
|
-
chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 0, 1), rZ))
|
|
341
|
-
chainInfo.ikRotation = chainInfo.ikRotation.multiply(Quat.fromAxisAngle(new Vec3(0, 1, 0), rY))
|
|
342
|
-
break
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const invertedLocalRotation = localRot.conjugate().normalize()
|
|
347
|
-
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())
|
|
348
255
|
}
|
|
349
256
|
}
|
|
350
257
|
|
|
@@ -368,11 +275,92 @@ export class IKSolverSystem {
|
|
|
368
275
|
}
|
|
369
276
|
}
|
|
370
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
|
+
|
|
371
284
|
private static getWorldTranslation(boneIndex: number, worldMatrices: Float32Array): Vec3 {
|
|
372
285
|
const offset = boneIndex * 16
|
|
373
286
|
return new Vec3(worldMatrices[offset + 12], worldMatrices[offset + 13], worldMatrices[offset + 14])
|
|
374
287
|
}
|
|
375
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
|
+
|
|
376
364
|
private static getParentWorldRotationMatrix(boneIndex: number, bones: Bone[], worldMatrices: Float32Array): Mat4 {
|
|
377
365
|
const bone = bones[boneIndex]
|
|
378
366
|
if (bone.parentIndex >= 0) {
|
|
@@ -409,13 +397,7 @@ export class IKSolverSystem {
|
|
|
409
397
|
const qi = boneIndex * 4
|
|
410
398
|
const ti = boneIndex * 3
|
|
411
399
|
|
|
412
|
-
|
|
413
|
-
const localRot = new Quat(
|
|
414
|
-
localRotations[qi],
|
|
415
|
-
localRotations[qi + 1],
|
|
416
|
-
localRotations[qi + 2],
|
|
417
|
-
localRotations[qi + 3]
|
|
418
|
-
)
|
|
400
|
+
const localRot = this.getQuatFromArray(localRotations, qi)
|
|
419
401
|
|
|
420
402
|
// Apply IK rotation if available
|
|
421
403
|
let finalRot = localRot
|
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,47 +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
|
-
// Static method: create quaternion that rotates from one direction to another
|
|
131
|
-
static fromTo(from: Vec3, to: Vec3): Quat {
|
|
132
|
-
const dot = from.dot(to)
|
|
133
|
-
if (dot > 0.999999) return new Quat(0, 0, 0, 1) // Already aligned
|
|
134
|
-
if (dot < -0.999999) {
|
|
135
|
-
// 180 degrees
|
|
136
|
-
let axis = from.cross(new Vec3(1, 0, 0))
|
|
137
|
-
if (axis.length() < 0.001) axis = from.cross(new Vec3(0, 1, 0))
|
|
138
|
-
return new Quat(axis.x, axis.y, axis.z, 0).normalize()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const axis = from.cross(to)
|
|
142
|
-
const w = Math.sqrt((1 + dot) * 2)
|
|
143
|
-
const invW = 1 / w
|
|
144
|
-
return new Quat(axis.x * invW, axis.y * invW, axis.z * invW, w * 0.5).normalize()
|
|
145
|
-
}
|
|
146
|
-
|
|
147
98
|
// Static method: create quaternion from rotation axis and angle
|
|
148
99
|
static fromAxisAngle(axis: Vec3, angle: number): Quat {
|
|
149
100
|
const normalizedAxis = axis.normalize()
|
|
@@ -209,31 +160,6 @@ export class Quat {
|
|
|
209
160
|
|
|
210
161
|
return new Quat(x, y, z, w).normalize()
|
|
211
162
|
}
|
|
212
|
-
|
|
213
|
-
// Convert quaternion to Euler angles (ZXY order, inverse of fromEuler)
|
|
214
|
-
toEuler(): Vec3 {
|
|
215
|
-
const qx = this.x
|
|
216
|
-
const qy = this.y
|
|
217
|
-
const qz = this.z
|
|
218
|
-
const qw = this.w
|
|
219
|
-
|
|
220
|
-
// ZXY order (left-handed)
|
|
221
|
-
// Roll (X): rotation around X axis
|
|
222
|
-
const sinr_cosp = 2 * (qw * qx + qy * qz)
|
|
223
|
-
const cosr_cosp = 1 - 2 * (qx * qx + qy * qy)
|
|
224
|
-
const rotX = Math.atan2(sinr_cosp, cosr_cosp)
|
|
225
|
-
|
|
226
|
-
// Pitch (Y): rotation around Y axis
|
|
227
|
-
const sinp = 2 * (qw * qy - qz * qx)
|
|
228
|
-
const rotY = Math.abs(sinp) >= 1 ? (sinp >= 0 ? Math.PI / 2 : -Math.PI / 2) : Math.asin(sinp)
|
|
229
|
-
|
|
230
|
-
// Yaw (Z): rotation around Z axis
|
|
231
|
-
const siny_cosp = 2 * (qw * qz + qx * qy)
|
|
232
|
-
const cosy_cosp = 1 - 2 * (qy * qy + qz * qz)
|
|
233
|
-
const rotZ = Math.atan2(siny_cosp, cosy_cosp)
|
|
234
|
-
|
|
235
|
-
return new Vec3(rotX, rotY, rotZ)
|
|
236
|
-
}
|
|
237
163
|
}
|
|
238
164
|
|
|
239
165
|
export class Mat4 {
|