@aura3d/physics 1.4.5 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,11 @@ import { Body as CannonBody, Box as CannonBox, Cylinder as CannonCylinder, Plane
2
2
  import { Collider } from "./Collider.js";
3
3
  import { CollisionEventQueue } from "./CollisionEvents.js";
4
4
  import { Constraint } from "./Constraint.js";
5
+ import { buildNativeNarrowPhaseContact } from "./NarrowPhase.js";
5
6
  import { raycastCollider, sphereCastCollider } from "./Raycast.js";
6
7
  import { RigidBody } from "./RigidBody.js";
7
- import { cloneVec3, dotVec3, normalizeVec3, scaleVec3, subVec3 } from "./Shape.js";
8
+ import { cloneVec3, dotVec3, lengthVec3, normalizeVec3, scaleVec3, subVec3 } from "./Shape.js";
9
+ import { timeOfImpact } from "./TimeOfImpact.js";
8
10
  export class PhysicsWorld {
9
11
  gravity;
10
12
  fixedDelta;
@@ -12,6 +14,7 @@ export class PhysicsWorld {
12
14
  enableSleeping;
13
15
  sleepVelocityThreshold;
14
16
  sleepDelay;
17
+ continuousCollision;
15
18
  bodiesById = new Map();
16
19
  collidersById = new Map();
17
20
  constraintsList = [];
@@ -26,6 +29,7 @@ export class PhysicsWorld {
26
29
  lastEvents = [];
27
30
  lastBroadphasePairs = 0;
28
31
  lastBroadphaseProfile = emptyBroadphaseProfile();
32
+ lastContinuousCollisionStep = emptyContinuousCollisionStepPlan();
29
33
  steps = 0;
30
34
  constructor(descriptor = {}) {
31
35
  this.requestedBackend = descriptor.backend ?? "auto";
@@ -35,6 +39,7 @@ export class PhysicsWorld {
35
39
  this.enableSleeping = descriptor.enableSleeping ?? true;
36
40
  this.sleepVelocityThreshold = descriptor.sleepVelocityThreshold ?? 0.02;
37
41
  this.sleepDelay = descriptor.sleepDelay ?? 0.5;
42
+ this.continuousCollision = normalizeContinuousCollisionDescriptor(descriptor.continuousCollision);
38
43
  if (!Number.isFinite(this.fixedDelta) || this.fixedDelta <= 0) {
39
44
  throw new Error("fixedDelta must be a finite positive number.");
40
45
  }
@@ -51,7 +56,8 @@ export class PhysicsWorld {
51
56
  requested: this.requestedBackend,
52
57
  active: this.requestedBackend === "aura-js" ? "aura-js" : "cannon-es",
53
58
  deterministic: true,
54
- jsFallbackAvailable: true
59
+ jsFallbackAvailable: true,
60
+ continuousCollision: this.continuousCollisionSelection(true)
55
61
  };
56
62
  if (this.backendSelection.active === "cannon-es") {
57
63
  this.cannonWorld = new CannonWorld({
@@ -154,21 +160,43 @@ export class PhysicsWorld {
154
160
  if (this.cannonWorld) {
155
161
  return this.stepCannon(dt);
156
162
  }
157
- for (const body of this.bodyValues()) {
158
- body.integrate(dt, this.gravity);
159
- }
160
- for (const constraint of this.constraintsList) {
161
- constraint.solve();
162
- }
163
+ const continuousStep = this.planContinuousCollisionStep(dt);
164
+ this.lastContinuousCollisionStep = continuousStep;
165
+ this.assertContinuousCollisionPlan(continuousStep);
166
+ const subDelta = dt / continuousStep.subSteps;
167
+ const outerStepTransforms = new Map(this.bodies().map((body) => [
168
+ body.id,
169
+ {
170
+ position: cloneVec3(body.position),
171
+ rotation: [...body.rotation]
172
+ }
173
+ ]));
163
174
  let contacts = [];
164
- for (let i = 0; i < this.solverIterations; i += 1) {
165
- contacts = this.detectContacts();
166
- for (const contact of contacts) {
167
- this.resolveContact(contact);
175
+ for (let subStep = 0; subStep < continuousStep.subSteps; subStep += 1) {
176
+ for (const body of this.bodyValues()) {
177
+ body.integrate(subDelta, this.gravity, false);
168
178
  }
169
179
  for (const constraint of this.constraintsList) {
170
180
  constraint.solve();
171
181
  }
182
+ const contactImpulses = new Map();
183
+ for (let i = 0; i < this.solverIterations; i += 1) {
184
+ contacts = this.detectContacts();
185
+ for (const contact of contacts) {
186
+ this.resolveContact(contact, contactImpulses);
187
+ }
188
+ for (const constraint of this.constraintsList) {
189
+ constraint.solve();
190
+ }
191
+ }
192
+ }
193
+ for (const body of this.bodyValues()) {
194
+ body.clearForces();
195
+ const outerTransform = outerStepTransforms.get(body.id);
196
+ if (outerTransform) {
197
+ body.previousPosition = outerTransform.position;
198
+ body.previousRotation = outerTransform.rotation;
199
+ }
172
200
  }
173
201
  this.lastEvents = this.eventQueue.update(contacts);
174
202
  this.updateSleeping(dt, contacts);
@@ -223,7 +251,10 @@ export class PhysicsWorld {
223
251
  const contacts = this.eventQueue.snapshotContacts();
224
252
  const bodies = this.bodies();
225
253
  return {
226
- backend: this.backendSelection,
254
+ backend: {
255
+ ...this.backendSelection,
256
+ continuousCollision: this.continuousCollisionSelection(true)
257
+ },
227
258
  bodies: bodies.map((body) => body.snapshot()),
228
259
  contacts,
229
260
  stats: {
@@ -267,7 +298,7 @@ export class PhysicsWorld {
267
298
  if (!body) {
268
299
  continue;
269
300
  }
270
- const bounds = collider.bounds(body.position);
301
+ const bounds = collider.bounds(body.position, body.rotation);
271
302
  const entry = { collider, body, bounds };
272
303
  if (isFiniteBounds(bounds)) {
273
304
  finiteEntries.push(entry);
@@ -314,7 +345,7 @@ export class PhysicsWorld {
314
345
  this.lastBroadphaseProfile = profile;
315
346
  return pairs;
316
347
  }
317
- resolveContact(contact) {
348
+ resolveContact(contact, contactImpulses) {
318
349
  if (contact.sensor) {
319
350
  return;
320
351
  }
@@ -334,30 +365,46 @@ export class PhysicsWorld {
334
365
  if (bodyB.inverseMass > 0) {
335
366
  bodyB.position = [bodyB.position[0] + correction[0] * bodyB.inverseMass, bodyB.position[1] + correction[1] * bodyB.inverseMass, bodyB.position[2] + correction[2] * bodyB.inverseMass];
336
367
  }
337
- const relativeVelocity = subVec3(bodyB.velocity, bodyA.velocity);
368
+ const relativeVelocity = relativeContactVelocity(bodyA, bodyB, contact.point);
338
369
  const velocityAlongNormal = dotVec3(relativeVelocity, contact.normal);
339
370
  const materialA = this.effectiveMaterial(bodyA, contact.colliderA);
340
371
  const materialB = this.effectiveMaterial(bodyB, contact.colliderB);
341
372
  const restitution = Math.max(materialA.restitution, materialB.restitution);
373
+ const pairKey = `${contact.colliderA}:${contact.colliderB}`;
374
+ const impulseState = contactImpulses.get(pairKey) ?? { normal: 0, tangent: [0, 0, 0] };
342
375
  let normalImpulseMagnitude = 0;
343
376
  if (velocityAlongNormal <= 0) {
344
- normalImpulseMagnitude = -(1 + restitution) * velocityAlongNormal / invMassSum;
345
- this.applyImpulsePair(bodyA, bodyB, scaleVec3(contact.normal, normalImpulseMagnitude));
377
+ const normalDenominator = contactImpulseDenominator(bodyA, bodyB, contact.normal, contact.point);
378
+ normalImpulseMagnitude = normalDenominator > 0 ? -(1 + restitution) * velocityAlongNormal / normalDenominator : 0;
379
+ impulseState.normal += normalImpulseMagnitude;
380
+ this.applyImpulsePair(bodyA, bodyB, scaleVec3(contact.normal, normalImpulseMagnitude), contact.point);
346
381
  }
347
- const updatedRelativeVelocity = subVec3(bodyB.velocity, bodyA.velocity);
382
+ const updatedRelativeVelocity = relativeContactVelocity(bodyA, bodyB, contact.point);
348
383
  const updatedNormalVelocity = dotVec3(updatedRelativeVelocity, contact.normal);
349
384
  const tangentVelocity = subVec3(updatedRelativeVelocity, scaleVec3(contact.normal, updatedNormalVelocity));
350
385
  const tangentSpeed = Math.hypot(tangentVelocity[0], tangentVelocity[1], tangentVelocity[2]);
351
386
  if (tangentSpeed > 1e-9) {
352
- const tangent = scaleVec3(tangentVelocity, 1 / tangentSpeed);
353
387
  const friction = Math.sqrt(Math.max(0, materialA.friction) * Math.max(0, materialB.friction));
354
- const targetMagnitude = -dotVec3(updatedRelativeVelocity, tangent) / invMassSum;
355
- const maxFriction = friction * (Math.abs(normalImpulseMagnitude) + contact.penetration);
356
- const frictionMagnitude = Math.max(-maxFriction, Math.min(maxFriction, targetMagnitude));
357
- if (frictionMagnitude !== 0) {
358
- this.applyImpulsePair(bodyA, bodyB, scaleVec3(tangent, frictionMagnitude));
388
+ const tangent = scaleVec3(tangentVelocity, 1 / tangentSpeed);
389
+ const tangentDenominator = contactImpulseDenominator(bodyA, bodyB, tangent, contact.point);
390
+ const targetImpulse = tangentDenominator > 0 ? scaleVec3(tangentVelocity, -1 / tangentDenominator) : [0, 0, 0];
391
+ const proposedTangent = [
392
+ impulseState.tangent[0] + targetImpulse[0],
393
+ impulseState.tangent[1] + targetImpulse[1],
394
+ impulseState.tangent[2] + targetImpulse[2]
395
+ ];
396
+ const proposedMagnitude = lengthVec3(proposedTangent);
397
+ const maxFriction = friction * impulseState.normal;
398
+ const accumulatedTangent = proposedMagnitude > maxFriction && proposedMagnitude > 0
399
+ ? scaleVec3(proposedTangent, maxFriction / proposedMagnitude)
400
+ : proposedTangent;
401
+ const frictionImpulse = subVec3(accumulatedTangent, impulseState.tangent);
402
+ impulseState.tangent = accumulatedTangent;
403
+ if (lengthVec3(frictionImpulse) > 0) {
404
+ this.applyImpulsePair(bodyA, bodyB, frictionImpulse, contact.point);
359
405
  }
360
406
  }
407
+ contactImpulses.set(pairKey, impulseState);
361
408
  }
362
409
  effectiveMaterial(body, colliderId) {
363
410
  const collider = this.collidersById.get(colliderId);
@@ -366,15 +413,21 @@ export class PhysicsWorld {
366
413
  friction: Math.max(body.friction, collider?.material.friction ?? 0)
367
414
  };
368
415
  }
369
- applyImpulsePair(bodyA, bodyB, impulse) {
416
+ applyImpulsePair(bodyA, bodyB, impulse, point) {
370
417
  if (bodyA.inverseMass > 0) {
371
- bodyA.velocity = [bodyA.velocity[0] - impulse[0] * bodyA.inverseMass, bodyA.velocity[1] - impulse[1] * bodyA.inverseMass, bodyA.velocity[2] - impulse[2] * bodyA.inverseMass];
418
+ if (point)
419
+ bodyA.applyContactImpulseAtPoint(scaleVec3(impulse, -1), point);
420
+ else
421
+ bodyA.velocity = [bodyA.velocity[0] - impulse[0] * bodyA.inverseMass, bodyA.velocity[1] - impulse[1] * bodyA.inverseMass, bodyA.velocity[2] - impulse[2] * bodyA.inverseMass];
372
422
  if (bodyA.sleeping && bodyA.speedSquared() > this.sleepVelocityThreshold * this.sleepVelocityThreshold) {
373
423
  bodyA.wake();
374
424
  }
375
425
  }
376
426
  if (bodyB.inverseMass > 0) {
377
- bodyB.velocity = [bodyB.velocity[0] + impulse[0] * bodyB.inverseMass, bodyB.velocity[1] + impulse[1] * bodyB.inverseMass, bodyB.velocity[2] + impulse[2] * bodyB.inverseMass];
427
+ if (point)
428
+ bodyB.applyContactImpulseAtPoint(impulse, point);
429
+ else
430
+ bodyB.velocity = [bodyB.velocity[0] + impulse[0] * bodyB.inverseMass, bodyB.velocity[1] + impulse[1] * bodyB.inverseMass, bodyB.velocity[2] + impulse[2] * bodyB.inverseMass];
378
431
  if (bodyB.sleeping && bodyB.speedSquared() > this.sleepVelocityThreshold * this.sleepVelocityThreshold) {
379
432
  bodyB.wake();
380
433
  }
@@ -460,7 +513,8 @@ export class PhysicsWorld {
460
513
  active: "aura-js",
461
514
  fallback: reason,
462
515
  deterministic: true,
463
- jsFallbackAvailable: true
516
+ jsFallbackAvailable: true,
517
+ continuousCollision: this.continuousCollisionSelection(true)
464
518
  };
465
519
  }
466
520
  stepCannon(dt) {
@@ -472,7 +526,14 @@ export class PhysicsWorld {
472
526
  if (cannonBody)
473
527
  syncCannonFromAura(body, cannonBody);
474
528
  }
475
- this.cannonWorld.step(dt);
529
+ const continuousStep = this.planContinuousCollisionStep(dt);
530
+ this.lastContinuousCollisionStep = continuousStep;
531
+ this.assertContinuousCollisionPlan(continuousStep);
532
+ const subSteps = continuousStep.subSteps;
533
+ const subDelta = dt / subSteps;
534
+ for (let index = 0; index < subSteps; index += 1) {
535
+ this.cannonWorld.step(subDelta);
536
+ }
476
537
  for (const body of this.bodyValues()) {
477
538
  const cannonBody = this.cannonBodiesByAuraId.get(body.id);
478
539
  if (cannonBody)
@@ -484,6 +545,167 @@ export class PhysicsWorld {
484
545
  this.steps += 1;
485
546
  return this.lastEvents;
486
547
  }
548
+ planContinuousCollisionStep(dt) {
549
+ if (!this.continuousCollision.active) {
550
+ return emptyContinuousCollisionStepPlan();
551
+ }
552
+ let maxMotion = 0;
553
+ for (const body of this.bodyValues()) {
554
+ if (body.type === "static" || body.sleeping)
555
+ continue;
556
+ const bodyRadius = this.bodyBoundingRadius(body.id);
557
+ const linearMotion = lengthVec3(body.velocity) * dt;
558
+ const angularMotion = lengthVec3(body.angularVelocity) * bodyRadius * dt;
559
+ maxMotion = Math.max(maxMotion, linearMotion + angularMotion);
560
+ }
561
+ const minColliderFeature = this.smallestFiniteColliderFeature();
562
+ const earliestImpact = this.earliestTimeOfImpact(dt);
563
+ if (maxMotion <= 0 || minColliderFeature === undefined) {
564
+ return {
565
+ subSteps: 1,
566
+ requiredSubSteps: 1,
567
+ maxMotion,
568
+ minColliderFeature,
569
+ timeOfImpact: earliestImpact,
570
+ limitExceeded: false
571
+ };
572
+ }
573
+ const maxMotionPerSubstep = minColliderFeature * this.continuousCollision.motionThreshold;
574
+ const requiredSubSteps = Math.max(1, Math.ceil(maxMotion / maxMotionPerSubstep));
575
+ const limitExceeded = requiredSubSteps > this.continuousCollision.maxSubSteps;
576
+ return {
577
+ subSteps: Math.min(requiredSubSteps, this.continuousCollision.maxSubSteps),
578
+ requiredSubSteps,
579
+ maxMotion,
580
+ minColliderFeature,
581
+ timeOfImpact: earliestImpact,
582
+ limitExceeded
583
+ };
584
+ }
585
+ earliestTimeOfImpact(dt) {
586
+ let earliest;
587
+ const colliders = this.colliders();
588
+ for (let firstIndex = 0; firstIndex < colliders.length; firstIndex += 1) {
589
+ const colliderA = colliders[firstIndex];
590
+ const bodyA = this.bodiesById.get(colliderA.bodyId);
591
+ if (!bodyA)
592
+ continue;
593
+ for (let secondIndex = firstIndex + 1; secondIndex < colliders.length; secondIndex += 1) {
594
+ const colliderB = colliders[secondIndex];
595
+ const bodyB = this.bodiesById.get(colliderB.bodyId);
596
+ if (!bodyB ||
597
+ bodyA.id === bodyB.id ||
598
+ (bodyA.type !== "dynamic" && bodyB.type !== "dynamic") ||
599
+ !colliderA.canCollideWith(colliderB)) {
600
+ continue;
601
+ }
602
+ const impact = timeOfImpact(colliderA.shape, bodyA.position, bodyA.velocity, colliderB.shape, bodyB.position, bodyB.velocity, dt);
603
+ if (impact && (earliest === undefined || impact.time < earliest)) {
604
+ earliest = impact.time;
605
+ }
606
+ }
607
+ }
608
+ return earliest;
609
+ }
610
+ assertContinuousCollisionPlan(plan) {
611
+ if (plan.limitExceeded && this.continuousCollision.onSubstepLimit === "error") {
612
+ throw new Error(`PhysicsWorld adaptive-substep CCD requires ${plan.requiredSubSteps} substeps, ` +
613
+ `above maxSubSteps ${this.continuousCollision.maxSubSteps}. Increase maxSubSteps, reduce dt, ` +
614
+ "or use a swept movement query; the step was rejected so tunneling is not silently accepted.");
615
+ }
616
+ }
617
+ smallestFiniteColliderFeature() {
618
+ let smallest;
619
+ for (const collider of this.colliderValues()) {
620
+ const feature = colliderFeatureSize(collider.shape);
621
+ if (feature === undefined)
622
+ continue;
623
+ smallest = smallest === undefined ? feature : Math.min(smallest, feature);
624
+ }
625
+ return smallest;
626
+ }
627
+ bodyBoundingRadius(bodyId) {
628
+ let radius = 0;
629
+ for (const colliderId of this.bodyColliders.get(bodyId) ?? []) {
630
+ const collider = this.collidersById.get(colliderId);
631
+ if (collider)
632
+ radius = Math.max(radius, colliderBoundingRadius(collider.shape));
633
+ }
634
+ return radius;
635
+ }
636
+ continuousCollisionSelection(backendSupportsWrapper) {
637
+ const active = this.continuousCollision.active && backendSupportsWrapper;
638
+ return {
639
+ active,
640
+ mode: active ? "adaptive-substeps" : "disabled",
641
+ provider: active ? "aura3d-adaptive-substep-wrapper" : "none",
642
+ maxSubSteps: this.continuousCollision.maxSubSteps,
643
+ motionThreshold: this.continuousCollision.motionThreshold,
644
+ onSubstepLimit: this.continuousCollision.onSubstepLimit,
645
+ lastSubSteps: active ? this.lastContinuousCollisionStep.subSteps : 1,
646
+ lastRequiredSubSteps: active ? this.lastContinuousCollisionStep.requiredSubSteps : 1,
647
+ lastMaxMotion: active ? this.lastContinuousCollisionStep.maxMotion : 0,
648
+ ...(active && this.lastContinuousCollisionStep.minColliderFeature !== undefined
649
+ ? { lastMinColliderFeature: this.lastContinuousCollisionStep.minColliderFeature }
650
+ : {}),
651
+ ...(active && this.lastContinuousCollisionStep.timeOfImpact !== undefined
652
+ ? { lastTimeOfImpact: this.lastContinuousCollisionStep.timeOfImpact }
653
+ : {}),
654
+ limitExceeded: active && this.lastContinuousCollisionStep.limitExceeded
655
+ };
656
+ }
657
+ }
658
+ function normalizeContinuousCollisionDescriptor(descriptor) {
659
+ if (!descriptor) {
660
+ return {
661
+ active: false,
662
+ maxSubSteps: 1,
663
+ motionThreshold: 0.5,
664
+ onSubstepLimit: "error"
665
+ };
666
+ }
667
+ const maxSubSteps = descriptor.maxSubSteps ?? 256;
668
+ const motionThreshold = descriptor.motionThreshold ?? 0.5;
669
+ const onSubstepLimit = descriptor.onSubstepLimit ?? "error";
670
+ if (!Number.isInteger(maxSubSteps) || maxSubSteps < 1) {
671
+ throw new Error("continuousCollision.maxSubSteps must be a positive integer.");
672
+ }
673
+ if (!Number.isFinite(motionThreshold) || motionThreshold <= 0 || motionThreshold > 1) {
674
+ throw new Error("continuousCollision.motionThreshold must be finite and in the range (0, 1].");
675
+ }
676
+ return {
677
+ active: true,
678
+ maxSubSteps,
679
+ motionThreshold,
680
+ onSubstepLimit
681
+ };
682
+ }
683
+ function emptyContinuousCollisionStepPlan() {
684
+ return {
685
+ subSteps: 1,
686
+ requiredSubSteps: 1,
687
+ maxMotion: 0,
688
+ limitExceeded: false
689
+ };
690
+ }
691
+ function colliderFeatureSize(shape) {
692
+ if (shape.kind === "box")
693
+ return Math.min(shape.halfExtents[0], shape.halfExtents[1], shape.halfExtents[2]);
694
+ if (shape.kind === "sphere" || shape.kind === "capsule")
695
+ return shape.radius;
696
+ return undefined;
697
+ }
698
+ function colliderBoundingRadius(shape) {
699
+ if (shape.kind === "box")
700
+ return Math.hypot(shape.halfExtents[0], shape.halfExtents[1], shape.halfExtents[2]);
701
+ if (shape.kind === "sphere")
702
+ return shape.radius;
703
+ if (shape.kind === "capsule")
704
+ return shape.radius + shape.halfHeight;
705
+ if (shape.kind === "mesh") {
706
+ return shape.vertices.reduce((radius, vertex) => Math.max(radius, lengthVec3(vertex)), 0);
707
+ }
708
+ return 0;
487
709
  }
488
710
  function toCannonVec3(value) {
489
711
  return new CannonVec3(value[0], value[1], value[2]);
@@ -543,6 +765,41 @@ function totalKineticEnergy(bodies) {
543
765
  }
544
766
  return total;
545
767
  }
768
+ function relativeContactVelocity(bodyA, bodyB, point) {
769
+ if (!point)
770
+ return subVec3(bodyB.velocity, bodyA.velocity);
771
+ const radiusA = subVec3(point, bodyA.position);
772
+ const radiusB = subVec3(point, bodyB.position);
773
+ const velocityA = addVectors(bodyA.velocity, crossVectors(bodyA.angularVelocity, radiusA));
774
+ const velocityB = addVectors(bodyB.velocity, crossVectors(bodyB.angularVelocity, radiusB));
775
+ return subVec3(velocityB, velocityA);
776
+ }
777
+ function contactImpulseDenominator(bodyA, bodyB, direction, point) {
778
+ let denominator = bodyA.inverseMass + bodyB.inverseMass;
779
+ if (!point)
780
+ return denominator;
781
+ if (bodyA.inverseMass > 0) {
782
+ const radius = subVec3(point, bodyA.position);
783
+ const angular = bodyA.multiplyInverseInertiaWorld(crossVectors(radius, direction));
784
+ denominator += dotVec3(direction, crossVectors(angular, radius));
785
+ }
786
+ if (bodyB.inverseMass > 0) {
787
+ const radius = subVec3(point, bodyB.position);
788
+ const angular = bodyB.multiplyInverseInertiaWorld(crossVectors(radius, direction));
789
+ denominator += dotVec3(direction, crossVectors(angular, radius));
790
+ }
791
+ return denominator;
792
+ }
793
+ function addVectors(a, b) {
794
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
795
+ }
796
+ function crossVectors(a, b) {
797
+ return [
798
+ a[1] * b[2] - a[2] * b[1],
799
+ a[2] * b[0] - a[0] * b[2],
800
+ a[0] * b[1] - a[1] * b[0]
801
+ ];
802
+ }
546
803
  function emptyBroadphaseProfile() {
547
804
  return {
548
805
  finiteColliders: 0,
@@ -634,6 +891,19 @@ function buildContact(a, bodyA, b, bodyB) {
634
891
  if (a.shape.kind === "capsule" && b.shape.kind === "capsule") {
635
892
  return buildCapsuleCapsuleContact(a, bodyA, b, bodyB);
636
893
  }
894
+ const nativeContact = buildNativeNarrowPhaseContact(a, bodyA, b, bodyB);
895
+ if (nativeContact) {
896
+ return {
897
+ colliderA: a.id,
898
+ colliderB: b.id,
899
+ bodyA: bodyA.id,
900
+ bodyB: bodyB.id,
901
+ normal: nativeContact.normal,
902
+ penetration: nativeContact.penetration,
903
+ point: nativeContact.point,
904
+ sensor: a.sensor || b.sensor
905
+ };
906
+ }
637
907
  const boundsA = a.bounds(bodyA.position);
638
908
  const boundsB = b.bounds(bodyB.position);
639
909
  const overlapX = Math.min(boundsA.max[0] - boundsB.min[0], boundsB.max[0] - boundsA.min[0]);
@@ -863,6 +1133,11 @@ function supportRadius(collider) {
863
1133
  const bounds = collider.bounds([0, 0, 0]);
864
1134
  return Math.max(Math.abs(bounds.min[1]), Math.abs(bounds.max[1]));
865
1135
  }
1136
+ case "convex-hull":
1137
+ case "heightfield": {
1138
+ const bounds = collider.bounds([0, 0, 0]);
1139
+ return Math.max(Math.abs(bounds.min[1]), Math.abs(bounds.max[1]));
1140
+ }
866
1141
  }
867
1142
  }
868
1143
  //# sourceMappingURL=PhysicsWorld.js.map