@aura3d/physics 1.4.0 → 1.4.1

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.
@@ -1,986 +0,0 @@
1
- import {
2
- Body as CannonBody,
3
- Box as CannonBox,
4
- Cylinder as CannonCylinder,
5
- Plane as CannonPlane,
6
- Quaternion as CannonQuaternion,
7
- Sphere as CannonSphere,
8
- Vec3 as CannonVec3,
9
- World as CannonWorld
10
- } from "cannon-es";
11
- import { Collider, type ColliderDescriptor } from "./Collider.js";
12
- import { CollisionEventQueue, type CollisionEvent, type Contact } from "./CollisionEvents.js";
13
- import { Constraint, type ConstraintDescriptor } from "./Constraint.js";
14
- import { raycastCollider, sphereCastCollider, type RaycastHit, type RaycastOptions, type SphereCastHit } from "./Raycast.js";
15
- import { RigidBody, type RigidBodyDescriptor, type RigidBodySnapshot } from "./RigidBody.js";
16
- import { cloneVec3, dotVec3, normalizeVec3, scaleVec3, subVec3, type Bounds, type PhysicsShape, type Vec3 } from "./Shape.js";
17
-
18
- export type PhysicsWorldDescriptor = {
19
- readonly backend?: PhysicsBackendPreference;
20
- readonly gravity?: Vec3;
21
- readonly fixedDelta?: number;
22
- readonly solverIterations?: number;
23
- readonly enableSleeping?: boolean;
24
- readonly sleepVelocityThreshold?: number;
25
- readonly sleepDelay?: number;
26
- };
27
-
28
- export type PhysicsBackend = "cannon-es" | "aura-js";
29
- export type PhysicsBackendPreference = PhysicsBackend | "auto";
30
-
31
- export type PhysicsBackendSelection = {
32
- readonly requested: PhysicsBackendPreference;
33
- readonly active: PhysicsBackend;
34
- readonly fallback?: string;
35
- readonly deterministic: boolean;
36
- readonly jsFallbackAvailable: boolean;
37
- };
38
-
39
- export type PhysicsStepStats = {
40
- readonly steps: number;
41
- readonly bodies: number;
42
- readonly colliders: number;
43
- readonly constraints: number;
44
- readonly broadphasePairs: number;
45
- readonly broadphaseFiniteColliders: number;
46
- readonly broadphaseInfiniteColliders: number;
47
- readonly broadphaseCandidateTests: number;
48
- readonly broadphaseActiveMax: number;
49
- readonly broadphaseRejectedByBounds: number;
50
- readonly contacts: number;
51
- readonly events: number;
52
- readonly sleepingBodies: number;
53
- readonly kineticEnergy: number;
54
- readonly maxContactPenetration: number;
55
- };
56
-
57
- export type PhysicsSnapshot = {
58
- readonly backend: PhysicsBackendSelection;
59
- readonly bodies: readonly RigidBodySnapshot[];
60
- readonly contacts: readonly Contact[];
61
- readonly stats: PhysicsStepStats;
62
- };
63
-
64
- export class PhysicsWorld {
65
- readonly gravity: [number, number, number];
66
- readonly fixedDelta: number;
67
- private readonly solverIterations: number;
68
- private readonly enableSleeping: boolean;
69
- private readonly sleepVelocityThreshold: number;
70
- private readonly sleepDelay: number;
71
- private readonly bodiesById = new Map<number, RigidBody>();
72
- private readonly collidersById = new Map<number, Collider>();
73
- private readonly constraintsList: Constraint[] = [];
74
- private readonly bodyColliders = new Map<number, Set<number>>();
75
- private readonly eventQueue = new CollisionEventQueue();
76
- private readonly requestedBackend: PhysicsBackendPreference;
77
- private backendSelection: PhysicsBackendSelection;
78
- private cannonWorld: CannonWorld | undefined;
79
- private readonly cannonBodiesByAuraId = new Map<number, CannonBody>();
80
- private nextBodyId = 1;
81
- private nextColliderId = 1;
82
- private lastEvents: readonly CollisionEvent[] = [];
83
- private lastBroadphasePairs = 0;
84
- private lastBroadphaseProfile: BroadphaseProfile = emptyBroadphaseProfile();
85
- private steps = 0;
86
-
87
- constructor(descriptor: PhysicsWorldDescriptor = {}) {
88
- this.requestedBackend = descriptor.backend ?? "auto";
89
- this.gravity = cloneVec3(descriptor.gravity ?? [0, -9.81, 0]);
90
- this.fixedDelta = descriptor.fixedDelta ?? 1 / 60;
91
- this.solverIterations = descriptor.solverIterations ?? 1;
92
- this.enableSleeping = descriptor.enableSleeping ?? true;
93
- this.sleepVelocityThreshold = descriptor.sleepVelocityThreshold ?? 0.02;
94
- this.sleepDelay = descriptor.sleepDelay ?? 0.5;
95
- if (!Number.isFinite(this.fixedDelta) || this.fixedDelta <= 0) {
96
- throw new Error("fixedDelta must be a finite positive number.");
97
- }
98
- if (!Number.isInteger(this.solverIterations) || this.solverIterations <= 0) {
99
- throw new Error("solverIterations must be a positive integer.");
100
- }
101
- if (!Number.isFinite(this.sleepVelocityThreshold) || this.sleepVelocityThreshold < 0) {
102
- throw new Error("sleepVelocityThreshold must be finite and non-negative.");
103
- }
104
- if (!Number.isFinite(this.sleepDelay) || this.sleepDelay < 0) {
105
- throw new Error("sleepDelay must be finite and non-negative.");
106
- }
107
- this.backendSelection = {
108
- requested: this.requestedBackend,
109
- active: this.requestedBackend === "aura-js" ? "aura-js" : "cannon-es",
110
- deterministic: true,
111
- jsFallbackAvailable: true
112
- };
113
- if (this.backendSelection.active === "cannon-es") {
114
- this.cannonWorld = new CannonWorld({
115
- gravity: toCannonVec3(this.gravity),
116
- allowSleep: this.enableSleeping
117
- });
118
- (this.cannonWorld.solver as { iterations?: number }).iterations = this.solverIterations;
119
- this.cannonWorld.defaultContactMaterial.friction = 0.5;
120
- this.cannonWorld.defaultContactMaterial.restitution = 0;
121
- }
122
- }
123
-
124
- createRigidBody(descriptor: RigidBodyDescriptor = {}): RigidBody {
125
- const body = new RigidBody(this.nextBodyId, descriptor);
126
- this.nextBodyId += 1;
127
- this.bodiesById.set(body.id, body);
128
- this.bodyColliders.set(body.id, new Set());
129
- const cannonBody = this.createCannonBody(body);
130
- if (cannonBody) {
131
- this.cannonBodiesByAuraId.set(body.id, cannonBody);
132
- this.cannonWorld?.addBody(cannonBody);
133
- }
134
- return body;
135
- }
136
-
137
- createCollider(body: RigidBody | number, descriptor: ColliderDescriptor): Collider {
138
- const bodyId = typeof body === "number" ? body : body.id;
139
- if (!this.bodiesById.has(bodyId)) {
140
- throw new Error(`Cannot create collider for missing body ${bodyId}.`);
141
- }
142
- const collider = new Collider(this.nextColliderId, bodyId, descriptor);
143
- this.nextColliderId += 1;
144
- this.collidersById.set(collider.id, collider);
145
- this.bodyColliders.get(bodyId)?.add(collider.id);
146
- this.addCannonCollider(collider);
147
- return collider;
148
- }
149
-
150
- createConstraint(descriptor: ConstraintDescriptor): Constraint {
151
- if (!this.bodiesById.has(descriptor.bodyA.id) || !this.bodiesById.has(descriptor.bodyB.id)) {
152
- throw new Error("Cannot create a constraint for bodies outside this PhysicsWorld.");
153
- }
154
- const constraint = new Constraint(descriptor);
155
- this.constraintsList.push(constraint);
156
- return constraint;
157
- }
158
-
159
- getBody(id: number): RigidBody | undefined {
160
- return this.bodiesById.get(id);
161
- }
162
-
163
- getCollider(id: number): Collider | undefined {
164
- return this.collidersById.get(id);
165
- }
166
-
167
- bodies(): readonly RigidBody[] {
168
- return Array.from(this.bodiesById.values()).sort((a, b) => a.id - b.id);
169
- }
170
-
171
- colliders(): readonly Collider[] {
172
- return Array.from(this.collidersById.values()).sort((a, b) => a.id - b.id);
173
- }
174
-
175
- private bodyValues(): IterableIterator<RigidBody> {
176
- return this.bodiesById.values();
177
- }
178
-
179
- private colliderValues(): IterableIterator<Collider> {
180
- return this.collidersById.values();
181
- }
182
-
183
- constraints(): readonly Constraint[] {
184
- return [...this.constraintsList];
185
- }
186
-
187
- removeRigidBody(id: number): void {
188
- const colliderIds = Array.from(this.bodyColliders.get(id) ?? []);
189
- for (const colliderId of colliderIds) {
190
- this.removeCollider(colliderId);
191
- }
192
- this.bodyColliders.delete(id);
193
- this.bodiesById.delete(id);
194
- const cannonBody = this.cannonBodiesByAuraId.get(id);
195
- if (cannonBody) {
196
- this.cannonWorld?.removeBody(cannonBody);
197
- this.cannonBodiesByAuraId.delete(id);
198
- }
199
- for (let index = this.constraintsList.length - 1; index >= 0; index -= 1) {
200
- const constraint = this.constraintsList[index]!;
201
- if (constraint.bodyA.id === id || constraint.bodyB.id === id) {
202
- this.constraintsList.splice(index, 1);
203
- }
204
- }
205
- }
206
-
207
- removeCollider(id: number): void {
208
- const collider = this.collidersById.get(id);
209
- if (!collider) {
210
- return;
211
- }
212
- this.bodyColliders.get(collider.bodyId)?.delete(id);
213
- this.collidersById.delete(id);
214
- const removalEvents = this.eventQueue.removeCollider(id);
215
- if (removalEvents.length > 0) {
216
- this.lastEvents = [...this.lastEvents, ...removalEvents].sort((a, b) => a.pairKey.localeCompare(b.pairKey) || a.type.localeCompare(b.type));
217
- }
218
- }
219
-
220
- step(dt = this.fixedDelta): readonly CollisionEvent[] {
221
- if (!Number.isFinite(dt) || dt <= 0) {
222
- throw new Error("PhysicsWorld.step dt must be finite and positive.");
223
- }
224
- if (this.cannonWorld) {
225
- return this.stepCannon(dt);
226
- }
227
- for (const body of this.bodyValues()) {
228
- body.integrate(dt, this.gravity);
229
- }
230
- for (const constraint of this.constraintsList) {
231
- constraint.solve();
232
- }
233
- let contacts: Contact[] = [];
234
- for (let i = 0; i < this.solverIterations; i += 1) {
235
- contacts = this.detectContacts();
236
- for (const contact of contacts) {
237
- this.resolveContact(contact);
238
- }
239
- for (const constraint of this.constraintsList) {
240
- constraint.solve();
241
- }
242
- }
243
- this.lastEvents = this.eventQueue.update(contacts);
244
- this.updateSleeping(dt, contacts);
245
- this.steps += 1;
246
- return this.lastEvents;
247
- }
248
-
249
- drainEvents(): readonly CollisionEvent[] {
250
- const events = this.lastEvents;
251
- this.lastEvents = [];
252
- return events;
253
- }
254
-
255
- raycast(origin: Vec3, direction: Vec3, options: RaycastOptions = {}): RaycastHit | undefined {
256
- const normalized = normalizeVec3(direction);
257
- return this.raycastAll(origin, normalized, options)[0];
258
- }
259
-
260
- raycastAll(origin: Vec3, direction: Vec3, options: RaycastOptions = {}): readonly RaycastHit[] {
261
- const normalized = normalizeVec3(direction);
262
- const hits: RaycastHit[] = [];
263
- for (const collider of this.colliderValues()) {
264
- const body = this.bodiesById.get(collider.bodyId);
265
- if (!body) {
266
- continue;
267
- }
268
- const hit = raycastCollider(origin, normalized, collider, body, options);
269
- if (hit) {
270
- hits.push(hit);
271
- }
272
- }
273
- hits.sort((a, b) => a.distance - b.distance || a.colliderId - b.colliderId);
274
- return hits;
275
- }
276
-
277
- sphereCast(origin: Vec3, radius: number, direction: Vec3, options: RaycastOptions = {}): SphereCastHit | undefined {
278
- return this.sphereCastAll(origin, radius, direction, options)[0];
279
- }
280
-
281
- sphereCastAll(origin: Vec3, radius: number, direction: Vec3, options: RaycastOptions = {}): readonly SphereCastHit[] {
282
- const normalized = normalizeVec3(direction);
283
- const hits: SphereCastHit[] = [];
284
- for (const collider of this.colliderValues()) {
285
- const body = this.bodiesById.get(collider.bodyId);
286
- if (!body) {
287
- continue;
288
- }
289
- const hit = sphereCastCollider(origin, radius, normalized, collider, body, options);
290
- if (hit) {
291
- hits.push(hit);
292
- }
293
- }
294
- hits.sort((a, b) => a.distance - b.distance || a.colliderId - b.colliderId);
295
- return hits;
296
- }
297
-
298
- snapshot(): PhysicsSnapshot {
299
- const contacts = this.eventQueue.snapshotContacts();
300
- const bodies = this.bodies();
301
- return {
302
- backend: this.backendSelection,
303
- bodies: bodies.map((body) => body.snapshot()),
304
- contacts,
305
- stats: {
306
- steps: this.steps,
307
- bodies: this.bodiesById.size,
308
- colliders: this.collidersById.size,
309
- constraints: this.constraintsList.length,
310
- broadphasePairs: this.lastBroadphasePairs,
311
- broadphaseFiniteColliders: this.lastBroadphaseProfile.finiteColliders,
312
- broadphaseInfiniteColliders: this.lastBroadphaseProfile.infiniteColliders,
313
- broadphaseCandidateTests: this.lastBroadphaseProfile.candidateTests,
314
- broadphaseActiveMax: this.lastBroadphaseProfile.activeMax,
315
- broadphaseRejectedByBounds: this.lastBroadphaseProfile.rejectedByBounds,
316
- contacts: contacts.length,
317
- events: this.lastEvents.length,
318
- sleepingBodies: bodies.filter((body) => body.sleeping).length,
319
- kineticEnergy: totalKineticEnergy(bodies),
320
- maxContactPenetration: contacts.reduce((max, contact) => Math.max(max, contact.penetration), 0)
321
- }
322
- };
323
- }
324
-
325
- private detectContacts(): Contact[] {
326
- const potentialPairs = this.collectPotentialPairs();
327
- this.lastBroadphasePairs = potentialPairs.length;
328
- const contacts: Contact[] = [];
329
- for (const [a, bodyA, b, bodyB] of potentialPairs) {
330
- const contact = buildContact(a, bodyA, b, bodyB);
331
- if (contact) {
332
- contacts.push(contact);
333
- }
334
- }
335
- contacts.sort((a, b) => a.colliderA - b.colliderA || a.colliderB - b.colliderB);
336
- return contacts;
337
- }
338
-
339
- private collectPotentialPairs(): readonly PotentialPair[] {
340
- const finiteEntries: BroadphaseEntry[] = [];
341
- const infiniteEntries: BroadphaseEntry[] = [];
342
- const profile = emptyBroadphaseProfile();
343
- for (const collider of this.colliderValues()) {
344
- const body = this.bodiesById.get(collider.bodyId);
345
- if (!body) {
346
- continue;
347
- }
348
- const bounds = collider.bounds(body.position);
349
- const entry = { collider, body, bounds };
350
- if (isFiniteBounds(bounds)) {
351
- finiteEntries.push(entry);
352
- profile.finiteColliders += 1;
353
- } else {
354
- infiniteEntries.push(entry);
355
- profile.infiniteColliders += 1;
356
- }
357
- }
358
-
359
- finiteEntries.sort((a, b) => a.bounds.min[0] - b.bounds.min[0] || a.collider.id - b.collider.id);
360
- const pairs: PotentialPair[] = [];
361
- const seen = new Set<string>();
362
- const active: BroadphaseEntry[] = [];
363
-
364
- for (const entry of finiteEntries) {
365
- for (let index = active.length - 1; index >= 0; index -= 1) {
366
- if (active[index]!.bounds.max[0] < entry.bounds.min[0]) {
367
- active.splice(index, 1);
368
- }
369
- }
370
- for (const other of active) {
371
- profile.candidateTests += 1;
372
- if (boundsOverlap(other.bounds, entry.bounds)) {
373
- pushPotentialPair(pairs, seen, other, entry);
374
- } else {
375
- profile.rejectedByBounds += 1;
376
- }
377
- }
378
- for (const infinite of infiniteEntries) {
379
- profile.candidateTests += 1;
380
- pushPotentialPair(pairs, seen, infinite, entry);
381
- }
382
- active.push(entry);
383
- profile.activeMax = Math.max(profile.activeMax, active.length);
384
- }
385
-
386
- for (let i = 0; i < infiniteEntries.length; i += 1) {
387
- for (let j = i + 1; j < infiniteEntries.length; j += 1) {
388
- profile.candidateTests += 1;
389
- pushPotentialPair(pairs, seen, infiniteEntries[i]!, infiniteEntries[j]!);
390
- }
391
- }
392
-
393
- pairs.sort((a, b) => a[0].id - b[0].id || a[2].id - b[2].id);
394
- this.lastBroadphaseProfile = profile;
395
- return pairs;
396
- }
397
-
398
- private resolveContact(contact: Contact): void {
399
- if (contact.sensor) {
400
- return;
401
- }
402
- const bodyA = this.bodiesById.get(contact.bodyA);
403
- const bodyB = this.bodiesById.get(contact.bodyB);
404
- if (!bodyA || !bodyB) {
405
- return;
406
- }
407
- const invMassSum = bodyA.inverseMass + bodyB.inverseMass;
408
- if (invMassSum <= 0) {
409
- return;
410
- }
411
- const correction = scaleVec3(contact.normal, contact.penetration / invMassSum);
412
- if (bodyA.inverseMass > 0) {
413
- bodyA.position = [bodyA.position[0] - correction[0] * bodyA.inverseMass, bodyA.position[1] - correction[1] * bodyA.inverseMass, bodyA.position[2] - correction[2] * bodyA.inverseMass];
414
- }
415
- if (bodyB.inverseMass > 0) {
416
- bodyB.position = [bodyB.position[0] + correction[0] * bodyB.inverseMass, bodyB.position[1] + correction[1] * bodyB.inverseMass, bodyB.position[2] + correction[2] * bodyB.inverseMass];
417
- }
418
- const relativeVelocity = subVec3(bodyB.velocity, bodyA.velocity);
419
- const velocityAlongNormal = dotVec3(relativeVelocity, contact.normal);
420
- const materialA = this.effectiveMaterial(bodyA, contact.colliderA);
421
- const materialB = this.effectiveMaterial(bodyB, contact.colliderB);
422
- const restitution = Math.max(materialA.restitution, materialB.restitution);
423
- let normalImpulseMagnitude = 0;
424
- if (velocityAlongNormal <= 0) {
425
- normalImpulseMagnitude = -(1 + restitution) * velocityAlongNormal / invMassSum;
426
- this.applyImpulsePair(bodyA, bodyB, scaleVec3(contact.normal, normalImpulseMagnitude));
427
- }
428
- const updatedRelativeVelocity = subVec3(bodyB.velocity, bodyA.velocity);
429
- const updatedNormalVelocity = dotVec3(updatedRelativeVelocity, contact.normal);
430
- const tangentVelocity = subVec3(updatedRelativeVelocity, scaleVec3(contact.normal, updatedNormalVelocity));
431
- const tangentSpeed = Math.hypot(tangentVelocity[0], tangentVelocity[1], tangentVelocity[2]);
432
- if (tangentSpeed > 1e-9) {
433
- const tangent = scaleVec3(tangentVelocity, 1 / tangentSpeed);
434
- const friction = Math.sqrt(Math.max(0, materialA.friction) * Math.max(0, materialB.friction));
435
- const targetMagnitude = -dotVec3(updatedRelativeVelocity, tangent) / invMassSum;
436
- const maxFriction = friction * (Math.abs(normalImpulseMagnitude) + contact.penetration);
437
- const frictionMagnitude = Math.max(-maxFriction, Math.min(maxFriction, targetMagnitude));
438
- if (frictionMagnitude !== 0) {
439
- this.applyImpulsePair(bodyA, bodyB, scaleVec3(tangent, frictionMagnitude));
440
- }
441
- }
442
- }
443
-
444
- private effectiveMaterial(body: RigidBody, colliderId: number): { readonly restitution: number; readonly friction: number } {
445
- const collider = this.collidersById.get(colliderId);
446
- return {
447
- restitution: Math.max(body.restitution, collider?.material.restitution ?? 0),
448
- friction: Math.max(body.friction, collider?.material.friction ?? 0)
449
- };
450
- }
451
-
452
- private applyImpulsePair(bodyA: RigidBody, bodyB: RigidBody, impulse: Vec3): void {
453
- if (bodyA.inverseMass > 0) {
454
- bodyA.velocity = [bodyA.velocity[0] - impulse[0] * bodyA.inverseMass, bodyA.velocity[1] - impulse[1] * bodyA.inverseMass, bodyA.velocity[2] - impulse[2] * bodyA.inverseMass];
455
- if (bodyA.sleeping && bodyA.speedSquared() > this.sleepVelocityThreshold * this.sleepVelocityThreshold) {
456
- bodyA.wake();
457
- }
458
- }
459
- if (bodyB.inverseMass > 0) {
460
- bodyB.velocity = [bodyB.velocity[0] + impulse[0] * bodyB.inverseMass, bodyB.velocity[1] + impulse[1] * bodyB.inverseMass, bodyB.velocity[2] + impulse[2] * bodyB.inverseMass];
461
- if (bodyB.sleeping && bodyB.speedSquared() > this.sleepVelocityThreshold * this.sleepVelocityThreshold) {
462
- bodyB.wake();
463
- }
464
- }
465
- }
466
-
467
- private updateSleeping(dt: number, contacts: readonly Contact[]): void {
468
- if (!this.enableSleeping) {
469
- return;
470
- }
471
- const contactBodyIds = new Set<number>();
472
- for (const contact of contacts) {
473
- if (!contact.sensor) {
474
- contactBodyIds.add(contact.bodyA);
475
- contactBodyIds.add(contact.bodyB);
476
- }
477
- }
478
- const gravityMagnitudeSquared = dotVec3(this.gravity, this.gravity);
479
- const thresholdSquared = this.sleepVelocityThreshold * this.sleepVelocityThreshold;
480
- for (const body of this.bodyValues()) {
481
- if (body.type !== "dynamic") {
482
- continue;
483
- }
484
- if (body.speedSquared() > thresholdSquared) {
485
- body.resetSleepTimer();
486
- continue;
487
- }
488
- const supportedOrUnaccelerated = contactBodyIds.has(body.id) || gravityMagnitudeSquared <= thresholdSquared;
489
- if (!supportedOrUnaccelerated) {
490
- body.resetSleepTimer();
491
- continue;
492
- }
493
- if (body.accumulateSleepTime(dt) >= this.sleepDelay) {
494
- body.sleep();
495
- }
496
- }
497
- }
498
-
499
- private createCannonBody(body: RigidBody): CannonBody | undefined {
500
- if (!this.cannonWorld) return undefined;
501
- const cannonBody = new CannonBody({
502
- type: body.type === "static" ? CannonBody.STATIC : body.type === "kinematic" ? CannonBody.KINEMATIC : CannonBody.DYNAMIC,
503
- mass: body.type === "dynamic" ? body.mass : 0,
504
- position: toCannonVec3(body.position),
505
- velocity: toCannonVec3(body.velocity),
506
- quaternion: new CannonQuaternion(body.rotation[0], body.rotation[1], body.rotation[2], body.rotation[3]),
507
- angularVelocity: toCannonVec3(body.angularVelocity),
508
- linearDamping: body.linearDamping,
509
- angularDamping: body.angularDamping,
510
- allowSleep: this.enableSleeping,
511
- sleepSpeedLimit: this.sleepVelocityThreshold,
512
- sleepTimeLimit: this.sleepDelay
513
- });
514
- if (body.sleeping) cannonBody.sleep();
515
- return cannonBody;
516
- }
517
-
518
- private addCannonCollider(collider: Collider): void {
519
- if (!this.cannonWorld) return;
520
- const body = this.bodiesById.get(collider.bodyId);
521
- const cannonBody = this.cannonBodiesByAuraId.get(collider.bodyId);
522
- if (!body || !cannonBody) return;
523
- const resolved = toCannonShape(collider.shape);
524
- if (!resolved) {
525
- this.disableCannonBackend(`unsupported shape '${collider.shape.kind}'`);
526
- return;
527
- }
528
- cannonBody.collisionFilterGroup = collider.filter.layer;
529
- cannonBody.collisionFilterMask = collider.filter.mask;
530
- cannonBody.isTrigger = cannonBody.isTrigger || collider.sensor;
531
- cannonBody.addShape(resolved.shape, resolved.offset, resolved.orientation);
532
- cannonBody.updateMassProperties();
533
- syncCannonFromAura(body, cannonBody);
534
- }
535
-
536
- private disableCannonBackend(reason: string): void {
537
- if (!this.cannonWorld) return;
538
- this.cannonWorld = undefined;
539
- this.cannonBodiesByAuraId.clear();
540
- this.backendSelection = {
541
- requested: this.requestedBackend,
542
- active: "aura-js",
543
- fallback: reason,
544
- deterministic: true,
545
- jsFallbackAvailable: true
546
- };
547
- }
548
-
549
- private stepCannon(dt: number): readonly CollisionEvent[] {
550
- if (!this.cannonWorld) return [];
551
- this.cannonWorld.gravity.copy(toCannonVec3(this.gravity));
552
- for (const body of this.bodyValues()) {
553
- const cannonBody = this.cannonBodiesByAuraId.get(body.id);
554
- if (cannonBody) syncCannonFromAura(body, cannonBody);
555
- }
556
- this.cannonWorld.step(dt);
557
- for (const body of this.bodyValues()) {
558
- const cannonBody = this.cannonBodiesByAuraId.get(body.id);
559
- if (cannonBody) syncAuraFromCannon(cannonBody, body);
560
- }
561
- const contacts = this.detectContacts();
562
- this.lastEvents = this.eventQueue.update(contacts);
563
- this.updateSleeping(dt, contacts);
564
- this.steps += 1;
565
- return this.lastEvents;
566
- }
567
- }
568
-
569
- function toCannonVec3(value: Vec3): CannonVec3 {
570
- return new CannonVec3(value[0], value[1], value[2]);
571
- }
572
-
573
- function fromCannonVec3(value: CannonVec3): [number, number, number] {
574
- return [value.x, value.y, value.z];
575
- }
576
-
577
- function syncCannonFromAura(body: RigidBody, cannonBody: CannonBody): void {
578
- cannonBody.position.set(body.position[0], body.position[1], body.position[2]);
579
- cannonBody.velocity.set(body.velocity[0], body.velocity[1], body.velocity[2]);
580
- cannonBody.quaternion.set(body.rotation[0], body.rotation[1], body.rotation[2], body.rotation[3]);
581
- cannonBody.angularVelocity.set(body.angularVelocity[0], body.angularVelocity[1], body.angularVelocity[2]);
582
- if (body.sleeping) cannonBody.sleep();
583
- else cannonBody.wakeUp();
584
- }
585
-
586
- function syncAuraFromCannon(cannonBody: CannonBody, body: RigidBody): void {
587
- body.previousPosition = cloneVec3(body.position);
588
- body.previousRotation = [body.rotation[0], body.rotation[1], body.rotation[2], body.rotation[3]];
589
- body.position = fromCannonVec3(cannonBody.position);
590
- body.velocity = fromCannonVec3(cannonBody.velocity);
591
- body.angularVelocity = fromCannonVec3(cannonBody.angularVelocity);
592
- body.rotation = [cannonBody.quaternion.x, cannonBody.quaternion.y, cannonBody.quaternion.z, cannonBody.quaternion.w];
593
- body.sleeping = cannonBody.sleepState === CannonBody.SLEEPING;
594
- }
595
-
596
- function toCannonShape(shape: PhysicsShape): { readonly shape: CannonBox | CannonSphere | CannonPlane | CannonCylinder; readonly offset?: CannonVec3; readonly orientation?: CannonQuaternion } | undefined {
597
- if (shape.kind === "box") return { shape: new CannonBox(toCannonVec3(shape.halfExtents)) };
598
- if (shape.kind === "sphere") return { shape: new CannonSphere(shape.radius) };
599
- if (shape.kind === "capsule") return { shape: new CannonCylinder(shape.radius, shape.radius, shape.halfHeight * 2 + shape.radius * 2, 12) };
600
- if (shape.kind === "plane") {
601
- const orientation = new CannonQuaternion();
602
- orientation.setFromVectors(new CannonVec3(0, 0, 1), toCannonVec3(shape.normal));
603
- return {
604
- shape: new CannonPlane(),
605
- offset: toCannonVec3(scaleVec3(shape.normal, shape.constant)),
606
- orientation
607
- };
608
- }
609
- return undefined;
610
- }
611
-
612
- function totalKineticEnergy(bodies: readonly RigidBody[]): number {
613
- let total = 0;
614
- for (const body of bodies) {
615
- if (body.type !== "dynamic") {
616
- continue;
617
- }
618
- const linearSpeedSquared = dotVec3(body.velocity, body.velocity);
619
- const angularEnergy = body.angularVelocity.reduce((sum, velocity, index) => {
620
- const inverseInertia = body.inverseInertia[index] ?? 0;
621
- return inverseInertia > 0 ? sum + 0.5 * velocity * velocity / inverseInertia : sum;
622
- }, 0);
623
- total += 0.5 * body.mass * linearSpeedSquared + angularEnergy;
624
- }
625
- return total;
626
- }
627
-
628
- type BroadphaseEntry = {
629
- readonly collider: Collider;
630
- readonly body: RigidBody;
631
- readonly bounds: Bounds;
632
- };
633
-
634
- type PotentialPair = readonly [Collider, RigidBody, Collider, RigidBody];
635
-
636
- type BroadphaseProfile = {
637
- finiteColliders: number;
638
- infiniteColliders: number;
639
- candidateTests: number;
640
- activeMax: number;
641
- rejectedByBounds: number;
642
- };
643
-
644
- function emptyBroadphaseProfile(): BroadphaseProfile {
645
- return {
646
- finiteColliders: 0,
647
- infiniteColliders: 0,
648
- candidateTests: 0,
649
- activeMax: 0,
650
- rejectedByBounds: 0
651
- };
652
- }
653
-
654
- function isFiniteBounds(bounds: Bounds): boolean {
655
- return isFiniteBoundValue(bounds.min[0]) && isFiniteBoundValue(bounds.min[1]) && isFiniteBoundValue(bounds.min[2]) &&
656
- isFiniteBoundValue(bounds.max[0]) && isFiniteBoundValue(bounds.max[1]) && isFiniteBoundValue(bounds.max[2]);
657
- }
658
-
659
- function isFiniteBoundValue(value: number): boolean {
660
- return Number.isFinite(value) && Math.abs(value) < Number.MAX_SAFE_INTEGER;
661
- }
662
-
663
- function boundsOverlap(a: Bounds, b: Bounds): boolean {
664
- return a.min[0] <= b.max[0] && a.max[0] >= b.min[0] &&
665
- a.min[1] <= b.max[1] && a.max[1] >= b.min[1] &&
666
- a.min[2] <= b.max[2] && a.max[2] >= b.min[2];
667
- }
668
-
669
- function pushPotentialPair(pairs: PotentialPair[], seen: Set<string>, a: BroadphaseEntry, b: BroadphaseEntry): void {
670
- if (a.collider.bodyId === b.collider.bodyId || !a.collider.canCollideWith(b.collider)) {
671
- return;
672
- }
673
- if (a.body.type !== "dynamic" && b.body.type !== "dynamic") {
674
- return;
675
- }
676
- const first = a.collider.id < b.collider.id ? a : b;
677
- const second = a.collider.id < b.collider.id ? b : a;
678
- const key = `${first.collider.id}:${second.collider.id}`;
679
- if (seen.has(key)) {
680
- return;
681
- }
682
- seen.add(key);
683
- pairs.push([first.collider, first.body, second.collider, second.body]);
684
- }
685
-
686
- function buildContact(a: Collider, bodyA: RigidBody, b: Collider, bodyB: RigidBody): Contact | undefined {
687
- const ab = subVec3(bodyB.position, bodyA.position);
688
- if (a.shape.kind === "plane" || b.shape.kind === "plane") {
689
- const planeCollider = a.shape.kind === "plane" ? a : b;
690
- const planeBody = a.shape.kind === "plane" ? bodyA : bodyB;
691
- const otherCollider = a.shape.kind === "plane" ? b : a;
692
- const otherBody = a.shape.kind === "plane" ? bodyB : bodyA;
693
- if (planeCollider.shape.kind !== "plane") {
694
- return undefined;
695
- }
696
- const radius = supportRadius(otherCollider);
697
- const signedDistance = dotVec3(planeCollider.shape.normal, otherBody.position) + planeCollider.shape.constant + dotVec3(planeCollider.shape.normal, planeBody.position);
698
- if (signedDistance > radius) {
699
- return undefined;
700
- }
701
- const normal = a.shape.kind === "plane" ? planeCollider.shape.normal : scaleVec3(planeCollider.shape.normal, -1);
702
- return {
703
- colliderA: a.id,
704
- colliderB: b.id,
705
- bodyA: bodyA.id,
706
- bodyB: bodyB.id,
707
- normal,
708
- penetration: radius - signedDistance,
709
- sensor: a.sensor || b.sensor
710
- };
711
- }
712
- if (a.shape.kind === "sphere" && b.shape.kind === "sphere") {
713
- const distance = Math.hypot(ab[0], ab[1], ab[2]);
714
- const radiusSum = a.shape.radius + b.shape.radius;
715
- if (distance >= radiusSum) {
716
- return undefined;
717
- }
718
- return {
719
- colliderA: a.id,
720
- colliderB: b.id,
721
- bodyA: bodyA.id,
722
- bodyB: bodyB.id,
723
- normal: distance > 1e-9 ? [ab[0] / distance, ab[1] / distance, ab[2] / distance] : [1, 0, 0],
724
- penetration: radiusSum - distance,
725
- sensor: a.sensor || b.sensor
726
- };
727
- }
728
- if ((a.shape.kind === "sphere" && b.shape.kind === "box") || (a.shape.kind === "box" && b.shape.kind === "sphere")) {
729
- return buildSphereBoxContact(a, bodyA, b, bodyB);
730
- }
731
- if ((a.shape.kind === "capsule" && b.shape.kind === "sphere") || (a.shape.kind === "sphere" && b.shape.kind === "capsule")) {
732
- return buildCapsuleSphereContact(a, bodyA, b, bodyB);
733
- }
734
- if ((a.shape.kind === "capsule" && b.shape.kind === "box") || (a.shape.kind === "box" && b.shape.kind === "capsule")) {
735
- return buildCapsuleBoxContact(a, bodyA, b, bodyB);
736
- }
737
- if (a.shape.kind === "capsule" && b.shape.kind === "capsule") {
738
- return buildCapsuleCapsuleContact(a, bodyA, b, bodyB);
739
- }
740
- const boundsA = a.bounds(bodyA.position);
741
- const boundsB = b.bounds(bodyB.position);
742
- const overlapX = Math.min(boundsA.max[0] - boundsB.min[0], boundsB.max[0] - boundsA.min[0]);
743
- const overlapY = Math.min(boundsA.max[1] - boundsB.min[1], boundsB.max[1] - boundsA.min[1]);
744
- const overlapZ = Math.min(boundsA.max[2] - boundsB.min[2], boundsB.max[2] - boundsA.min[2]);
745
- if (overlapX <= 0 || overlapY <= 0 || overlapZ <= 0) {
746
- return undefined;
747
- }
748
- let normal: Vec3 = [Math.sign(ab[0]) || 1, 0, 0];
749
- let penetration = overlapX;
750
- if (overlapY < penetration) {
751
- normal = [0, Math.sign(ab[1]) || 1, 0];
752
- penetration = overlapY;
753
- }
754
- if (overlapZ < penetration) {
755
- normal = [0, 0, Math.sign(ab[2]) || 1];
756
- penetration = overlapZ;
757
- }
758
- return {
759
- colliderA: a.id,
760
- colliderB: b.id,
761
- bodyA: bodyA.id,
762
- bodyB: bodyB.id,
763
- normal,
764
- penetration,
765
- sensor: a.sensor || b.sensor
766
- };
767
- }
768
-
769
- function buildSphereBoxContact(a: Collider, bodyA: RigidBody, b: Collider, bodyB: RigidBody): Contact | undefined {
770
- const sphereCollider = a.shape.kind === "sphere" ? a : b;
771
- const sphereBody = a.shape.kind === "sphere" ? bodyA : bodyB;
772
- const boxCollider = a.shape.kind === "box" ? a : b;
773
- const boxBody = a.shape.kind === "box" ? bodyA : bodyB;
774
- if (sphereCollider.shape.kind !== "sphere" || boxCollider.shape.kind !== "box") {
775
- return undefined;
776
- }
777
- const boxBounds = boxCollider.bounds(boxBody.position);
778
- const closest: Vec3 = [
779
- clamp(sphereBody.position[0], boxBounds.min[0], boxBounds.max[0]),
780
- clamp(sphereBody.position[1], boxBounds.min[1], boxBounds.max[1]),
781
- clamp(sphereBody.position[2], boxBounds.min[2], boxBounds.max[2])
782
- ];
783
- const fromBoxToSphere = subVec3(sphereBody.position, closest);
784
- const distance = Math.hypot(fromBoxToSphere[0], fromBoxToSphere[1], fromBoxToSphere[2]);
785
- if (distance >= sphereCollider.shape.radius) {
786
- return undefined;
787
- }
788
- const normalFromBoxToSphere = distance > 1e-9 ? scaleVec3(fromBoxToSphere, 1 / distance) : axisFromBoxCenter(boxBody.position, sphereBody.position, boxCollider.shape.halfExtents);
789
- const normal = a.shape.kind === "box" ? normalFromBoxToSphere : scaleVec3(normalFromBoxToSphere, -1);
790
- return {
791
- colliderA: a.id,
792
- colliderB: b.id,
793
- bodyA: bodyA.id,
794
- bodyB: bodyB.id,
795
- normal,
796
- penetration: sphereCollider.shape.radius - distance,
797
- sensor: a.sensor || b.sensor
798
- };
799
- }
800
-
801
- function buildCapsuleSphereContact(a: Collider, bodyA: RigidBody, b: Collider, bodyB: RigidBody): Contact | undefined {
802
- const capsuleCollider = a.shape.kind === "capsule" ? a : b;
803
- const capsuleBody = a.shape.kind === "capsule" ? bodyA : bodyB;
804
- const sphereCollider = a.shape.kind === "sphere" ? a : b;
805
- const sphereBody = a.shape.kind === "sphere" ? bodyA : bodyB;
806
- if (capsuleCollider.shape.kind !== "capsule" || sphereCollider.shape.kind !== "sphere") {
807
- return undefined;
808
- }
809
- const closest = closestPointOnSegment(sphereBody.position, capsuleSegment(capsuleBody.position, capsuleCollider.shape.halfHeight));
810
- const delta = subVec3(sphereBody.position, closest);
811
- const distance = Math.hypot(delta[0], delta[1], delta[2]);
812
- const radiusSum = capsuleCollider.shape.radius + sphereCollider.shape.radius;
813
- if (distance >= radiusSum) {
814
- return undefined;
815
- }
816
- const normalFromCapsuleToSphere = distance > 1e-9 ? scaleVec3(delta, 1 / distance) : [1, 0, 0] as Vec3;
817
- const normal = a.shape.kind === "capsule" ? normalFromCapsuleToSphere : scaleVec3(normalFromCapsuleToSphere, -1);
818
- return {
819
- colliderA: a.id,
820
- colliderB: b.id,
821
- bodyA: bodyA.id,
822
- bodyB: bodyB.id,
823
- normal,
824
- penetration: radiusSum - distance,
825
- sensor: a.sensor || b.sensor
826
- };
827
- }
828
-
829
- function buildCapsuleCapsuleContact(a: Collider, bodyA: RigidBody, b: Collider, bodyB: RigidBody): Contact | undefined {
830
- if (a.shape.kind !== "capsule" || b.shape.kind !== "capsule") {
831
- return undefined;
832
- }
833
- const closest = closestPointsBetweenSegments(
834
- capsuleSegment(bodyA.position, a.shape.halfHeight),
835
- capsuleSegment(bodyB.position, b.shape.halfHeight)
836
- );
837
- const delta = subVec3(closest.b, closest.a);
838
- const distance = Math.hypot(delta[0], delta[1], delta[2]);
839
- const radiusSum = a.shape.radius + b.shape.radius;
840
- if (distance >= radiusSum) {
841
- return undefined;
842
- }
843
- return {
844
- colliderA: a.id,
845
- colliderB: b.id,
846
- bodyA: bodyA.id,
847
- bodyB: bodyB.id,
848
- normal: distance > 1e-9 ? scaleVec3(delta, 1 / distance) : [1, 0, 0],
849
- penetration: radiusSum - distance,
850
- sensor: a.sensor || b.sensor
851
- };
852
- }
853
-
854
- function buildCapsuleBoxContact(a: Collider, bodyA: RigidBody, b: Collider, bodyB: RigidBody): Contact | undefined {
855
- const capsuleCollider = a.shape.kind === "capsule" ? a : b;
856
- const capsuleBody = a.shape.kind === "capsule" ? bodyA : bodyB;
857
- const boxCollider = a.shape.kind === "box" ? a : b;
858
- const boxBody = a.shape.kind === "box" ? bodyA : bodyB;
859
- if (capsuleCollider.shape.kind !== "capsule" || boxCollider.shape.kind !== "box") {
860
- return undefined;
861
- }
862
- const bounds = boxCollider.bounds(boxBody.position);
863
- const closest = closestVerticalSegmentPointToAabb(capsuleBody.position, capsuleCollider.shape.halfHeight, bounds);
864
- const fromBoxToCapsule = subVec3(closest.segmentPoint, closest.boxPoint);
865
- const distance = Math.hypot(fromBoxToCapsule[0], fromBoxToCapsule[1], fromBoxToCapsule[2]);
866
- if (distance >= capsuleCollider.shape.radius) {
867
- return undefined;
868
- }
869
- const normalFromBoxToCapsule = distance > 1e-9 ? scaleVec3(fromBoxToCapsule, 1 / distance) : axisFromBoxCenter(boxBody.position, closest.segmentPoint, boxCollider.shape.halfExtents);
870
- const normal = a.shape.kind === "box" ? normalFromBoxToCapsule : scaleVec3(normalFromBoxToCapsule, -1);
871
- return {
872
- colliderA: a.id,
873
- colliderB: b.id,
874
- bodyA: bodyA.id,
875
- bodyB: bodyB.id,
876
- normal,
877
- penetration: capsuleCollider.shape.radius - distance,
878
- sensor: a.sensor || b.sensor
879
- };
880
- }
881
-
882
- type Segment = {
883
- readonly start: Vec3;
884
- readonly end: Vec3;
885
- };
886
-
887
- function capsuleSegment(position: Vec3, halfHeight: number): Segment {
888
- return {
889
- start: [position[0], position[1] - halfHeight, position[2]],
890
- end: [position[0], position[1] + halfHeight, position[2]]
891
- };
892
- }
893
-
894
- function closestPointOnSegment(point: Vec3, segment: Segment): Vec3 {
895
- const ab = subVec3(segment.end, segment.start);
896
- const lengthSquared = dotVec3(ab, ab);
897
- if (lengthSquared <= 1e-9) {
898
- return segment.start;
899
- }
900
- const t = clamp(dotVec3(subVec3(point, segment.start), ab) / lengthSquared, 0, 1);
901
- return [
902
- segment.start[0] + ab[0] * t,
903
- segment.start[1] + ab[1] * t,
904
- segment.start[2] + ab[2] * t
905
- ];
906
- }
907
-
908
- function closestPointsBetweenSegments(a: Segment, b: Segment): { readonly a: Vec3; readonly b: Vec3 } {
909
- const d1 = subVec3(a.end, a.start);
910
- const d2 = subVec3(b.end, b.start);
911
- const r = subVec3(a.start, b.start);
912
- const aLength = dotVec3(d1, d1);
913
- const bLength = dotVec3(d2, d2);
914
- const d12 = dotVec3(d1, d2);
915
- const d1r = dotVec3(d1, r);
916
- const d2r = dotVec3(d2, r);
917
- const denominator = aLength * bLength - d12 * d12;
918
- let s = denominator <= 1e-9 ? 0 : clamp((d12 * d2r - bLength * d1r) / denominator, 0, 1);
919
- let t = bLength <= 1e-9 ? 0 : (d12 * s + d2r) / bLength;
920
- if (t < 0) {
921
- t = 0;
922
- s = aLength <= 1e-9 ? 0 : clamp(-d1r / aLength, 0, 1);
923
- } else if (t > 1) {
924
- t = 1;
925
- s = aLength <= 1e-9 ? 0 : clamp((d12 - d1r) / aLength, 0, 1);
926
- }
927
- return {
928
- a: [a.start[0] + d1[0] * s, a.start[1] + d1[1] * s, a.start[2] + d1[2] * s],
929
- b: [b.start[0] + d2[0] * t, b.start[1] + d2[1] * t, b.start[2] + d2[2] * t]
930
- };
931
- }
932
-
933
- function closestVerticalSegmentPointToAabb(position: Vec3, halfHeight: number, bounds: Bounds): { readonly segmentPoint: Vec3; readonly boxPoint: Vec3 } {
934
- const segmentMinY = position[1] - halfHeight;
935
- const segmentMaxY = position[1] + halfHeight;
936
- let segmentY: number;
937
- let boxY: number;
938
- if (segmentMaxY < bounds.min[1]) {
939
- segmentY = segmentMaxY;
940
- boxY = bounds.min[1];
941
- } else if (segmentMinY > bounds.max[1]) {
942
- segmentY = segmentMinY;
943
- boxY = bounds.max[1];
944
- } else {
945
- segmentY = clamp(position[1], Math.max(segmentMinY, bounds.min[1]), Math.min(segmentMaxY, bounds.max[1]));
946
- boxY = segmentY;
947
- }
948
- const segmentPoint: Vec3 = [position[0], segmentY, position[2]];
949
- const boxPoint: Vec3 = [
950
- clamp(position[0], bounds.min[0], bounds.max[0]),
951
- boxY,
952
- clamp(position[2], bounds.min[2], bounds.max[2])
953
- ];
954
- return { segmentPoint, boxPoint };
955
- }
956
-
957
- function axisFromBoxCenter(boxPosition: Vec3, spherePosition: Vec3, halfExtents: Vec3): Vec3 {
958
- const local = subVec3(spherePosition, boxPosition);
959
- const distances: readonly [number, Vec3][] = [
960
- [halfExtents[0] - Math.abs(local[0]), [Math.sign(local[0]) || 1, 0, 0]],
961
- [halfExtents[1] - Math.abs(local[1]), [0, Math.sign(local[1]) || 1, 0]],
962
- [halfExtents[2] - Math.abs(local[2]), [0, 0, Math.sign(local[2]) || 1]]
963
- ];
964
- return distances.reduce((best, candidate) => candidate[0] < best[0] ? candidate : best)[1];
965
- }
966
-
967
- function clamp(value: number, min: number, max: number): number {
968
- return Math.max(min, Math.min(max, value));
969
- }
970
-
971
- function supportRadius(collider: Collider): number {
972
- switch (collider.shape.kind) {
973
- case "box":
974
- return collider.shape.halfExtents[1];
975
- case "sphere":
976
- return collider.shape.radius;
977
- case "capsule":
978
- return collider.shape.radius + collider.shape.halfHeight;
979
- case "plane":
980
- return 0;
981
- case "mesh": {
982
- const bounds = collider.bounds([0, 0, 0]);
983
- return Math.max(Math.abs(bounds.min[1]), Math.abs(bounds.max[1]));
984
- }
985
- }
986
- }