@driftengine/physics 4.8.1 → 4.8.2

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.
@@ -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
  /**
@@ -73,9 +97,7 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
73
97
  const tx = worldMatrices[j * 16 + 12] ?? 0;
74
98
  const ty = worldMatrices[j * 16 + 13] ?? 0;
75
99
  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;
100
+ intoParentFrame(worldMatrices, p, tx - hx, ty - hy, tz - hz, localRest, j * 3);
79
101
  const dx = tx - hx;
80
102
  const dy = ty - hy;
81
103
  const dz = tz - hz;
@@ -101,8 +123,14 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
101
123
  });
102
124
  bodyOf[j] = body;
103
125
  halfOf[j] = length / 2;
104
- // What takes the body's rotation back to the joint's, captured while both are at rest.
126
+ // What takes the body's rotation back to each of its two joints', captured at rest. Parents
127
+ // come before children in a rig, so the start joint's rotation is already read.
105
128
  multiplyConjugate(TMP, restRotation, j * 4, bodyToJoint, j * 4);
129
+ if (p >= j)
130
+ matrixRotation(worldMatrices, p, restRotation, p * 4);
131
+ multiplyConjugate(TMP, restRotation, p * 4, bodyToStart, j * 4);
132
+ if ((startBone[p] ?? -1) < 0)
133
+ startBone[p] = j;
106
134
  boneCount++;
107
135
  if (firstBone < 0) {
108
136
  firstBone = j;
@@ -210,28 +238,52 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
210
238
  world.ignorePair(body, other);
211
239
  }
212
240
  }
241
+ /**
242
+ * One joint's world rotation, from the body that speaks for it: the first bone starting at the
243
+ * joint, or — for a joint with none below it, a fingertip — the bone it hangs off, which it then
244
+ * rides rigidly. False where neither exists and nothing can be said.
245
+ */
246
+ const recoverJoint = (w, j, out, at) => {
247
+ const start = startBone[j] ?? -1;
248
+ if (start >= 0) {
249
+ readBodyRotation(w, bodyOf[start] ?? 0, TMP);
250
+ multiplyInto(TMP, bodyToStart, start * 4, out, at);
251
+ return true;
252
+ }
253
+ const own = bodyOf[j] ?? -1;
254
+ if (own < 0)
255
+ return false;
256
+ readBodyRotation(w, own, TMP);
257
+ multiplyInto(TMP, bodyToJoint, j * 4, out, at);
258
+ return true;
259
+ };
213
260
  const root = firstBone;
214
261
  return {
215
262
  bodyOf,
216
263
  boneCount,
217
264
  writePose(out) {
265
+ /*
266
+ * Two passes, because a joint's local rotation needs its parent's world rotation and a rig
267
+ * is not obliged to list parents first. The first recovers every joint a body speaks for.
268
+ */
269
+ for (let j = 0; j < jointCount; j++)
270
+ KNOWN[j] = recoverJoint(world, j, WORLD_ROT, j * 4) ? 1 : 0;
218
271
  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.
272
+ if (KNOWN[j] !== 1) {
273
+ // No bone at either end: leave the joint as the pose already had it.
223
274
  continue;
224
275
  }
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) {
276
+ const p = parents[j] ?? -1;
277
+ if (p >= 0 && KNOWN[p] === 1) {
230
278
  conjugateMultiply(WORLD_ROT, p * 4, WORLD_ROT, j * 4, out.rotation, j * 4);
231
279
  }
232
280
  else {
281
+ /* A root, or a joint whose parent no bone speaks for: its rotation in the frame the
282
+ caller places the character's node in, which `rootX` and its two siblings answer. */
233
283
  copy4(WORLD_ROT, j * 4, out.rotation, j * 4);
234
284
  }
285
+ if (p < 0)
286
+ continue;
235
287
  // Bones are rigid, so translation stays what the rig was built with.
236
288
  out.translation[j * 3] = localRest[j * 3] ?? 0;
237
289
  out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
@@ -245,27 +297,32 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
245
297
  if (weight <= 0)
246
298
  return;
247
299
  const k = weight > 1 ? 1 : weight;
248
- for (let j = 0; j < jointCount; j++) {
249
- const body = bodyOf[j] ?? -1;
300
+ for (let c = 0; c < jointCount; c++) {
301
+ const body = bodyOf[c] ?? -1;
250
302
  if (body < 0)
251
303
  continue;
252
- const p = parents[j] ?? -1;
253
- const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
304
+ /*
305
+ * **A bone is steered by the joint it starts at**, the one whose rotation points it — the
306
+ * forearm by the elbow — which is the same joint `writePose` reads it back as. Steered by
307
+ * the joint it ends at, a pose's elbow bend went into the upper arm.
308
+ */
309
+ const j = parents[c] ?? -1;
310
+ if (j < 0)
311
+ continue;
254
312
  /*
255
313
  * The target is the pose's local rotation composed onto the parent's *current* world
256
314
  * rotation, so a driven limb follows the animation relative to a body that may itself have
257
315
  * been knocked aside — which is what a partial ragdoll is.
258
316
  */
259
- if (parentBody >= 0) {
260
- readBodyRotation(w, parentBody, TMP);
261
- multiplyInto(TMP, bodyToJoint, p * 4, PARENT_ROT, 0);
317
+ const p = parents[j] ?? -1;
318
+ if (p >= 0 && recoverJoint(w, p, PARENT_ROT, 0)) {
262
319
  multiplyRaw(PARENT_ROT, 0, pose.rotation, j * 4, TARGET_ROT, 0);
263
320
  }
264
321
  else {
265
322
  copy4(pose.rotation, j * 4, TARGET_ROT, 0);
266
323
  }
267
324
  readBodyRotation(w, body, TMP);
268
- multiplyInto(TMP, bodyToJoint, j * 4, CURRENT_ROT, 0);
325
+ multiplyInto(TMP, bodyToStart, c * 4, CURRENT_ROT, 0);
269
326
  // Twice the vector part of target · conj(current) is the rotation that closes the gap.
270
327
  conjugateMultiply(CURRENT_ROT, 0, TARGET_ROT, 0, ERROR_ROT, 0);
271
328
  let ex = ERROR_ROT[0] ?? 0;
@@ -301,22 +358,23 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
301
358
  w.bodies.posY[body] = (hy + ty) / 2;
302
359
  w.bodies.posZ[body] = (hz + tz) / 2;
303
360
  /*
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.
361
+ * **The rotation is the start joint's, carried through the relation captured at rest**,
362
+ * which is the inverse of what `writePose` reads: `jointRotation · conj(bodyToStart)`.
363
+ *
364
+ * It was the shortest arc from +y onto the bone, which points the capsule correctly and
365
+ * throws away the bone's twist about its own length — so a joint with two bones below it, a
366
+ * wrist with a thumb beside the fingers, came back turned about the first and put the
367
+ * second somewhere else. The joint's world matrix alone would be wrong too, wherever a
368
+ * rig's bind orientation is not the bone direction, which is most rigs; the captured
369
+ * relation is what absorbs that. **What would make it wrong** is a pose that stretches a
370
+ * bone, which a ragdoll's rigid bones cannot follow either way.
308
371
  */
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
- }
372
+ matrixRotation(worldMatrices, p, START_ROT, 0);
373
+ multiplyByConjugate(START_ROT, bodyToStart, j * 4, TMP);
374
+ w.bodies.rotX[body] = TMP[0] ?? 0;
375
+ w.bodies.rotY[body] = TMP[1] ?? 0;
376
+ w.bodies.rotZ[body] = TMP[2] ?? 0;
377
+ w.bodies.rotW[body] = TMP[3] ?? 1;
320
378
  /* At rest, which is the half that makes it a reaction rather than a continuation: a body
321
379
  carrying the speed of a two-second fall is thrown across the character the instant it
322
380
  is written into the pose. */
@@ -341,6 +399,9 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
341
399
  };
342
400
  }
343
401
  const WORLD_ROT = new Float32Array(256 * 4);
402
+ /** Whether `writePose` recovered each joint's rotation this call: 1 if a body speaks for it. */
403
+ const KNOWN = new Uint8Array(256);
404
+ const START_ROT = new Float32Array(4);
344
405
  const PARENT_ROT = new Float32Array(4);
345
406
  const TARGET_ROT = new Float32Array(4);
346
407
  const CURRENT_ROT = new Float32Array(4);
@@ -454,6 +515,51 @@ function alignYTo(dx, dy, dz, out) {
454
515
  out[2] = az / len;
455
516
  out[3] = w / len;
456
517
  }
518
+ /**
519
+ * A world-space offset from joint `p`, expressed in `p`'s own frame: the inverse of the matrix's
520
+ * upper 3x3 applied to it, so a scaled joint is undone as well as a rotated one.
521
+ */
522
+ function intoParentFrame(m, p, dx, dy, dz, out, at) {
523
+ const o = p * 16;
524
+ const a = m[o] ?? 1;
525
+ const b = m[o + 4] ?? 0;
526
+ const c = m[o + 8] ?? 0;
527
+ const d = m[o + 1] ?? 0;
528
+ const e = m[o + 5] ?? 1;
529
+ const f = m[o + 9] ?? 0;
530
+ const g = m[o + 2] ?? 0;
531
+ const h = m[o + 6] ?? 0;
532
+ const i = m[o + 10] ?? 1;
533
+ const ei = e * i - f * h;
534
+ const fg = f * g - d * i;
535
+ const dh = d * h - e * g;
536
+ const det = a * ei + b * fg + c * dh;
537
+ if (Math.abs(det) < 1e-12) {
538
+ out[at] = dx;
539
+ out[at + 1] = dy;
540
+ out[at + 2] = dz;
541
+ return;
542
+ }
543
+ const inv = 1 / det;
544
+ out[at] = (ei * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
545
+ out[at + 1] = (fg * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
546
+ out[at + 2] = (dh * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
547
+ }
548
+ /** `a · conj(b)` into a Float64 quaternion, for turning a joint's rotation back into a body's. */
549
+ function multiplyByConjugate(a, b, bAt, out) {
550
+ const ax = a[0] ?? 0;
551
+ const ay = a[1] ?? 0;
552
+ const az = a[2] ?? 0;
553
+ const aw = a[3] ?? 1;
554
+ const bx = -(b[bAt] ?? 0);
555
+ const by = -(b[bAt + 1] ?? 0);
556
+ const bz = -(b[bAt + 2] ?? 0);
557
+ const bw = b[bAt + 3] ?? 1;
558
+ out[0] = aw * bx + ax * bw + ay * bz - az * by;
559
+ out[1] = aw * by - ax * bz + ay * bw + az * bx;
560
+ out[2] = aw * bz + ax * by - ay * bx + az * bw;
561
+ out[3] = aw * bw - ax * bx - ay * by - az * bz;
562
+ }
457
563
  /** `conj(a) · b` into `out`. */
458
564
  function multiplyConjugate(a, b, bAt, out, at) {
459
565
  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.2",
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
  /**
@@ -214,9 +246,7 @@ export function ragdollFromBones(
214
246
  const tx = worldMatrices[j * 16 + 12] ?? 0;
215
247
  const ty = worldMatrices[j * 16 + 13] ?? 0;
216
248
  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;
249
+ intoParentFrame(worldMatrices, p, tx - hx, ty - hy, tz - hz, localRest, j * 3);
220
250
 
221
251
  const dx = tx - hx;
222
252
  const dy = ty - hy;
@@ -243,8 +273,12 @@ export function ragdollFromBones(
243
273
  });
244
274
  bodyOf[j] = body;
245
275
  halfOf[j] = length / 2;
246
- // What takes the body's rotation back to the joint's, captured while both are at rest.
276
+ // What takes the body's rotation back to each of its two joints', captured at rest. Parents
277
+ // come before children in a rig, so the start joint's rotation is already read.
247
278
  multiplyConjugate(TMP, restRotation, j * 4, bodyToJoint, j * 4);
279
+ if (p >= j) matrixRotation(worldMatrices, p, restRotation, p * 4);
280
+ multiplyConjugate(TMP, restRotation, p * 4, bodyToStart, j * 4);
281
+ if ((startBone[p] ?? -1) < 0) startBone[p] = j;
248
282
  boneCount++;
249
283
  if (firstBone < 0) {
250
284
  firstBone = j;
@@ -349,27 +383,50 @@ export function ragdollFromBones(
349
383
  }
350
384
  }
351
385
 
386
+ /**
387
+ * One joint's world rotation, from the body that speaks for it: the first bone starting at the
388
+ * joint, or — for a joint with none below it, a fingertip — the bone it hangs off, which it then
389
+ * rides rigidly. False where neither exists and nothing can be said.
390
+ */
391
+ const recoverJoint = (w: PhysicsWorld, j: number, out: Float32Array, at: number): boolean => {
392
+ const start = startBone[j] ?? -1;
393
+ if (start >= 0) {
394
+ readBodyRotation(w, bodyOf[start] ?? 0, TMP);
395
+ multiplyInto(TMP, bodyToStart, start * 4, out, at);
396
+ return true;
397
+ }
398
+ const own = bodyOf[j] ?? -1;
399
+ if (own < 0) return false;
400
+ readBodyRotation(w, own, TMP);
401
+ multiplyInto(TMP, bodyToJoint, j * 4, out, at);
402
+ return true;
403
+ };
404
+
352
405
  const root = firstBone;
353
406
  return {
354
407
  bodyOf,
355
408
  boneCount,
356
409
  writePose(out: PoseTarget): void {
410
+ /*
411
+ * Two passes, because a joint's local rotation needs its parent's world rotation and a rig
412
+ * is not obliged to list parents first. The first recovers every joint a body speaks for.
413
+ */
414
+ for (let j = 0; j < jointCount; j++)
415
+ KNOWN[j] = recoverJoint(world, j, WORLD_ROT, j * 4) ? 1 : 0;
357
416
  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.
417
+ if (KNOWN[j] !== 1) {
418
+ // No bone at either end: leave the joint as the pose already had it.
362
419
  continue;
363
420
  }
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) {
421
+ const p = parents[j] ?? -1;
422
+ if (p >= 0 && KNOWN[p] === 1) {
369
423
  conjugateMultiply(WORLD_ROT, p * 4, WORLD_ROT, j * 4, out.rotation, j * 4);
370
424
  } else {
425
+ /* A root, or a joint whose parent no bone speaks for: its rotation in the frame the
426
+ caller places the character's node in, which `rootX` and its two siblings answer. */
371
427
  copy4(WORLD_ROT, j * 4, out.rotation, j * 4);
372
428
  }
429
+ if (p < 0) continue;
373
430
  // Bones are rigid, so translation stays what the rig was built with.
374
431
  out.translation[j * 3] = localRest[j * 3] ?? 0;
375
432
  out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
@@ -382,25 +439,29 @@ export function ragdollFromBones(
382
439
  drive(w: PhysicsWorld, pose: PoseTarget, weight: number): void {
383
440
  if (weight <= 0) return;
384
441
  const k = weight > 1 ? 1 : weight;
385
- for (let j = 0; j < jointCount; j++) {
386
- const body = bodyOf[j] ?? -1;
442
+ for (let c = 0; c < jointCount; c++) {
443
+ const body = bodyOf[c] ?? -1;
387
444
  if (body < 0) continue;
388
- const p = parents[j] ?? -1;
389
- const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
445
+ /*
446
+ * **A bone is steered by the joint it starts at**, the one whose rotation points it — the
447
+ * forearm by the elbow — which is the same joint `writePose` reads it back as. Steered by
448
+ * the joint it ends at, a pose's elbow bend went into the upper arm.
449
+ */
450
+ const j = parents[c] ?? -1;
451
+ if (j < 0) continue;
390
452
  /*
391
453
  * The target is the pose's local rotation composed onto the parent's *current* world
392
454
  * rotation, so a driven limb follows the animation relative to a body that may itself have
393
455
  * been knocked aside — which is what a partial ragdoll is.
394
456
  */
395
- if (parentBody >= 0) {
396
- readBodyRotation(w, parentBody, TMP);
397
- multiplyInto(TMP, bodyToJoint, p * 4, PARENT_ROT, 0);
457
+ const p = parents[j] ?? -1;
458
+ if (p >= 0 && recoverJoint(w, p, PARENT_ROT, 0)) {
398
459
  multiplyRaw(PARENT_ROT, 0, pose.rotation, j * 4, TARGET_ROT, 0);
399
460
  } else {
400
461
  copy4(pose.rotation, j * 4, TARGET_ROT, 0);
401
462
  }
402
463
  readBodyRotation(w, body, TMP);
403
- multiplyInto(TMP, bodyToJoint, j * 4, CURRENT_ROT, 0);
464
+ multiplyInto(TMP, bodyToStart, c * 4, CURRENT_ROT, 0);
404
465
  // Twice the vector part of target · conj(current) is the rotation that closes the gap.
405
466
  conjugateMultiply(CURRENT_ROT, 0, TARGET_ROT, 0, ERROR_ROT, 0);
406
467
  let ex = ERROR_ROT[0] ?? 0;
@@ -437,22 +498,23 @@ export function ragdollFromBones(
437
498
  w.bodies.posZ[body] = (hz + tz) / 2;
438
499
 
439
500
  /*
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.
501
+ * **The rotation is the start joint's, carried through the relation captured at rest**,
502
+ * which is the inverse of what `writePose` reads: `jointRotation · conj(bodyToStart)`.
503
+ *
504
+ * It was the shortest arc from +y onto the bone, which points the capsule correctly and
505
+ * throws away the bone's twist about its own length — so a joint with two bones below it, a
506
+ * wrist with a thumb beside the fingers, came back turned about the first and put the
507
+ * second somewhere else. The joint's world matrix alone would be wrong too, wherever a
508
+ * rig's bind orientation is not the bone direction, which is most rigs; the captured
509
+ * relation is what absorbs that. **What would make it wrong** is a pose that stretches a
510
+ * bone, which a ragdoll's rigid bones cannot follow either way.
444
511
  */
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
- }
512
+ matrixRotation(worldMatrices, p, START_ROT, 0);
513
+ multiplyByConjugate(START_ROT, bodyToStart, j * 4, TMP);
514
+ w.bodies.rotX[body] = TMP[0] ?? 0;
515
+ w.bodies.rotY[body] = TMP[1] ?? 0;
516
+ w.bodies.rotZ[body] = TMP[2] ?? 0;
517
+ w.bodies.rotW[body] = TMP[3] ?? 1;
456
518
 
457
519
  /* At rest, which is the half that makes it a reaction rather than a continuation: a body
458
520
  carrying the speed of a two-second fall is thrown across the character the instant it
@@ -479,6 +541,9 @@ export function ragdollFromBones(
479
541
  }
480
542
 
481
543
  const WORLD_ROT = new Float32Array(256 * 4);
544
+ /** Whether `writePose` recovered each joint's rotation this call: 1 if a body speaks for it. */
545
+ const KNOWN = new Uint8Array(256);
546
+ const START_ROT = new Float32Array(4);
482
547
  const PARENT_ROT = new Float32Array(4);
483
548
  const TARGET_ROT = new Float32Array(4);
484
549
  const CURRENT_ROT = new Float32Array(4);
@@ -596,6 +661,66 @@ function alignYTo(dx: number, dy: number, dz: number, out: Float64Array): void {
596
661
  out[3] = w / len;
597
662
  }
598
663
 
664
+ /**
665
+ * A world-space offset from joint `p`, expressed in `p`'s own frame: the inverse of the matrix's
666
+ * upper 3x3 applied to it, so a scaled joint is undone as well as a rotated one.
667
+ */
668
+ function intoParentFrame(
669
+ m: Float32Array,
670
+ p: number,
671
+ dx: number,
672
+ dy: number,
673
+ dz: number,
674
+ out: Float32Array,
675
+ at: number,
676
+ ): void {
677
+ const o = p * 16;
678
+ const a = m[o] ?? 1;
679
+ const b = m[o + 4] ?? 0;
680
+ const c = m[o + 8] ?? 0;
681
+ const d = m[o + 1] ?? 0;
682
+ const e = m[o + 5] ?? 1;
683
+ const f = m[o + 9] ?? 0;
684
+ const g = m[o + 2] ?? 0;
685
+ const h = m[o + 6] ?? 0;
686
+ const i = m[o + 10] ?? 1;
687
+ const ei = e * i - f * h;
688
+ const fg = f * g - d * i;
689
+ const dh = d * h - e * g;
690
+ const det = a * ei + b * fg + c * dh;
691
+ if (Math.abs(det) < 1e-12) {
692
+ out[at] = dx;
693
+ out[at + 1] = dy;
694
+ out[at + 2] = dz;
695
+ return;
696
+ }
697
+ const inv = 1 / det;
698
+ out[at] = (ei * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
699
+ out[at + 1] = (fg * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
700
+ out[at + 2] = (dh * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
701
+ }
702
+
703
+ /** `a · conj(b)` into a Float64 quaternion, for turning a joint's rotation back into a body's. */
704
+ function multiplyByConjugate(
705
+ a: Float32Array,
706
+ b: Float32Array,
707
+ bAt: number,
708
+ out: Float64Array,
709
+ ): void {
710
+ const ax = a[0] ?? 0;
711
+ const ay = a[1] ?? 0;
712
+ const az = a[2] ?? 0;
713
+ const aw = a[3] ?? 1;
714
+ const bx = -(b[bAt] ?? 0);
715
+ const by = -(b[bAt + 1] ?? 0);
716
+ const bz = -(b[bAt + 2] ?? 0);
717
+ const bw = b[bAt + 3] ?? 1;
718
+ out[0] = aw * bx + ax * bw + ay * bz - az * by;
719
+ out[1] = aw * by - ax * bz + ay * bw + az * bx;
720
+ out[2] = aw * bz + ax * by - ay * bx + az * bw;
721
+ out[3] = aw * bw - ax * bx - ay * by - az * bz;
722
+ }
723
+
599
724
  /** `conj(a) · b` into `out`. */
600
725
  function multiplyConjugate(
601
726
  a: Float64Array,