react-three-rapier-unified 1.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.
@@ -0,0 +1,894 @@
1
+ import type Rapier from "@dimforge/rapier3d-compat";
2
+ import {
3
+ Collider,
4
+ ColliderHandle,
5
+ EventQueue,
6
+ PhysicsHooks,
7
+ RigidBody,
8
+ RigidBodyHandle,
9
+ SolverFlags,
10
+ World
11
+ } from "@dimforge/rapier3d-compat";
12
+ import { useThree } from "@react-three/fiber";
13
+ import React, {
14
+ createContext,
15
+ FC,
16
+ ReactNode,
17
+ useCallback,
18
+ useEffect,
19
+ useMemo,
20
+ useState
21
+ } from "react";
22
+ import { MathUtils, Matrix4, Object3D, Quaternion, Vector3 } from "three";
23
+ import { suspend } from "suspend-react";
24
+ import {
25
+ CollisionPayload,
26
+ CollisionEnterHandler,
27
+ CollisionExitHandler,
28
+ ContactForceHandler,
29
+ IntersectionEnterHandler,
30
+ IntersectionExitHandler,
31
+ RigidBodyAutoCollider,
32
+ Vector3Tuple
33
+ } from "../types";
34
+ import {
35
+ _matrix4,
36
+ _position,
37
+ _rotation,
38
+ _scale
39
+ } from "../utils/shared-objects";
40
+ import {
41
+ rapierQuaternionToQuaternion,
42
+ useConst,
43
+ vectorArrayToVector3,
44
+ vector3ToRapierVector
45
+ } from "../utils/utils";
46
+ import FrameStepper from "./FrameStepper";
47
+ import { Debug } from "./Debug";
48
+ import { createSingletonProxy } from "../utils/singleton-proxy";
49
+
50
+ export interface RigidBodyState {
51
+ meshType: "instancedMesh" | "mesh";
52
+ rigidBody: RigidBody;
53
+ object: Object3D;
54
+ invertedWorldMatrix: Matrix4;
55
+ setMatrix: (matrix: Matrix4) => void;
56
+ getMatrix: (matrix: Matrix4) => Matrix4;
57
+ /**
58
+ * Required for instanced rigid bodies.
59
+ */
60
+ scale: Vector3;
61
+ isSleeping: boolean;
62
+ }
63
+
64
+ export type RigidBodyStateMap = Map<RigidBody["handle"], RigidBodyState>;
65
+
66
+ export type WorldStepCallback = (world: World) => void;
67
+
68
+ export type WorldStepCallbackSet = Set<{ current: WorldStepCallback }>;
69
+
70
+ export type FilterContactPairCallback = (
71
+ collider1: ColliderHandle,
72
+ collider2: ColliderHandle,
73
+ body1: RigidBodyHandle,
74
+ body2: RigidBodyHandle
75
+ ) => SolverFlags | null;
76
+
77
+ export type FilterIntersectionPairCallback = (
78
+ collider1: ColliderHandle,
79
+ collider2: ColliderHandle,
80
+ body1: RigidBodyHandle,
81
+ body2: RigidBodyHandle
82
+ ) => boolean;
83
+
84
+ export type FilterContactPairCallbackSet = Set<{
85
+ current: FilterContactPairCallback;
86
+ }>;
87
+
88
+ export type FilterIntersectionPairCallbackSet = Set<{
89
+ current: FilterIntersectionPairCallback;
90
+ }>;
91
+
92
+ export interface ColliderState {
93
+ collider: Collider;
94
+ object: Object3D;
95
+
96
+ /**
97
+ * The parent of which this collider needs to base its
98
+ * world position on, can be empty
99
+ */
100
+ worldParent?: Object3D;
101
+ }
102
+
103
+ export type ColliderStateMap = Map<Collider["handle"], ColliderState>;
104
+
105
+ export interface RapierContext {
106
+ /**
107
+ * Used by the world to keep track of RigidBody states
108
+ * @internal
109
+ */
110
+ rigidBodyStates: RigidBodyStateMap;
111
+
112
+ /**
113
+ * Used by the world to keep track of Collider states
114
+ * @internal
115
+ */
116
+ colliderStates: ColliderStateMap;
117
+
118
+ /**
119
+ * Used by the world to keep track of RigidBody events
120
+ * @internal
121
+ */
122
+ rigidBodyEvents: EventMap;
123
+ /**
124
+ * Used by the world to keep track of Collider events
125
+ * @internal
126
+ */
127
+ colliderEvents: EventMap;
128
+
129
+ /**
130
+ * Default options for rigid bodies and colliders
131
+ * @internal
132
+ */
133
+ physicsOptions: {
134
+ colliders: RigidBodyAutoCollider;
135
+ };
136
+
137
+ /**
138
+ * Triggered before the physics world is stepped
139
+ * @internal
140
+ */
141
+ beforeStepCallbacks: WorldStepCallbackSet;
142
+
143
+ /**
144
+ * Triggered after the physics world is stepped
145
+ * @internal
146
+ */
147
+ afterStepCallbacks: WorldStepCallbackSet;
148
+
149
+ /**
150
+ * Hooks to filter contact pairs
151
+ * @internal
152
+ */
153
+ filterContactPairHooks: FilterContactPairCallbackSet;
154
+
155
+ /**
156
+ * Hooks to filter intersection pairs
157
+ * @internal
158
+ */
159
+ filterIntersectionPairHooks: FilterIntersectionPairCallbackSet;
160
+
161
+ /**
162
+ * Direct access to the Rapier instance
163
+ */
164
+ rapier: typeof Rapier;
165
+
166
+ /**
167
+ * The Rapier physics world
168
+ */
169
+ world: World;
170
+
171
+ /**
172
+ * Can be used to overwrite the current World. Useful when working with snapshots.
173
+ *
174
+ * @example
175
+ * ```tsx
176
+ * import { useRapier } from '@react-three/rapier';
177
+ *
178
+ * const SnapshottingComponent = () => {
179
+ * const { world, setWorld, rapier } = useRapier();
180
+ * const worldSnapshot = useRef<Uint8Array>();
181
+ *
182
+ * // Store the snapshot
183
+ * const takeSnapshot = () => {
184
+ * const snapshot = world.takeSnapshot()
185
+ * worldSnapshot.current = snapshot
186
+ * }
187
+ *
188
+ * // Create a new World from the snapshot, and replace the current one
189
+ * const restoreSnapshot = () => {
190
+ * setWorld(rapier.World.restoreSnapshot(worldSnapshot.current))
191
+ * }
192
+ *
193
+ * return <>
194
+ * <Rigidbody>...</RigidBody>
195
+ * <Rigidbody>...</RigidBody>
196
+ * <Rigidbody>...</RigidBody>
197
+ * <Rigidbody>...</RigidBody>
198
+ * <Rigidbody>...</RigidBody>
199
+ * </>
200
+ * }
201
+ * ```
202
+ */
203
+ setWorld: (world: World) => void;
204
+
205
+ /**
206
+ * If the physics simulation is paused
207
+ */
208
+ isPaused: boolean;
209
+
210
+ /**
211
+ * Step the physics world one step
212
+ *
213
+ * @param deltaTime The delta time to step the world with
214
+ *
215
+ * @example
216
+ * ```
217
+ * step(1/60)
218
+ * ```
219
+ */
220
+ step: (deltaTime: number) => void;
221
+
222
+ /**
223
+ * Is debug mode enabled
224
+ */
225
+ isDebug: boolean;
226
+ }
227
+
228
+ export const rapierContext = createContext<RapierContext | undefined>(
229
+ undefined
230
+ );
231
+
232
+ type CollisionSource = {
233
+ collider: {
234
+ object: Collider;
235
+ events?: EventMapValue;
236
+ state?: ColliderState;
237
+ };
238
+ rigidBody: {
239
+ object?: RigidBody;
240
+ events?: EventMapValue;
241
+ state?: RigidBodyState;
242
+ };
243
+ };
244
+
245
+ const getCollisionPayloadFromSource = (
246
+ target: CollisionSource,
247
+ other: CollisionSource
248
+ ): CollisionPayload => ({
249
+ target: {
250
+ rigidBody: target.rigidBody.object,
251
+ collider: target.collider.object,
252
+ colliderObject: target.collider.state?.object,
253
+ rigidBodyObject: target.rigidBody.state?.object
254
+ },
255
+
256
+ other: {
257
+ rigidBody: other.rigidBody.object,
258
+ collider: other.collider.object,
259
+ colliderObject: other.collider.state?.object,
260
+ rigidBodyObject: other.rigidBody.state?.object
261
+ },
262
+
263
+ rigidBody: other.rigidBody.object,
264
+ collider: other.collider.object,
265
+ colliderObject: other.collider.state?.object,
266
+ rigidBodyObject: other.rigidBody.state?.object
267
+ });
268
+
269
+ const importRapier = async () => {
270
+ let r = await import("@dimforge/rapier3d-compat");
271
+ await r.init();
272
+ return r;
273
+ };
274
+
275
+ export type EventMapValue = {
276
+ onSleep?(): void;
277
+ onWake?(): void;
278
+ onCollisionEnter?: CollisionEnterHandler;
279
+ onCollisionExit?: CollisionExitHandler;
280
+ onIntersectionEnter?: IntersectionEnterHandler;
281
+ onIntersectionExit?: IntersectionExitHandler;
282
+ onContactForce?: ContactForceHandler;
283
+ };
284
+
285
+ export type EventMap = Map<ColliderHandle | RigidBodyHandle, EventMapValue>;
286
+
287
+ export interface PhysicsProps {
288
+ children: ReactNode;
289
+ /**
290
+ * Set the gravity of the physics world
291
+ * @defaultValue [0, -9.81, 0]
292
+ */
293
+ gravity?: Vector3Tuple;
294
+
295
+ /**
296
+ * Amount of penetration the engine wont attempt to correct
297
+ * @defaultValue 0.001
298
+ */
299
+ allowedLinearError?: number;
300
+
301
+ /**
302
+ * The number of solver iterations run by the constraints solver for calculating forces.
303
+ * The greater this value is, the most rigid and realistic the physics simulation will be.
304
+ * However a greater number of iterations is more computationally intensive.
305
+ *
306
+ * @defaultValue 4
307
+ */
308
+ numSolverIterations?: number;
309
+
310
+ /**
311
+ * Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration.
312
+ * Increasing this parameter will improve stability of the simulation. It will have a lesser effect than
313
+ * increasing `numSolverIterations` but is also less computationally expensive.
314
+ *
315
+ * @defaultValue 1
316
+ */
317
+ numInternalPgsIterations?: number;
318
+
319
+ /**
320
+ * The maximal distance separating two objects that will generate predictive contacts
321
+ *
322
+ * @defaultValue 0.002
323
+ *
324
+ */
325
+ predictionDistance?: number;
326
+
327
+ /**
328
+ * Minimum number of dynamic bodies in each active island
329
+ *
330
+ * @defaultValue 128
331
+ */
332
+ minIslandSize?: number;
333
+
334
+ /**
335
+ * Maximum number of substeps performed by the solver
336
+ *
337
+ * @defaultValue 1
338
+ */
339
+ maxCcdSubsteps?: number;
340
+
341
+ /**
342
+ * Directly affects the `erp` (Error Reduction Parameter) which is the proportion (0 to 1) of the positional error to be corrected at each time step.
343
+ * The higher this value is, the more the physics engine will try to correct errors.
344
+ *
345
+ * This prop is currently undocumented in the Rapier documentation.
346
+ *
347
+ * @see https://github.com/dimforge/rapier/pull/651 where this change was made to Rapier
348
+ * @defaultValue 30
349
+ */
350
+ contactNaturalFrequency?: number;
351
+
352
+ /**
353
+ * The approximate size of most dynamic objects in the scene.
354
+ *
355
+ * This value is used internally to estimate some length-based tolerance.
356
+ * This value can be understood as the number of units-per-meter in your physical world compared to a human-sized world in meter.
357
+ *
358
+ * @defaultValue 1
359
+ */
360
+ lengthUnit?: number;
361
+
362
+ /**
363
+ * Set the base automatic colliders for this physics world
364
+ * All Meshes inside RigidBodies will generate a collider
365
+ * based on this value, if not overridden.
366
+ */
367
+ colliders?: RigidBodyAutoCollider;
368
+
369
+ /**
370
+ * Set the timestep for the simulation.
371
+ * Setting this to a number (eg. 1/60) will run the
372
+ * simulation at that framerate. Alternatively, you can set this to
373
+ * "vary", which will cause the simulation to always synchronize with
374
+ * the current frame delta times.
375
+ *
376
+ * @defaultValue 1/60
377
+ */
378
+ timeStep?: number | "vary";
379
+
380
+ /**
381
+ * Pause the physics simulation
382
+ *
383
+ * @defaultValue false
384
+ */
385
+ paused?: boolean;
386
+
387
+ /**
388
+ * Interpolate the world transform using the frame delta times.
389
+ * Has no effect if timeStep is set to "vary".
390
+ *
391
+ * @defaultValue true
392
+ **/
393
+ interpolate?: boolean;
394
+
395
+ /**
396
+ * The update priority at which the physics simulation should run.
397
+ * Only used when `updateLoop` is set to "follow".
398
+ *
399
+ * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#taking-over-the-render-loop
400
+ * @defaultValue undefined
401
+ */
402
+ updatePriority?: number;
403
+
404
+ /**
405
+ * Set the update loop strategy for the physics world.
406
+ *
407
+ * If set to "follow", the physics world will be stepped
408
+ * in a `useFrame` callback, managed by @react-three/fiber.
409
+ * You can use `updatePriority` prop to manage the scheduling.
410
+ *
411
+ * If set to "independent", the physics world will be stepped
412
+ * in a separate loop, not tied to the render loop.
413
+ * This is useful when using the "demand" `frameloop` strategy for the
414
+ * @react-three/fiber `<Canvas />`.
415
+ *
416
+ * @see https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance#on-demand-rendering
417
+ * @defaultValue "follow"
418
+ */
419
+ updateLoop?: "follow" | "independent";
420
+
421
+ /**
422
+ * Enable debug rendering of the physics world.
423
+ * @defaultValue false
424
+ */
425
+ debug?: boolean;
426
+ }
427
+
428
+ /**
429
+ * The main physics component used to create a physics world.
430
+ * @category Components
431
+ */
432
+ export const Physics: FC<PhysicsProps> = (props) => {
433
+ const {
434
+ colliders = "cuboid",
435
+ children,
436
+ timeStep = 1 / 60,
437
+ paused = false,
438
+ interpolate = true,
439
+ updatePriority,
440
+ updateLoop = "follow",
441
+ debug = false,
442
+
443
+ gravity = [0, -9.81, 0],
444
+ allowedLinearError = 0.001,
445
+ predictionDistance = 0.002,
446
+ numSolverIterations = 4,
447
+ numInternalPgsIterations = 1,
448
+ minIslandSize = 128,
449
+ maxCcdSubsteps = 1,
450
+ contactNaturalFrequency = 30,
451
+ lengthUnit = 1
452
+ } = props;
453
+ const rapier = suspend(importRapier, ["@react-thee/rapier", importRapier]);
454
+ const { invalidate } = useThree();
455
+
456
+ const rigidBodyStates = useConst<RigidBodyStateMap>(() => new Map());
457
+ const colliderStates = useConst<ColliderStateMap>(() => new Map());
458
+ const rigidBodyEvents = useConst<EventMap>(() => new Map());
459
+ const colliderEvents = useConst<EventMap>(() => new Map());
460
+ const eventQueue = useConst(() => new EventQueue(false));
461
+
462
+ const filterContactPairHooks = useConst<FilterContactPairCallbackSet>(
463
+ () => new Set()
464
+ );
465
+ const filterIntersectionPairHooks =
466
+ useConst<FilterIntersectionPairCallbackSet>(() => new Set());
467
+
468
+ const hooks = useConst<PhysicsHooks>(() => ({
469
+ filterContactPair: (...args) => {
470
+ for (const hook of filterContactPairHooks) {
471
+ const result = hook.current(...args);
472
+ if (result !== null) return result;
473
+ }
474
+ return null;
475
+ },
476
+ filterIntersectionPair: (...args) => {
477
+ for (const hook of filterIntersectionPairHooks) {
478
+ const result = hook.current(...args);
479
+ if (result === false) return false;
480
+ }
481
+ return true;
482
+ }
483
+ }));
484
+ const beforeStepCallbacks = useConst<WorldStepCallbackSet>(() => new Set());
485
+ const afterStepCallbacks = useConst<WorldStepCallbackSet>(() => new Set());
486
+
487
+ /**
488
+ * Initiate the world
489
+ * This creates a singleton proxy, so that the world is only created when
490
+ * something within it is accessed.
491
+ */
492
+ const {
493
+ proxy: worldProxy,
494
+ reset: resetWorldProxy,
495
+ set: setWorldProxy
496
+ } = useConst(() =>
497
+ createSingletonProxy<World>(
498
+ () => new rapier.World(vectorArrayToVector3(gravity))
499
+ )
500
+ );
501
+ useEffect(() => {
502
+ return () => {
503
+ worldProxy.free();
504
+ resetWorldProxy();
505
+ };
506
+ }, []);
507
+
508
+ // Update mutable props
509
+ useEffect(() => {
510
+ worldProxy.gravity = vector3ToRapierVector(gravity);
511
+
512
+ worldProxy.integrationParameters.numSolverIterations = numSolverIterations;
513
+ worldProxy.integrationParameters.numInternalPgsIterations =
514
+ numInternalPgsIterations;
515
+
516
+ worldProxy.integrationParameters.normalizedAllowedLinearError =
517
+ allowedLinearError;
518
+ worldProxy.integrationParameters.minIslandSize = minIslandSize;
519
+ worldProxy.integrationParameters.maxCcdSubsteps = maxCcdSubsteps;
520
+ worldProxy.integrationParameters.normalizedPredictionDistance =
521
+ predictionDistance;
522
+ worldProxy.lengthUnit = lengthUnit;
523
+ worldProxy.integrationParameters.contact_natural_frequency =
524
+ contactNaturalFrequency;
525
+ }, [
526
+ worldProxy,
527
+ ...gravity,
528
+ numSolverIterations,
529
+ numInternalPgsIterations,
530
+ allowedLinearError,
531
+ minIslandSize,
532
+ maxCcdSubsteps,
533
+ predictionDistance,
534
+ lengthUnit,
535
+ contactNaturalFrequency
536
+ ]);
537
+
538
+ const getSourceFromColliderHandle = useCallback((handle: ColliderHandle) => {
539
+ const collider = worldProxy.getCollider(handle);
540
+ const colEvents = colliderEvents.get(handle);
541
+ const colliderState = colliderStates.get(handle);
542
+
543
+ const rigidBodyHandle = collider?.parent()?.handle;
544
+ const rigidBody =
545
+ rigidBodyHandle !== undefined
546
+ ? worldProxy.getRigidBody(rigidBodyHandle)
547
+ : undefined;
548
+ const rbEvents =
549
+ rigidBody && rigidBodyHandle !== undefined
550
+ ? rigidBodyEvents.get(rigidBodyHandle)
551
+ : undefined;
552
+ const rigidBodyState =
553
+ rigidBodyHandle !== undefined
554
+ ? rigidBodyStates.get(rigidBodyHandle)
555
+ : undefined;
556
+
557
+ const source: CollisionSource = {
558
+ collider: {
559
+ object: collider,
560
+ events: colEvents,
561
+ state: colliderState
562
+ },
563
+ rigidBody: {
564
+ object: rigidBody,
565
+ events: rbEvents,
566
+ state: rigidBodyState
567
+ }
568
+ };
569
+
570
+ return source;
571
+ }, []);
572
+
573
+ const [steppingState] = useState<{
574
+ accumulator: number;
575
+ previousState: Record<number, any>;
576
+ }>({
577
+ previousState: {},
578
+ accumulator: 0
579
+ });
580
+
581
+ const step = useCallback(
582
+ (dt: number) => {
583
+ const world = worldProxy;
584
+
585
+ /* Check if the timestep is supposed to be variable. We'll do this here
586
+ once so we don't have to string-check every frame. */
587
+ const timeStepVariable = timeStep === "vary";
588
+
589
+ /**
590
+ * Fixed timeStep simulation progression
591
+ * @see https://gafferongames.com/post/fix_your_timestep/
592
+ */
593
+
594
+ const clampedDelta = MathUtils.clamp(dt, 0, 0.5);
595
+
596
+ const stepWorld = (delta: number) => {
597
+ // Trigger beforeStep callbacks
598
+ beforeStepCallbacks.forEach((callback) => {
599
+ callback.current(world);
600
+ });
601
+
602
+ world.timestep = delta;
603
+
604
+ const hasHooks =
605
+ filterContactPairHooks.size > 0 ||
606
+ filterIntersectionPairHooks.size > 0;
607
+
608
+ world.step(eventQueue, hasHooks ? hooks : undefined);
609
+
610
+ // Trigger afterStep callbacks
611
+ afterStepCallbacks.forEach((callback) => {
612
+ callback.current(world);
613
+ });
614
+ };
615
+
616
+ if (timeStepVariable) {
617
+ stepWorld(clampedDelta);
618
+ } else {
619
+ // don't step time forwards if paused
620
+ // Increase accumulator
621
+ steppingState.accumulator += clampedDelta;
622
+
623
+ while (steppingState.accumulator >= timeStep) {
624
+ // Set up previous state
625
+ // needed for accurate interpolations if the world steps more than once
626
+ if (interpolate) {
627
+ steppingState.previousState = {};
628
+ world.forEachRigidBody((body) => {
629
+ steppingState.previousState[body.handle] = {
630
+ position: body.translation(),
631
+ rotation: body.rotation()
632
+ };
633
+ });
634
+ }
635
+
636
+ stepWorld(timeStep);
637
+
638
+ steppingState.accumulator -= timeStep;
639
+ }
640
+ }
641
+
642
+ const interpolationAlpha =
643
+ timeStepVariable || !interpolate || paused
644
+ ? 1
645
+ : steppingState.accumulator / timeStep;
646
+
647
+ // Update meshes
648
+ rigidBodyStates.forEach((state, handle) => {
649
+ const rigidBody = world.getRigidBody(handle);
650
+
651
+ const events = rigidBodyEvents.get(handle);
652
+ if (events?.onSleep || events?.onWake) {
653
+ if (rigidBody.isSleeping() && !state.isSleeping) {
654
+ events?.onSleep?.();
655
+ }
656
+ if (!rigidBody.isSleeping() && state.isSleeping) {
657
+ events?.onWake?.();
658
+ }
659
+ state.isSleeping = rigidBody.isSleeping();
660
+ }
661
+
662
+ if (
663
+ !rigidBody ||
664
+ (rigidBody.isSleeping() && !("isInstancedMesh" in state.object)) ||
665
+ !state.setMatrix
666
+ ) {
667
+ return;
668
+ }
669
+
670
+ // New states
671
+ let t = rigidBody.translation() as Vector3;
672
+ let r = rigidBody.rotation() as Quaternion;
673
+
674
+ let previousState = steppingState.previousState[handle];
675
+
676
+ if (previousState) {
677
+ // Get previous simulated world position
678
+ _matrix4
679
+ .compose(
680
+ previousState.position,
681
+ rapierQuaternionToQuaternion(previousState.rotation),
682
+ state.scale
683
+ )
684
+ .premultiply(state.invertedWorldMatrix)
685
+ .decompose(_position, _rotation, _scale);
686
+
687
+ // Apply previous tick position
688
+ if (state.meshType == "mesh") {
689
+ state.object.position.copy(_position);
690
+ state.object.quaternion.copy(_rotation);
691
+ }
692
+ }
693
+
694
+ // Get new position
695
+ _matrix4
696
+ .compose(t, rapierQuaternionToQuaternion(r), state.scale)
697
+ .premultiply(state.invertedWorldMatrix)
698
+ .decompose(_position, _rotation, _scale);
699
+
700
+ if (state.meshType == "instancedMesh") {
701
+ state.setMatrix(_matrix4);
702
+ } else {
703
+ // Interpolate to new position
704
+ state.object.position.lerp(_position, interpolationAlpha);
705
+ state.object.quaternion.slerp(_rotation, interpolationAlpha);
706
+ }
707
+ });
708
+
709
+ eventQueue.drainCollisionEvents((handle1, handle2, started) => {
710
+ const source1 = getSourceFromColliderHandle(handle1);
711
+ const source2 = getSourceFromColliderHandle(handle2);
712
+
713
+ // Collision Events
714
+ if (!source1?.collider.object || !source2?.collider.object) {
715
+ return;
716
+ }
717
+
718
+ const collisionPayload1 = getCollisionPayloadFromSource(
719
+ source1,
720
+ source2
721
+ );
722
+ const collisionPayload2 = getCollisionPayloadFromSource(
723
+ source2,
724
+ source1
725
+ );
726
+
727
+ if (started) {
728
+ world.contactPair(
729
+ source1.collider.object,
730
+ source2.collider.object,
731
+ (manifold, flipped) => {
732
+ /* RigidBody events */
733
+ source1.rigidBody.events?.onCollisionEnter?.({
734
+ ...collisionPayload1,
735
+ manifold,
736
+ flipped
737
+ });
738
+
739
+ source2.rigidBody.events?.onCollisionEnter?.({
740
+ ...collisionPayload2,
741
+ manifold,
742
+ flipped
743
+ });
744
+
745
+ /* Collider events */
746
+ source1.collider.events?.onCollisionEnter?.({
747
+ ...collisionPayload1,
748
+ manifold,
749
+ flipped
750
+ });
751
+
752
+ source2.collider.events?.onCollisionEnter?.({
753
+ ...collisionPayload2,
754
+ manifold,
755
+ flipped
756
+ });
757
+ }
758
+ );
759
+ } else {
760
+ source1.rigidBody.events?.onCollisionExit?.(collisionPayload1);
761
+ source2.rigidBody.events?.onCollisionExit?.(collisionPayload2);
762
+ source1.collider.events?.onCollisionExit?.(collisionPayload1);
763
+ source2.collider.events?.onCollisionExit?.(collisionPayload2);
764
+ }
765
+
766
+ // Sensor Intersections
767
+ if (started) {
768
+ if (
769
+ world.intersectionPair(
770
+ source1.collider.object,
771
+ source2.collider.object
772
+ )
773
+ ) {
774
+ source1.rigidBody.events?.onIntersectionEnter?.(collisionPayload1);
775
+
776
+ source2.rigidBody.events?.onIntersectionEnter?.(collisionPayload2);
777
+
778
+ source1.collider.events?.onIntersectionEnter?.(collisionPayload1);
779
+
780
+ source2.collider.events?.onIntersectionEnter?.(collisionPayload2);
781
+ }
782
+ } else {
783
+ source1.rigidBody.events?.onIntersectionExit?.(collisionPayload1);
784
+ source2.rigidBody.events?.onIntersectionExit?.(collisionPayload2);
785
+ source1.collider.events?.onIntersectionExit?.(collisionPayload1);
786
+ source2.collider.events?.onIntersectionExit?.(collisionPayload2);
787
+ }
788
+ });
789
+
790
+ eventQueue.drainContactForceEvents((event) => {
791
+ const source1 = getSourceFromColliderHandle(event.collider1());
792
+ const source2 = getSourceFromColliderHandle(event.collider2());
793
+
794
+ // Collision Events
795
+ if (!source1?.collider.object || !source2?.collider.object) {
796
+ return;
797
+ }
798
+
799
+ const collisionPayload1 = getCollisionPayloadFromSource(
800
+ source1,
801
+ source2
802
+ );
803
+ const collisionPayload2 = getCollisionPayloadFromSource(
804
+ source2,
805
+ source1
806
+ );
807
+
808
+ source1.rigidBody.events?.onContactForce?.({
809
+ ...collisionPayload1,
810
+ totalForce: event.totalForce(),
811
+ totalForceMagnitude: event.totalForceMagnitude(),
812
+ maxForceDirection: event.maxForceDirection(),
813
+ maxForceMagnitude: event.maxForceMagnitude()
814
+ });
815
+
816
+ source2.rigidBody.events?.onContactForce?.({
817
+ ...collisionPayload2,
818
+ totalForce: event.totalForce(),
819
+ totalForceMagnitude: event.totalForceMagnitude(),
820
+ maxForceDirection: event.maxForceDirection(),
821
+ maxForceMagnitude: event.maxForceMagnitude()
822
+ });
823
+
824
+ source1.collider.events?.onContactForce?.({
825
+ ...collisionPayload1,
826
+ totalForce: event.totalForce(),
827
+ totalForceMagnitude: event.totalForceMagnitude(),
828
+ maxForceDirection: event.maxForceDirection(),
829
+ maxForceMagnitude: event.maxForceMagnitude()
830
+ });
831
+
832
+ source2.collider.events?.onContactForce?.({
833
+ ...collisionPayload2,
834
+ totalForce: event.totalForce(),
835
+ totalForceMagnitude: event.totalForceMagnitude(),
836
+ maxForceDirection: event.maxForceDirection(),
837
+ maxForceMagnitude: event.maxForceMagnitude()
838
+ });
839
+ });
840
+
841
+ world.forEachActiveRigidBody(() => {
842
+ invalidate();
843
+ });
844
+ },
845
+ [paused, timeStep, interpolate, worldProxy]
846
+ );
847
+
848
+ const context = useMemo<RapierContext>(
849
+ () => ({
850
+ rapier,
851
+ world: worldProxy,
852
+ setWorld: (world: World) => {
853
+ setWorldProxy(world);
854
+ },
855
+ physicsOptions: {
856
+ colliders,
857
+ gravity
858
+ },
859
+ rigidBodyStates,
860
+ colliderStates,
861
+ rigidBodyEvents,
862
+ colliderEvents,
863
+ beforeStepCallbacks,
864
+ afterStepCallbacks,
865
+ isPaused: paused,
866
+ isDebug: debug,
867
+ step,
868
+ filterContactPairHooks,
869
+ filterIntersectionPairHooks
870
+ }),
871
+ [paused, step, debug, colliders, gravity]
872
+ );
873
+
874
+ const stepCallback = useCallback(
875
+ (delta: number) => {
876
+ if (!paused) {
877
+ step(delta);
878
+ }
879
+ },
880
+ [paused, step]
881
+ );
882
+
883
+ return (
884
+ <rapierContext.Provider value={context}>
885
+ <FrameStepper
886
+ onStep={stepCallback}
887
+ type={updateLoop}
888
+ updatePriority={updatePriority}
889
+ />
890
+ {debug && <Debug />}
891
+ {children}
892
+ </rapierContext.Provider>
893
+ );
894
+ };