@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 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. **45.4 KB gzipped** standalone, measured
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
@@ -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
- this.velX = approach(this.velX, wantX, step);
317
- this.velY = approach(this.velY, wantY, step);
318
- this.velZ = approach(this.velZ, wantZ, step);
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
- tangentX = approach(tangentX, wantTangentX, step);
330
- tangentY = approach(tangentY, wantTangentY, step);
331
- tangentZ = approach(tangentZ, wantTangentZ, step);
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
- /** Move `from` toward `to` by at most `step`. */
679
- function approach(from, to, step) {
680
- const d = to - from;
681
- if (d > step)
682
- return from + step;
683
- if (d < -step)
684
- return from - step;
685
- return to;
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
- /** Write the ragdoll's current shape into a pose. */
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
- /** `conj(bodyRotation) · jointWorldRotation`, so a pose can be recovered from a body. */
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
- localRest[j * 3] = tx - hx;
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
- // What takes the body's rotation back to the joint's, captured while both are at rest.
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 = parents[j] ?? -1;
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
- anchorBY: -(halfOf[j] ?? 0),
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 = parents[j] ?? -1;
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
- anchorAY: -(halfOf[first] ?? 0),
182
- anchorBY: -(halfOf[j] ?? 0),
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
- const pj = parents[j] ?? -1;
206
- const pk = parents[k] ?? -1;
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
- const body = bodyOf[j] ?? -1;
220
- const p = parents[j] ?? -1;
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
- // The joint's world rotation, recovered from the body's.
226
- readBodyRotation(world, body, TMP);
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
- // Bones are rigid, so translation stays what the rig was built with.
236
- out.translation[j * 3] = localRest[j * 3] ?? 0;
237
- out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
238
- out.translation[j * 3 + 2] = localRest[j * 3 + 2] ?? 0;
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 j = 0; j < jointCount; j++) {
249
- const body = bodyOf[j] ?? -1;
388
+ for (let c = 0; c < jointCount; c++) {
389
+ const body = bodyOf[c] ?? -1;
250
390
  if (body < 0)
251
391
  continue;
252
- const p = parents[j] ?? -1;
253
- const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
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
- if (parentBody >= 0) {
260
- readBodyRotation(w, parentBody, TMP);
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, bodyToJoint, j * 4, CURRENT_ROT, 0);
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 taken from the *bone*, the same way the build took it: the capsule
305
- * stands along y, so the body is turned to put its y along the bone. Taking it from the
306
- * joint's world matrix instead would be wrong wherever a rig's bind orientation is not
307
- * the bone direction, which is most rigs.
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
- const dx = tx - hx;
310
- const dy = ty - hy;
311
- const dz = tz - hz;
312
- const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
313
- if (length > 0) {
314
- alignYTo(dx / length, dy / length, dz / length, TMP);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/physics",
3
- "version": "4.8.1",
3
+ "version": "4.8.3",
4
4
  "description": "Collision shapes, spatial queries and swept kinematic collision",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
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
- this.velX = approach(this.velX, wantX, step);
457
- this.velY = approach(this.velY, wantY, step);
458
- this.velZ = approach(this.velZ, wantZ, step);
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
- tangentX = approach(tangentX, wantTangentX, step);
469
- tangentY = approach(tangentY, wantTangentY, step);
470
- tangentZ = approach(tangentZ, wantTangentZ, step);
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
- /** Move `from` toward `to` by at most `step`. */
862
- function approach(from: number, to: number, step: number): number {
863
- const d = to - from;
864
- if (d > step) return from + step;
865
- if (d < -step) return from - step;
866
- return to;
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
- /** Write the ragdoll's current shape into a pose. */
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
- /** `conj(bodyRotation) · jointWorldRotation`, so a pose can be recovered from a body. */
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
- localRest[j * 3] = tx - hx;
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
- // What takes the body's rotation back to the joint's, captured while both are at rest.
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 = parents[j] ?? -1;
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
- anchorBY: -(halfOf[j] ?? 0),
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 = parents[j] ?? -1;
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
- anchorAY: -(halfOf[first] ?? 0),
322
- anchorBY: -(halfOf[j] ?? 0),
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
- const pj = parents[j] ?? -1;
345
- const pk = parents[k] ?? -1;
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
- const body = bodyOf[j] ?? -1;
359
- const p = parents[j] ?? -1;
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
- // The joint's world rotation, recovered from the body's.
365
- readBodyRotation(world, body, TMP);
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
- // Bones are rigid, so translation stays what the rig was built with.
374
- out.translation[j * 3] = localRest[j * 3] ?? 0;
375
- out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
376
- out.translation[j * 3 + 2] = localRest[j * 3 + 2] ?? 0;
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 j = 0; j < jointCount; j++) {
386
- const body = bodyOf[j] ?? -1;
537
+ for (let c = 0; c < jointCount; c++) {
538
+ const body = bodyOf[c] ?? -1;
387
539
  if (body < 0) continue;
388
- const p = parents[j] ?? -1;
389
- const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
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
- if (parentBody >= 0) {
396
- readBodyRotation(w, parentBody, TMP);
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, bodyToJoint, j * 4, CURRENT_ROT, 0);
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 taken from the *bone*, the same way the build took it: the capsule
441
- * stands along y, so the body is turned to put its y along the bone. Taking it from the
442
- * joint's world matrix instead would be wrong wherever a rig's bind orientation is not
443
- * the bone direction, which is most rigs.
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
- const dx = tx - hx;
446
- const dy = ty - hy;
447
- const dz = tz - hz;
448
- const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
449
- if (length > 0) {
450
- alignYTo(dx / length, dy / length, dz / length, TMP);
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,