@driftengine/physics 4.8.1 → 4.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/controller.js +22 -14
- package/dist/ragdoll.d.ts +9 -1
- package/dist/ragdoll.js +269 -48
- package/package.json +1 -1
- package/src/controller.ts +27 -12
- package/src/ragdoll.ts +305 -49
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ kinematic sweep that resolves a moving body against a static world one axis at a
|
|
|
5
5
|
|
|
6
6
|
**It imports no other engine package.** Together with `@driftengine/entities` that makes a
|
|
7
7
|
deterministic simulation you can run with no renderer in its module graph — in Node, in a worker,
|
|
8
|
-
or on a server, which is what an authoritative host needs. **
|
|
8
|
+
or on a server, which is what an authoritative host needs. **47.0 KB gzipped** standalone, measured
|
|
9
9
|
by `scripts/size-gate.test.mjs` against `scripts/fixtures/size/physics-only.ts`.
|
|
10
10
|
|
|
11
11
|
**`@driftengine/core` re-exports every name here**, so reaching for collision through the core
|
package/dist/controller.js
CHANGED
|
@@ -313,9 +313,10 @@ export class CharacterController {
|
|
|
313
313
|
* the same clamp and air control — only the plane is given up, not the feel.
|
|
314
314
|
*/
|
|
315
315
|
if (input.projectMove === false) {
|
|
316
|
-
|
|
317
|
-
this.
|
|
318
|
-
this.
|
|
316
|
+
const t = approachFraction(wantX - this.velX, wantY - this.velY, wantZ - this.velZ, step);
|
|
317
|
+
this.velX = t === 1 ? wantX : this.velX + (wantX - this.velX) * t;
|
|
318
|
+
this.velY = t === 1 ? wantY : this.velY + (wantY - this.velY) * t;
|
|
319
|
+
this.velZ = t === 1 ? wantZ : this.velZ + (wantZ - this.velZ) * t;
|
|
319
320
|
}
|
|
320
321
|
else {
|
|
321
322
|
const alongUp = this.speedAlongUp;
|
|
@@ -326,9 +327,10 @@ export class CharacterController {
|
|
|
326
327
|
const wantTangentX = wantX - this.upX * wantIntoUp;
|
|
327
328
|
const wantTangentY = wantY - this.upY * wantIntoUp;
|
|
328
329
|
const wantTangentZ = wantZ - this.upZ * wantIntoUp;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
const t = approachFraction(wantTangentX - tangentX, wantTangentY - tangentY, wantTangentZ - tangentZ, step);
|
|
331
|
+
tangentX = t === 1 ? wantTangentX : tangentX + (wantTangentX - tangentX) * t;
|
|
332
|
+
tangentY = t === 1 ? wantTangentY : tangentY + (wantTangentY - tangentY) * t;
|
|
333
|
+
tangentZ = t === 1 ? wantTangentZ : tangentZ + (wantTangentZ - tangentZ) * t;
|
|
332
334
|
this.velX = tangentX + this.upX * alongUp;
|
|
333
335
|
this.velY = tangentY + this.upY * alongUp;
|
|
334
336
|
this.velZ = tangentZ + this.upZ * alongUp;
|
|
@@ -675,12 +677,18 @@ export class CharacterController {
|
|
|
675
677
|
this.pose.qw = w * inverse;
|
|
676
678
|
}
|
|
677
679
|
}
|
|
678
|
-
/**
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
680
|
+
/**
|
|
681
|
+
* How much of the way to close a gap of `(dx, dy, dz)` this tick: exactly 1 when the gap is no
|
|
682
|
+
* longer than `step`, which the caller reads as "land on the target", and `step` of its length
|
|
683
|
+
* otherwise.
|
|
684
|
+
*
|
|
685
|
+
* **Toward the wanted velocity as a vector, by at most `step` in length.** It was one world axis at
|
|
686
|
+
* a time, each by the whole step, so a diagonal gained and lost speed √2 times as fast as a straight
|
|
687
|
+
* run while the clamp on the wanted vector kept its top speed honest — the faster diagonal lived in
|
|
688
|
+
* the acceleration. What it gives up is that an axis no longer reaches its target on its own
|
|
689
|
+
* schedule: turning from one direction to another now curves rather than squaring the corner.
|
|
690
|
+
*/
|
|
691
|
+
function approachFraction(dx, dy, dz, step) {
|
|
692
|
+
const gap = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
693
|
+
return gap <= step ? 1 : step / gap;
|
|
686
694
|
}
|
package/dist/ragdoll.d.ts
CHANGED
|
@@ -88,7 +88,15 @@ export interface Ragdoll {
|
|
|
88
88
|
readonly bodyOf: Int32Array;
|
|
89
89
|
/** How many bones were built. */
|
|
90
90
|
readonly boneCount: number;
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Write the ragdoll's current shape into a pose.
|
|
93
|
+
*
|
|
94
|
+
* Each joint is turned by the bone that **starts** at it, which is the joint a skinned limb
|
|
95
|
+
* follows — the forearm's body turns the elbow — and a joint with no bone below it rides the one
|
|
96
|
+
* it hangs off. Translations are the rig's own, in each parent's frame. A root's rotation is
|
|
97
|
+
* written in the frame the character's node is placed in, at `rootX`, `rootY` and `rootZ`, with
|
|
98
|
+
* no rotation of its own; a joint no bone reaches is left as the pose had it.
|
|
99
|
+
*/
|
|
92
100
|
writePose(out: PoseTarget): void;
|
|
93
101
|
/** Steer the bodies toward a pose. Weight 1 tracks it, weight 0 goes limp. */
|
|
94
102
|
drive(world: PhysicsWorld, pose: PoseTarget, weight: number): void;
|
package/dist/ragdoll.js
CHANGED
|
@@ -45,8 +45,32 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
45
45
|
return value === undefined || !Number.isFinite(value) ? fallback : value;
|
|
46
46
|
};
|
|
47
47
|
const bodyOf = new Int32Array(jointCount).fill(-1);
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **ends** at, so a joint with no
|
|
50
|
+
* bone of its own below it — a fingertip, a head's tip — can ride the bone it hangs off.
|
|
51
|
+
*/
|
|
49
52
|
const bodyToJoint = new Float32Array(jointCount * 4);
|
|
53
|
+
/**
|
|
54
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **starts** at, indexed like
|
|
55
|
+
* `bodyOf` — by the bone's own joint — and naming its parent's rotation.
|
|
56
|
+
*
|
|
57
|
+
* **This is the joint a skinned limb follows.** A forearm's vertices are weighted to the elbow,
|
|
58
|
+
* so the elbow's rotation is what points the forearm, and the body standing along the forearm is
|
|
59
|
+
* what carries it. Reading each joint off the bone that *ends* at it — the upper arm for the
|
|
60
|
+
* elbow — drew every limb one bone late: a hand 0.47 m from its body in a doll synced and never
|
|
61
|
+
* stepped, reported from a game. A bone is rigid in its start joint's frame, so the relation
|
|
62
|
+
* captured at rest holds for as long as the doll does.
|
|
63
|
+
*/
|
|
64
|
+
const bodyToStart = new Float32Array(jointCount * 4);
|
|
65
|
+
/** The first bone starting at each joint, by that bone's own index, or −1 where none does. */
|
|
66
|
+
const startBone = new Int32Array(jointCount).fill(-1);
|
|
67
|
+
/**
|
|
68
|
+
* Each joint's translation in its parent's frame at rest, which is what a pose holds.
|
|
69
|
+
*
|
|
70
|
+
* **In the parent's frame, not the world's.** It was the world offset between the two, which is
|
|
71
|
+
* the same number only where the parent is unrotated at rest — true of every chain this module's
|
|
72
|
+
* tests were built from, and of no rig a game loads.
|
|
73
|
+
*/
|
|
50
74
|
const localRest = new Float32Array(jointCount * 3);
|
|
51
75
|
const restRotation = new Float32Array(jointCount * 4);
|
|
52
76
|
/**
|
|
@@ -58,6 +82,11 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
58
82
|
* the bones meet.
|
|
59
83
|
*/
|
|
60
84
|
const halfOf = new Float32Array(jointCount);
|
|
85
|
+
/**
|
|
86
|
+
* Each bone's rig offset over its length: 1 for a rig at unit scale, and what turns a length in
|
|
87
|
+
* metres back into the parent's own units where the rig is scaled.
|
|
88
|
+
*/
|
|
89
|
+
const restScale = new Float32Array(jointCount).fill(1);
|
|
61
90
|
let boneCount = 0;
|
|
62
91
|
let firstBone = -1;
|
|
63
92
|
/** Half the first bone's length, which is how far its head sits from the body's centre. */
|
|
@@ -73,9 +102,7 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
73
102
|
const tx = worldMatrices[j * 16 + 12] ?? 0;
|
|
74
103
|
const ty = worldMatrices[j * 16 + 13] ?? 0;
|
|
75
104
|
const tz = worldMatrices[j * 16 + 14] ?? 0;
|
|
76
|
-
|
|
77
|
-
localRest[j * 3 + 1] = ty - hy;
|
|
78
|
-
localRest[j * 3 + 2] = tz - hz;
|
|
105
|
+
intoParentFrame(worldMatrices, p, tx - hx, ty - hy, tz - hz, localRest, j * 3);
|
|
79
106
|
const dx = tx - hx;
|
|
80
107
|
const dy = ty - hy;
|
|
81
108
|
const dz = tz - hz;
|
|
@@ -101,8 +128,18 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
101
128
|
});
|
|
102
129
|
bodyOf[j] = body;
|
|
103
130
|
halfOf[j] = length / 2;
|
|
104
|
-
|
|
131
|
+
const rx = localRest[j * 3] ?? 0;
|
|
132
|
+
const ry = localRest[j * 3 + 1] ?? 0;
|
|
133
|
+
const rz = localRest[j * 3 + 2] ?? 0;
|
|
134
|
+
restScale[j] = Math.sqrt(rx * rx + ry * ry + rz * rz) / length;
|
|
135
|
+
// What takes the body's rotation back to each of its two joints', captured at rest. Parents
|
|
136
|
+
// come before children in a rig, so the start joint's rotation is already read.
|
|
105
137
|
multiplyConjugate(TMP, restRotation, j * 4, bodyToJoint, j * 4);
|
|
138
|
+
if (p >= j)
|
|
139
|
+
matrixRotation(worldMatrices, p, restRotation, p * 4);
|
|
140
|
+
multiplyConjugate(TMP, restRotation, p * 4, bodyToStart, j * 4);
|
|
141
|
+
if ((startBone[p] ?? -1) < 0)
|
|
142
|
+
startBone[p] = j;
|
|
106
143
|
boneCount++;
|
|
107
144
|
if (firstBone < 0) {
|
|
108
145
|
firstBone = j;
|
|
@@ -111,15 +148,67 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
111
148
|
firstBoneHalf = length / 2;
|
|
112
149
|
}
|
|
113
150
|
}
|
|
151
|
+
/*
|
|
152
|
+
* **A bone too short for a body is transparent.** Each joint resolves to the nearest one at or
|
|
153
|
+
* above it that a body ends at, or to its root: what hangs from a collar sitting on the chest
|
|
154
|
+
* joint hangs from the bone that ends at the chest. A rig puts one joint exactly on another as
|
|
155
|
+
* often as not, and a bone under `minLength` gets no body, so until 4.8.3 the spine above such a
|
|
156
|
+
* collar was jointed to nothing and collided with every bone it met there — compared by index,
|
|
157
|
+
* they shared no end. **What it gives up** is the skipped bone's own length, under `minLength`,
|
|
158
|
+
* which the joints below absorb exactly at rest: see `restAnchor`.
|
|
159
|
+
*/
|
|
160
|
+
const attach = new Int32Array(jointCount);
|
|
161
|
+
for (let j = 0; j < jointCount; j++) {
|
|
162
|
+
let a = j;
|
|
163
|
+
while ((bodyOf[a] ?? -1) < 0 && (parents[a] ?? -1) >= 0)
|
|
164
|
+
a = parents[a] ?? -1;
|
|
165
|
+
attach[j] = a;
|
|
166
|
+
}
|
|
167
|
+
/** Where a bone's parent end really hangs: its parent joint, seen through any bone too short. */
|
|
168
|
+
const hangsFrom = (j) => {
|
|
169
|
+
const p = parents[j] ?? -1;
|
|
170
|
+
return p < 0 ? -1 : (attach[p] ?? p);
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Bone `j`'s body-frame point at joint `at`'s rest position, into `REST_ANCHOR`. Where `at` is
|
|
174
|
+
* the bone's own parent joint this is its head, `(0, -half, 0)`, exactly as before; through a
|
|
175
|
+
* short bone it is where the two bones meet at rest, so a joint anchored there moves nothing on
|
|
176
|
+
* the first tick.
|
|
177
|
+
*/
|
|
178
|
+
const restAnchor = (j, at) => {
|
|
179
|
+
const p = parents[j] ?? -1;
|
|
180
|
+
if (at === p) {
|
|
181
|
+
REST_ANCHOR[0] = 0;
|
|
182
|
+
REST_ANCHOR[1] = -(halfOf[j] ?? 0);
|
|
183
|
+
REST_ANCHOR[2] = 0;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const body = bodyOf[j] ?? 0;
|
|
187
|
+
const dx = (worldMatrices[at * 16 + 12] ?? 0) - (world.bodies.posX[body] ?? 0);
|
|
188
|
+
const dy = (worldMatrices[at * 16 + 13] ?? 0) - (world.bodies.posY[body] ?? 0);
|
|
189
|
+
const dz = (worldMatrices[at * 16 + 14] ?? 0) - (world.bodies.posZ[body] ?? 0);
|
|
190
|
+
/* Into the body's frame: turned by the conjugate of its rotation. */
|
|
191
|
+
const ux = -(world.bodies.rotX[body] ?? 0);
|
|
192
|
+
const uy = -(world.bodies.rotY[body] ?? 0);
|
|
193
|
+
const uz = -(world.bodies.rotZ[body] ?? 0);
|
|
194
|
+
const w = world.bodies.rotW[body] ?? 1;
|
|
195
|
+
const cx = uy * dz - uz * dy;
|
|
196
|
+
const cy = uz * dx - ux * dz;
|
|
197
|
+
const cz = ux * dy - uy * dx;
|
|
198
|
+
REST_ANCHOR[0] = dx + 2 * (w * cx + (uy * cz - uz * cy));
|
|
199
|
+
REST_ANCHOR[1] = dy + 2 * (w * cy + (uz * cx - ux * cz));
|
|
200
|
+
REST_ANCHOR[2] = dz + 2 * (w * cz + (ux * cy - uy * cx));
|
|
201
|
+
};
|
|
114
202
|
// Joint each bone to its parent's bone, at the shared end.
|
|
115
203
|
for (let j = 0; j < jointCount; j++) {
|
|
116
204
|
const body = bodyOf[j] ?? -1;
|
|
117
205
|
if (body < 0)
|
|
118
206
|
continue;
|
|
119
|
-
const p =
|
|
207
|
+
const p = hangsFrom(j);
|
|
120
208
|
const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
|
|
121
209
|
if (parentBody < 0)
|
|
122
210
|
continue;
|
|
211
|
+
restAnchor(j, p);
|
|
123
212
|
world.addJoint({
|
|
124
213
|
type: JOINT_CONE_TWIST,
|
|
125
214
|
bodyA: parentBody,
|
|
@@ -146,7 +235,9 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
146
235
|
* so the first tick still moves nothing.
|
|
147
236
|
*/
|
|
148
237
|
anchorAY: halfOf[p] ?? 0,
|
|
149
|
-
|
|
238
|
+
anchorBX: REST_ANCHOR[0],
|
|
239
|
+
anchorBY: REST_ANCHOR[1],
|
|
240
|
+
anchorBZ: REST_ANCHOR[2],
|
|
150
241
|
});
|
|
151
242
|
}
|
|
152
243
|
/*
|
|
@@ -163,7 +254,7 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
163
254
|
const body = bodyOf[j] ?? -1;
|
|
164
255
|
if (body < 0)
|
|
165
256
|
continue;
|
|
166
|
-
const p =
|
|
257
|
+
const p = hangsFrom(j);
|
|
167
258
|
if (p < 0 || (bodyOf[p] ?? -1) >= 0)
|
|
168
259
|
continue;
|
|
169
260
|
const first = firstUnder[p] ?? -1;
|
|
@@ -171,6 +262,11 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
171
262
|
firstUnder[p] = j;
|
|
172
263
|
continue;
|
|
173
264
|
}
|
|
265
|
+
restAnchor(first, p);
|
|
266
|
+
const ax = REST_ANCHOR[0];
|
|
267
|
+
const ay = REST_ANCHOR[1];
|
|
268
|
+
const az = REST_ANCHOR[2];
|
|
269
|
+
restAnchor(j, p);
|
|
174
270
|
world.addJoint({
|
|
175
271
|
type: JOINT_CONE_TWIST,
|
|
176
272
|
bodyA: bodyOf[first] ?? 0,
|
|
@@ -178,8 +274,12 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
178
274
|
swingCos: limitAt(swingCos, Math.SQRT1_2, j),
|
|
179
275
|
twistSin: limitAt(twistSin, 0.3826834323650898, j),
|
|
180
276
|
axisY: 1,
|
|
181
|
-
|
|
182
|
-
|
|
277
|
+
anchorAX: ax,
|
|
278
|
+
anchorAY: ay,
|
|
279
|
+
anchorAZ: az,
|
|
280
|
+
anchorBX: REST_ANCHOR[0],
|
|
281
|
+
anchorBY: REST_ANCHOR[1],
|
|
282
|
+
anchorBZ: REST_ANCHOR[2],
|
|
183
283
|
});
|
|
184
284
|
}
|
|
185
285
|
/*
|
|
@@ -202,40 +302,80 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
202
302
|
world.ignorePair(body, other);
|
|
203
303
|
continue;
|
|
204
304
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
305
|
+
/* Seen through any bone too short for a body, which is where they really meet. */
|
|
306
|
+
const pj = hangsFrom(j);
|
|
307
|
+
const pk = hangsFrom(k);
|
|
207
308
|
/* Jointed, either way round, or two bones hanging off the same parent joint: siblings share
|
|
208
309
|
a head the way a parent and child share a point, so they overlap there too. */
|
|
209
310
|
if (pk === j || pj === k || (pj === pk && pj >= 0))
|
|
210
311
|
world.ignorePair(body, other);
|
|
211
312
|
}
|
|
212
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* One joint's world rotation, from the body that speaks for it: the first bone starting at the
|
|
316
|
+
* joint, or — for a joint with none below it, a fingertip — the bone it hangs off, which it then
|
|
317
|
+
* rides rigidly. False where neither exists and nothing can be said.
|
|
318
|
+
*/
|
|
319
|
+
const recoverJoint = (w, j, out, at) => {
|
|
320
|
+
const start = startBone[j] ?? -1;
|
|
321
|
+
if (start >= 0) {
|
|
322
|
+
readBodyRotation(w, bodyOf[start] ?? 0, TMP);
|
|
323
|
+
multiplyInto(TMP, bodyToStart, start * 4, out, at);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
const own = bodyOf[j] ?? -1;
|
|
327
|
+
if (own < 0)
|
|
328
|
+
return false;
|
|
329
|
+
readBodyRotation(w, own, TMP);
|
|
330
|
+
multiplyInto(TMP, bodyToJoint, j * 4, out, at);
|
|
331
|
+
return true;
|
|
332
|
+
};
|
|
213
333
|
const root = firstBone;
|
|
214
334
|
return {
|
|
215
335
|
bodyOf,
|
|
216
336
|
boneCount,
|
|
217
337
|
writePose(out) {
|
|
338
|
+
/*
|
|
339
|
+
* Two passes, because a joint's local rotation needs its parent's world rotation and a rig
|
|
340
|
+
* is not obliged to list parents first. The first recovers every joint a body speaks for.
|
|
341
|
+
*/
|
|
342
|
+
for (let j = 0; j < jointCount; j++)
|
|
343
|
+
KNOWN[j] = recoverJoint(world, j, WORLD_ROT, j * 4) ? 1 : 0;
|
|
218
344
|
for (let j = 0; j < jointCount; j++) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (body < 0) {
|
|
222
|
-
// No bone: leave the joint as the pose already had it.
|
|
345
|
+
if (KNOWN[j] !== 1) {
|
|
346
|
+
// No bone at either end: leave the joint as the pose already had it.
|
|
223
347
|
continue;
|
|
224
348
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
multiplyInto(TMP, bodyToJoint, j * 4, WORLD_ROT, j * 4);
|
|
228
|
-
const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
|
|
229
|
-
if (parentBody >= 0) {
|
|
349
|
+
const p = parents[j] ?? -1;
|
|
350
|
+
if (p >= 0 && KNOWN[p] === 1) {
|
|
230
351
|
conjugateMultiply(WORLD_ROT, p * 4, WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
231
352
|
}
|
|
232
353
|
else {
|
|
354
|
+
/* A root, or a joint whose parent no bone speaks for: its rotation in the frame the
|
|
355
|
+
caller places the character's node in, which `rootX` and its two siblings answer. */
|
|
233
356
|
copy4(WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
234
357
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
358
|
+
if (p < 0)
|
|
359
|
+
continue;
|
|
360
|
+
/*
|
|
361
|
+
* **Placed along its own bone as the body holds it**, at the rig's length: the body's +y,
|
|
362
|
+
* in the parent's frame. For the first bone under a joint that is the rig's offset exactly,
|
|
363
|
+
* because the joint turns with that body; for a second or third — a thigh beside the spine,
|
|
364
|
+
* a clavicle beside the neck — it is what keeps the branch on its own body. Placed by the
|
|
365
|
+
* rig's offset, every branch but the first was swung by the first one's turn: reported as
|
|
366
|
+
* a foot 0.24 m and an arm 0.17 m from their bodies after a fall. A joint with no bone of
|
|
367
|
+
* its own keeps the rig's offset. **What would make it wrong** is a bone the body
|
|
368
|
+
* stretches, which a rigid capsule cannot.
|
|
369
|
+
*/
|
|
370
|
+
const body = bodyOf[j] ?? -1;
|
|
371
|
+
if (body >= 0 && KNOWN[p] === 1) {
|
|
372
|
+
boneInParentFrame(world, body, 2 * (halfOf[j] ?? 0) * (restScale[j] ?? 1), WORLD_ROT, p * 4, out.translation, j * 3);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
out.translation[j * 3] = localRest[j * 3] ?? 0;
|
|
376
|
+
out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
|
|
377
|
+
out.translation[j * 3 + 2] = localRest[j * 3 + 2] ?? 0;
|
|
378
|
+
}
|
|
239
379
|
out.scale[j * 3] = 1;
|
|
240
380
|
out.scale[j * 3 + 1] = 1;
|
|
241
381
|
out.scale[j * 3 + 2] = 1;
|
|
@@ -245,27 +385,32 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
245
385
|
if (weight <= 0)
|
|
246
386
|
return;
|
|
247
387
|
const k = weight > 1 ? 1 : weight;
|
|
248
|
-
for (let
|
|
249
|
-
const body = bodyOf[
|
|
388
|
+
for (let c = 0; c < jointCount; c++) {
|
|
389
|
+
const body = bodyOf[c] ?? -1;
|
|
250
390
|
if (body < 0)
|
|
251
391
|
continue;
|
|
252
|
-
|
|
253
|
-
|
|
392
|
+
/*
|
|
393
|
+
* **A bone is steered by the joint it starts at**, the one whose rotation points it — the
|
|
394
|
+
* forearm by the elbow — which is the same joint `writePose` reads it back as. Steered by
|
|
395
|
+
* the joint it ends at, a pose's elbow bend went into the upper arm.
|
|
396
|
+
*/
|
|
397
|
+
const j = parents[c] ?? -1;
|
|
398
|
+
if (j < 0)
|
|
399
|
+
continue;
|
|
254
400
|
/*
|
|
255
401
|
* The target is the pose's local rotation composed onto the parent's *current* world
|
|
256
402
|
* rotation, so a driven limb follows the animation relative to a body that may itself have
|
|
257
403
|
* been knocked aside — which is what a partial ragdoll is.
|
|
258
404
|
*/
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
multiplyInto(TMP, bodyToJoint, p * 4, PARENT_ROT, 0);
|
|
405
|
+
const p = parents[j] ?? -1;
|
|
406
|
+
if (p >= 0 && recoverJoint(w, p, PARENT_ROT, 0)) {
|
|
262
407
|
multiplyRaw(PARENT_ROT, 0, pose.rotation, j * 4, TARGET_ROT, 0);
|
|
263
408
|
}
|
|
264
409
|
else {
|
|
265
410
|
copy4(pose.rotation, j * 4, TARGET_ROT, 0);
|
|
266
411
|
}
|
|
267
412
|
readBodyRotation(w, body, TMP);
|
|
268
|
-
multiplyInto(TMP,
|
|
413
|
+
multiplyInto(TMP, bodyToStart, c * 4, CURRENT_ROT, 0);
|
|
269
414
|
// Twice the vector part of target · conj(current) is the rotation that closes the gap.
|
|
270
415
|
conjugateMultiply(CURRENT_ROT, 0, TARGET_ROT, 0, ERROR_ROT, 0);
|
|
271
416
|
let ex = ERROR_ROT[0] ?? 0;
|
|
@@ -301,22 +446,23 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
301
446
|
w.bodies.posY[body] = (hy + ty) / 2;
|
|
302
447
|
w.bodies.posZ[body] = (hz + tz) / 2;
|
|
303
448
|
/*
|
|
304
|
-
* The rotation is
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* the bone
|
|
449
|
+
* **The rotation is the start joint's, carried through the relation captured at rest**,
|
|
450
|
+
* which is the inverse of what `writePose` reads: `jointRotation · conj(bodyToStart)`.
|
|
451
|
+
*
|
|
452
|
+
* It was the shortest arc from +y onto the bone, which points the capsule correctly and
|
|
453
|
+
* throws away the bone's twist about its own length — so a joint with two bones below it, a
|
|
454
|
+
* wrist with a thumb beside the fingers, came back turned about the first and put the
|
|
455
|
+
* second somewhere else. The joint's world matrix alone would be wrong too, wherever a
|
|
456
|
+
* rig's bind orientation is not the bone direction, which is most rigs; the captured
|
|
457
|
+
* relation is what absorbs that. **What would make it wrong** is a pose that stretches a
|
|
458
|
+
* bone, which a ragdoll's rigid bones cannot follow either way.
|
|
308
459
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
316
|
-
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
317
|
-
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
318
|
-
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
319
|
-
}
|
|
460
|
+
matrixRotation(worldMatrices, p, START_ROT, 0);
|
|
461
|
+
multiplyByConjugate(START_ROT, bodyToStart, j * 4, TMP);
|
|
462
|
+
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
463
|
+
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
464
|
+
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
465
|
+
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
320
466
|
/* At rest, which is the half that makes it a reaction rather than a continuation: a body
|
|
321
467
|
carrying the speed of a two-second fall is thrown across the character the instant it
|
|
322
468
|
is written into the pose. */
|
|
@@ -341,6 +487,11 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
341
487
|
};
|
|
342
488
|
}
|
|
343
489
|
const WORLD_ROT = new Float32Array(256 * 4);
|
|
490
|
+
/** Whether `writePose` recovered each joint's rotation this call: 1 if a body speaks for it. */
|
|
491
|
+
const KNOWN = new Uint8Array(256);
|
|
492
|
+
const START_ROT = new Float32Array(4);
|
|
493
|
+
/** One joint anchor in a body's own frame, filled by `restAnchor` and read at once. */
|
|
494
|
+
const REST_ANCHOR = new Float64Array(3);
|
|
344
495
|
const PARENT_ROT = new Float32Array(4);
|
|
345
496
|
const TARGET_ROT = new Float32Array(4);
|
|
346
497
|
const CURRENT_ROT = new Float32Array(4);
|
|
@@ -454,6 +605,76 @@ function alignYTo(dx, dy, dz, out) {
|
|
|
454
605
|
out[2] = az / len;
|
|
455
606
|
out[3] = w / len;
|
|
456
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* A world-space offset from joint `p`, expressed in `p`'s own frame: the inverse of the matrix's
|
|
610
|
+
* upper 3x3 applied to it, so a scaled joint is undone as well as a rotated one.
|
|
611
|
+
*/
|
|
612
|
+
function intoParentFrame(m, p, dx, dy, dz, out, at) {
|
|
613
|
+
const o = p * 16;
|
|
614
|
+
const a = m[o] ?? 1;
|
|
615
|
+
const b = m[o + 4] ?? 0;
|
|
616
|
+
const c = m[o + 8] ?? 0;
|
|
617
|
+
const d = m[o + 1] ?? 0;
|
|
618
|
+
const e = m[o + 5] ?? 1;
|
|
619
|
+
const f = m[o + 9] ?? 0;
|
|
620
|
+
const g = m[o + 2] ?? 0;
|
|
621
|
+
const h = m[o + 6] ?? 0;
|
|
622
|
+
const i = m[o + 10] ?? 1;
|
|
623
|
+
const ei = e * i - f * h;
|
|
624
|
+
const fg = f * g - d * i;
|
|
625
|
+
const dh = d * h - e * g;
|
|
626
|
+
const det = a * ei + b * fg + c * dh;
|
|
627
|
+
if (Math.abs(det) < 1e-12) {
|
|
628
|
+
out[at] = dx;
|
|
629
|
+
out[at + 1] = dy;
|
|
630
|
+
out[at + 2] = dz;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const inv = 1 / det;
|
|
634
|
+
out[at] = (ei * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
|
|
635
|
+
out[at + 1] = (fg * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
|
|
636
|
+
out[at + 2] = (dh * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
|
|
637
|
+
}
|
|
638
|
+
/** `a · conj(b)` into a Float64 quaternion, for turning a joint's rotation back into a body's. */
|
|
639
|
+
function multiplyByConjugate(a, b, bAt, out) {
|
|
640
|
+
const ax = a[0] ?? 0;
|
|
641
|
+
const ay = a[1] ?? 0;
|
|
642
|
+
const az = a[2] ?? 0;
|
|
643
|
+
const aw = a[3] ?? 1;
|
|
644
|
+
const bx = -(b[bAt] ?? 0);
|
|
645
|
+
const by = -(b[bAt + 1] ?? 0);
|
|
646
|
+
const bz = -(b[bAt + 2] ?? 0);
|
|
647
|
+
const bw = b[bAt + 3] ?? 1;
|
|
648
|
+
out[0] = aw * bx + ax * bw + ay * bz - az * by;
|
|
649
|
+
out[1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
650
|
+
out[2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
651
|
+
out[3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* A body's own +y at `length`, turned by the body and then back by the inverse of a parent's world
|
|
655
|
+
* rotation: the bone as the body holds it, in the parent's frame.
|
|
656
|
+
*/
|
|
657
|
+
function boneInParentFrame(world, body, length, parent, parentAt, out, outAt) {
|
|
658
|
+
const bx = world.bodies.rotX[body] ?? 0;
|
|
659
|
+
const by = world.bodies.rotY[body] ?? 0;
|
|
660
|
+
const bz = world.bodies.rotZ[body] ?? 0;
|
|
661
|
+
const bw = world.bodies.rotW[body] ?? 1;
|
|
662
|
+
/* The body's +y in the world, scaled. */
|
|
663
|
+
const vx = 2 * length * (bx * by - bw * bz);
|
|
664
|
+
const vy = length * (1 - 2 * (bx * bx + bz * bz));
|
|
665
|
+
const vz = 2 * length * (by * bz + bw * bx);
|
|
666
|
+
/* Turned by the conjugate of the parent's rotation: v + 2w(u×v) + 2u×(u×v), u = −q.xyz. */
|
|
667
|
+
const ux = -(parent[parentAt] ?? 0);
|
|
668
|
+
const uy = -(parent[parentAt + 1] ?? 0);
|
|
669
|
+
const uz = -(parent[parentAt + 2] ?? 0);
|
|
670
|
+
const w = parent[parentAt + 3] ?? 1;
|
|
671
|
+
const cx = uy * vz - uz * vy;
|
|
672
|
+
const cy = uz * vx - ux * vz;
|
|
673
|
+
const cz = ux * vy - uy * vx;
|
|
674
|
+
out[outAt] = vx + 2 * (w * cx + (uy * cz - uz * cy));
|
|
675
|
+
out[outAt + 1] = vy + 2 * (w * cy + (uz * cx - ux * cz));
|
|
676
|
+
out[outAt + 2] = vz + 2 * (w * cz + (ux * cy - uy * cx));
|
|
677
|
+
}
|
|
457
678
|
/** `conj(a) · b` into `out`. */
|
|
458
679
|
function multiplyConjugate(a, b, bAt, out, at) {
|
|
459
680
|
const ax = -a[0];
|
package/package.json
CHANGED
package/src/controller.ts
CHANGED
|
@@ -453,9 +453,10 @@ export class CharacterController {
|
|
|
453
453
|
* the same clamp and air control — only the plane is given up, not the feel.
|
|
454
454
|
*/
|
|
455
455
|
if (input.projectMove === false) {
|
|
456
|
-
|
|
457
|
-
this.
|
|
458
|
-
this.
|
|
456
|
+
const t = approachFraction(wantX - this.velX, wantY - this.velY, wantZ - this.velZ, step);
|
|
457
|
+
this.velX = t === 1 ? wantX : this.velX + (wantX - this.velX) * t;
|
|
458
|
+
this.velY = t === 1 ? wantY : this.velY + (wantY - this.velY) * t;
|
|
459
|
+
this.velZ = t === 1 ? wantZ : this.velZ + (wantZ - this.velZ) * t;
|
|
459
460
|
} else {
|
|
460
461
|
const alongUp = this.speedAlongUp;
|
|
461
462
|
let tangentX = this.velX - this.upX * alongUp;
|
|
@@ -465,9 +466,15 @@ export class CharacterController {
|
|
|
465
466
|
const wantTangentX = wantX - this.upX * wantIntoUp;
|
|
466
467
|
const wantTangentY = wantY - this.upY * wantIntoUp;
|
|
467
468
|
const wantTangentZ = wantZ - this.upZ * wantIntoUp;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
469
|
+
const t = approachFraction(
|
|
470
|
+
wantTangentX - tangentX,
|
|
471
|
+
wantTangentY - tangentY,
|
|
472
|
+
wantTangentZ - tangentZ,
|
|
473
|
+
step,
|
|
474
|
+
);
|
|
475
|
+
tangentX = t === 1 ? wantTangentX : tangentX + (wantTangentX - tangentX) * t;
|
|
476
|
+
tangentY = t === 1 ? wantTangentY : tangentY + (wantTangentY - tangentY) * t;
|
|
477
|
+
tangentZ = t === 1 ? wantTangentZ : tangentZ + (wantTangentZ - tangentZ) * t;
|
|
471
478
|
this.velX = tangentX + this.upX * alongUp;
|
|
472
479
|
this.velY = tangentY + this.upY * alongUp;
|
|
473
480
|
this.velZ = tangentZ + this.upZ * alongUp;
|
|
@@ -858,10 +865,18 @@ export class CharacterController {
|
|
|
858
865
|
}
|
|
859
866
|
}
|
|
860
867
|
|
|
861
|
-
/**
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
868
|
+
/**
|
|
869
|
+
* How much of the way to close a gap of `(dx, dy, dz)` this tick: exactly 1 when the gap is no
|
|
870
|
+
* longer than `step`, which the caller reads as "land on the target", and `step` of its length
|
|
871
|
+
* otherwise.
|
|
872
|
+
*
|
|
873
|
+
* **Toward the wanted velocity as a vector, by at most `step` in length.** It was one world axis at
|
|
874
|
+
* a time, each by the whole step, so a diagonal gained and lost speed √2 times as fast as a straight
|
|
875
|
+
* run while the clamp on the wanted vector kept its top speed honest — the faster diagonal lived in
|
|
876
|
+
* the acceleration. What it gives up is that an axis no longer reaches its target on its own
|
|
877
|
+
* schedule: turning from one direction to another now curves rather than squaring the corner.
|
|
878
|
+
*/
|
|
879
|
+
function approachFraction(dx: number, dy: number, dz: number, step: number): number {
|
|
880
|
+
const gap = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
881
|
+
return gap <= step ? 1 : step / gap;
|
|
867
882
|
}
|
package/src/ragdoll.ts
CHANGED
|
@@ -95,7 +95,15 @@ export interface Ragdoll {
|
|
|
95
95
|
readonly bodyOf: Int32Array;
|
|
96
96
|
/** How many bones were built. */
|
|
97
97
|
readonly boneCount: number;
|
|
98
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Write the ragdoll's current shape into a pose.
|
|
100
|
+
*
|
|
101
|
+
* Each joint is turned by the bone that **starts** at it, which is the joint a skinned limb
|
|
102
|
+
* follows — the forearm's body turns the elbow — and a joint with no bone below it rides the one
|
|
103
|
+
* it hangs off. Translations are the rig's own, in each parent's frame. A root's rotation is
|
|
104
|
+
* written in the frame the character's node is placed in, at `rootX`, `rootY` and `rootZ`, with
|
|
105
|
+
* no rotation of its own; a joint no bone reaches is left as the pose had it.
|
|
106
|
+
*/
|
|
99
107
|
writePose(out: PoseTarget): void;
|
|
100
108
|
/** Steer the bodies toward a pose. Weight 1 tracks it, weight 0 goes limp. */
|
|
101
109
|
drive(world: PhysicsWorld, pose: PoseTarget, weight: number): void;
|
|
@@ -185,8 +193,32 @@ export function ragdollFromBones(
|
|
|
185
193
|
};
|
|
186
194
|
|
|
187
195
|
const bodyOf = new Int32Array(jointCount).fill(-1);
|
|
188
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **ends** at, so a joint with no
|
|
198
|
+
* bone of its own below it — a fingertip, a head's tip — can ride the bone it hangs off.
|
|
199
|
+
*/
|
|
189
200
|
const bodyToJoint = new Float32Array(jointCount * 4);
|
|
201
|
+
/**
|
|
202
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **starts** at, indexed like
|
|
203
|
+
* `bodyOf` — by the bone's own joint — and naming its parent's rotation.
|
|
204
|
+
*
|
|
205
|
+
* **This is the joint a skinned limb follows.** A forearm's vertices are weighted to the elbow,
|
|
206
|
+
* so the elbow's rotation is what points the forearm, and the body standing along the forearm is
|
|
207
|
+
* what carries it. Reading each joint off the bone that *ends* at it — the upper arm for the
|
|
208
|
+
* elbow — drew every limb one bone late: a hand 0.47 m from its body in a doll synced and never
|
|
209
|
+
* stepped, reported from a game. A bone is rigid in its start joint's frame, so the relation
|
|
210
|
+
* captured at rest holds for as long as the doll does.
|
|
211
|
+
*/
|
|
212
|
+
const bodyToStart = new Float32Array(jointCount * 4);
|
|
213
|
+
/** The first bone starting at each joint, by that bone's own index, or −1 where none does. */
|
|
214
|
+
const startBone = new Int32Array(jointCount).fill(-1);
|
|
215
|
+
/**
|
|
216
|
+
* Each joint's translation in its parent's frame at rest, which is what a pose holds.
|
|
217
|
+
*
|
|
218
|
+
* **In the parent's frame, not the world's.** It was the world offset between the two, which is
|
|
219
|
+
* the same number only where the parent is unrotated at rest — true of every chain this module's
|
|
220
|
+
* tests were built from, and of no rig a game loads.
|
|
221
|
+
*/
|
|
190
222
|
const localRest = new Float32Array(jointCount * 3);
|
|
191
223
|
const restRotation = new Float32Array(jointCount * 4);
|
|
192
224
|
/**
|
|
@@ -198,6 +230,11 @@ export function ragdollFromBones(
|
|
|
198
230
|
* the bones meet.
|
|
199
231
|
*/
|
|
200
232
|
const halfOf = new Float32Array(jointCount);
|
|
233
|
+
/**
|
|
234
|
+
* Each bone's rig offset over its length: 1 for a rig at unit scale, and what turns a length in
|
|
235
|
+
* metres back into the parent's own units where the rig is scaled.
|
|
236
|
+
*/
|
|
237
|
+
const restScale = new Float32Array(jointCount).fill(1);
|
|
201
238
|
let boneCount = 0;
|
|
202
239
|
let firstBone = -1;
|
|
203
240
|
/** Half the first bone's length, which is how far its head sits from the body's centre. */
|
|
@@ -214,9 +251,7 @@ export function ragdollFromBones(
|
|
|
214
251
|
const tx = worldMatrices[j * 16 + 12] ?? 0;
|
|
215
252
|
const ty = worldMatrices[j * 16 + 13] ?? 0;
|
|
216
253
|
const tz = worldMatrices[j * 16 + 14] ?? 0;
|
|
217
|
-
|
|
218
|
-
localRest[j * 3 + 1] = ty - hy;
|
|
219
|
-
localRest[j * 3 + 2] = tz - hz;
|
|
254
|
+
intoParentFrame(worldMatrices, p, tx - hx, ty - hy, tz - hz, localRest, j * 3);
|
|
220
255
|
|
|
221
256
|
const dx = tx - hx;
|
|
222
257
|
const dy = ty - hy;
|
|
@@ -243,8 +278,16 @@ export function ragdollFromBones(
|
|
|
243
278
|
});
|
|
244
279
|
bodyOf[j] = body;
|
|
245
280
|
halfOf[j] = length / 2;
|
|
246
|
-
|
|
281
|
+
const rx = localRest[j * 3] ?? 0;
|
|
282
|
+
const ry = localRest[j * 3 + 1] ?? 0;
|
|
283
|
+
const rz = localRest[j * 3 + 2] ?? 0;
|
|
284
|
+
restScale[j] = Math.sqrt(rx * rx + ry * ry + rz * rz) / length;
|
|
285
|
+
// What takes the body's rotation back to each of its two joints', captured at rest. Parents
|
|
286
|
+
// come before children in a rig, so the start joint's rotation is already read.
|
|
247
287
|
multiplyConjugate(TMP, restRotation, j * 4, bodyToJoint, j * 4);
|
|
288
|
+
if (p >= j) matrixRotation(worldMatrices, p, restRotation, p * 4);
|
|
289
|
+
multiplyConjugate(TMP, restRotation, p * 4, bodyToStart, j * 4);
|
|
290
|
+
if ((startBone[p] ?? -1) < 0) startBone[p] = j;
|
|
248
291
|
boneCount++;
|
|
249
292
|
if (firstBone < 0) {
|
|
250
293
|
firstBone = j;
|
|
@@ -254,13 +297,65 @@ export function ragdollFromBones(
|
|
|
254
297
|
}
|
|
255
298
|
}
|
|
256
299
|
|
|
300
|
+
/*
|
|
301
|
+
* **A bone too short for a body is transparent.** Each joint resolves to the nearest one at or
|
|
302
|
+
* above it that a body ends at, or to its root: what hangs from a collar sitting on the chest
|
|
303
|
+
* joint hangs from the bone that ends at the chest. A rig puts one joint exactly on another as
|
|
304
|
+
* often as not, and a bone under `minLength` gets no body, so until 4.8.3 the spine above such a
|
|
305
|
+
* collar was jointed to nothing and collided with every bone it met there — compared by index,
|
|
306
|
+
* they shared no end. **What it gives up** is the skipped bone's own length, under `minLength`,
|
|
307
|
+
* which the joints below absorb exactly at rest: see `restAnchor`.
|
|
308
|
+
*/
|
|
309
|
+
const attach = new Int32Array(jointCount);
|
|
310
|
+
for (let j = 0; j < jointCount; j++) {
|
|
311
|
+
let a = j;
|
|
312
|
+
while ((bodyOf[a] ?? -1) < 0 && (parents[a] ?? -1) >= 0) a = parents[a] ?? -1;
|
|
313
|
+
attach[j] = a;
|
|
314
|
+
}
|
|
315
|
+
/** Where a bone's parent end really hangs: its parent joint, seen through any bone too short. */
|
|
316
|
+
const hangsFrom = (j: number): number => {
|
|
317
|
+
const p = parents[j] ?? -1;
|
|
318
|
+
return p < 0 ? -1 : (attach[p] ?? p);
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* Bone `j`'s body-frame point at joint `at`'s rest position, into `REST_ANCHOR`. Where `at` is
|
|
322
|
+
* the bone's own parent joint this is its head, `(0, -half, 0)`, exactly as before; through a
|
|
323
|
+
* short bone it is where the two bones meet at rest, so a joint anchored there moves nothing on
|
|
324
|
+
* the first tick.
|
|
325
|
+
*/
|
|
326
|
+
const restAnchor = (j: number, at: number): void => {
|
|
327
|
+
const p = parents[j] ?? -1;
|
|
328
|
+
if (at === p) {
|
|
329
|
+
REST_ANCHOR[0] = 0;
|
|
330
|
+
REST_ANCHOR[1] = -(halfOf[j] ?? 0);
|
|
331
|
+
REST_ANCHOR[2] = 0;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const body = bodyOf[j] ?? 0;
|
|
335
|
+
const dx = (worldMatrices[at * 16 + 12] ?? 0) - (world.bodies.posX[body] ?? 0);
|
|
336
|
+
const dy = (worldMatrices[at * 16 + 13] ?? 0) - (world.bodies.posY[body] ?? 0);
|
|
337
|
+
const dz = (worldMatrices[at * 16 + 14] ?? 0) - (world.bodies.posZ[body] ?? 0);
|
|
338
|
+
/* Into the body's frame: turned by the conjugate of its rotation. */
|
|
339
|
+
const ux = -(world.bodies.rotX[body] ?? 0);
|
|
340
|
+
const uy = -(world.bodies.rotY[body] ?? 0);
|
|
341
|
+
const uz = -(world.bodies.rotZ[body] ?? 0);
|
|
342
|
+
const w = world.bodies.rotW[body] ?? 1;
|
|
343
|
+
const cx = uy * dz - uz * dy;
|
|
344
|
+
const cy = uz * dx - ux * dz;
|
|
345
|
+
const cz = ux * dy - uy * dx;
|
|
346
|
+
REST_ANCHOR[0] = dx + 2 * (w * cx + (uy * cz - uz * cy));
|
|
347
|
+
REST_ANCHOR[1] = dy + 2 * (w * cy + (uz * cx - ux * cz));
|
|
348
|
+
REST_ANCHOR[2] = dz + 2 * (w * cz + (ux * cy - uy * cx));
|
|
349
|
+
};
|
|
350
|
+
|
|
257
351
|
// Joint each bone to its parent's bone, at the shared end.
|
|
258
352
|
for (let j = 0; j < jointCount; j++) {
|
|
259
353
|
const body = bodyOf[j] ?? -1;
|
|
260
354
|
if (body < 0) continue;
|
|
261
|
-
const p =
|
|
355
|
+
const p = hangsFrom(j);
|
|
262
356
|
const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
|
|
263
357
|
if (parentBody < 0) continue;
|
|
358
|
+
restAnchor(j, p);
|
|
264
359
|
world.addJoint({
|
|
265
360
|
type: JOINT_CONE_TWIST,
|
|
266
361
|
bodyA: parentBody,
|
|
@@ -287,7 +382,9 @@ export function ragdollFromBones(
|
|
|
287
382
|
* so the first tick still moves nothing.
|
|
288
383
|
*/
|
|
289
384
|
anchorAY: halfOf[p] ?? 0,
|
|
290
|
-
|
|
385
|
+
anchorBX: REST_ANCHOR[0],
|
|
386
|
+
anchorBY: REST_ANCHOR[1],
|
|
387
|
+
anchorBZ: REST_ANCHOR[2],
|
|
291
388
|
});
|
|
292
389
|
}
|
|
293
390
|
|
|
@@ -304,13 +401,18 @@ export function ragdollFromBones(
|
|
|
304
401
|
for (let j = 0; j < jointCount; j++) {
|
|
305
402
|
const body = bodyOf[j] ?? -1;
|
|
306
403
|
if (body < 0) continue;
|
|
307
|
-
const p =
|
|
404
|
+
const p = hangsFrom(j);
|
|
308
405
|
if (p < 0 || (bodyOf[p] ?? -1) >= 0) continue;
|
|
309
406
|
const first = firstUnder[p] ?? -1;
|
|
310
407
|
if (first < 0) {
|
|
311
408
|
firstUnder[p] = j;
|
|
312
409
|
continue;
|
|
313
410
|
}
|
|
411
|
+
restAnchor(first, p);
|
|
412
|
+
const ax = REST_ANCHOR[0];
|
|
413
|
+
const ay = REST_ANCHOR[1];
|
|
414
|
+
const az = REST_ANCHOR[2];
|
|
415
|
+
restAnchor(j, p);
|
|
314
416
|
world.addJoint({
|
|
315
417
|
type: JOINT_CONE_TWIST,
|
|
316
418
|
bodyA: bodyOf[first] ?? 0,
|
|
@@ -318,8 +420,12 @@ export function ragdollFromBones(
|
|
|
318
420
|
swingCos: limitAt(swingCos, Math.SQRT1_2, j),
|
|
319
421
|
twistSin: limitAt(twistSin, 0.3826834323650898, j),
|
|
320
422
|
axisY: 1,
|
|
321
|
-
|
|
322
|
-
|
|
423
|
+
anchorAX: ax,
|
|
424
|
+
anchorAY: ay,
|
|
425
|
+
anchorAZ: az,
|
|
426
|
+
anchorBX: REST_ANCHOR[0],
|
|
427
|
+
anchorBY: REST_ANCHOR[1],
|
|
428
|
+
anchorBZ: REST_ANCHOR[2],
|
|
323
429
|
});
|
|
324
430
|
}
|
|
325
431
|
|
|
@@ -341,39 +447,85 @@ export function ragdollFromBones(
|
|
|
341
447
|
world.ignorePair(body, other);
|
|
342
448
|
continue;
|
|
343
449
|
}
|
|
344
|
-
|
|
345
|
-
const
|
|
450
|
+
/* Seen through any bone too short for a body, which is where they really meet. */
|
|
451
|
+
const pj = hangsFrom(j);
|
|
452
|
+
const pk = hangsFrom(k);
|
|
346
453
|
/* Jointed, either way round, or two bones hanging off the same parent joint: siblings share
|
|
347
454
|
a head the way a parent and child share a point, so they overlap there too. */
|
|
348
455
|
if (pk === j || pj === k || (pj === pk && pj >= 0)) world.ignorePair(body, other);
|
|
349
456
|
}
|
|
350
457
|
}
|
|
351
458
|
|
|
459
|
+
/**
|
|
460
|
+
* One joint's world rotation, from the body that speaks for it: the first bone starting at the
|
|
461
|
+
* joint, or — for a joint with none below it, a fingertip — the bone it hangs off, which it then
|
|
462
|
+
* rides rigidly. False where neither exists and nothing can be said.
|
|
463
|
+
*/
|
|
464
|
+
const recoverJoint = (w: PhysicsWorld, j: number, out: Float32Array, at: number): boolean => {
|
|
465
|
+
const start = startBone[j] ?? -1;
|
|
466
|
+
if (start >= 0) {
|
|
467
|
+
readBodyRotation(w, bodyOf[start] ?? 0, TMP);
|
|
468
|
+
multiplyInto(TMP, bodyToStart, start * 4, out, at);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
const own = bodyOf[j] ?? -1;
|
|
472
|
+
if (own < 0) return false;
|
|
473
|
+
readBodyRotation(w, own, TMP);
|
|
474
|
+
multiplyInto(TMP, bodyToJoint, j * 4, out, at);
|
|
475
|
+
return true;
|
|
476
|
+
};
|
|
477
|
+
|
|
352
478
|
const root = firstBone;
|
|
353
479
|
return {
|
|
354
480
|
bodyOf,
|
|
355
481
|
boneCount,
|
|
356
482
|
writePose(out: PoseTarget): void {
|
|
483
|
+
/*
|
|
484
|
+
* Two passes, because a joint's local rotation needs its parent's world rotation and a rig
|
|
485
|
+
* is not obliged to list parents first. The first recovers every joint a body speaks for.
|
|
486
|
+
*/
|
|
487
|
+
for (let j = 0; j < jointCount; j++)
|
|
488
|
+
KNOWN[j] = recoverJoint(world, j, WORLD_ROT, j * 4) ? 1 : 0;
|
|
357
489
|
for (let j = 0; j < jointCount; j++) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (body < 0) {
|
|
361
|
-
// No bone: leave the joint as the pose already had it.
|
|
490
|
+
if (KNOWN[j] !== 1) {
|
|
491
|
+
// No bone at either end: leave the joint as the pose already had it.
|
|
362
492
|
continue;
|
|
363
493
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
multiplyInto(TMP, bodyToJoint, j * 4, WORLD_ROT, j * 4);
|
|
367
|
-
const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
|
|
368
|
-
if (parentBody >= 0) {
|
|
494
|
+
const p = parents[j] ?? -1;
|
|
495
|
+
if (p >= 0 && KNOWN[p] === 1) {
|
|
369
496
|
conjugateMultiply(WORLD_ROT, p * 4, WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
370
497
|
} else {
|
|
498
|
+
/* A root, or a joint whose parent no bone speaks for: its rotation in the frame the
|
|
499
|
+
caller places the character's node in, which `rootX` and its two siblings answer. */
|
|
371
500
|
copy4(WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
372
501
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
502
|
+
if (p < 0) continue;
|
|
503
|
+
/*
|
|
504
|
+
* **Placed along its own bone as the body holds it**, at the rig's length: the body's +y,
|
|
505
|
+
* in the parent's frame. For the first bone under a joint that is the rig's offset exactly,
|
|
506
|
+
* because the joint turns with that body; for a second or third — a thigh beside the spine,
|
|
507
|
+
* a clavicle beside the neck — it is what keeps the branch on its own body. Placed by the
|
|
508
|
+
* rig's offset, every branch but the first was swung by the first one's turn: reported as
|
|
509
|
+
* a foot 0.24 m and an arm 0.17 m from their bodies after a fall. A joint with no bone of
|
|
510
|
+
* its own keeps the rig's offset. **What would make it wrong** is a bone the body
|
|
511
|
+
* stretches, which a rigid capsule cannot.
|
|
512
|
+
*/
|
|
513
|
+
const body = bodyOf[j] ?? -1;
|
|
514
|
+
if (body >= 0 && KNOWN[p] === 1) {
|
|
515
|
+
boneInParentFrame(
|
|
516
|
+
world,
|
|
517
|
+
body,
|
|
518
|
+
2 * (halfOf[j] ?? 0) * (restScale[j] ?? 1),
|
|
519
|
+
WORLD_ROT,
|
|
520
|
+
p * 4,
|
|
521
|
+
out.translation,
|
|
522
|
+
j * 3,
|
|
523
|
+
);
|
|
524
|
+
} else {
|
|
525
|
+
out.translation[j * 3] = localRest[j * 3] ?? 0;
|
|
526
|
+
out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
|
|
527
|
+
out.translation[j * 3 + 2] = localRest[j * 3 + 2] ?? 0;
|
|
528
|
+
}
|
|
377
529
|
out.scale[j * 3] = 1;
|
|
378
530
|
out.scale[j * 3 + 1] = 1;
|
|
379
531
|
out.scale[j * 3 + 2] = 1;
|
|
@@ -382,25 +534,29 @@ export function ragdollFromBones(
|
|
|
382
534
|
drive(w: PhysicsWorld, pose: PoseTarget, weight: number): void {
|
|
383
535
|
if (weight <= 0) return;
|
|
384
536
|
const k = weight > 1 ? 1 : weight;
|
|
385
|
-
for (let
|
|
386
|
-
const body = bodyOf[
|
|
537
|
+
for (let c = 0; c < jointCount; c++) {
|
|
538
|
+
const body = bodyOf[c] ?? -1;
|
|
387
539
|
if (body < 0) continue;
|
|
388
|
-
|
|
389
|
-
|
|
540
|
+
/*
|
|
541
|
+
* **A bone is steered by the joint it starts at**, the one whose rotation points it — the
|
|
542
|
+
* forearm by the elbow — which is the same joint `writePose` reads it back as. Steered by
|
|
543
|
+
* the joint it ends at, a pose's elbow bend went into the upper arm.
|
|
544
|
+
*/
|
|
545
|
+
const j = parents[c] ?? -1;
|
|
546
|
+
if (j < 0) continue;
|
|
390
547
|
/*
|
|
391
548
|
* The target is the pose's local rotation composed onto the parent's *current* world
|
|
392
549
|
* rotation, so a driven limb follows the animation relative to a body that may itself have
|
|
393
550
|
* been knocked aside — which is what a partial ragdoll is.
|
|
394
551
|
*/
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
multiplyInto(TMP, bodyToJoint, p * 4, PARENT_ROT, 0);
|
|
552
|
+
const p = parents[j] ?? -1;
|
|
553
|
+
if (p >= 0 && recoverJoint(w, p, PARENT_ROT, 0)) {
|
|
398
554
|
multiplyRaw(PARENT_ROT, 0, pose.rotation, j * 4, TARGET_ROT, 0);
|
|
399
555
|
} else {
|
|
400
556
|
copy4(pose.rotation, j * 4, TARGET_ROT, 0);
|
|
401
557
|
}
|
|
402
558
|
readBodyRotation(w, body, TMP);
|
|
403
|
-
multiplyInto(TMP,
|
|
559
|
+
multiplyInto(TMP, bodyToStart, c * 4, CURRENT_ROT, 0);
|
|
404
560
|
// Twice the vector part of target · conj(current) is the rotation that closes the gap.
|
|
405
561
|
conjugateMultiply(CURRENT_ROT, 0, TARGET_ROT, 0, ERROR_ROT, 0);
|
|
406
562
|
let ex = ERROR_ROT[0] ?? 0;
|
|
@@ -437,22 +593,23 @@ export function ragdollFromBones(
|
|
|
437
593
|
w.bodies.posZ[body] = (hz + tz) / 2;
|
|
438
594
|
|
|
439
595
|
/*
|
|
440
|
-
* The rotation is
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* the bone
|
|
596
|
+
* **The rotation is the start joint's, carried through the relation captured at rest**,
|
|
597
|
+
* which is the inverse of what `writePose` reads: `jointRotation · conj(bodyToStart)`.
|
|
598
|
+
*
|
|
599
|
+
* It was the shortest arc from +y onto the bone, which points the capsule correctly and
|
|
600
|
+
* throws away the bone's twist about its own length — so a joint with two bones below it, a
|
|
601
|
+
* wrist with a thumb beside the fingers, came back turned about the first and put the
|
|
602
|
+
* second somewhere else. The joint's world matrix alone would be wrong too, wherever a
|
|
603
|
+
* rig's bind orientation is not the bone direction, which is most rigs; the captured
|
|
604
|
+
* relation is what absorbs that. **What would make it wrong** is a pose that stretches a
|
|
605
|
+
* bone, which a ragdoll's rigid bones cannot follow either way.
|
|
444
606
|
*/
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
452
|
-
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
453
|
-
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
454
|
-
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
455
|
-
}
|
|
607
|
+
matrixRotation(worldMatrices, p, START_ROT, 0);
|
|
608
|
+
multiplyByConjugate(START_ROT, bodyToStart, j * 4, TMP);
|
|
609
|
+
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
610
|
+
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
611
|
+
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
612
|
+
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
456
613
|
|
|
457
614
|
/* At rest, which is the half that makes it a reaction rather than a continuation: a body
|
|
458
615
|
carrying the speed of a two-second fall is thrown across the character the instant it
|
|
@@ -479,6 +636,11 @@ export function ragdollFromBones(
|
|
|
479
636
|
}
|
|
480
637
|
|
|
481
638
|
const WORLD_ROT = new Float32Array(256 * 4);
|
|
639
|
+
/** Whether `writePose` recovered each joint's rotation this call: 1 if a body speaks for it. */
|
|
640
|
+
const KNOWN = new Uint8Array(256);
|
|
641
|
+
const START_ROT = new Float32Array(4);
|
|
642
|
+
/** One joint anchor in a body's own frame, filled by `restAnchor` and read at once. */
|
|
643
|
+
const REST_ANCHOR = new Float64Array(3);
|
|
482
644
|
const PARENT_ROT = new Float32Array(4);
|
|
483
645
|
const TARGET_ROT = new Float32Array(4);
|
|
484
646
|
const CURRENT_ROT = new Float32Array(4);
|
|
@@ -596,6 +758,100 @@ function alignYTo(dx: number, dy: number, dz: number, out: Float64Array): void {
|
|
|
596
758
|
out[3] = w / len;
|
|
597
759
|
}
|
|
598
760
|
|
|
761
|
+
/**
|
|
762
|
+
* A world-space offset from joint `p`, expressed in `p`'s own frame: the inverse of the matrix's
|
|
763
|
+
* upper 3x3 applied to it, so a scaled joint is undone as well as a rotated one.
|
|
764
|
+
*/
|
|
765
|
+
function intoParentFrame(
|
|
766
|
+
m: Float32Array,
|
|
767
|
+
p: number,
|
|
768
|
+
dx: number,
|
|
769
|
+
dy: number,
|
|
770
|
+
dz: number,
|
|
771
|
+
out: Float32Array,
|
|
772
|
+
at: number,
|
|
773
|
+
): void {
|
|
774
|
+
const o = p * 16;
|
|
775
|
+
const a = m[o] ?? 1;
|
|
776
|
+
const b = m[o + 4] ?? 0;
|
|
777
|
+
const c = m[o + 8] ?? 0;
|
|
778
|
+
const d = m[o + 1] ?? 0;
|
|
779
|
+
const e = m[o + 5] ?? 1;
|
|
780
|
+
const f = m[o + 9] ?? 0;
|
|
781
|
+
const g = m[o + 2] ?? 0;
|
|
782
|
+
const h = m[o + 6] ?? 0;
|
|
783
|
+
const i = m[o + 10] ?? 1;
|
|
784
|
+
const ei = e * i - f * h;
|
|
785
|
+
const fg = f * g - d * i;
|
|
786
|
+
const dh = d * h - e * g;
|
|
787
|
+
const det = a * ei + b * fg + c * dh;
|
|
788
|
+
if (Math.abs(det) < 1e-12) {
|
|
789
|
+
out[at] = dx;
|
|
790
|
+
out[at + 1] = dy;
|
|
791
|
+
out[at + 2] = dz;
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const inv = 1 / det;
|
|
795
|
+
out[at] = (ei * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
|
|
796
|
+
out[at + 1] = (fg * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
|
|
797
|
+
out[at + 2] = (dh * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** `a · conj(b)` into a Float64 quaternion, for turning a joint's rotation back into a body's. */
|
|
801
|
+
function multiplyByConjugate(
|
|
802
|
+
a: Float32Array,
|
|
803
|
+
b: Float32Array,
|
|
804
|
+
bAt: number,
|
|
805
|
+
out: Float64Array,
|
|
806
|
+
): void {
|
|
807
|
+
const ax = a[0] ?? 0;
|
|
808
|
+
const ay = a[1] ?? 0;
|
|
809
|
+
const az = a[2] ?? 0;
|
|
810
|
+
const aw = a[3] ?? 1;
|
|
811
|
+
const bx = -(b[bAt] ?? 0);
|
|
812
|
+
const by = -(b[bAt + 1] ?? 0);
|
|
813
|
+
const bz = -(b[bAt + 2] ?? 0);
|
|
814
|
+
const bw = b[bAt + 3] ?? 1;
|
|
815
|
+
out[0] = aw * bx + ax * bw + ay * bz - az * by;
|
|
816
|
+
out[1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
817
|
+
out[2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
818
|
+
out[3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* A body's own +y at `length`, turned by the body and then back by the inverse of a parent's world
|
|
823
|
+
* rotation: the bone as the body holds it, in the parent's frame.
|
|
824
|
+
*/
|
|
825
|
+
function boneInParentFrame(
|
|
826
|
+
world: PhysicsWorld,
|
|
827
|
+
body: number,
|
|
828
|
+
length: number,
|
|
829
|
+
parent: Float32Array,
|
|
830
|
+
parentAt: number,
|
|
831
|
+
out: Float32Array,
|
|
832
|
+
outAt: number,
|
|
833
|
+
): void {
|
|
834
|
+
const bx = world.bodies.rotX[body] ?? 0;
|
|
835
|
+
const by = world.bodies.rotY[body] ?? 0;
|
|
836
|
+
const bz = world.bodies.rotZ[body] ?? 0;
|
|
837
|
+
const bw = world.bodies.rotW[body] ?? 1;
|
|
838
|
+
/* The body's +y in the world, scaled. */
|
|
839
|
+
const vx = 2 * length * (bx * by - bw * bz);
|
|
840
|
+
const vy = length * (1 - 2 * (bx * bx + bz * bz));
|
|
841
|
+
const vz = 2 * length * (by * bz + bw * bx);
|
|
842
|
+
/* Turned by the conjugate of the parent's rotation: v + 2w(u×v) + 2u×(u×v), u = −q.xyz. */
|
|
843
|
+
const ux = -(parent[parentAt] ?? 0);
|
|
844
|
+
const uy = -(parent[parentAt + 1] ?? 0);
|
|
845
|
+
const uz = -(parent[parentAt + 2] ?? 0);
|
|
846
|
+
const w = parent[parentAt + 3] ?? 1;
|
|
847
|
+
const cx = uy * vz - uz * vy;
|
|
848
|
+
const cy = uz * vx - ux * vz;
|
|
849
|
+
const cz = ux * vy - uy * vx;
|
|
850
|
+
out[outAt] = vx + 2 * (w * cx + (uy * cz - uz * cy));
|
|
851
|
+
out[outAt + 1] = vy + 2 * (w * cy + (uz * cx - ux * cz));
|
|
852
|
+
out[outAt + 2] = vz + 2 * (w * cz + (ux * cy - uy * cx));
|
|
853
|
+
}
|
|
854
|
+
|
|
599
855
|
/** `conj(a) · b` into `out`. */
|
|
600
856
|
function multiplyConjugate(
|
|
601
857
|
a: Float64Array,
|