@aura3d/physics 1.5.2 → 2.0.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.
Files changed (81) hide show
  1. package/dist/ArcadeCharacterController.d.ts +53 -0
  2. package/dist/ArcadeCharacterController.d.ts.map +1 -0
  3. package/dist/ArcadeCharacterController.js +98 -0
  4. package/dist/ArcadeCharacterController.js.map +1 -0
  5. package/dist/ArcadeVehicleTelemetry.d.ts +162 -0
  6. package/dist/ArcadeVehicleTelemetry.d.ts.map +1 -0
  7. package/dist/ArcadeVehicleTelemetry.js +363 -0
  8. package/dist/ArcadeVehicleTelemetry.js.map +1 -0
  9. package/dist/CharacterController.d.ts +52 -0
  10. package/dist/CharacterController.d.ts.map +1 -1
  11. package/dist/CharacterController.js +380 -7
  12. package/dist/CharacterController.js.map +1 -1
  13. package/dist/Constraint.d.ts +20 -4
  14. package/dist/Constraint.d.ts.map +1 -1
  15. package/dist/Constraint.js +34 -60
  16. package/dist/Constraint.js.map +1 -1
  17. package/dist/FightingCharacterController.d.ts +32 -0
  18. package/dist/FightingCharacterController.d.ts.map +1 -0
  19. package/dist/FightingCharacterController.js +76 -0
  20. package/dist/FightingCharacterController.js.map +1 -0
  21. package/dist/MeshBVH.d.ts +72 -0
  22. package/dist/MeshBVH.d.ts.map +1 -0
  23. package/dist/MeshBVH.js +244 -0
  24. package/dist/MeshBVH.js.map +1 -0
  25. package/dist/PathFollowDriver.d.ts +162 -0
  26. package/dist/PathFollowDriver.d.ts.map +1 -0
  27. package/dist/PathFollowDriver.js +163 -0
  28. package/dist/PathFollowDriver.js.map +1 -0
  29. package/dist/PhysicsDebugDraw.d.ts +40 -1
  30. package/dist/PhysicsDebugDraw.d.ts.map +1 -1
  31. package/dist/PhysicsDebugDraw.js +87 -17
  32. package/dist/PhysicsDebugDraw.js.map +1 -1
  33. package/dist/PhysicsWorld.d.ts +37 -18
  34. package/dist/PhysicsWorld.d.ts.map +1 -1
  35. package/dist/PhysicsWorld.js +231 -630
  36. package/dist/PhysicsWorld.js.map +1 -1
  37. package/dist/RacingLineProfile.d.ts +116 -0
  38. package/dist/RacingLineProfile.d.ts.map +1 -0
  39. package/dist/RacingLineProfile.js +181 -0
  40. package/dist/RacingLineProfile.js.map +1 -0
  41. package/dist/Raycast.d.ts +11 -0
  42. package/dist/Raycast.d.ts.map +1 -1
  43. package/dist/Raycast.js +62 -8
  44. package/dist/Raycast.js.map +1 -1
  45. package/dist/RigidBody.d.ts +16 -9
  46. package/dist/RigidBody.d.ts.map +1 -1
  47. package/dist/RigidBody.js +23 -75
  48. package/dist/RigidBody.js.map +1 -1
  49. package/dist/SurfaceQuery.d.ts +72 -0
  50. package/dist/SurfaceQuery.d.ts.map +1 -0
  51. package/dist/SurfaceQuery.js +89 -0
  52. package/dist/SurfaceQuery.js.map +1 -0
  53. package/dist/VehicleDynamics.d.ts +10 -0
  54. package/dist/VehicleDynamics.d.ts.map +1 -1
  55. package/dist/VehicleDynamics.js +31 -3
  56. package/dist/VehicleDynamics.js.map +1 -1
  57. package/dist/VehicleMotion.d.ts +125 -0
  58. package/dist/VehicleMotion.d.ts.map +1 -0
  59. package/dist/VehicleMotion.js +373 -0
  60. package/dist/VehicleMotion.js.map +1 -0
  61. package/dist/index.d.ts +62 -17
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +70 -17
  64. package/dist/index.js.map +1 -1
  65. package/dist/solverless-entry.d.ts +41 -0
  66. package/dist/solverless-entry.d.ts.map +1 -0
  67. package/dist/solverless-entry.js +40 -0
  68. package/dist/solverless-entry.js.map +1 -0
  69. package/dist/solverless.d.ts +37 -0
  70. package/dist/solverless.d.ts.map +1 -0
  71. package/dist/solverless.js +36 -0
  72. package/dist/solverless.js.map +1 -0
  73. package/dist/world-entry.d.ts +31 -0
  74. package/dist/world-entry.d.ts.map +1 -0
  75. package/dist/world-entry.js +30 -0
  76. package/dist/world-entry.js.map +1 -0
  77. package/dist/world.d.ts +18 -0
  78. package/dist/world.d.ts.map +1 -0
  79. package/dist/world.js +17 -0
  80. package/dist/world.js.map +1 -0
  81. package/package.json +10 -2
@@ -1,19 +1,16 @@
1
- import { Body as CannonBody, Box as CannonBox, Cylinder as CannonCylinder, Plane as CannonPlane, Quaternion as CannonQuaternion, Sphere as CannonSphere, Vec3 as CannonVec3, World as CannonWorld } from "cannon-es";
1
+ import { createRapierPhysicsSync } from "@aura3d/physics-rapier";
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";
6
5
  import { raycastCollider, sphereCastCollider } from "./Raycast.js";
7
6
  import { RigidBody } from "./RigidBody.js";
8
- import { cloneVec3, dotVec3, lengthVec3, normalizeVec3, scaleVec3, subVec3 } from "./Shape.js";
7
+ import { cloneVec3, dotVec3, lengthVec3, normalizeVec3, scaleVec3, subVec3, validateFiniteVec3 } from "./Shape.js";
9
8
  import { timeOfImpact } from "./TimeOfImpact.js";
10
9
  export class PhysicsWorld {
11
10
  gravity;
12
11
  fixedDelta;
13
12
  solverIterations;
14
13
  enableSleeping;
15
- sleepVelocityThreshold;
16
- sleepDelay;
17
14
  continuousCollision;
18
15
  bodiesById = new Map();
19
16
  collidersById = new Map();
@@ -22,8 +19,10 @@ export class PhysicsWorld {
22
19
  eventQueue = new CollisionEventQueue();
23
20
  requestedBackend;
24
21
  backendSelection;
25
- cannonWorld;
26
- cannonBodiesByAuraId = new Map();
22
+ rapierWorld;
23
+ rapierBodiesByAuraId = new Map();
24
+ rapierCollidersByAuraId = new Map();
25
+ rapierJointsByConstraint = new Map();
27
26
  nextBodyId = 1;
28
27
  nextColliderId = 1;
29
28
  lastEvents = [];
@@ -35,10 +34,9 @@ export class PhysicsWorld {
35
34
  this.requestedBackend = descriptor.backend ?? "auto";
36
35
  this.gravity = cloneVec3(descriptor.gravity ?? [0, -9.81, 0]);
37
36
  this.fixedDelta = descriptor.fixedDelta ?? 1 / 60;
38
- this.solverIterations = descriptor.solverIterations ?? 1;
37
+ // Keep the established public default while configuring Rapier's native solver.
38
+ this.solverIterations = descriptor.solverIterations ?? 10;
39
39
  this.enableSleeping = descriptor.enableSleeping ?? true;
40
- this.sleepVelocityThreshold = descriptor.sleepVelocityThreshold ?? 0.02;
41
- this.sleepDelay = descriptor.sleepDelay ?? 0.5;
42
40
  this.continuousCollision = normalizeContinuousCollisionDescriptor(descriptor.continuousCollision);
43
41
  if (!Number.isFinite(this.fixedDelta) || this.fixedDelta <= 0) {
44
42
  throw new Error("fixedDelta must be a finite positive number.");
@@ -46,39 +44,51 @@ export class PhysicsWorld {
46
44
  if (!Number.isInteger(this.solverIterations) || this.solverIterations <= 0) {
47
45
  throw new Error("solverIterations must be a positive integer.");
48
46
  }
49
- if (!Number.isFinite(this.sleepVelocityThreshold) || this.sleepVelocityThreshold < 0) {
47
+ if (descriptor.sleepVelocityThreshold !== undefined && (!Number.isFinite(descriptor.sleepVelocityThreshold) || descriptor.sleepVelocityThreshold < 0)) {
50
48
  throw new Error("sleepVelocityThreshold must be finite and non-negative.");
51
49
  }
52
- if (!Number.isFinite(this.sleepDelay) || this.sleepDelay < 0) {
50
+ if (descriptor.sleepDelay !== undefined && (!Number.isFinite(descriptor.sleepDelay) || descriptor.sleepDelay < 0)) {
53
51
  throw new Error("sleepDelay must be finite and non-negative.");
54
52
  }
53
+ if (this.requestedBackend !== "auto" && this.requestedBackend !== "rapier") {
54
+ // Reached from JavaScript callers and from code compiled against 1.5.x, where
55
+ // `"aura-js"` was a selectable backend. Failing loudly is the point: the previous
56
+ // behaviour of that string was "quietly run a different solver".
57
+ throw new Error(`Unknown physics backend '${String(this.requestedBackend)}'. Aura3D 2.0 has one ` +
58
+ "production solver, 'rapier'; the legacy 'aura-js' and 'cannon-es' backends were removed " +
59
+ "(see docs/architecture/physics-backend-decision.md). Omit `backend` or pass " +
60
+ "'rapier'.");
61
+ }
55
62
  this.backendSelection = {
56
63
  requested: this.requestedBackend,
57
- active: this.requestedBackend === "aura-js" ? "aura-js" : "cannon-es",
64
+ active: "rapier",
58
65
  deterministic: true,
59
- jsFallbackAvailable: true,
60
66
  continuousCollision: this.continuousCollisionSelection(true)
61
67
  };
62
- if (this.backendSelection.active === "cannon-es") {
63
- this.cannonWorld = new CannonWorld({
64
- gravity: toCannonVec3(this.gravity),
65
- allowSleep: this.enableSleeping
66
- });
67
- this.cannonWorld.solver.iterations = this.solverIterations;
68
- this.cannonWorld.defaultContactMaterial.friction = 0.5;
69
- this.cannonWorld.defaultContactMaterial.restitution = 0;
70
- }
68
+ this.rapierWorld = createRapierPhysicsSync({ gravity: this.gravity });
69
+ this.rapierWorld.unsafeRapierWorld().integrationParameters.numSolverIterations = this.solverIterations;
71
70
  }
72
71
  createRigidBody(descriptor = {}) {
73
72
  const body = new RigidBody(this.nextBodyId, descriptor);
74
73
  this.nextBodyId += 1;
75
74
  this.bodiesById.set(body.id, body);
76
75
  this.bodyColliders.set(body.id, new Set());
77
- const cannonBody = this.createCannonBody(body);
78
- if (cannonBody) {
79
- this.cannonBodiesByAuraId.set(body.id, cannonBody);
80
- this.cannonWorld?.addBody(cannonBody);
81
- }
76
+ const rapierBody = this.rapierWorld.createRigidBody({
77
+ type: body.type === "static" ? "fixed" : body.type === "kinematic" ? "kinematic-position" : "dynamic",
78
+ position: body.position,
79
+ rotation: body.rotation,
80
+ linearVelocity: body.velocity,
81
+ angularVelocity: body.angularVelocity,
82
+ linearDamping: body.linearDamping,
83
+ angularDamping: body.angularDamping,
84
+ ccd: body.type === "dynamic",
85
+ canSleep: this.enableSleeping,
86
+ ...(body.type === "dynamic" ? { mass: body.mass } : {}),
87
+ ...(body.declaredInertia ? { principalAngularInertia: body.declaredInertia } : {})
88
+ });
89
+ if (body.sleeping)
90
+ rapierBody.sleep();
91
+ this.rapierBodiesByAuraId.set(body.id, rapierBody);
82
92
  return body;
83
93
  }
84
94
  createCollider(body, descriptor) {
@@ -90,7 +100,18 @@ export class PhysicsWorld {
90
100
  this.nextColliderId += 1;
91
101
  this.collidersById.set(collider.id, collider);
92
102
  this.bodyColliders.get(bodyId)?.add(collider.id);
93
- this.addCannonCollider(collider);
103
+ const rapierBody = this.rapierBodiesByAuraId.get(bodyId);
104
+ if (!rapierBody)
105
+ throw new Error(`Missing Rapier body for Aura body ${bodyId}.`);
106
+ const rapierCollider = this.rapierWorld.createCollider(rapierBody, {
107
+ shape: collider.shape,
108
+ density: 0,
109
+ friction: collider.material.friction,
110
+ restitution: collider.material.restitution,
111
+ sensor: collider.sensor,
112
+ collisionGroups: encodeRapierCollisionGroups(collider.filter.layer, collider.filter.mask)
113
+ });
114
+ this.rapierCollidersByAuraId.set(collider.id, rapierCollider);
94
115
  return collider;
95
116
  }
96
117
  createConstraint(descriptor) {
@@ -99,8 +120,23 @@ export class PhysicsWorld {
99
120
  }
100
121
  const constraint = new Constraint(descriptor);
101
122
  this.constraintsList.push(constraint);
123
+ this.rapierJointsByConstraint.set(constraint, this.createRapierJoint(constraint));
102
124
  return constraint;
103
125
  }
126
+ /**
127
+ * Change world gravity after construction.
128
+ *
129
+ * Needed for anything that flips or scales gravity at runtime — a low-gravity level, a
130
+ * zero-g section, a game that inverts it as a mechanic. Without a setter the only way to
131
+ * change it was to rebuild the world and lose every body.
132
+ */
133
+ setGravity(gravity) {
134
+ validateFiniteVec3(gravity, "world gravity");
135
+ this.gravity[0] = gravity[0];
136
+ this.gravity[1] = gravity[1];
137
+ this.gravity[2] = gravity[2];
138
+ this.rapierWorld.setGravity(this.gravity);
139
+ }
104
140
  getBody(id) {
105
141
  return this.bodiesById.get(id);
106
142
  }
@@ -129,14 +165,18 @@ export class PhysicsWorld {
129
165
  }
130
166
  this.bodyColliders.delete(id);
131
167
  this.bodiesById.delete(id);
132
- const cannonBody = this.cannonBodiesByAuraId.get(id);
133
- if (cannonBody) {
134
- this.cannonWorld?.removeBody(cannonBody);
135
- this.cannonBodiesByAuraId.delete(id);
168
+ const rapierBody = this.rapierBodiesByAuraId.get(id);
169
+ if (rapierBody) {
170
+ this.rapierWorld.removeBody(rapierBody);
171
+ this.rapierBodiesByAuraId.delete(id);
136
172
  }
137
173
  for (let index = this.constraintsList.length - 1; index >= 0; index -= 1) {
138
174
  const constraint = this.constraintsList[index];
139
175
  if (constraint.bodyA.id === id || constraint.bodyB.id === id) {
176
+ const joint = this.rapierJointsByConstraint.get(constraint);
177
+ if (joint)
178
+ joint.remove();
179
+ this.rapierJointsByConstraint.delete(constraint);
140
180
  this.constraintsList.splice(index, 1);
141
181
  }
142
182
  }
@@ -148,6 +188,10 @@ export class PhysicsWorld {
148
188
  }
149
189
  this.bodyColliders.get(collider.bodyId)?.delete(id);
150
190
  this.collidersById.delete(id);
191
+ const rapierCollider = this.rapierCollidersByAuraId.get(id);
192
+ if (rapierCollider)
193
+ rapierCollider.remove();
194
+ this.rapierCollidersByAuraId.delete(id);
151
195
  const removalEvents = this.eventQueue.removeCollider(id);
152
196
  if (removalEvents.length > 0) {
153
197
  this.lastEvents = [...this.lastEvents, ...removalEvents].sort((a, b) => a.pairKey.localeCompare(b.pairKey) || a.type.localeCompare(b.type));
@@ -157,51 +201,7 @@ export class PhysicsWorld {
157
201
  if (!Number.isFinite(dt) || dt <= 0) {
158
202
  throw new Error("PhysicsWorld.step dt must be finite and positive.");
159
203
  }
160
- if (this.cannonWorld) {
161
- return this.stepCannon(dt);
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
- ]));
174
- let contacts = [];
175
- for (let subStep = 0; subStep < continuousStep.subSteps; subStep += 1) {
176
- for (const body of this.bodyValues()) {
177
- body.integrate(subDelta, this.gravity, false);
178
- }
179
- for (const constraint of this.constraintsList) {
180
- constraint.solve();
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
- }
200
- }
201
- this.lastEvents = this.eventQueue.update(contacts);
202
- this.updateSleeping(dt, contacts);
203
- this.steps += 1;
204
- return this.lastEvents;
204
+ return this.stepProduction(dt);
205
205
  }
206
206
  drainEvents() {
207
207
  const events = this.lastEvents;
@@ -220,6 +220,9 @@ export class PhysicsWorld {
220
220
  if (!body) {
221
221
  continue;
222
222
  }
223
+ if (options.ignoreColliders?.includes(collider.id) || options.ignoreBodies?.includes(collider.bodyId)) {
224
+ continue;
225
+ }
223
226
  const hit = raycastCollider(origin, normalized, collider, body, options);
224
227
  if (hit) {
225
228
  hits.push(hit);
@@ -239,6 +242,9 @@ export class PhysicsWorld {
239
242
  if (!body) {
240
243
  continue;
241
244
  }
245
+ if (options.ignoreColliders?.includes(collider.id) || options.ignoreBodies?.includes(collider.bodyId)) {
246
+ continue;
247
+ }
242
248
  const hit = sphereCastCollider(origin, radius, normalized, collider, body, options);
243
249
  if (hit) {
244
250
  hits.push(hit);
@@ -276,15 +282,61 @@ export class PhysicsWorld {
276
282
  }
277
283
  };
278
284
  }
285
+ /** Release the selected native solver and every world-owned handle. Idempotent. */
286
+ dispose() {
287
+ this.rapierWorld.dispose();
288
+ this.rapierJointsByConstraint.clear();
289
+ this.rapierCollidersByAuraId.clear();
290
+ this.rapierBodiesByAuraId.clear();
291
+ this.constraintsList.length = 0;
292
+ this.collidersById.clear();
293
+ this.bodiesById.clear();
294
+ this.bodyColliders.clear();
295
+ this.lastEvents = [];
296
+ }
279
297
  detectContacts() {
280
298
  const potentialPairs = this.collectPotentialPairs();
281
299
  this.lastBroadphasePairs = potentialPairs.length;
282
300
  const contacts = [];
283
301
  for (const [a, bodyA, b, bodyB] of potentialPairs) {
284
- const contact = buildContact(a, bodyA, b, bodyB);
285
- if (contact) {
286
- contacts.push(contact);
302
+ const colliderA = this.rapierCollidersByAuraId.get(a.id)?.unsafeRapierCollider();
303
+ const colliderB = this.rapierCollidersByAuraId.get(b.id)?.unsafeRapierCollider();
304
+ if (!colliderA || !colliderB)
305
+ continue;
306
+ if (a.sensor || b.sensor) {
307
+ if (!this.rapierWorld.unsafeRapierWorld().intersectionPair(colliderA, colliderB))
308
+ continue;
309
+ const separation = subVec3(bodyB.position, bodyA.position);
310
+ const normal = lengthVec3(separation) > 1e-9 ? normalizeVec3(separation) : [0, 1, 0];
311
+ contacts.push({ colliderA: a.id, colliderB: b.id, bodyA: bodyA.id, bodyB: bodyB.id, normal, penetration: 0, sensor: true });
312
+ continue;
287
313
  }
314
+ let best;
315
+ this.rapierWorld.unsafeRapierWorld().contactPair(colliderA, colliderB, (manifold, flipped) => {
316
+ const normalValue = manifold.normal();
317
+ const direction = flipped ? -1 : 1;
318
+ const normal = [normalValue.x * direction, normalValue.y * direction, normalValue.z * direction];
319
+ const count = Math.max(manifold.numSolverContacts(), manifold.numContacts());
320
+ for (let index = 0; index < count; index += 1) {
321
+ const distance = index < manifold.numSolverContacts() ? manifold.solverContactDist(index) : manifold.contactDist(index);
322
+ const penetration = Math.max(0, -distance);
323
+ const pointValue = index < manifold.numSolverContacts() ? manifold.solverContactPoint(index) : undefined;
324
+ const candidate = {
325
+ colliderA: a.id,
326
+ colliderB: b.id,
327
+ bodyA: bodyA.id,
328
+ bodyB: bodyB.id,
329
+ normal,
330
+ penetration,
331
+ ...(pointValue ? { point: [pointValue.x, pointValue.y, pointValue.z] } : {}),
332
+ sensor: false
333
+ };
334
+ if (!best || candidate.penetration > best.penetration)
335
+ best = candidate;
336
+ }
337
+ });
338
+ if (best)
339
+ contacts.push(best);
288
340
  }
289
341
  contacts.sort((a, b) => a.colliderA - b.colliderA || a.colliderB - b.colliderB);
290
342
  return contacts;
@@ -345,203 +397,88 @@ export class PhysicsWorld {
345
397
  this.lastBroadphaseProfile = profile;
346
398
  return pairs;
347
399
  }
348
- resolveContact(contact, contactImpulses) {
349
- if (contact.sensor) {
350
- return;
351
- }
352
- const bodyA = this.bodiesById.get(contact.bodyA);
353
- const bodyB = this.bodiesById.get(contact.bodyB);
354
- if (!bodyA || !bodyB) {
355
- return;
356
- }
357
- const invMassSum = bodyA.inverseMass + bodyB.inverseMass;
358
- if (invMassSum <= 0) {
359
- return;
360
- }
361
- const correction = scaleVec3(contact.normal, contact.penetration / invMassSum);
362
- if (bodyA.inverseMass > 0) {
363
- bodyA.position = [bodyA.position[0] - correction[0] * bodyA.inverseMass, bodyA.position[1] - correction[1] * bodyA.inverseMass, bodyA.position[2] - correction[2] * bodyA.inverseMass];
364
- }
365
- if (bodyB.inverseMass > 0) {
366
- bodyB.position = [bodyB.position[0] + correction[0] * bodyB.inverseMass, bodyB.position[1] + correction[1] * bodyB.inverseMass, bodyB.position[2] + correction[2] * bodyB.inverseMass];
367
- }
368
- const relativeVelocity = relativeContactVelocity(bodyA, bodyB, contact.point);
369
- const velocityAlongNormal = dotVec3(relativeVelocity, contact.normal);
370
- const materialA = this.effectiveMaterial(bodyA, contact.colliderA);
371
- const materialB = this.effectiveMaterial(bodyB, contact.colliderB);
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] };
375
- let normalImpulseMagnitude = 0;
376
- if (velocityAlongNormal <= 0) {
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);
381
- }
382
- const updatedRelativeVelocity = relativeContactVelocity(bodyA, bodyB, contact.point);
383
- const updatedNormalVelocity = dotVec3(updatedRelativeVelocity, contact.normal);
384
- const tangentVelocity = subVec3(updatedRelativeVelocity, scaleVec3(contact.normal, updatedNormalVelocity));
385
- const tangentSpeed = Math.hypot(tangentVelocity[0], tangentVelocity[1], tangentVelocity[2]);
386
- if (tangentSpeed > 1e-9) {
387
- const friction = Math.sqrt(Math.max(0, materialA.friction) * Math.max(0, materialB.friction));
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);
405
- }
406
- }
407
- contactImpulses.set(pairKey, impulseState);
408
- }
409
- effectiveMaterial(body, colliderId) {
410
- const collider = this.collidersById.get(colliderId);
411
- return {
412
- restitution: Math.max(body.restitution, collider?.material.restitution ?? 0),
413
- friction: Math.max(body.friction, collider?.material.friction ?? 0)
414
- };
415
- }
416
- applyImpulsePair(bodyA, bodyB, impulse, point) {
417
- if (bodyA.inverseMass > 0) {
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];
422
- if (bodyA.sleeping && bodyA.speedSquared() > this.sleepVelocityThreshold * this.sleepVelocityThreshold) {
423
- bodyA.wake();
400
+ createRapierJoint(constraint) {
401
+ const bodyA = this.rapierBodiesByAuraId.get(constraint.bodyA.id);
402
+ const bodyB = this.rapierBodiesByAuraId.get(constraint.bodyB.id);
403
+ if (!bodyA || !bodyB)
404
+ throw new Error("Cannot create a Rapier joint for missing bodies.");
405
+ return this.rapierWorld.createJoint({
406
+ type: constraint.type,
407
+ localAnchorA: constraint.type === "fixed"
408
+ && constraint.localAnchorA.every((value) => value === 0)
409
+ && constraint.localAnchorB.every((value) => value === 0)
410
+ ? constraint.restOffset
411
+ : constraint.localAnchorA,
412
+ localAnchorB: constraint.localAnchorB,
413
+ axis: constraint.axis,
414
+ restLength: constraint.restLength,
415
+ stiffness: constraint.stiffness,
416
+ damping: constraint.damping,
417
+ motorSpeed: constraint.motorSpeed(),
418
+ maxMotorTorque: Number.isFinite(constraint.maxMotorTorque) ? constraint.maxMotorTorque : Number.MAX_VALUE,
419
+ ...(constraint.limits ? { limits: constraint.limits } : {})
420
+ }, bodyA, bodyB);
421
+ }
422
+ synchronizeRapierJoints() {
423
+ for (const constraint of this.constraintsList) {
424
+ const existing = this.rapierJointsByConstraint.get(constraint);
425
+ if (!constraint.isEnabled()) {
426
+ if (existing)
427
+ existing.remove();
428
+ this.rapierJointsByConstraint.delete(constraint);
429
+ continue;
424
430
  }
425
- }
426
- if (bodyB.inverseMass > 0) {
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];
431
- if (bodyB.sleeping && bodyB.speedSquared() > this.sleepVelocityThreshold * this.sleepVelocityThreshold) {
432
- bodyB.wake();
431
+ const joint = existing ?? this.createRapierJoint(constraint);
432
+ this.rapierJointsByConstraint.set(constraint, joint);
433
+ if (constraint.type === "motorised-hinge") {
434
+ joint.configureMotor(constraint.motorSpeed(), Number.isFinite(constraint.maxMotorTorque) ? constraint.maxMotorTorque : Number.MAX_VALUE);
433
435
  }
434
436
  }
435
437
  }
436
- updateSleeping(dt, contacts) {
437
- if (!this.enableSleeping) {
438
- return;
439
- }
440
- const contactBodyIds = new Set();
441
- for (const contact of contacts) {
442
- if (!contact.sensor) {
443
- contactBodyIds.add(contact.bodyA);
444
- contactBodyIds.add(contact.bodyB);
445
- }
446
- }
447
- const gravityMagnitudeSquared = dotVec3(this.gravity, this.gravity);
448
- const thresholdSquared = this.sleepVelocityThreshold * this.sleepVelocityThreshold;
438
+ stepProduction(dt) {
439
+ this.rapierWorld.setGravity(this.gravity);
440
+ const stepForces = new Map();
449
441
  for (const body of this.bodyValues()) {
450
- if (body.type !== "dynamic") {
442
+ const rapierBody = this.rapierBodiesByAuraId.get(body.id);
443
+ if (!rapierBody)
451
444
  continue;
445
+ const force = body.pendingForce();
446
+ const torque = body.pendingTorque();
447
+ if (force[0] !== 0 || force[1] !== 0 || force[2] !== 0 || torque[0] !== 0 || torque[1] !== 0 || torque[2] !== 0) {
448
+ stepForces.set(body.id, { force, torque });
449
+ body.clearForces();
452
450
  }
453
- if (body.speedSquared() > thresholdSquared) {
454
- body.resetSleepTimer();
455
- continue;
456
- }
457
- const supportedOrUnaccelerated = contactBodyIds.has(body.id) || gravityMagnitudeSquared <= thresholdSquared;
458
- if (!supportedOrUnaccelerated) {
459
- body.resetSleepTimer();
460
- continue;
461
- }
462
- if (body.accumulateSleepTime(dt) >= this.sleepDelay) {
463
- body.sleep();
464
- }
465
- }
466
- }
467
- createCannonBody(body) {
468
- if (!this.cannonWorld)
469
- return undefined;
470
- const cannonBody = new CannonBody({
471
- type: body.type === "static" ? CannonBody.STATIC : body.type === "kinematic" ? CannonBody.KINEMATIC : CannonBody.DYNAMIC,
472
- mass: body.type === "dynamic" ? body.mass : 0,
473
- position: toCannonVec3(body.position),
474
- velocity: toCannonVec3(body.velocity),
475
- quaternion: new CannonQuaternion(body.rotation[0], body.rotation[1], body.rotation[2], body.rotation[3]),
476
- angularVelocity: toCannonVec3(body.angularVelocity),
477
- linearDamping: body.linearDamping,
478
- angularDamping: body.angularDamping,
479
- allowSleep: this.enableSleeping,
480
- sleepSpeedLimit: this.sleepVelocityThreshold,
481
- sleepTimeLimit: this.sleepDelay
482
- });
483
- if (body.sleeping)
484
- cannonBody.sleep();
485
- return cannonBody;
486
- }
487
- addCannonCollider(collider) {
488
- if (!this.cannonWorld)
489
- return;
490
- const body = this.bodiesById.get(collider.bodyId);
491
- const cannonBody = this.cannonBodiesByAuraId.get(collider.bodyId);
492
- if (!body || !cannonBody)
493
- return;
494
- const resolved = toCannonShape(collider.shape);
495
- if (!resolved) {
496
- this.disableCannonBackend(`unsupported shape '${collider.shape.kind}'`);
497
- return;
498
- }
499
- cannonBody.collisionFilterGroup = collider.filter.layer;
500
- cannonBody.collisionFilterMask = collider.filter.mask;
501
- cannonBody.isTrigger = cannonBody.isTrigger || collider.sensor;
502
- cannonBody.addShape(resolved.shape, resolved.offset, resolved.orientation);
503
- cannonBody.updateMassProperties();
504
- syncCannonFromAura(body, cannonBody);
505
- }
506
- disableCannonBackend(reason) {
507
- if (!this.cannonWorld)
508
- return;
509
- this.cannonWorld = undefined;
510
- this.cannonBodiesByAuraId.clear();
511
- this.backendSelection = {
512
- requested: this.requestedBackend,
513
- active: "aura-js",
514
- fallback: reason,
515
- deterministic: true,
516
- jsFallbackAvailable: true,
517
- continuousCollision: this.continuousCollisionSelection(true)
518
- };
519
- }
520
- stepCannon(dt) {
521
- if (!this.cannonWorld)
522
- return [];
523
- this.cannonWorld.gravity.copy(toCannonVec3(this.gravity));
524
- for (const body of this.bodyValues()) {
525
- const cannonBody = this.cannonBodiesByAuraId.get(body.id);
526
- if (cannonBody)
527
- syncCannonFromAura(body, cannonBody);
451
+ syncRapierFromAura(body, rapierBody);
528
452
  }
453
+ this.synchronizeRapierJoints();
529
454
  const continuousStep = this.planContinuousCollisionStep(dt);
530
455
  this.lastContinuousCollisionStep = continuousStep;
531
456
  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);
457
+ const subDelta = dt / continuousStep.subSteps;
458
+ for (let index = 0; index < continuousStep.subSteps; index += 1) {
459
+ for (const [bodyId, pending] of stepForces) {
460
+ const raw = this.rapierBodiesByAuraId.get(bodyId)?.unsafeRapierBody();
461
+ if (!raw)
462
+ continue;
463
+ raw.resetForces(false);
464
+ raw.resetTorques(false);
465
+ raw.addForce(toRapierVector(pending.force), true);
466
+ raw.addTorque(toRapierVector(pending.torque), true);
467
+ }
468
+ this.rapierWorld.step(subDelta);
469
+ }
470
+ for (const bodyId of stepForces.keys()) {
471
+ const raw = this.rapierBodiesByAuraId.get(bodyId)?.unsafeRapierBody();
472
+ raw?.resetForces(false);
473
+ raw?.resetTorques(false);
536
474
  }
537
475
  for (const body of this.bodyValues()) {
538
- const cannonBody = this.cannonBodiesByAuraId.get(body.id);
539
- if (cannonBody)
540
- syncAuraFromCannon(cannonBody, body);
476
+ const rapierBody = this.rapierBodiesByAuraId.get(body.id);
477
+ if (rapierBody)
478
+ syncAuraFromRapier(rapierBody, body);
541
479
  }
542
480
  const contacts = this.detectContacts();
543
481
  this.lastEvents = this.eventQueue.update(contacts);
544
- this.updateSleeping(dt, contacts);
545
482
  this.steps += 1;
546
483
  return this.lastEvents;
547
484
  }
@@ -638,7 +575,7 @@ export class PhysicsWorld {
638
575
  return {
639
576
  active,
640
577
  mode: active ? "adaptive-substeps" : "disabled",
641
- provider: active ? "aura3d-adaptive-substep-wrapper" : "none",
578
+ provider: active ? "rapier-native-ccd+adaptive-substeps" : "none",
642
579
  maxSubSteps: this.continuousCollision.maxSubSteps,
643
580
  motionThreshold: this.continuousCollision.motionThreshold,
644
581
  onSubstepLimit: this.continuousCollision.onSubstepLimit,
@@ -707,48 +644,40 @@ function colliderBoundingRadius(shape) {
707
644
  }
708
645
  return 0;
709
646
  }
710
- function toCannonVec3(value) {
711
- return new CannonVec3(value[0], value[1], value[2]);
712
- }
713
- function fromCannonVec3(value) {
714
- return [value.x, value.y, value.z];
715
- }
716
- function syncCannonFromAura(body, cannonBody) {
717
- cannonBody.position.set(body.position[0], body.position[1], body.position[2]);
718
- cannonBody.velocity.set(body.velocity[0], body.velocity[1], body.velocity[2]);
719
- cannonBody.quaternion.set(body.rotation[0], body.rotation[1], body.rotation[2], body.rotation[3]);
720
- cannonBody.angularVelocity.set(body.angularVelocity[0], body.angularVelocity[1], body.angularVelocity[2]);
647
+ function toRapierVector(value) {
648
+ return { x: value[0], y: value[1], z: value[2] };
649
+ }
650
+ function encodeRapierCollisionGroups(layer, mask) {
651
+ return (((layer & 0xffff) << 16) | (mask & 0xffff)) >>> 0;
652
+ }
653
+ /** Mirror the public Aura body state onto its sole physical-simulation owner. */
654
+ function syncRapierFromAura(body, rapierBody) {
655
+ const raw = rapierBody.unsafeRapierBody();
656
+ raw.setTranslation(toRapierVector(body.position), false);
657
+ raw.setRotation({ x: body.rotation[0], y: body.rotation[1], z: body.rotation[2], w: body.rotation[3] }, false);
658
+ raw.setLinvel(toRapierVector(body.velocity), false);
659
+ raw.setAngvel(toRapierVector(body.angularVelocity), false);
660
+ raw.setLinearDamping(body.linearDamping);
661
+ raw.setAngularDamping(body.angularDamping);
721
662
  if (body.sleeping)
722
- cannonBody.sleep();
723
- else
724
- cannonBody.wakeUp();
725
- }
726
- function syncAuraFromCannon(cannonBody, body) {
663
+ raw.sleep();
664
+ else if (raw.isSleeping())
665
+ raw.wakeUp();
666
+ }
667
+ /** Publish Rapier's solved transform and velocity through the stable Aura wrapper. */
668
+ function syncAuraFromRapier(rapierBody, body) {
669
+ const raw = rapierBody.unsafeRapierBody();
670
+ const position = raw.translation();
671
+ const rotation = raw.rotation();
672
+ const velocity = raw.linvel();
673
+ const angularVelocity = raw.angvel();
727
674
  body.previousPosition = cloneVec3(body.position);
728
675
  body.previousRotation = [body.rotation[0], body.rotation[1], body.rotation[2], body.rotation[3]];
729
- body.position = fromCannonVec3(cannonBody.position);
730
- body.velocity = fromCannonVec3(cannonBody.velocity);
731
- body.angularVelocity = fromCannonVec3(cannonBody.angularVelocity);
732
- body.rotation = [cannonBody.quaternion.x, cannonBody.quaternion.y, cannonBody.quaternion.z, cannonBody.quaternion.w];
733
- body.sleeping = cannonBody.sleepState === CannonBody.SLEEPING;
734
- }
735
- function toCannonShape(shape) {
736
- if (shape.kind === "box")
737
- return { shape: new CannonBox(toCannonVec3(shape.halfExtents)) };
738
- if (shape.kind === "sphere")
739
- return { shape: new CannonSphere(shape.radius) };
740
- if (shape.kind === "capsule")
741
- return { shape: new CannonCylinder(shape.radius, shape.radius, shape.halfHeight * 2 + shape.radius * 2, 12) };
742
- if (shape.kind === "plane") {
743
- const orientation = new CannonQuaternion();
744
- orientation.setFromVectors(new CannonVec3(0, 0, 1), toCannonVec3(shape.normal));
745
- return {
746
- shape: new CannonPlane(),
747
- offset: toCannonVec3(scaleVec3(shape.normal, shape.constant)),
748
- orientation
749
- };
750
- }
751
- return undefined;
676
+ body.position = [position.x, position.y, position.z];
677
+ body.rotation = [rotation.x, rotation.y, rotation.z, rotation.w];
678
+ body.velocity = [velocity.x, velocity.y, velocity.z];
679
+ body.angularVelocity = [angularVelocity.x, angularVelocity.y, angularVelocity.z];
680
+ body.sleeping = raw.isSleeping();
752
681
  }
753
682
  function totalKineticEnergy(bodies) {
754
683
  let total = 0;
@@ -765,31 +694,6 @@ function totalKineticEnergy(bodies) {
765
694
  }
766
695
  return total;
767
696
  }
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
697
  function addVectors(a, b) {
794
698
  return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
795
699
  }
@@ -837,307 +741,4 @@ function pushPotentialPair(pairs, seen, a, b) {
837
741
  seen.add(key);
838
742
  pairs.push([first.collider, first.body, second.collider, second.body]);
839
743
  }
840
- function buildContact(a, bodyA, b, bodyB) {
841
- const ab = subVec3(bodyB.position, bodyA.position);
842
- if (a.shape.kind === "plane" || b.shape.kind === "plane") {
843
- const planeCollider = a.shape.kind === "plane" ? a : b;
844
- const planeBody = a.shape.kind === "plane" ? bodyA : bodyB;
845
- const otherCollider = a.shape.kind === "plane" ? b : a;
846
- const otherBody = a.shape.kind === "plane" ? bodyB : bodyA;
847
- if (planeCollider.shape.kind !== "plane") {
848
- return undefined;
849
- }
850
- const radius = supportRadius(otherCollider);
851
- const signedDistance = dotVec3(planeCollider.shape.normal, otherBody.position) + planeCollider.shape.constant + dotVec3(planeCollider.shape.normal, planeBody.position);
852
- if (signedDistance > radius) {
853
- return undefined;
854
- }
855
- const normal = a.shape.kind === "plane" ? planeCollider.shape.normal : scaleVec3(planeCollider.shape.normal, -1);
856
- return {
857
- colliderA: a.id,
858
- colliderB: b.id,
859
- bodyA: bodyA.id,
860
- bodyB: bodyB.id,
861
- normal,
862
- penetration: radius - signedDistance,
863
- sensor: a.sensor || b.sensor
864
- };
865
- }
866
- if (a.shape.kind === "sphere" && b.shape.kind === "sphere") {
867
- const distance = Math.hypot(ab[0], ab[1], ab[2]);
868
- const radiusSum = a.shape.radius + b.shape.radius;
869
- if (distance >= radiusSum) {
870
- return undefined;
871
- }
872
- return {
873
- colliderA: a.id,
874
- colliderB: b.id,
875
- bodyA: bodyA.id,
876
- bodyB: bodyB.id,
877
- normal: distance > 1e-9 ? [ab[0] / distance, ab[1] / distance, ab[2] / distance] : [1, 0, 0],
878
- penetration: radiusSum - distance,
879
- sensor: a.sensor || b.sensor
880
- };
881
- }
882
- if ((a.shape.kind === "sphere" && b.shape.kind === "box") || (a.shape.kind === "box" && b.shape.kind === "sphere")) {
883
- return buildSphereBoxContact(a, bodyA, b, bodyB);
884
- }
885
- if ((a.shape.kind === "capsule" && b.shape.kind === "sphere") || (a.shape.kind === "sphere" && b.shape.kind === "capsule")) {
886
- return buildCapsuleSphereContact(a, bodyA, b, bodyB);
887
- }
888
- if ((a.shape.kind === "capsule" && b.shape.kind === "box") || (a.shape.kind === "box" && b.shape.kind === "capsule")) {
889
- return buildCapsuleBoxContact(a, bodyA, b, bodyB);
890
- }
891
- if (a.shape.kind === "capsule" && b.shape.kind === "capsule") {
892
- return buildCapsuleCapsuleContact(a, bodyA, b, bodyB);
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
- }
907
- const boundsA = a.bounds(bodyA.position);
908
- const boundsB = b.bounds(bodyB.position);
909
- const overlapX = Math.min(boundsA.max[0] - boundsB.min[0], boundsB.max[0] - boundsA.min[0]);
910
- const overlapY = Math.min(boundsA.max[1] - boundsB.min[1], boundsB.max[1] - boundsA.min[1]);
911
- const overlapZ = Math.min(boundsA.max[2] - boundsB.min[2], boundsB.max[2] - boundsA.min[2]);
912
- if (overlapX <= 0 || overlapY <= 0 || overlapZ <= 0) {
913
- return undefined;
914
- }
915
- let normal = [Math.sign(ab[0]) || 1, 0, 0];
916
- let penetration = overlapX;
917
- if (overlapY < penetration) {
918
- normal = [0, Math.sign(ab[1]) || 1, 0];
919
- penetration = overlapY;
920
- }
921
- if (overlapZ < penetration) {
922
- normal = [0, 0, Math.sign(ab[2]) || 1];
923
- penetration = overlapZ;
924
- }
925
- return {
926
- colliderA: a.id,
927
- colliderB: b.id,
928
- bodyA: bodyA.id,
929
- bodyB: bodyB.id,
930
- normal,
931
- penetration,
932
- sensor: a.sensor || b.sensor
933
- };
934
- }
935
- function buildSphereBoxContact(a, bodyA, b, bodyB) {
936
- const sphereCollider = a.shape.kind === "sphere" ? a : b;
937
- const sphereBody = a.shape.kind === "sphere" ? bodyA : bodyB;
938
- const boxCollider = a.shape.kind === "box" ? a : b;
939
- const boxBody = a.shape.kind === "box" ? bodyA : bodyB;
940
- if (sphereCollider.shape.kind !== "sphere" || boxCollider.shape.kind !== "box") {
941
- return undefined;
942
- }
943
- const boxBounds = boxCollider.bounds(boxBody.position);
944
- const closest = [
945
- clamp(sphereBody.position[0], boxBounds.min[0], boxBounds.max[0]),
946
- clamp(sphereBody.position[1], boxBounds.min[1], boxBounds.max[1]),
947
- clamp(sphereBody.position[2], boxBounds.min[2], boxBounds.max[2])
948
- ];
949
- const fromBoxToSphere = subVec3(sphereBody.position, closest);
950
- const distance = Math.hypot(fromBoxToSphere[0], fromBoxToSphere[1], fromBoxToSphere[2]);
951
- if (distance >= sphereCollider.shape.radius) {
952
- return undefined;
953
- }
954
- const normalFromBoxToSphere = distance > 1e-9 ? scaleVec3(fromBoxToSphere, 1 / distance) : axisFromBoxCenter(boxBody.position, sphereBody.position, boxCollider.shape.halfExtents);
955
- const normal = a.shape.kind === "box" ? normalFromBoxToSphere : scaleVec3(normalFromBoxToSphere, -1);
956
- return {
957
- colliderA: a.id,
958
- colliderB: b.id,
959
- bodyA: bodyA.id,
960
- bodyB: bodyB.id,
961
- normal,
962
- penetration: sphereCollider.shape.radius - distance,
963
- sensor: a.sensor || b.sensor
964
- };
965
- }
966
- function buildCapsuleSphereContact(a, bodyA, b, bodyB) {
967
- const capsuleCollider = a.shape.kind === "capsule" ? a : b;
968
- const capsuleBody = a.shape.kind === "capsule" ? bodyA : bodyB;
969
- const sphereCollider = a.shape.kind === "sphere" ? a : b;
970
- const sphereBody = a.shape.kind === "sphere" ? bodyA : bodyB;
971
- if (capsuleCollider.shape.kind !== "capsule" || sphereCollider.shape.kind !== "sphere") {
972
- return undefined;
973
- }
974
- const closest = closestPointOnSegment(sphereBody.position, capsuleSegment(capsuleBody.position, capsuleCollider.shape.halfHeight));
975
- const delta = subVec3(sphereBody.position, closest);
976
- const distance = Math.hypot(delta[0], delta[1], delta[2]);
977
- const radiusSum = capsuleCollider.shape.radius + sphereCollider.shape.radius;
978
- if (distance >= radiusSum) {
979
- return undefined;
980
- }
981
- const normalFromCapsuleToSphere = distance > 1e-9 ? scaleVec3(delta, 1 / distance) : [1, 0, 0];
982
- const normal = a.shape.kind === "capsule" ? normalFromCapsuleToSphere : scaleVec3(normalFromCapsuleToSphere, -1);
983
- return {
984
- colliderA: a.id,
985
- colliderB: b.id,
986
- bodyA: bodyA.id,
987
- bodyB: bodyB.id,
988
- normal,
989
- penetration: radiusSum - distance,
990
- sensor: a.sensor || b.sensor
991
- };
992
- }
993
- function buildCapsuleCapsuleContact(a, bodyA, b, bodyB) {
994
- if (a.shape.kind !== "capsule" || b.shape.kind !== "capsule") {
995
- return undefined;
996
- }
997
- const closest = closestPointsBetweenSegments(capsuleSegment(bodyA.position, a.shape.halfHeight), capsuleSegment(bodyB.position, b.shape.halfHeight));
998
- const delta = subVec3(closest.b, closest.a);
999
- const distance = Math.hypot(delta[0], delta[1], delta[2]);
1000
- const radiusSum = a.shape.radius + b.shape.radius;
1001
- if (distance >= radiusSum) {
1002
- return undefined;
1003
- }
1004
- return {
1005
- colliderA: a.id,
1006
- colliderB: b.id,
1007
- bodyA: bodyA.id,
1008
- bodyB: bodyB.id,
1009
- normal: distance > 1e-9 ? scaleVec3(delta, 1 / distance) : [1, 0, 0],
1010
- penetration: radiusSum - distance,
1011
- sensor: a.sensor || b.sensor
1012
- };
1013
- }
1014
- function buildCapsuleBoxContact(a, bodyA, b, bodyB) {
1015
- const capsuleCollider = a.shape.kind === "capsule" ? a : b;
1016
- const capsuleBody = a.shape.kind === "capsule" ? bodyA : bodyB;
1017
- const boxCollider = a.shape.kind === "box" ? a : b;
1018
- const boxBody = a.shape.kind === "box" ? bodyA : bodyB;
1019
- if (capsuleCollider.shape.kind !== "capsule" || boxCollider.shape.kind !== "box") {
1020
- return undefined;
1021
- }
1022
- const bounds = boxCollider.bounds(boxBody.position);
1023
- const closest = closestVerticalSegmentPointToAabb(capsuleBody.position, capsuleCollider.shape.halfHeight, bounds);
1024
- const fromBoxToCapsule = subVec3(closest.segmentPoint, closest.boxPoint);
1025
- const distance = Math.hypot(fromBoxToCapsule[0], fromBoxToCapsule[1], fromBoxToCapsule[2]);
1026
- if (distance >= capsuleCollider.shape.radius) {
1027
- return undefined;
1028
- }
1029
- const normalFromBoxToCapsule = distance > 1e-9 ? scaleVec3(fromBoxToCapsule, 1 / distance) : axisFromBoxCenter(boxBody.position, closest.segmentPoint, boxCollider.shape.halfExtents);
1030
- const normal = a.shape.kind === "box" ? normalFromBoxToCapsule : scaleVec3(normalFromBoxToCapsule, -1);
1031
- return {
1032
- colliderA: a.id,
1033
- colliderB: b.id,
1034
- bodyA: bodyA.id,
1035
- bodyB: bodyB.id,
1036
- normal,
1037
- penetration: capsuleCollider.shape.radius - distance,
1038
- sensor: a.sensor || b.sensor
1039
- };
1040
- }
1041
- function capsuleSegment(position, halfHeight) {
1042
- return {
1043
- start: [position[0], position[1] - halfHeight, position[2]],
1044
- end: [position[0], position[1] + halfHeight, position[2]]
1045
- };
1046
- }
1047
- function closestPointOnSegment(point, segment) {
1048
- const ab = subVec3(segment.end, segment.start);
1049
- const lengthSquared = dotVec3(ab, ab);
1050
- if (lengthSquared <= 1e-9) {
1051
- return segment.start;
1052
- }
1053
- const t = clamp(dotVec3(subVec3(point, segment.start), ab) / lengthSquared, 0, 1);
1054
- return [
1055
- segment.start[0] + ab[0] * t,
1056
- segment.start[1] + ab[1] * t,
1057
- segment.start[2] + ab[2] * t
1058
- ];
1059
- }
1060
- function closestPointsBetweenSegments(a, b) {
1061
- const d1 = subVec3(a.end, a.start);
1062
- const d2 = subVec3(b.end, b.start);
1063
- const r = subVec3(a.start, b.start);
1064
- const aLength = dotVec3(d1, d1);
1065
- const bLength = dotVec3(d2, d2);
1066
- const d12 = dotVec3(d1, d2);
1067
- const d1r = dotVec3(d1, r);
1068
- const d2r = dotVec3(d2, r);
1069
- const denominator = aLength * bLength - d12 * d12;
1070
- let s = denominator <= 1e-9 ? 0 : clamp((d12 * d2r - bLength * d1r) / denominator, 0, 1);
1071
- let t = bLength <= 1e-9 ? 0 : (d12 * s + d2r) / bLength;
1072
- if (t < 0) {
1073
- t = 0;
1074
- s = aLength <= 1e-9 ? 0 : clamp(-d1r / aLength, 0, 1);
1075
- }
1076
- else if (t > 1) {
1077
- t = 1;
1078
- s = aLength <= 1e-9 ? 0 : clamp((d12 - d1r) / aLength, 0, 1);
1079
- }
1080
- return {
1081
- a: [a.start[0] + d1[0] * s, a.start[1] + d1[1] * s, a.start[2] + d1[2] * s],
1082
- b: [b.start[0] + d2[0] * t, b.start[1] + d2[1] * t, b.start[2] + d2[2] * t]
1083
- };
1084
- }
1085
- function closestVerticalSegmentPointToAabb(position, halfHeight, bounds) {
1086
- const segmentMinY = position[1] - halfHeight;
1087
- const segmentMaxY = position[1] + halfHeight;
1088
- let segmentY;
1089
- let boxY;
1090
- if (segmentMaxY < bounds.min[1]) {
1091
- segmentY = segmentMaxY;
1092
- boxY = bounds.min[1];
1093
- }
1094
- else if (segmentMinY > bounds.max[1]) {
1095
- segmentY = segmentMinY;
1096
- boxY = bounds.max[1];
1097
- }
1098
- else {
1099
- segmentY = clamp(position[1], Math.max(segmentMinY, bounds.min[1]), Math.min(segmentMaxY, bounds.max[1]));
1100
- boxY = segmentY;
1101
- }
1102
- const segmentPoint = [position[0], segmentY, position[2]];
1103
- const boxPoint = [
1104
- clamp(position[0], bounds.min[0], bounds.max[0]),
1105
- boxY,
1106
- clamp(position[2], bounds.min[2], bounds.max[2])
1107
- ];
1108
- return { segmentPoint, boxPoint };
1109
- }
1110
- function axisFromBoxCenter(boxPosition, spherePosition, halfExtents) {
1111
- const local = subVec3(spherePosition, boxPosition);
1112
- const distances = [
1113
- [halfExtents[0] - Math.abs(local[0]), [Math.sign(local[0]) || 1, 0, 0]],
1114
- [halfExtents[1] - Math.abs(local[1]), [0, Math.sign(local[1]) || 1, 0]],
1115
- [halfExtents[2] - Math.abs(local[2]), [0, 0, Math.sign(local[2]) || 1]]
1116
- ];
1117
- return distances.reduce((best, candidate) => candidate[0] < best[0] ? candidate : best)[1];
1118
- }
1119
- function clamp(value, min, max) {
1120
- return Math.max(min, Math.min(max, value));
1121
- }
1122
- function supportRadius(collider) {
1123
- switch (collider.shape.kind) {
1124
- case "box":
1125
- return collider.shape.halfExtents[1];
1126
- case "sphere":
1127
- return collider.shape.radius;
1128
- case "capsule":
1129
- return collider.shape.radius + collider.shape.halfHeight;
1130
- case "plane":
1131
- return 0;
1132
- case "mesh": {
1133
- const bounds = collider.bounds([0, 0, 0]);
1134
- return Math.max(Math.abs(bounds.min[1]), Math.abs(bounds.max[1]));
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
- }
1141
- }
1142
- }
1143
744
  //# sourceMappingURL=PhysicsWorld.js.map