@aura3d/animation 3.0.0 → 4.0.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.
- package/dist/AnimationController.d.ts +8 -0
- package/dist/AnimationController.d.ts.map +1 -1
- package/dist/AnimationController.js +93 -0
- package/dist/AnimationController.js.map +1 -1
- package/dist/AnimationMixer.d.ts +4 -1
- package/dist/AnimationMixer.d.ts.map +1 -1
- package/dist/AnimationMixer.js +47 -29
- package/dist/AnimationMixer.js.map +1 -1
- package/dist/FootIk.d.ts +50 -0
- package/dist/FootIk.d.ts.map +1 -1
- package/dist/FootIk.js +224 -8
- package/dist/FootIk.js.map +1 -1
- package/dist/HumanoidRetargeting.d.ts +6 -2
- package/dist/HumanoidRetargeting.d.ts.map +1 -1
- package/dist/HumanoidRetargeting.js +18 -8
- package/dist/HumanoidRetargeting.js.map +1 -1
- package/dist/IK.d.ts +26 -0
- package/dist/IK.d.ts.map +1 -1
- package/dist/IK.js +515 -0
- package/dist/IK.js.map +1 -1
- package/dist/Keyframe.d.ts.map +1 -1
- package/dist/Keyframe.js +13 -12
- package/dist/Keyframe.js.map +1 -1
- package/dist/LocomotionEvidence.d.ts +34 -0
- package/dist/LocomotionEvidence.d.ts.map +1 -0
- package/dist/LocomotionEvidence.js +48 -0
- package/dist/LocomotionEvidence.js.map +1 -0
- package/dist/RootMotion.d.ts +13 -4
- package/dist/RootMotion.d.ts.map +1 -1
- package/dist/RootMotion.js +21 -1
- package/dist/RootMotion.js.map +1 -1
- package/dist/SpringBones.d.ts +80 -0
- package/dist/SpringBones.d.ts.map +1 -1
- package/dist/SpringBones.js +593 -49
- package/dist/SpringBones.js.map +1 -1
- package/dist/browser-index.d.ts +1 -1
- package/dist/browser-index.d.ts.map +1 -1
- package/dist/browser-index.js +1 -1
- package/dist/browser-index.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +7 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/pose.d.ts +25 -0
- package/dist/contracts/pose.d.ts.map +1 -0
- package/dist/contracts/pose.js +41 -0
- package/dist/contracts/pose.js.map +1 -0
- package/dist/experimental/CrowdAnimation.d.ts +13 -0
- package/dist/experimental/CrowdAnimation.d.ts.map +1 -0
- package/dist/experimental/CrowdAnimation.js +14 -0
- package/dist/experimental/CrowdAnimation.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lanes/index.d.ts +17 -0
- package/dist/lanes/index.d.ts.map +1 -0
- package/dist/lanes/index.js +17 -0
- package/dist/lanes/index.js.map +1 -0
- package/dist/lanes/prd01.d.ts +6 -0
- package/dist/lanes/prd01.d.ts.map +1 -0
- package/dist/lanes/prd01.js +6 -0
- package/dist/lanes/prd01.js.map +1 -0
- package/dist/lanes/prd02.d.ts +6 -0
- package/dist/lanes/prd02.d.ts.map +1 -0
- package/dist/lanes/prd02.js +6 -0
- package/dist/lanes/prd02.js.map +1 -0
- package/dist/lanes/prd03.d.ts +6 -0
- package/dist/lanes/prd03.d.ts.map +1 -0
- package/dist/lanes/prd03.js +6 -0
- package/dist/lanes/prd03.js.map +1 -0
- package/dist/lanes/prd04.d.ts +6 -0
- package/dist/lanes/prd04.d.ts.map +1 -0
- package/dist/lanes/prd04.js +6 -0
- package/dist/lanes/prd04.js.map +1 -0
- package/dist/lanes/prd05.d.ts +6 -0
- package/dist/lanes/prd05.d.ts.map +1 -0
- package/dist/lanes/prd05.js +6 -0
- package/dist/lanes/prd05.js.map +1 -0
- package/dist/lanes/prd06.d.ts +37 -0
- package/dist/lanes/prd06.d.ts.map +1 -0
- package/dist/lanes/prd06.js +26 -0
- package/dist/lanes/prd06.js.map +1 -0
- package/dist/lanes/prd07.d.ts +6 -0
- package/dist/lanes/prd07.d.ts.map +1 -0
- package/dist/lanes/prd07.js +6 -0
- package/dist/lanes/prd07.js.map +1 -0
- package/dist/lanes/prd08.d.ts +6 -0
- package/dist/lanes/prd08.d.ts.map +1 -0
- package/dist/lanes/prd08.js +6 -0
- package/dist/lanes/prd08.js.map +1 -0
- package/dist/lanes/prd09.d.ts +6 -0
- package/dist/lanes/prd09.d.ts.map +1 -0
- package/dist/lanes/prd09.js +6 -0
- package/dist/lanes/prd09.js.map +1 -0
- package/dist/lanes/prd10.d.ts +6 -0
- package/dist/lanes/prd10.d.ts.map +1 -0
- package/dist/lanes/prd10.js +6 -0
- package/dist/lanes/prd10.js.map +1 -0
- package/dist/lanes/prd11.d.ts +6 -0
- package/dist/lanes/prd11.d.ts.map +1 -0
- package/dist/lanes/prd11.js +6 -0
- package/dist/lanes/prd11.js.map +1 -0
- package/dist/lanes/prd12.d.ts +6 -0
- package/dist/lanes/prd12.d.ts.map +1 -0
- package/dist/lanes/prd12.js +6 -0
- package/dist/lanes/prd12.js.map +1 -0
- package/dist/lanes/prd13.d.ts +6 -0
- package/dist/lanes/prd13.d.ts.map +1 -0
- package/dist/lanes/prd13.js +6 -0
- package/dist/lanes/prd13.js.map +1 -0
- package/dist/lanes/prd14.d.ts +6 -0
- package/dist/lanes/prd14.d.ts.map +1 -0
- package/dist/lanes/prd14.js +6 -0
- package/dist/lanes/prd14.js.map +1 -0
- package/dist/lanes/prd15.d.ts +6 -0
- package/dist/lanes/prd15.d.ts.map +1 -0
- package/dist/lanes/prd15.js +6 -0
- package/dist/lanes/prd15.js.map +1 -0
- package/dist/pose/BoneMask.d.ts +39 -0
- package/dist/pose/BoneMask.d.ts.map +1 -0
- package/dist/pose/BoneMask.js +160 -0
- package/dist/pose/BoneMask.js.map +1 -0
- package/dist/pose/CcdIkConstraint.d.ts +38 -0
- package/dist/pose/CcdIkConstraint.d.ts.map +1 -0
- package/dist/pose/CcdIkConstraint.js +198 -0
- package/dist/pose/CcdIkConstraint.js.map +1 -0
- package/dist/pose/CompiledClip.d.ts +48 -0
- package/dist/pose/CompiledClip.d.ts.map +1 -0
- package/dist/pose/CompiledClip.js +209 -0
- package/dist/pose/CompiledClip.js.map +1 -0
- package/dist/pose/LookAtConstraint.d.ts +48 -0
- package/dist/pose/LookAtConstraint.d.ts.map +1 -0
- package/dist/pose/LookAtConstraint.js +251 -0
- package/dist/pose/LookAtConstraint.js.map +1 -0
- package/dist/pose/MotionMetrics.d.ts +136 -0
- package/dist/pose/MotionMetrics.d.ts.map +1 -0
- package/dist/pose/MotionMetrics.js +253 -0
- package/dist/pose/MotionMetrics.js.map +1 -0
- package/dist/pose/PoseBuffer.d.ts +23 -0
- package/dist/pose/PoseBuffer.d.ts.map +1 -0
- package/dist/pose/PoseBuffer.js +48 -0
- package/dist/pose/PoseBuffer.js.map +1 -0
- package/dist/pose/PoseInertializer.d.ts +38 -0
- package/dist/pose/PoseInertializer.d.ts.map +1 -0
- package/dist/pose/PoseInertializer.js +208 -0
- package/dist/pose/PoseInertializer.js.map +1 -0
- package/dist/pose/PoseMixer.d.ts +256 -0
- package/dist/pose/PoseMixer.d.ts.map +1 -0
- package/dist/pose/PoseMixer.js +848 -0
- package/dist/pose/PoseMixer.js.map +1 -0
- package/dist/pose/Retarget.d.ts +77 -0
- package/dist/pose/Retarget.d.ts.map +1 -0
- package/dist/pose/Retarget.js +378 -0
- package/dist/pose/Retarget.js.map +1 -0
- package/dist/pose/RetargetCache.d.ts +21 -0
- package/dist/pose/RetargetCache.d.ts.map +1 -0
- package/dist/pose/RetargetCache.js +102 -0
- package/dist/pose/RetargetCache.js.map +1 -0
- package/dist/pose/RetargetWorker.d.ts +21 -0
- package/dist/pose/RetargetWorker.d.ts.map +1 -0
- package/dist/pose/RetargetWorker.js +56 -0
- package/dist/pose/RetargetWorker.js.map +1 -0
- package/dist/pose/SkeletonBinding.d.ts +55 -0
- package/dist/pose/SkeletonBinding.d.ts.map +1 -0
- package/dist/pose/SkeletonBinding.js +62 -0
- package/dist/pose/SkeletonBinding.js.map +1 -0
- package/dist/pose/blendKernels.d.ts +32 -0
- package/dist/pose/blendKernels.d.ts.map +1 -0
- package/dist/pose/blendKernels.js +101 -0
- package/dist/pose/blendKernels.js.map +1 -0
- package/dist/pose/makeClipAdditive.d.ts +22 -0
- package/dist/pose/makeClipAdditive.d.ts.map +1 -0
- package/dist/pose/makeClipAdditive.js +101 -0
- package/dist/pose/makeClipAdditive.js.map +1 -0
- package/dist/pose/poseMixerFlags.d.ts +18 -0
- package/dist/pose/poseMixerFlags.d.ts.map +1 -0
- package/dist/pose/poseMixerFlags.js +30 -0
- package/dist/pose/poseMixerFlags.js.map +1 -0
- package/dist/pose/quatFlat.d.ts +16 -0
- package/dist/pose/quatFlat.d.ts.map +1 -0
- package/dist/pose/quatFlat.js +88 -0
- package/dist/pose/quatFlat.js.map +1 -0
- package/dist/pose/retarget.worker.d.ts +25 -0
- package/dist/pose/retarget.worker.d.ts.map +1 -0
- package/dist/pose/retarget.worker.js +22 -0
- package/dist/pose/retarget.worker.js.map +1 -0
- package/package.json +14 -2
package/dist/SpringBones.js
CHANGED
|
@@ -22,91 +22,268 @@ export function createSpringChain(options) {
|
|
|
22
22
|
const gravity = options.gravity ?? [0, -9.81, 0];
|
|
23
23
|
const colliders = options.colliders ?? [];
|
|
24
24
|
const substeps = Math.max(1, Math.floor(options.substeps ?? 2));
|
|
25
|
+
const relativeDamping = Math.max(0, options.relativeDamping ?? 0);
|
|
25
26
|
const name = options.name ?? "spring-chain";
|
|
26
27
|
// Rest offset of each particle relative to its parent, in the root's initial frame.
|
|
27
|
-
const restOffsets =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const restOffsets = new Float32Array(options.bones.length * 3);
|
|
29
|
+
for (let i = 0; i < options.bones.length; i += 1) {
|
|
30
|
+
const bone = options.bones[i];
|
|
31
|
+
restOffsets[i * 3] = i === 0 ? 0 : bone[0] - options.bones[i - 1][0];
|
|
32
|
+
restOffsets[i * 3 + 1] = i === 0 ? 0 : bone[1] - options.bones[i - 1][1];
|
|
33
|
+
restOffsets[i * 3 + 2] = i === 0 ? 0 : bone[2] - options.bones[i - 1][2];
|
|
34
|
+
}
|
|
35
|
+
const restLengths = new Float32Array(options.bones.length);
|
|
36
|
+
for (let i = 0; i < options.bones.length; i += 1) {
|
|
37
|
+
restLengths[i] = Math.hypot(restOffsets[i * 3], restOffsets[i * 3 + 1], restOffsets[i * 3 + 2]);
|
|
38
|
+
}
|
|
39
|
+
// §13 — flat particle state so `step` performs scalar math with no per-call
|
|
40
|
+
// allocation (the Vec3-of-arrays version measured ~5× over the spring
|
|
41
|
+
// micro-budget).
|
|
42
|
+
const count = options.bones.length;
|
|
43
|
+
const positions = new Float32Array(count * 3);
|
|
44
|
+
const velocities = new Float32Array(count * 3);
|
|
45
|
+
for (let i = 0; i < count; i += 1) {
|
|
46
|
+
const bone = options.bones[i];
|
|
47
|
+
positions[i * 3] = bone[0];
|
|
48
|
+
positions[i * 3 + 1] = bone[1];
|
|
49
|
+
positions[i * 3 + 2] = bone[2];
|
|
50
|
+
}
|
|
31
51
|
let contacts = 0;
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
const anchorSpace = options.space === "anchor";
|
|
53
|
+
// Anchor rotation at the previous step (anchor space only).
|
|
54
|
+
const prevAnchorRot = new Float64Array([0, 0, 0, 1]);
|
|
55
|
+
/**
|
|
56
|
+
* Anchor space: carry every simulated particle and its velocity rigidly
|
|
57
|
+
* with the root's motion since the last step (translation + delta
|
|
58
|
+
* rotation about the root), before the spring/gravity update.
|
|
59
|
+
*/
|
|
60
|
+
function transportWithAnchor(root) {
|
|
61
|
+
const q = root.rotation;
|
|
62
|
+
let dx = 0, dy = 0, dz = 0, dw = 1;
|
|
63
|
+
{
|
|
64
|
+
const qx = q?.[0] ?? 0, qy = q?.[1] ?? 0, qz = q?.[2] ?? 0, qw = q?.[3] ?? 1;
|
|
65
|
+
// dq = q * conj(prev)
|
|
66
|
+
const bx = -prevAnchorRot[0], by = -prevAnchorRot[1], bz = -prevAnchorRot[2], bw = prevAnchorRot[3];
|
|
67
|
+
dx = qx * bw + qw * bx + qy * bz - qz * by;
|
|
68
|
+
dy = qy * bw + qw * by + qz * bx - qx * bz;
|
|
69
|
+
dz = qz * bw + qw * bz + qx * by - qy * bx;
|
|
70
|
+
dw = qw * bw - qx * bx - qy * by - qz * bz;
|
|
71
|
+
}
|
|
72
|
+
const ox = positions[0], oy = positions[1], oz = positions[2];
|
|
73
|
+
const rx = root.position[0], ry = root.position[1], rz = root.position[2];
|
|
74
|
+
for (let i = 1; i < count; i += 1) {
|
|
75
|
+
const ci = i * 3;
|
|
76
|
+
// Scalar quaternion rotation keeps the hot transport path allocation-free.
|
|
77
|
+
const px = positions[ci] - ox, py = positions[ci + 1] - oy, pz = positions[ci + 2] - oz;
|
|
78
|
+
const tx = 2 * (dy * pz - dz * py), ty = 2 * (dz * px - dx * pz), tz = 2 * (dx * py - dy * px);
|
|
79
|
+
positions[ci] = rx + px + dw * tx + dy * tz - dz * ty;
|
|
80
|
+
positions[ci + 1] = ry + py + dw * ty + dz * tx - dx * tz;
|
|
81
|
+
positions[ci + 2] = rz + pz + dw * tz + dx * ty - dy * tx;
|
|
82
|
+
const vx = velocities[ci], vy = velocities[ci + 1], vz = velocities[ci + 2];
|
|
83
|
+
const ux = 2 * (dy * vz - dz * vy), uy = 2 * (dz * vx - dx * vz), uz = 2 * (dx * vy - dy * vx);
|
|
84
|
+
velocities[ci] = vx + dw * ux + dy * uz - dz * uy;
|
|
85
|
+
velocities[ci + 1] = vy + dw * uy + dz * ux - dx * uz;
|
|
86
|
+
velocities[ci + 2] = vz + dw * uz + dx * uy - dy * ux;
|
|
87
|
+
}
|
|
88
|
+
prevAnchorRot[0] = q?.[0] ?? 0;
|
|
89
|
+
prevAnchorRot[1] = q?.[1] ?? 0;
|
|
90
|
+
prevAnchorRot[2] = q?.[2] ?? 0;
|
|
91
|
+
prevAnchorRot[3] = q?.[3] ?? 1;
|
|
34
92
|
}
|
|
35
93
|
function step(dt, root) {
|
|
36
94
|
contacts = 0;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
const q = root.rotation;
|
|
96
|
+
const offs = root.offsets;
|
|
97
|
+
if (anchorSpace)
|
|
98
|
+
transportWithAnchor(root);
|
|
99
|
+
// Root particle is kinematic (zero velocity: in anchor space it is the
|
|
100
|
+
// frame origin; in model space this is the legacy behaviour).
|
|
101
|
+
velocities[0] = 0;
|
|
102
|
+
velocities[1] = 0;
|
|
103
|
+
velocities[2] = 0;
|
|
104
|
+
positions[0] = root.position[0];
|
|
105
|
+
positions[1] = root.position[1];
|
|
106
|
+
positions[2] = root.position[2];
|
|
107
|
+
for (let i = 1; i < count; i += 1) {
|
|
108
|
+
const pi = (i - 1) * 3;
|
|
109
|
+
const ci = i * 3;
|
|
110
|
+
// Rest target = parent position + rest offset: the animated offset
|
|
111
|
+
// when supplied, else the bind offset rotated into the root frame.
|
|
112
|
+
let ox, oy, oz;
|
|
113
|
+
if (offs !== undefined) {
|
|
114
|
+
ox = offs[ci];
|
|
115
|
+
oy = offs[ci + 1];
|
|
116
|
+
oz = offs[ci + 2];
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
ox = restOffsets[ci];
|
|
120
|
+
oy = restOffsets[ci + 1];
|
|
121
|
+
oz = restOffsets[ci + 2];
|
|
122
|
+
if (q !== undefined) {
|
|
123
|
+
const ix = q[3] * ox + q[1] * oz - q[2] * oy;
|
|
124
|
+
const iy = q[3] * oy + q[2] * ox - q[0] * oz;
|
|
125
|
+
const iz = q[3] * oz + q[0] * oy - q[1] * ox;
|
|
126
|
+
const iw = -q[0] * ox - q[1] * oy - q[2] * oz;
|
|
127
|
+
ox = ix * q[3] + iw * -q[0] + iy * -q[2] - iz * -q[1];
|
|
128
|
+
oy = iy * q[3] + iw * -q[1] + iz * -q[0] - ix * -q[2];
|
|
129
|
+
oz = iz * q[3] + iw * -q[2] + ix * -q[1] - iy * -q[0];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const restLength = offs !== undefined ? Math.hypot(ox, oy, oz) : restLengths[i];
|
|
133
|
+
const rx = positions[pi] + ox;
|
|
134
|
+
const ry = positions[pi + 1] + oy;
|
|
135
|
+
const rz = positions[pi + 2] + oz;
|
|
136
|
+
const px = positions[ci], py = positions[ci + 1], pz = positions[ci + 2];
|
|
137
|
+
const vx = velocities[ci], vy = velocities[ci + 1], vz = velocities[ci + 2];
|
|
45
138
|
// a = stiffness*(rest - p) + gravity - damping*v (semi-implicit: update v, then p)
|
|
46
|
-
const ax = stiffness * (
|
|
47
|
-
const ay = stiffness * (
|
|
48
|
-
const az = stiffness * (
|
|
49
|
-
|
|
50
|
-
let
|
|
139
|
+
const ax = stiffness * (rx - px) + gravity[0] - damping * vx;
|
|
140
|
+
const ay = stiffness * (ry - py) + gravity[1] - damping * vy;
|
|
141
|
+
const az = stiffness * (rz - pz) + gravity[2] - damping * vz;
|
|
142
|
+
let nvx = vx + ax * dt;
|
|
143
|
+
let nvy = vy + ay * dt;
|
|
144
|
+
let nvz = vz + az * dt;
|
|
145
|
+
let npx = px + nvx * dt;
|
|
146
|
+
let npy = py + nvy * dt;
|
|
147
|
+
let npz = pz + nvz * dt;
|
|
51
148
|
// Distance constraint: keep the bone length to the parent.
|
|
52
|
-
|
|
53
|
-
|
|
149
|
+
{
|
|
150
|
+
const dx = npx - positions[pi], dy = npy - positions[pi + 1], dz = npz - positions[pi + 2];
|
|
151
|
+
const l = Math.hypot(dx, dy, dz);
|
|
152
|
+
if (l <= 1e-8) {
|
|
153
|
+
npx = positions[pi];
|
|
154
|
+
npy = positions[pi + 1] + restLength;
|
|
155
|
+
npz = positions[pi + 2];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const s = restLength / l;
|
|
159
|
+
npx = positions[pi] + dx * s;
|
|
160
|
+
npy = positions[pi + 1] + dy * s;
|
|
161
|
+
npz = positions[pi + 2] + dz * s;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Collider push-out (scalar form of pushOut/closestPointOnSegment).
|
|
54
165
|
for (const collider of colliders) {
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
nv[1] -= dot * resolved.normal[1];
|
|
63
|
-
nv[2] -= dot * resolved.normal[2];
|
|
64
|
-
}
|
|
65
|
-
contacts += 1;
|
|
166
|
+
let cx = collider.center[0], cy = collider.center[1], cz = collider.center[2];
|
|
167
|
+
if (collider.kind === "capsule" && collider.tail) {
|
|
168
|
+
const abx = collider.tail[0] - cx, aby = collider.tail[1] - cy, abz = collider.tail[2] - cz;
|
|
169
|
+
const t = clamp(((npx - cx) * abx + (npy - cy) * aby + (npz - cz) * abz) / Math.max(1e-8, abx * abx + aby * aby + abz * abz), 0, 1);
|
|
170
|
+
cx += abx * t;
|
|
171
|
+
cy += aby * t;
|
|
172
|
+
cz += abz * t;
|
|
66
173
|
}
|
|
174
|
+
const dx = npx - cx, dy = npy - cy, dz = npz - cz;
|
|
175
|
+
const l = Math.hypot(dx, dy, dz);
|
|
176
|
+
if (l >= collider.radius)
|
|
177
|
+
continue;
|
|
178
|
+
const inx = l <= 1e-8 ? 0 : dx / l;
|
|
179
|
+
const iny = l <= 1e-8 ? 1 : dy / l;
|
|
180
|
+
const inz = l <= 1e-8 ? 0 : dz / l;
|
|
181
|
+
npx = cx + inx * collider.radius;
|
|
182
|
+
npy = cy + iny * collider.radius;
|
|
183
|
+
npz = cz + inz * collider.radius;
|
|
184
|
+
// Remove the inward velocity component so it doesn't tunnel back in.
|
|
185
|
+
const d = nvx * inx + nvy * iny + nvz * inz;
|
|
186
|
+
if (d < 0) {
|
|
187
|
+
nvx -= d * inx;
|
|
188
|
+
nvy -= d * iny;
|
|
189
|
+
nvz -= d * inz;
|
|
190
|
+
}
|
|
191
|
+
contacts += 1;
|
|
67
192
|
}
|
|
68
193
|
// Recompute velocity from the constrained position so energy bookkeeping stays consistent.
|
|
69
|
-
|
|
70
|
-
|
|
194
|
+
let evx = (npx - px) / dt;
|
|
195
|
+
let evy = (npy - py) / dt;
|
|
196
|
+
let evz = (npz - pz) / dt;
|
|
197
|
+
if (relativeDamping > 0) {
|
|
198
|
+
const keep = Math.exp(-relativeDamping * dt);
|
|
199
|
+
const pvx = velocities[pi], pvy = velocities[pi + 1], pvz = velocities[pi + 2];
|
|
200
|
+
evx = pvx + (evx - pvx) * keep;
|
|
201
|
+
evy = pvy + (evy - pvy) * keep;
|
|
202
|
+
evz = pvz + (evz - pvz) * keep;
|
|
203
|
+
}
|
|
204
|
+
velocities[ci] = evx;
|
|
205
|
+
velocities[ci + 1] = evy;
|
|
206
|
+
velocities[ci + 2] = evz;
|
|
207
|
+
positions[ci] = npx;
|
|
208
|
+
positions[ci + 1] = npy;
|
|
209
|
+
positions[ci + 2] = npz;
|
|
71
210
|
}
|
|
72
211
|
}
|
|
73
212
|
return {
|
|
74
213
|
integrate(dt, rootTransform) {
|
|
75
214
|
if (!Number.isFinite(dt) || dt <= 0)
|
|
76
215
|
throw new Error("Spring chain dt must be finite and positive.");
|
|
216
|
+
if (rootTransform.offsets !== undefined && rootTransform.offsets.length !== count * 3)
|
|
217
|
+
throw new Error("Spring chain offsets must contain 3 floats per particle.");
|
|
77
218
|
const sub = dt / substeps;
|
|
78
219
|
for (let s = 0; s < substeps; s += 1)
|
|
79
220
|
step(sub, rootTransform);
|
|
80
221
|
},
|
|
81
222
|
positions() {
|
|
82
|
-
|
|
223
|
+
const out = new Array(count);
|
|
224
|
+
for (let i = 0; i < count; i += 1) {
|
|
225
|
+
out[i] = [positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]];
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
},
|
|
229
|
+
positionsFlat(out) {
|
|
230
|
+
out.set(positions.subarray(0, count * 3));
|
|
231
|
+
return out;
|
|
83
232
|
},
|
|
84
233
|
telemetry() {
|
|
85
|
-
const
|
|
86
|
-
const root = positions[0];
|
|
234
|
+
const ti = (count - 1) * 3;
|
|
87
235
|
let ke = 0;
|
|
88
|
-
for (
|
|
89
|
-
ke += 0.5 *
|
|
236
|
+
for (let i = 0; i < count * 3; i += 1)
|
|
237
|
+
ke += 0.5 * velocities[i] * velocities[i];
|
|
90
238
|
return {
|
|
91
239
|
name,
|
|
92
|
-
boneCount:
|
|
240
|
+
boneCount: count,
|
|
93
241
|
stiffness,
|
|
94
242
|
damping,
|
|
95
|
-
rootPosition: [
|
|
96
|
-
tipPosition: [
|
|
97
|
-
maxDisplacement:
|
|
243
|
+
rootPosition: [positions[0], positions[1], positions[2]],
|
|
244
|
+
tipPosition: [positions[ti], positions[ti + 1], positions[ti + 2]],
|
|
245
|
+
maxDisplacement: Math.hypot(positions[ti] - positions[0], positions[ti + 1] - positions[1], positions[ti + 2] - positions[2]),
|
|
98
246
|
collisionContacts: contacts,
|
|
99
247
|
kineticEnergy: ke
|
|
100
248
|
};
|
|
101
249
|
},
|
|
102
250
|
reset(rootTransform) {
|
|
103
251
|
const rootPos = rootTransform?.position ?? options.bones[0];
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
252
|
+
const q = rootTransform?.rotation;
|
|
253
|
+
const offs = rootTransform?.offsets;
|
|
254
|
+
if (offs !== undefined && offs.length !== count * 3)
|
|
255
|
+
throw new Error("Spring chain offsets must contain 3 floats per particle.");
|
|
256
|
+
prevAnchorRot[0] = q?.[0] ?? 0;
|
|
257
|
+
prevAnchorRot[1] = q?.[1] ?? 0;
|
|
258
|
+
prevAnchorRot[2] = q?.[2] ?? 0;
|
|
259
|
+
prevAnchorRot[3] = q?.[3] ?? 1;
|
|
260
|
+
contacts = 0;
|
|
261
|
+
positions[0] = rootPos[0];
|
|
262
|
+
positions[1] = rootPos[1];
|
|
263
|
+
positions[2] = rootPos[2];
|
|
264
|
+
velocities.fill(0);
|
|
265
|
+
for (let i = 1; i < count; i += 1) {
|
|
266
|
+
const pi = (i - 1) * 3;
|
|
267
|
+
const ci = i * 3;
|
|
268
|
+
if (offs !== undefined) {
|
|
269
|
+
positions[ci] = positions[pi] + offs[ci];
|
|
270
|
+
positions[ci + 1] = positions[pi + 1] + offs[ci + 1];
|
|
271
|
+
positions[ci + 2] = positions[pi + 2] + offs[ci + 2];
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
let ox = restOffsets[ci], oy = restOffsets[ci + 1], oz = restOffsets[ci + 2];
|
|
275
|
+
if (q !== undefined) {
|
|
276
|
+
const ix = q[3] * ox + q[1] * oz - q[2] * oy;
|
|
277
|
+
const iy = q[3] * oy + q[2] * ox - q[0] * oz;
|
|
278
|
+
const iz = q[3] * oz + q[0] * oy - q[1] * ox;
|
|
279
|
+
const iw = -q[0] * ox - q[1] * oy - q[2] * oz;
|
|
280
|
+
ox = ix * q[3] + iw * -q[0] + iy * -q[2] - iz * -q[1];
|
|
281
|
+
oy = iy * q[3] + iw * -q[1] + iz * -q[0] - ix * -q[2];
|
|
282
|
+
oz = iz * q[3] + iw * -q[2] + ix * -q[1] - iy * -q[0];
|
|
283
|
+
}
|
|
284
|
+
positions[ci] = positions[pi] + ox;
|
|
285
|
+
positions[ci + 1] = positions[pi + 1] + oy;
|
|
286
|
+
positions[ci + 2] = positions[pi + 2] + oz;
|
|
110
287
|
}
|
|
111
288
|
}
|
|
112
289
|
};
|
|
@@ -193,4 +370,371 @@ function rotateVec3(v, q) {
|
|
|
193
370
|
iz * w + iw * -z + ix * -y - iy * -x
|
|
194
371
|
];
|
|
195
372
|
}
|
|
373
|
+
function springFkFrame(pose, skeleton, joint) {
|
|
374
|
+
const rot = [pose.rotations[joint * 4], pose.rotations[joint * 4 + 1], pose.rotations[joint * 4 + 2], pose.rotations[joint * 4 + 3]];
|
|
375
|
+
const scl = [pose.scales[joint * 3], pose.scales[joint * 3 + 1], pose.scales[joint * 3 + 2]];
|
|
376
|
+
const pos = [pose.positions[joint * 3], pose.positions[joint * 3 + 1], pose.positions[joint * 3 + 2]];
|
|
377
|
+
const parent = skeleton.parentIndices[joint] ?? -1;
|
|
378
|
+
if (parent < 0 || parent >= skeleton.boneCount || parent === joint) {
|
|
379
|
+
return { position: pos, rotation: rot, scale: scl };
|
|
380
|
+
}
|
|
381
|
+
const pf = springFkFrame(pose, skeleton, parent);
|
|
382
|
+
const scaled = [pos[0] * pf.scale[0], pos[1] * pf.scale[1], pos[2] * pf.scale[2]];
|
|
383
|
+
const rotated = rotateVec3(scaled, pf.rotation);
|
|
384
|
+
return {
|
|
385
|
+
position: [pf.position[0] + rotated[0], pf.position[1] + rotated[1], pf.position[2] + rotated[2]],
|
|
386
|
+
rotation: multiplyQuat(pf.rotation, rot),
|
|
387
|
+
scale: [pf.scale[0] * scl[0], pf.scale[1] * scl[1], pf.scale[2] * scl[2]]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function multiplyQuat(a, b) {
|
|
391
|
+
const [ax, ay, az, aw] = a;
|
|
392
|
+
const [bx, by, bz, bw] = b;
|
|
393
|
+
return [
|
|
394
|
+
ax * bw + aw * bx + ay * bz - az * by,
|
|
395
|
+
ay * bw + aw * by + az * bx - ax * bz,
|
|
396
|
+
az * bw + aw * bz + ax * by - ay * bx,
|
|
397
|
+
aw * bw - ax * bx - ay * by - az * bz
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
function conjugateQuat(q) {
|
|
401
|
+
return [-q[0], -q[1], -q[2], q[3]];
|
|
402
|
+
}
|
|
403
|
+
function quatFromUnitVectors(from, to) {
|
|
404
|
+
const d = Math.max(-1, Math.min(1, from[0] * to[0] + from[1] * to[1] + from[2] * to[2]));
|
|
405
|
+
if (d > 1 - 1e-9)
|
|
406
|
+
return [0, 0, 0, 1];
|
|
407
|
+
if (d < -1 + 1e-9) {
|
|
408
|
+
const axis = Math.abs(from[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
|
|
409
|
+
const c = [
|
|
410
|
+
from[1] * axis[2] - from[2] * axis[1],
|
|
411
|
+
from[2] * axis[0] - from[0] * axis[2],
|
|
412
|
+
from[0] * axis[1] - from[1] * axis[0]
|
|
413
|
+
];
|
|
414
|
+
const l = Math.hypot(c[0], c[1], c[2]) || 1;
|
|
415
|
+
return [c[0] / l, c[1] / l, c[2] / l, 0];
|
|
416
|
+
}
|
|
417
|
+
const c = [
|
|
418
|
+
from[1] * to[2] - from[2] * to[1],
|
|
419
|
+
from[2] * to[0] - from[0] * to[2],
|
|
420
|
+
from[0] * to[1] - from[1] * to[0]
|
|
421
|
+
];
|
|
422
|
+
const w = Math.sqrt((1 + d) / 2);
|
|
423
|
+
const s = 1 / (2 * w);
|
|
424
|
+
return [c[0] * s, c[1] * s, c[2] * s, w];
|
|
425
|
+
}
|
|
426
|
+
function normalizeVec3(v) {
|
|
427
|
+
const l = len(v);
|
|
428
|
+
return l <= 1e-9 ? [0, 1, 0] : [v[0] / l, v[1] / l, v[2] / l];
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* T4.1 (PRD-06 §7.1) — bind a {@link SpringChain} to a skeleton bone chain.
|
|
432
|
+
* `boneNames` lists the chain root first (the kinematic anchor, driven by
|
|
433
|
+
* animation) followed by the simulated bones. Unknown names throw — a
|
|
434
|
+
* misspelled chain is louder than a silently dead spring.
|
|
435
|
+
*
|
|
436
|
+
* Per `step`, the kinematic root frame is read from the pose, the fixed-step
|
|
437
|
+
* accumulator integrates at `substepHz`, and each bone with a simulated child
|
|
438
|
+
* gets a local rotation aiming its rest child-direction at the simulated
|
|
439
|
+
* segment direction. Positions stay FK-driven: the sim only bends the chain.
|
|
440
|
+
*/
|
|
441
|
+
export function bindSpringChainToSkeleton(chain, skeleton, boneNames, options) {
|
|
442
|
+
if (boneNames.length < 2) {
|
|
443
|
+
throw new Error("SpringBones: a bound chain needs a root plus at least one simulated bone.");
|
|
444
|
+
}
|
|
445
|
+
const indices = boneNames.map((name) => {
|
|
446
|
+
const slots = skeleton.jointIndicesByName.get(name);
|
|
447
|
+
if (slots === undefined || slots.length === 0) {
|
|
448
|
+
throw new Error(`SpringBones: unknown bone "${name}".`);
|
|
449
|
+
}
|
|
450
|
+
return slots[0];
|
|
451
|
+
});
|
|
452
|
+
const substepHz = Math.max(1, options?.substepHz ?? 60);
|
|
453
|
+
const maxSubsteps = Math.max(1, Math.floor(options?.maxSubsteps ?? 8));
|
|
454
|
+
const h = 1 / substepHz;
|
|
455
|
+
let accumulator = 0;
|
|
456
|
+
let initialized = false;
|
|
457
|
+
// §13 — flat FK-frame scratch: `springFkFrame` re-walked the ancestor chain
|
|
458
|
+
// and allocated per call (~allocs × depth × bones per step, ~5× over the
|
|
459
|
+
// spring micro-budget). Now frames resolve once per step into flat arrays,
|
|
460
|
+
// memoized by generation mark, and the aim write is pure scalar math.
|
|
461
|
+
const fPos = new Float32Array(skeleton.boneCount * 3);
|
|
462
|
+
const fRot = new Float32Array(skeleton.boneCount * 4);
|
|
463
|
+
const fScl = new Float32Array(skeleton.boneCount * 3);
|
|
464
|
+
const fMark = new Uint32Array(skeleton.boneCount);
|
|
465
|
+
const simPos = new Float32Array(indices.length * 3);
|
|
466
|
+
const rootPosArr = [0, 0, 0];
|
|
467
|
+
const rootRotArr = [0, 0, 0, 1];
|
|
468
|
+
// Animated rest targets (this frame and the last integrated frame) and the
|
|
469
|
+
// per-substep interpolated copies handed to the chain.
|
|
470
|
+
const curOffsets = new Float32Array(indices.length * 3);
|
|
471
|
+
const prevOffsets = new Float32Array(indices.length * 3);
|
|
472
|
+
const subOffsets = new Float32Array(indices.length * 3);
|
|
473
|
+
const prevRoot = new Float32Array(3);
|
|
474
|
+
const subRoot = [0, 0, 0];
|
|
475
|
+
// Aim write bookkeeping (see `restoreUnauthoredAims`).
|
|
476
|
+
const aimRot = new Float32Array(indices.length * 4);
|
|
477
|
+
const authoredRot = new Float32Array(indices.length * 4);
|
|
478
|
+
const aimWritten = new Uint8Array(indices.length);
|
|
479
|
+
let fGen = 0;
|
|
480
|
+
/** World-frame of `joint` in `pose`, computed once per generation into the flat scratch. */
|
|
481
|
+
function frameOf(pose, joint) {
|
|
482
|
+
if (fMark[joint] === fGen)
|
|
483
|
+
return;
|
|
484
|
+
const lp = joint * 3;
|
|
485
|
+
const lr = joint * 4;
|
|
486
|
+
const lpx = pose.positions[lp], lpy = pose.positions[lp + 1], lpz = pose.positions[lp + 2];
|
|
487
|
+
const lrx = pose.rotations[lr], lry = pose.rotations[lr + 1], lrz = pose.rotations[lr + 2], lrw = pose.rotations[lr + 3];
|
|
488
|
+
const lsx = pose.scales[lp], lsy = pose.scales[lp + 1], lsz = pose.scales[lp + 2];
|
|
489
|
+
const parent = skeleton.parentIndices[joint] ?? -1;
|
|
490
|
+
if (parent < 0 || parent >= skeleton.boneCount || parent === joint) {
|
|
491
|
+
fPos[lp] = lpx;
|
|
492
|
+
fPos[lp + 1] = lpy;
|
|
493
|
+
fPos[lp + 2] = lpz;
|
|
494
|
+
fRot[lr] = lrx;
|
|
495
|
+
fRot[lr + 1] = lry;
|
|
496
|
+
fRot[lr + 2] = lrz;
|
|
497
|
+
fRot[lr + 3] = lrw;
|
|
498
|
+
fScl[lp] = lsx;
|
|
499
|
+
fScl[lp + 1] = lsy;
|
|
500
|
+
fScl[lp + 2] = lsz;
|
|
501
|
+
fMark[joint] = fGen;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
frameOf(pose, parent);
|
|
505
|
+
const pp = parent * 3;
|
|
506
|
+
const pr = parent * 4;
|
|
507
|
+
const qx = fRot[pr], qy = fRot[pr + 1], qz = fRot[pr + 2], qw = fRot[pr + 3];
|
|
508
|
+
// pos = parentPos + rotate(parentRot, localPos * parentScale)
|
|
509
|
+
const sx = lpx * fScl[pp], sy = lpy * fScl[pp + 1], sz = lpz * fScl[pp + 2];
|
|
510
|
+
const ix = qw * sx + qy * sz - qz * sy;
|
|
511
|
+
const iy = qw * sy + qz * sx - qx * sz;
|
|
512
|
+
const iz = qw * sz + qx * sy - qy * sx;
|
|
513
|
+
const iw = -qx * sx - qy * sy - qz * sz;
|
|
514
|
+
fPos[lp] = fPos[pp] + (ix * qw + iw * -qx + iy * -qz - iz * -qy);
|
|
515
|
+
fPos[lp + 1] = fPos[pp + 1] + (iy * qw + iw * -qy + iz * -qx - ix * -qz);
|
|
516
|
+
fPos[lp + 2] = fPos[pp + 2] + (iz * qw + iw * -qz + ix * -qy - iy * -qx);
|
|
517
|
+
// rot = parentRot * localRot
|
|
518
|
+
fRot[lr] = qx * lrw + qw * lrx + qy * lrz - qz * lry;
|
|
519
|
+
fRot[lr + 1] = qy * lrw + qw * lry + qz * lrx - qx * lrz;
|
|
520
|
+
fRot[lr + 2] = qz * lrw + qw * lrz + qx * lry - qy * lrx;
|
|
521
|
+
fRot[lr + 3] = qw * lrw - qx * lrx - qy * lry - qz * lrz;
|
|
522
|
+
fScl[lp] = fScl[pp] * lsx;
|
|
523
|
+
fScl[lp + 1] = fScl[pp + 1] * lsy;
|
|
524
|
+
fScl[lp + 2] = fScl[pp + 2] * lsz;
|
|
525
|
+
fMark[joint] = fGen;
|
|
526
|
+
}
|
|
527
|
+
// Rest child-direction per bone expressed in its own frame. Derived from the
|
|
528
|
+
// bind-time (rest) pose so the aim write preserves the authored chain shape
|
|
529
|
+
// and only bends it toward the simulated segment.
|
|
530
|
+
const restFrames = indices.map((joint) => springFkFrame(skeleton.restPose, skeleton, joint));
|
|
531
|
+
const childDirLocal = indices.map((joint, i) => {
|
|
532
|
+
if (i + 1 >= indices.length)
|
|
533
|
+
return [0, 0, 0];
|
|
534
|
+
const restDir = normalizeVec3([
|
|
535
|
+
restFrames[i + 1].position[0] - restFrames[i].position[0],
|
|
536
|
+
restFrames[i + 1].position[1] - restFrames[i].position[1],
|
|
537
|
+
restFrames[i + 1].position[2] - restFrames[i].position[2]
|
|
538
|
+
]);
|
|
539
|
+
return normalizeVec3(rotateVec3(restDir, conjugateQuat(restFrames[i].rotation)));
|
|
540
|
+
});
|
|
541
|
+
function initialize(pose) {
|
|
542
|
+
fGen += 1;
|
|
543
|
+
restoreUnauthoredAims(pose);
|
|
544
|
+
readAnimatedTargets(pose);
|
|
545
|
+
chain.reset({ position: rootPosArr, rotation: rootRotArr, offsets: curOffsets });
|
|
546
|
+
prevRoot.set(rootPosArr);
|
|
547
|
+
prevOffsets.set(curOffsets);
|
|
548
|
+
aimWritten.fill(0);
|
|
549
|
+
accumulator = 0;
|
|
550
|
+
initialized = true;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* The aim write below edits `pose.rotations` in place. A host that mixes a
|
|
554
|
+
* fresh pose every frame overwrites it; one that re-steps the same buffer
|
|
555
|
+
* would otherwise feed the aimed rotation back in as the "animated" target
|
|
556
|
+
* (the spring would chase itself). An aimed joint whose rotation is still
|
|
557
|
+
* bit-identical to the value written is restored to the authored rotation.
|
|
558
|
+
*/
|
|
559
|
+
function restoreUnauthoredAims(pose) {
|
|
560
|
+
for (let i = 1; i + 1 < indices.length; i += 1) {
|
|
561
|
+
if (aimWritten[i] === 0)
|
|
562
|
+
continue;
|
|
563
|
+
const jr = indices[i] * 4;
|
|
564
|
+
const ar = i * 4;
|
|
565
|
+
if (pose.rotations[jr] === aimRot[ar] && pose.rotations[jr + 1] === aimRot[ar + 1] &&
|
|
566
|
+
pose.rotations[jr + 2] === aimRot[ar + 2] && pose.rotations[jr + 3] === aimRot[ar + 3]) {
|
|
567
|
+
pose.rotations[jr] = authoredRot[ar];
|
|
568
|
+
pose.rotations[jr + 1] = authoredRot[ar + 1];
|
|
569
|
+
pose.rotations[jr + 2] = authoredRot[ar + 2];
|
|
570
|
+
pose.rotations[jr + 3] = authoredRot[ar + 3];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Animated (pre-aim) chain frame: root world position/rotation and each
|
|
576
|
+
* particle's FK offset from its parent. These are the spring's rest
|
|
577
|
+
* targets, so the chain settles onto the *animated* pose — a clip that
|
|
578
|
+
* re-poses the chain (a crouch, a landing) moves the target with it instead
|
|
579
|
+
* of leaving the particles springing toward bind-pose offsets.
|
|
580
|
+
*/
|
|
581
|
+
function readAnimatedTargets(pose) {
|
|
582
|
+
for (let i = 0; i < indices.length; i += 1)
|
|
583
|
+
frameOf(pose, indices[i]);
|
|
584
|
+
const ri = indices[0];
|
|
585
|
+
rootPosArr[0] = fPos[ri * 3];
|
|
586
|
+
rootPosArr[1] = fPos[ri * 3 + 1];
|
|
587
|
+
rootPosArr[2] = fPos[ri * 3 + 2];
|
|
588
|
+
rootRotArr[0] = fRot[ri * 4];
|
|
589
|
+
rootRotArr[1] = fRot[ri * 4 + 1];
|
|
590
|
+
rootRotArr[2] = fRot[ri * 4 + 2];
|
|
591
|
+
rootRotArr[3] = fRot[ri * 4 + 3];
|
|
592
|
+
for (let i = 1; i < indices.length; i += 1) {
|
|
593
|
+
const a = indices[i - 1] * 3;
|
|
594
|
+
const b = indices[i] * 3;
|
|
595
|
+
curOffsets[i * 3] = fPos[b] - fPos[a];
|
|
596
|
+
curOffsets[i * 3 + 1] = fPos[b + 1] - fPos[a + 1];
|
|
597
|
+
curOffsets[i * 3 + 2] = fPos[b + 2] - fPos[a + 2];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function step(pose, dt) {
|
|
601
|
+
if (!Number.isFinite(dt) || dt < 0) {
|
|
602
|
+
throw new Error("SpringBones: step dt must be finite and non-negative.");
|
|
603
|
+
}
|
|
604
|
+
if (!initialized)
|
|
605
|
+
initialize(pose);
|
|
606
|
+
fGen += 1;
|
|
607
|
+
restoreUnauthoredAims(pose);
|
|
608
|
+
accumulator = Math.min(accumulator + dt, 0.25);
|
|
609
|
+
const substeps = Math.min(Math.floor(accumulator / h), maxSubsteps);
|
|
610
|
+
if (substeps > 0) {
|
|
611
|
+
readAnimatedTargets(pose);
|
|
612
|
+
// The animated pose only advances once per host frame; spread that
|
|
613
|
+
// motion across this call's substeps (linear in position/offset)
|
|
614
|
+
// instead of teleporting the anchor on the first substep. A teleport
|
|
615
|
+
// makes the length constraint convert a whole frame's anchor travel
|
|
616
|
+
// into one substep's velocity — ~substeps× the real anchor speed —
|
|
617
|
+
// which flings the chain on every fast anchor move (the landing).
|
|
618
|
+
for (let k = 0; k < substeps; k += 1) {
|
|
619
|
+
const u = (k + 1) / substeps;
|
|
620
|
+
subRoot[0] = prevRoot[0] + (rootPosArr[0] - prevRoot[0]) * u;
|
|
621
|
+
subRoot[1] = prevRoot[1] + (rootPosArr[1] - prevRoot[1]) * u;
|
|
622
|
+
subRoot[2] = prevRoot[2] + (rootPosArr[2] - prevRoot[2]) * u;
|
|
623
|
+
for (let c = 3; c < subOffsets.length; c += 1) {
|
|
624
|
+
subOffsets[c] = prevOffsets[c] + (curOffsets[c] - prevOffsets[c]) * u;
|
|
625
|
+
}
|
|
626
|
+
chain.integrate(h, { position: subRoot, rotation: rootRotArr, offsets: subOffsets });
|
|
627
|
+
}
|
|
628
|
+
prevRoot.set(rootPosArr);
|
|
629
|
+
prevOffsets.set(curOffsets);
|
|
630
|
+
accumulator -= substeps * h;
|
|
631
|
+
}
|
|
632
|
+
chain.positionsFlat(simPos);
|
|
633
|
+
// Animated-target FK frames predate the aim writes. Recompute the parent
|
|
634
|
+
// chain lazily as each newly aimed rotation becomes a descendant's parent.
|
|
635
|
+
fGen += 1;
|
|
636
|
+
// Aim bones[1..n-2]: each simulated bone that has a simulated child. The
|
|
637
|
+
// chain root stays purely kinematic — writing its rotation would feed the
|
|
638
|
+
// aim back into the next frame's root frame and the rest target would
|
|
639
|
+
// chase itself; the tip has no child to aim at.
|
|
640
|
+
for (let i = 1; i + 1 < indices.length; i += 1) {
|
|
641
|
+
const joint = indices[i];
|
|
642
|
+
const si = i * 3;
|
|
643
|
+
const sdx = simPos[si + 3] - simPos[si];
|
|
644
|
+
const sdy = simPos[si + 4] - simPos[si + 1];
|
|
645
|
+
const sdz = simPos[si + 5] - simPos[si + 2];
|
|
646
|
+
const sl = Math.hypot(sdx, sdy, sdz);
|
|
647
|
+
if (sl <= 1e-9)
|
|
648
|
+
continue;
|
|
649
|
+
const ndx = sdx / sl, ndy = sdy / sl, ndz = sdz / sl;
|
|
650
|
+
const parent = skeleton.parentIndices[joint] ?? -1;
|
|
651
|
+
let prx = 0, pry = 0, prz = 0, prw = 1;
|
|
652
|
+
if (parent >= 0 && parent < skeleton.boneCount) {
|
|
653
|
+
frameOf(pose, parent);
|
|
654
|
+
const pr = parent * 4;
|
|
655
|
+
prx = fRot[pr];
|
|
656
|
+
pry = fRot[pr + 1];
|
|
657
|
+
prz = fRot[pr + 2];
|
|
658
|
+
prw = fRot[pr + 3];
|
|
659
|
+
}
|
|
660
|
+
const jr = joint * 4;
|
|
661
|
+
const lrx = pose.rotations[jr], lry = pose.rotations[jr + 1], lrz = pose.rotations[jr + 2], lrw = pose.rotations[jr + 3];
|
|
662
|
+
// worldRot = parentRot * localRot
|
|
663
|
+
const wx = prx * lrw + prw * lrx + pry * lrz - prz * lry;
|
|
664
|
+
const wy = pry * lrw + prw * lry + prz * lrx - prx * lrz;
|
|
665
|
+
const wz = prz * lrw + prw * lrz + prx * lry - pry * lrx;
|
|
666
|
+
const ww = prw * lrw - prx * lrx - pry * lry - prz * lrz;
|
|
667
|
+
// currentDir = normalize(rotate(childDirLocal[i], worldRot))
|
|
668
|
+
const cv = childDirLocal[i];
|
|
669
|
+
const cix = ww * cv[0] + wy * cv[2] - wz * cv[1];
|
|
670
|
+
const ciy = ww * cv[1] + wz * cv[0] - wx * cv[2];
|
|
671
|
+
const ciz = ww * cv[2] + wx * cv[1] - wy * cv[0];
|
|
672
|
+
const ciw = -wx * cv[0] - wy * cv[1] - wz * cv[2];
|
|
673
|
+
let cdx = cix * ww + ciw * -wx + ciy * -wz - ciz * -wy;
|
|
674
|
+
let cdy = ciy * ww + ciw * -wy + ciz * -wx - cix * -wz;
|
|
675
|
+
let cdz = ciz * ww + ciw * -wz + cix * -wy - ciy * -wx;
|
|
676
|
+
const cl = Math.hypot(cdx, cdy, cdz);
|
|
677
|
+
if (cl <= 1e-9) {
|
|
678
|
+
cdx = 0;
|
|
679
|
+
cdy = 1;
|
|
680
|
+
cdz = 0;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
cdx /= cl;
|
|
684
|
+
cdy /= cl;
|
|
685
|
+
cdz /= cl;
|
|
686
|
+
}
|
|
687
|
+
// aim = quatFromUnitVectors(currentDir, simDir)
|
|
688
|
+
let ax = 0, ay = 0, az = 0, aw = 1;
|
|
689
|
+
const d = Math.max(-1, Math.min(1, cdx * ndx + cdy * ndy + cdz * ndz));
|
|
690
|
+
if (d > 1 - 1e-9) {
|
|
691
|
+
// aim stays identity
|
|
692
|
+
}
|
|
693
|
+
else if (d < -1 + 1e-9) {
|
|
694
|
+
const ux = Math.abs(cdx) < 0.9 ? 1 : 0;
|
|
695
|
+
const uy = Math.abs(cdx) < 0.9 ? 0 : 1;
|
|
696
|
+
let cxx = cdy * 0 - cdz * uy;
|
|
697
|
+
let cyy = cdz * ux - cdx * 0;
|
|
698
|
+
let czz = cdx * uy - cdy * ux;
|
|
699
|
+
const ul = Math.hypot(cxx, cyy, czz) || 1;
|
|
700
|
+
ax = cxx / ul;
|
|
701
|
+
ay = cyy / ul;
|
|
702
|
+
az = czz / ul;
|
|
703
|
+
aw = 0;
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
const cx = cdy * ndz - cdz * ndy;
|
|
707
|
+
const cy = cdz * ndx - cdx * ndz;
|
|
708
|
+
const cz = cdx * ndy - cdy * ndx;
|
|
709
|
+
const hw = Math.sqrt((1 + d) / 2);
|
|
710
|
+
const s = 1 / (2 * hw);
|
|
711
|
+
ax = cx * s;
|
|
712
|
+
ay = cy * s;
|
|
713
|
+
az = cz * s;
|
|
714
|
+
aw = hw;
|
|
715
|
+
}
|
|
716
|
+
// newLocal = conjugate(parentRot) * (aim * worldRot)
|
|
717
|
+
const m1x = ax * ww + aw * wx + ay * wz - az * wy;
|
|
718
|
+
const m1y = ay * ww + aw * wy + az * wx - ax * wz;
|
|
719
|
+
const m1z = az * ww + aw * wz + ax * wy - ay * wx;
|
|
720
|
+
const m1w = aw * ww - ax * wx - ay * wy - az * wz;
|
|
721
|
+
pose.rotations[jr] = -prx * m1w + prw * m1x - pry * m1z + prz * m1y;
|
|
722
|
+
pose.rotations[jr + 1] = -pry * m1w + prw * m1y - prz * m1x + prx * m1z;
|
|
723
|
+
pose.rotations[jr + 2] = -prz * m1w + prw * m1z - prx * m1y + pry * m1x;
|
|
724
|
+
pose.rotations[jr + 3] = prw * m1w + prx * m1x + pry * m1y + prz * m1z;
|
|
725
|
+
const ar = i * 4;
|
|
726
|
+
authoredRot[ar] = lrx;
|
|
727
|
+
authoredRot[ar + 1] = lry;
|
|
728
|
+
authoredRot[ar + 2] = lrz;
|
|
729
|
+
authoredRot[ar + 3] = lrw;
|
|
730
|
+
aimRot[ar] = pose.rotations[jr];
|
|
731
|
+
aimRot[ar + 1] = pose.rotations[jr + 1];
|
|
732
|
+
aimRot[ar + 2] = pose.rotations[jr + 2];
|
|
733
|
+
aimRot[ar + 3] = pose.rotations[jr + 3];
|
|
734
|
+
aimWritten[i] = 1;
|
|
735
|
+
fMark[joint] = 0;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { initialize, step };
|
|
739
|
+
}
|
|
196
740
|
//# sourceMappingURL=SpringBones.js.map
|