@driftengine/physics 4.8.0 → 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.
package/src/query.ts CHANGED
@@ -858,7 +858,14 @@ export function shapecastWorld(
858
858
  if (chosen === undefined) break;
859
859
  copyManifold(chosen, manifold);
860
860
  } else if (!collideShapes(shape, poseA, other, poseB, 1e9, manifold)) break;
861
- const gap = manifold.separations[0] ?? 0;
861
+ /*
862
+ * **The nearest contact, not the first one written.** A capsule's manifold holds a point per
863
+ * end, and the first is its lower end: reading only that advanced the sweep by the distance
864
+ * from the lower ball while the upper one was the nearer, so a standing capsule swept clean
865
+ * through a beam at head height.
866
+ */
867
+ let gap = Infinity;
868
+ for (let c = 0; c < manifold.count; c++) gap = Math.min(gap, manifold.separations[c] ?? gap);
862
869
  if (gap < 1e-4) {
863
870
  /*
864
871
  * A shape already touching a surface it is travelling *along* has not run into it.
@@ -874,7 +881,23 @@ export function shapecastWorld(
874
881
  touched = true;
875
882
  break;
876
883
  }
877
- t += gap / travel;
884
+ /*
885
+ * **How far the shapes may close along the contact normal, not along the travel.** The
886
+ * manifold's normal separates the two by `gap`, so they cannot meet before the sweep has
887
+ * closed that much along it. Dividing by the whole travel was always safe and crawled
888
+ * wherever the travel grazes the surface: a ball dropping past the lip of a step closes on it
889
+ * at a sixth of its speed, and twenty-four steps ended before it arrived, so the sweep said
890
+ * nothing was there. A mesh keeps the old step, since one triangle's normal does not separate
891
+ * the whole mesh.
892
+ */
893
+ if (isMesh(shape) || isMesh(other)) {
894
+ t += gap / travel;
895
+ } else {
896
+ const closing = dx * manifold.nx + dy * manifold.ny + dz * manifold.nz;
897
+ // Not closing along the normal that separates them, so they never meet on this travel.
898
+ if (closing <= 1e-12) break;
899
+ t += gap / closing;
900
+ }
878
901
  if (t > 1) break;
879
902
  }
880
903
  if (brushing || !touched || t > 1) continue;
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,