@react-three/rapier 2.0.1 → 2.2.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.
@@ -1,5 +1,5 @@
1
1
  import type Rapier from "@dimforge/rapier3d-compat";
2
- import { Collider, ColliderHandle, RigidBody, RigidBodyHandle, World } from "@dimforge/rapier3d-compat";
2
+ import { Collider, ColliderHandle, RigidBody, RigidBodyHandle, SolverFlags, World } from "@dimforge/rapier3d-compat";
3
3
  import React, { FC, ReactNode } from "react";
4
4
  import { Matrix4, Object3D, Vector3 } from "three";
5
5
  import { CollisionEnterHandler, CollisionExitHandler, ContactForceHandler, IntersectionEnterHandler, IntersectionExitHandler, RigidBodyAutoCollider, Vector3Tuple } from "../types.js";
@@ -21,6 +21,14 @@ export type WorldStepCallback = (world: World) => void;
21
21
  export type WorldStepCallbackSet = Set<{
22
22
  current: WorldStepCallback;
23
23
  }>;
24
+ export type FilterContactPairCallback = (collider1: ColliderHandle, collider2: ColliderHandle, body1: RigidBodyHandle, body2: RigidBodyHandle) => SolverFlags | null;
25
+ export type FilterIntersectionPairCallback = (collider1: ColliderHandle, collider2: ColliderHandle, body1: RigidBodyHandle, body2: RigidBodyHandle) => boolean;
26
+ export type FilterContactPairCallbackSet = Set<{
27
+ current: FilterContactPairCallback;
28
+ }>;
29
+ export type FilterIntersectionPairCallbackSet = Set<{
30
+ current: FilterIntersectionPairCallback;
31
+ }>;
24
32
  export interface ColliderState {
25
33
  collider: Collider;
26
34
  object: Object3D;
@@ -69,6 +77,16 @@ export interface RapierContext {
69
77
  * @internal
70
78
  */
71
79
  afterStepCallbacks: WorldStepCallbackSet;
80
+ /**
81
+ * Hooks to filter contact pairs
82
+ * @internal
83
+ */
84
+ filterContactPairHooks: FilterContactPairCallbackSet;
85
+ /**
86
+ * Hooks to filter intersection pairs
87
+ * @internal
88
+ */
89
+ filterIntersectionPairHooks: FilterIntersectionPairCallbackSet;
72
90
  /**
73
91
  * Direct access to the Rapier instance
74
92
  */
@@ -161,14 +179,6 @@ export interface PhysicsProps {
161
179
  * @defaultValue 4
162
180
  */
163
181
  numSolverIterations?: number;
164
- /**
165
- * Number of addition friction resolution iteration run during the last solver sub-step.
166
- * The greater this value is, the most realistic friction will be.
167
- * However a greater number of iterations is more computationally intensive.
168
- *
169
- * @defaultValue 4
170
- */
171
- numAdditionalFrictionIterations?: number;
172
182
  /**
173
183
  * Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration.
174
184
  * Increasing this parameter will improve stability of the simulation. It will have a lesser effect than
@@ -17,6 +17,74 @@ export declare const useBeforePhysicsStep: (callback: WorldStepCallback) => void
17
17
  * @category Hooks
18
18
  */
19
19
  export declare const useAfterPhysicsStep: (callback: WorldStepCallback) => void;
20
+ /**
21
+ * Registers a callback to filter contact pairs.
22
+ *
23
+ * The callback determines if contact computation should happen between two colliders,
24
+ * and how the constraints solver should behave for these contacts.
25
+ *
26
+ * This will only be executed if at least one of the involved colliders contains the
27
+ * `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks.
28
+ *
29
+ * @param callback - Function that returns:
30
+ * - `SolverFlags.COMPUTE_IMPULSE` (1) - Process the collision normally (compute impulses and resolve penetration)
31
+ * - `SolverFlags.EMPTY` (0) - Skip computing impulses for this collision pair (colliders pass through each other)
32
+ * - `null` - Skip this hook; let the next registered hook decide, or use Rapier's default behavior if no hook handles it
33
+ *
34
+ * When multiple hooks are registered, they are called in order until one returns a non-null value.
35
+ * That value is then passed to Rapier's physics engine.
36
+ *
37
+ * @category Hooks
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * import { useFilterContactPair } from '@react-three/rapier';
42
+ * import { SolverFlags } from '@dimforge/rapier3d-compat';
43
+ *
44
+ * useFilterContactPair((collider1, collider2, body1, body2) => {
45
+ * // Only process collisions for specific bodies
46
+ * if (body1 === myBodyHandle) {
47
+ * return SolverFlags.COMPUTE_IMPULSE;
48
+ * }
49
+ * // Let other hooks or default behavior handle it
50
+ * return null;
51
+ * });
52
+ * ```
53
+ */
54
+ export declare const useFilterContactPair: (callback: (collider1: number, collider2: number, body1: number, body2: number) => number | null) => void;
55
+ /**
56
+ * Registers a callback to filter intersection pairs.
57
+ *
58
+ * The callback determines if intersection computation should happen between two colliders
59
+ * (where at least one is a sensor).
60
+ *
61
+ * This will only be executed if at least one of the involved colliders contains the
62
+ * `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks.
63
+ *
64
+ * @param callback - Function that returns:
65
+ * - `true` - Allow the intersection to be detected (trigger intersection events)
66
+ * - `false` - Block the intersection (no intersection events will fire)
67
+ *
68
+ * When multiple hooks are registered, the **first hook that returns `false` blocks** the intersection.
69
+ * If all hooks return `true`, the intersection is allowed.
70
+ *
71
+ * @category Hooks
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * import { useFilterIntersectionPair } from '@react-three/rapier';
76
+ *
77
+ * useFilterIntersectionPair((collider1, collider2, body1, body2) => {
78
+ * // Block intersections for specific body pairs
79
+ * if (body1 === myBodyHandle && body2 === otherBodyHandle) {
80
+ * return false;
81
+ * }
82
+ * // Allow all other intersections
83
+ * return true;
84
+ * });
85
+ * ```
86
+ */
87
+ export declare const useFilterIntersectionPair: (callback: (collider1: number, collider2: number, body1: number, body2: number) => boolean) => void;
20
88
  /**
21
89
  * @internal
22
90
  */
@@ -2,7 +2,7 @@ export * from "./types.js";
2
2
  export type { RigidBodyProps } from "./components/RigidBody.js";
3
3
  export type { InstancedRigidBodiesProps, InstancedRigidBodyProps } from "./components/InstancedRigidBodies.js";
4
4
  export type { CylinderColliderProps, BallColliderProps, CapsuleColliderProps, ConeColliderProps, ConvexHullColliderProps, CuboidColliderProps, HeightfieldColliderProps, RoundCuboidColliderProps, TrimeshColliderProps, ColliderOptionsRequiredArgs } from "./components/AnyCollider.js";
5
- export type { PhysicsProps, RapierContext, WorldStepCallback } from "./components/Physics.js";
5
+ export type { PhysicsProps, RapierContext, WorldStepCallback, FilterContactPairCallback, FilterIntersectionPairCallback } from "./components/Physics.js";
6
6
  export type { MeshColliderProps } from "./components/MeshCollider.js";
7
7
  export { Physics } from "./components/Physics.js";
8
8
  export { RigidBody } from "./components/RigidBody.js";
@@ -10,6 +10,6 @@ export { MeshCollider } from "./components/MeshCollider.js";
10
10
  export { InstancedRigidBodies } from "./components/InstancedRigidBodies.js";
11
11
  export * from "./components/AnyCollider.js";
12
12
  export * from "./hooks/joints.js";
13
- export { useRapier, useBeforePhysicsStep, useAfterPhysicsStep } from "./hooks/hooks.js";
13
+ export { useRapier, useBeforePhysicsStep, useAfterPhysicsStep, useFilterContactPair, useFilterIntersectionPair } from "./hooks/hooks.js";
14
14
  export * from "./utils/interaction-groups.js";
15
15
  export * from "./utils/three-object-helpers.js";
@@ -580,6 +580,98 @@ const useAfterPhysicsStep = callback => {
580
580
  }, []);
581
581
  };
582
582
 
583
+ /**
584
+ * Registers a callback to filter contact pairs.
585
+ *
586
+ * The callback determines if contact computation should happen between two colliders,
587
+ * and how the constraints solver should behave for these contacts.
588
+ *
589
+ * This will only be executed if at least one of the involved colliders contains the
590
+ * `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks.
591
+ *
592
+ * @param callback - Function that returns:
593
+ * - `SolverFlags.COMPUTE_IMPULSE` (1) - Process the collision normally (compute impulses and resolve penetration)
594
+ * - `SolverFlags.EMPTY` (0) - Skip computing impulses for this collision pair (colliders pass through each other)
595
+ * - `null` - Skip this hook; let the next registered hook decide, or use Rapier's default behavior if no hook handles it
596
+ *
597
+ * When multiple hooks are registered, they are called in order until one returns a non-null value.
598
+ * That value is then passed to Rapier's physics engine.
599
+ *
600
+ * @category Hooks
601
+ *
602
+ * @example
603
+ * ```tsx
604
+ * import { useFilterContactPair } from '@react-three/rapier';
605
+ * import { SolverFlags } from '@dimforge/rapier3d-compat';
606
+ *
607
+ * useFilterContactPair((collider1, collider2, body1, body2) => {
608
+ * // Only process collisions for specific bodies
609
+ * if (body1 === myBodyHandle) {
610
+ * return SolverFlags.COMPUTE_IMPULSE;
611
+ * }
612
+ * // Let other hooks or default behavior handle it
613
+ * return null;
614
+ * });
615
+ * ```
616
+ */
617
+ const useFilterContactPair = callback => {
618
+ const {
619
+ filterContactPairHooks
620
+ } = useRapier();
621
+ const ref = useMutableCallback(callback);
622
+ React.useEffect(() => {
623
+ filterContactPairHooks.add(ref);
624
+ return () => {
625
+ filterContactPairHooks.delete(ref);
626
+ };
627
+ }, []);
628
+ };
629
+
630
+ /**
631
+ * Registers a callback to filter intersection pairs.
632
+ *
633
+ * The callback determines if intersection computation should happen between two colliders
634
+ * (where at least one is a sensor).
635
+ *
636
+ * This will only be executed if at least one of the involved colliders contains the
637
+ * `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks.
638
+ *
639
+ * @param callback - Function that returns:
640
+ * - `true` - Allow the intersection to be detected (trigger intersection events)
641
+ * - `false` - Block the intersection (no intersection events will fire)
642
+ *
643
+ * When multiple hooks are registered, the **first hook that returns `false` blocks** the intersection.
644
+ * If all hooks return `true`, the intersection is allowed.
645
+ *
646
+ * @category Hooks
647
+ *
648
+ * @example
649
+ * ```tsx
650
+ * import { useFilterIntersectionPair } from '@react-three/rapier';
651
+ *
652
+ * useFilterIntersectionPair((collider1, collider2, body1, body2) => {
653
+ * // Block intersections for specific body pairs
654
+ * if (body1 === myBodyHandle && body2 === otherBodyHandle) {
655
+ * return false;
656
+ * }
657
+ * // Allow all other intersections
658
+ * return true;
659
+ * });
660
+ * ```
661
+ */
662
+ const useFilterIntersectionPair = callback => {
663
+ const {
664
+ filterIntersectionPairHooks
665
+ } = useRapier();
666
+ const ref = useMutableCallback(callback);
667
+ React.useEffect(() => {
668
+ filterIntersectionPairHooks.add(ref);
669
+ return () => {
670
+ filterIntersectionPairHooks.delete(ref);
671
+ };
672
+ }, []);
673
+ };
674
+
583
675
  // Internal hooks
584
676
  /**
585
677
  * @internal
@@ -708,7 +800,6 @@ const Physics = props => {
708
800
  allowedLinearError = 0.001,
709
801
  predictionDistance = 0.002,
710
802
  numSolverIterations = 4,
711
- numAdditionalFrictionIterations = 4,
712
803
  numInternalPgsIterations = 1,
713
804
  minIslandSize = 128,
714
805
  maxCcdSubsteps = 1,
@@ -724,6 +815,24 @@ const Physics = props => {
724
815
  const rigidBodyEvents = useConst(() => new Map());
725
816
  const colliderEvents = useConst(() => new Map());
726
817
  const eventQueue = useConst(() => new rapier3dCompat.EventQueue(false));
818
+ const filterContactPairHooks = useConst(() => new Set());
819
+ const filterIntersectionPairHooks = useConst(() => new Set());
820
+ const hooks = useConst(() => ({
821
+ filterContactPair: (...args) => {
822
+ for (const hook of filterContactPairHooks) {
823
+ const result = hook.current(...args);
824
+ if (result !== null) return result;
825
+ }
826
+ return null;
827
+ },
828
+ filterIntersectionPair: (...args) => {
829
+ for (const hook of filterIntersectionPairHooks) {
830
+ const result = hook.current(...args);
831
+ if (result === false) return false;
832
+ }
833
+ return true;
834
+ }
835
+ }));
727
836
  const beforeStepCallbacks = useConst(() => new Set());
728
837
  const afterStepCallbacks = useConst(() => new Set());
729
838
 
@@ -748,7 +857,6 @@ const Physics = props => {
748
857
  React.useEffect(() => {
749
858
  worldProxy.gravity = vector3ToRapierVector(gravity);
750
859
  worldProxy.integrationParameters.numSolverIterations = numSolverIterations;
751
- worldProxy.integrationParameters.numAdditionalFrictionIterations = numAdditionalFrictionIterations;
752
860
  worldProxy.integrationParameters.numInternalPgsIterations = numInternalPgsIterations;
753
861
  worldProxy.integrationParameters.normalizedAllowedLinearError = allowedLinearError;
754
862
  worldProxy.integrationParameters.minIslandSize = minIslandSize;
@@ -756,7 +864,7 @@ const Physics = props => {
756
864
  worldProxy.integrationParameters.normalizedPredictionDistance = predictionDistance;
757
865
  worldProxy.lengthUnit = lengthUnit;
758
866
  worldProxy.integrationParameters.contact_natural_frequency = contactNaturalFrequency;
759
- }, [worldProxy, ...gravity, numSolverIterations, numAdditionalFrictionIterations, numInternalPgsIterations, allowedLinearError, minIslandSize, maxCcdSubsteps, predictionDistance, lengthUnit, contactNaturalFrequency]);
867
+ }, [worldProxy, ...gravity, numSolverIterations, numInternalPgsIterations, allowedLinearError, minIslandSize, maxCcdSubsteps, predictionDistance, lengthUnit, contactNaturalFrequency]);
760
868
  const getSourceFromColliderHandle = React.useCallback(handle => {
761
869
  var _collider$parent;
762
870
  const collider = worldProxy.getCollider(handle);
@@ -803,7 +911,8 @@ const Physics = props => {
803
911
  callback.current(world);
804
912
  });
805
913
  world.timestep = delta;
806
- world.step(eventQueue);
914
+ const hasHooks = filterContactPairHooks.size > 0 || filterIntersectionPairHooks.size > 0;
915
+ world.step(eventQueue, hasHooks ? hooks : undefined);
807
916
 
808
917
  // Trigger afterStep callbacks
809
918
  afterStepCallbacks.forEach(callback => {
@@ -994,7 +1103,9 @@ const Physics = props => {
994
1103
  afterStepCallbacks,
995
1104
  isPaused: paused,
996
1105
  isDebug: debug,
997
- step
1106
+ step,
1107
+ filterContactPairHooks,
1108
+ filterIntersectionPairHooks
998
1109
  }), [paused, step, debug, colliders, gravity]);
999
1110
  const stepCallback = React.useCallback(delta => {
1000
1111
  if (!paused) {
@@ -1812,6 +1923,8 @@ exports.interactionGroups = interactionGroups;
1812
1923
  exports.quat = quat;
1813
1924
  exports.useAfterPhysicsStep = useAfterPhysicsStep;
1814
1925
  exports.useBeforePhysicsStep = useBeforePhysicsStep;
1926
+ exports.useFilterContactPair = useFilterContactPair;
1927
+ exports.useFilterIntersectionPair = useFilterIntersectionPair;
1815
1928
  exports.useFixedJoint = useFixedJoint;
1816
1929
  exports.useImpulseJoint = useImpulseJoint;
1817
1930
  exports.usePrismaticJoint = usePrismaticJoint;
@@ -580,6 +580,98 @@ const useAfterPhysicsStep = callback => {
580
580
  }, []);
581
581
  };
582
582
 
583
+ /**
584
+ * Registers a callback to filter contact pairs.
585
+ *
586
+ * The callback determines if contact computation should happen between two colliders,
587
+ * and how the constraints solver should behave for these contacts.
588
+ *
589
+ * This will only be executed if at least one of the involved colliders contains the
590
+ * `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks.
591
+ *
592
+ * @param callback - Function that returns:
593
+ * - `SolverFlags.COMPUTE_IMPULSE` (1) - Process the collision normally (compute impulses and resolve penetration)
594
+ * - `SolverFlags.EMPTY` (0) - Skip computing impulses for this collision pair (colliders pass through each other)
595
+ * - `null` - Skip this hook; let the next registered hook decide, or use Rapier's default behavior if no hook handles it
596
+ *
597
+ * When multiple hooks are registered, they are called in order until one returns a non-null value.
598
+ * That value is then passed to Rapier's physics engine.
599
+ *
600
+ * @category Hooks
601
+ *
602
+ * @example
603
+ * ```tsx
604
+ * import { useFilterContactPair } from '@react-three/rapier';
605
+ * import { SolverFlags } from '@dimforge/rapier3d-compat';
606
+ *
607
+ * useFilterContactPair((collider1, collider2, body1, body2) => {
608
+ * // Only process collisions for specific bodies
609
+ * if (body1 === myBodyHandle) {
610
+ * return SolverFlags.COMPUTE_IMPULSE;
611
+ * }
612
+ * // Let other hooks or default behavior handle it
613
+ * return null;
614
+ * });
615
+ * ```
616
+ */
617
+ const useFilterContactPair = callback => {
618
+ const {
619
+ filterContactPairHooks
620
+ } = useRapier();
621
+ const ref = useMutableCallback(callback);
622
+ React.useEffect(() => {
623
+ filterContactPairHooks.add(ref);
624
+ return () => {
625
+ filterContactPairHooks.delete(ref);
626
+ };
627
+ }, []);
628
+ };
629
+
630
+ /**
631
+ * Registers a callback to filter intersection pairs.
632
+ *
633
+ * The callback determines if intersection computation should happen between two colliders
634
+ * (where at least one is a sensor).
635
+ *
636
+ * This will only be executed if at least one of the involved colliders contains the
637
+ * `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks.
638
+ *
639
+ * @param callback - Function that returns:
640
+ * - `true` - Allow the intersection to be detected (trigger intersection events)
641
+ * - `false` - Block the intersection (no intersection events will fire)
642
+ *
643
+ * When multiple hooks are registered, the **first hook that returns `false` blocks** the intersection.
644
+ * If all hooks return `true`, the intersection is allowed.
645
+ *
646
+ * @category Hooks
647
+ *
648
+ * @example
649
+ * ```tsx
650
+ * import { useFilterIntersectionPair } from '@react-three/rapier';
651
+ *
652
+ * useFilterIntersectionPair((collider1, collider2, body1, body2) => {
653
+ * // Block intersections for specific body pairs
654
+ * if (body1 === myBodyHandle && body2 === otherBodyHandle) {
655
+ * return false;
656
+ * }
657
+ * // Allow all other intersections
658
+ * return true;
659
+ * });
660
+ * ```
661
+ */
662
+ const useFilterIntersectionPair = callback => {
663
+ const {
664
+ filterIntersectionPairHooks
665
+ } = useRapier();
666
+ const ref = useMutableCallback(callback);
667
+ React.useEffect(() => {
668
+ filterIntersectionPairHooks.add(ref);
669
+ return () => {
670
+ filterIntersectionPairHooks.delete(ref);
671
+ };
672
+ }, []);
673
+ };
674
+
583
675
  // Internal hooks
584
676
  /**
585
677
  * @internal
@@ -708,7 +800,6 @@ const Physics = props => {
708
800
  allowedLinearError = 0.001,
709
801
  predictionDistance = 0.002,
710
802
  numSolverIterations = 4,
711
- numAdditionalFrictionIterations = 4,
712
803
  numInternalPgsIterations = 1,
713
804
  minIslandSize = 128,
714
805
  maxCcdSubsteps = 1,
@@ -724,6 +815,24 @@ const Physics = props => {
724
815
  const rigidBodyEvents = useConst(() => new Map());
725
816
  const colliderEvents = useConst(() => new Map());
726
817
  const eventQueue = useConst(() => new rapier3dCompat.EventQueue(false));
818
+ const filterContactPairHooks = useConst(() => new Set());
819
+ const filterIntersectionPairHooks = useConst(() => new Set());
820
+ const hooks = useConst(() => ({
821
+ filterContactPair: (...args) => {
822
+ for (const hook of filterContactPairHooks) {
823
+ const result = hook.current(...args);
824
+ if (result !== null) return result;
825
+ }
826
+ return null;
827
+ },
828
+ filterIntersectionPair: (...args) => {
829
+ for (const hook of filterIntersectionPairHooks) {
830
+ const result = hook.current(...args);
831
+ if (result === false) return false;
832
+ }
833
+ return true;
834
+ }
835
+ }));
727
836
  const beforeStepCallbacks = useConst(() => new Set());
728
837
  const afterStepCallbacks = useConst(() => new Set());
729
838
 
@@ -748,7 +857,6 @@ const Physics = props => {
748
857
  React.useEffect(() => {
749
858
  worldProxy.gravity = vector3ToRapierVector(gravity);
750
859
  worldProxy.integrationParameters.numSolverIterations = numSolverIterations;
751
- worldProxy.integrationParameters.numAdditionalFrictionIterations = numAdditionalFrictionIterations;
752
860
  worldProxy.integrationParameters.numInternalPgsIterations = numInternalPgsIterations;
753
861
  worldProxy.integrationParameters.normalizedAllowedLinearError = allowedLinearError;
754
862
  worldProxy.integrationParameters.minIslandSize = minIslandSize;
@@ -756,7 +864,7 @@ const Physics = props => {
756
864
  worldProxy.integrationParameters.normalizedPredictionDistance = predictionDistance;
757
865
  worldProxy.lengthUnit = lengthUnit;
758
866
  worldProxy.integrationParameters.contact_natural_frequency = contactNaturalFrequency;
759
- }, [worldProxy, ...gravity, numSolverIterations, numAdditionalFrictionIterations, numInternalPgsIterations, allowedLinearError, minIslandSize, maxCcdSubsteps, predictionDistance, lengthUnit, contactNaturalFrequency]);
867
+ }, [worldProxy, ...gravity, numSolverIterations, numInternalPgsIterations, allowedLinearError, minIslandSize, maxCcdSubsteps, predictionDistance, lengthUnit, contactNaturalFrequency]);
760
868
  const getSourceFromColliderHandle = React.useCallback(handle => {
761
869
  var _collider$parent;
762
870
  const collider = worldProxy.getCollider(handle);
@@ -803,7 +911,8 @@ const Physics = props => {
803
911
  callback.current(world);
804
912
  });
805
913
  world.timestep = delta;
806
- world.step(eventQueue);
914
+ const hasHooks = filterContactPairHooks.size > 0 || filterIntersectionPairHooks.size > 0;
915
+ world.step(eventQueue, hasHooks ? hooks : undefined);
807
916
 
808
917
  // Trigger afterStep callbacks
809
918
  afterStepCallbacks.forEach(callback => {
@@ -994,7 +1103,9 @@ const Physics = props => {
994
1103
  afterStepCallbacks,
995
1104
  isPaused: paused,
996
1105
  isDebug: debug,
997
- step
1106
+ step,
1107
+ filterContactPairHooks,
1108
+ filterIntersectionPairHooks
998
1109
  }), [paused, step, debug, colliders, gravity]);
999
1110
  const stepCallback = React.useCallback(delta => {
1000
1111
  if (!paused) {
@@ -1812,6 +1923,8 @@ exports.interactionGroups = interactionGroups;
1812
1923
  exports.quat = quat;
1813
1924
  exports.useAfterPhysicsStep = useAfterPhysicsStep;
1814
1925
  exports.useBeforePhysicsStep = useBeforePhysicsStep;
1926
+ exports.useFilterContactPair = useFilterContactPair;
1927
+ exports.useFilterIntersectionPair = useFilterIntersectionPair;
1815
1928
  exports.useFixedJoint = useFixedJoint;
1816
1929
  exports.useImpulseJoint = useImpulseJoint;
1817
1930
  exports.usePrismaticJoint = usePrismaticJoint;
@@ -555,6 +555,98 @@ const useAfterPhysicsStep = callback => {
555
555
  }, []);
556
556
  };
557
557
 
558
+ /**
559
+ * Registers a callback to filter contact pairs.
560
+ *
561
+ * The callback determines if contact computation should happen between two colliders,
562
+ * and how the constraints solver should behave for these contacts.
563
+ *
564
+ * This will only be executed if at least one of the involved colliders contains the
565
+ * `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks.
566
+ *
567
+ * @param callback - Function that returns:
568
+ * - `SolverFlags.COMPUTE_IMPULSE` (1) - Process the collision normally (compute impulses and resolve penetration)
569
+ * - `SolverFlags.EMPTY` (0) - Skip computing impulses for this collision pair (colliders pass through each other)
570
+ * - `null` - Skip this hook; let the next registered hook decide, or use Rapier's default behavior if no hook handles it
571
+ *
572
+ * When multiple hooks are registered, they are called in order until one returns a non-null value.
573
+ * That value is then passed to Rapier's physics engine.
574
+ *
575
+ * @category Hooks
576
+ *
577
+ * @example
578
+ * ```tsx
579
+ * import { useFilterContactPair } from '@react-three/rapier';
580
+ * import { SolverFlags } from '@dimforge/rapier3d-compat';
581
+ *
582
+ * useFilterContactPair((collider1, collider2, body1, body2) => {
583
+ * // Only process collisions for specific bodies
584
+ * if (body1 === myBodyHandle) {
585
+ * return SolverFlags.COMPUTE_IMPULSE;
586
+ * }
587
+ * // Let other hooks or default behavior handle it
588
+ * return null;
589
+ * });
590
+ * ```
591
+ */
592
+ const useFilterContactPair = callback => {
593
+ const {
594
+ filterContactPairHooks
595
+ } = useRapier();
596
+ const ref = useMutableCallback(callback);
597
+ useEffect(() => {
598
+ filterContactPairHooks.add(ref);
599
+ return () => {
600
+ filterContactPairHooks.delete(ref);
601
+ };
602
+ }, []);
603
+ };
604
+
605
+ /**
606
+ * Registers a callback to filter intersection pairs.
607
+ *
608
+ * The callback determines if intersection computation should happen between two colliders
609
+ * (where at least one is a sensor).
610
+ *
611
+ * This will only be executed if at least one of the involved colliders contains the
612
+ * `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks.
613
+ *
614
+ * @param callback - Function that returns:
615
+ * - `true` - Allow the intersection to be detected (trigger intersection events)
616
+ * - `false` - Block the intersection (no intersection events will fire)
617
+ *
618
+ * When multiple hooks are registered, the **first hook that returns `false` blocks** the intersection.
619
+ * If all hooks return `true`, the intersection is allowed.
620
+ *
621
+ * @category Hooks
622
+ *
623
+ * @example
624
+ * ```tsx
625
+ * import { useFilterIntersectionPair } from '@react-three/rapier';
626
+ *
627
+ * useFilterIntersectionPair((collider1, collider2, body1, body2) => {
628
+ * // Block intersections for specific body pairs
629
+ * if (body1 === myBodyHandle && body2 === otherBodyHandle) {
630
+ * return false;
631
+ * }
632
+ * // Allow all other intersections
633
+ * return true;
634
+ * });
635
+ * ```
636
+ */
637
+ const useFilterIntersectionPair = callback => {
638
+ const {
639
+ filterIntersectionPairHooks
640
+ } = useRapier();
641
+ const ref = useMutableCallback(callback);
642
+ useEffect(() => {
643
+ filterIntersectionPairHooks.add(ref);
644
+ return () => {
645
+ filterIntersectionPairHooks.delete(ref);
646
+ };
647
+ }, []);
648
+ };
649
+
558
650
  // Internal hooks
559
651
  /**
560
652
  * @internal
@@ -683,7 +775,6 @@ const Physics = props => {
683
775
  allowedLinearError = 0.001,
684
776
  predictionDistance = 0.002,
685
777
  numSolverIterations = 4,
686
- numAdditionalFrictionIterations = 4,
687
778
  numInternalPgsIterations = 1,
688
779
  minIslandSize = 128,
689
780
  maxCcdSubsteps = 1,
@@ -699,6 +790,24 @@ const Physics = props => {
699
790
  const rigidBodyEvents = useConst(() => new Map());
700
791
  const colliderEvents = useConst(() => new Map());
701
792
  const eventQueue = useConst(() => new EventQueue(false));
793
+ const filterContactPairHooks = useConst(() => new Set());
794
+ const filterIntersectionPairHooks = useConst(() => new Set());
795
+ const hooks = useConst(() => ({
796
+ filterContactPair: (...args) => {
797
+ for (const hook of filterContactPairHooks) {
798
+ const result = hook.current(...args);
799
+ if (result !== null) return result;
800
+ }
801
+ return null;
802
+ },
803
+ filterIntersectionPair: (...args) => {
804
+ for (const hook of filterIntersectionPairHooks) {
805
+ const result = hook.current(...args);
806
+ if (result === false) return false;
807
+ }
808
+ return true;
809
+ }
810
+ }));
702
811
  const beforeStepCallbacks = useConst(() => new Set());
703
812
  const afterStepCallbacks = useConst(() => new Set());
704
813
 
@@ -723,7 +832,6 @@ const Physics = props => {
723
832
  useEffect(() => {
724
833
  worldProxy.gravity = vector3ToRapierVector(gravity);
725
834
  worldProxy.integrationParameters.numSolverIterations = numSolverIterations;
726
- worldProxy.integrationParameters.numAdditionalFrictionIterations = numAdditionalFrictionIterations;
727
835
  worldProxy.integrationParameters.numInternalPgsIterations = numInternalPgsIterations;
728
836
  worldProxy.integrationParameters.normalizedAllowedLinearError = allowedLinearError;
729
837
  worldProxy.integrationParameters.minIslandSize = minIslandSize;
@@ -731,7 +839,7 @@ const Physics = props => {
731
839
  worldProxy.integrationParameters.normalizedPredictionDistance = predictionDistance;
732
840
  worldProxy.lengthUnit = lengthUnit;
733
841
  worldProxy.integrationParameters.contact_natural_frequency = contactNaturalFrequency;
734
- }, [worldProxy, ...gravity, numSolverIterations, numAdditionalFrictionIterations, numInternalPgsIterations, allowedLinearError, minIslandSize, maxCcdSubsteps, predictionDistance, lengthUnit, contactNaturalFrequency]);
842
+ }, [worldProxy, ...gravity, numSolverIterations, numInternalPgsIterations, allowedLinearError, minIslandSize, maxCcdSubsteps, predictionDistance, lengthUnit, contactNaturalFrequency]);
735
843
  const getSourceFromColliderHandle = useCallback(handle => {
736
844
  var _collider$parent;
737
845
  const collider = worldProxy.getCollider(handle);
@@ -778,7 +886,8 @@ const Physics = props => {
778
886
  callback.current(world);
779
887
  });
780
888
  world.timestep = delta;
781
- world.step(eventQueue);
889
+ const hasHooks = filterContactPairHooks.size > 0 || filterIntersectionPairHooks.size > 0;
890
+ world.step(eventQueue, hasHooks ? hooks : undefined);
782
891
 
783
892
  // Trigger afterStep callbacks
784
893
  afterStepCallbacks.forEach(callback => {
@@ -969,7 +1078,9 @@ const Physics = props => {
969
1078
  afterStepCallbacks,
970
1079
  isPaused: paused,
971
1080
  isDebug: debug,
972
- step
1081
+ step,
1082
+ filterContactPairHooks,
1083
+ filterIntersectionPairHooks
973
1084
  }), [paused, step, debug, colliders, gravity]);
974
1085
  const stepCallback = useCallback(delta => {
975
1086
  if (!paused) {
@@ -1754,4 +1865,4 @@ const useSpringJoint = (body1, body2, [body1Anchor, body2Anchor, restLength, sti
1754
1865
  const interactionGroups = (memberships, filters) => (bitmask(memberships) << 16) + (filters !== undefined ? bitmask(filters) : 0b1111111111111111);
1755
1866
  const bitmask = groups => [groups].flat().reduce((acc, layer) => acc | 1 << layer, 0);
1756
1867
 
1757
- export { AnyCollider, BallCollider, CapsuleCollider, ConeCollider, ConvexHullCollider, CuboidCollider, CylinderCollider, HeightfieldCollider, InstancedRigidBodies, MeshCollider, Physics, RigidBody, RoundConeCollider, RoundCuboidCollider, RoundCylinderCollider, TrimeshCollider, euler, interactionGroups, quat, useAfterPhysicsStep, useBeforePhysicsStep, useFixedJoint, useImpulseJoint, usePrismaticJoint, useRapier, useRevoluteJoint, useRopeJoint, useSphericalJoint, useSpringJoint, vec3 };
1868
+ export { AnyCollider, BallCollider, CapsuleCollider, ConeCollider, ConvexHullCollider, CuboidCollider, CylinderCollider, HeightfieldCollider, InstancedRigidBodies, MeshCollider, Physics, RigidBody, RoundConeCollider, RoundCuboidCollider, RoundCylinderCollider, TrimeshCollider, euler, interactionGroups, quat, useAfterPhysicsStep, useBeforePhysicsStep, useFilterContactPair, useFilterIntersectionPair, useFixedJoint, useImpulseJoint, usePrismaticJoint, useRapier, useRevoluteJoint, useRopeJoint, useSphericalJoint, useSpringJoint, vec3 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-three/rapier",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "source": "src/index.ts",
5
5
  "main": "dist/react-three-rapier.cjs.js",
6
6
  "module": "dist/react-three-rapier.esm.js",
@@ -25,7 +25,7 @@
25
25
  "three": ">=0.159.0"
26
26
  },
27
27
  "dependencies": {
28
- "@dimforge/rapier3d-compat": "0.14.0",
28
+ "@dimforge/rapier3d-compat": "0.19.2",
29
29
  "suspend-react": "^0.1.3",
30
30
  "three-stdlib": "^2.35.12"
31
31
  },
package/readme.md CHANGED
@@ -2,6 +2,11 @@
2
2
  <a href="#"><img src="https://raw.githubusercontent.com/pmndrs/react-three-rapier/HEAD/packages/react-three-rapier/misc/hero.svg" alt="@react-three/rapier" /></a>
3
3
  </p>
4
4
 
5
+ <p align="center">
6
+ 📣 @react-three/rapier v2 has been released, which adds support for @react-three/fiber v9 and react v19. If you are using react v18, you will need to use @react-three/rapier v1 and @react-three/fiber v8.
7
+ </p>
8
+
9
+
5
10
  <p align="center">
6
11
  <a href="https://www.npmjs.com/package/@react-three/rapier"><img src="https://img.shields.io/npm/v/@react-three/rapier?style=for-the-badge&colorA=0099DA&colorB=ffffff" /></a>
7
12
  <a href="https://discord.gg/ZZjjNvJ"><img src="https://img.shields.io/discord/740090768164651008?style=for-the-badge&colorA=0099DA&colorB=ffffff&label=discord&logo=discord&logoColor=ffffff" /></a>
@@ -80,6 +85,7 @@ For full API outline and documentation, see 🧩 [API Docs](https://pmndrs.githu
80
85
  - [Spring Joint](#spring-joint)
81
86
  - [🖼 Joints Example](#-joints-example)
82
87
  - [Advanced hooks usage](#advanced-hooks-usage)
88
+ - [Physics Hooks (Collision Filtering)](#physics-hooks-collision-filtering)
83
89
  - [Manual stepping](#manual-stepping)
84
90
  - [On-demand rendering](#on-demand-rendering)
85
91
  - [Snapshots](#snapshots)
@@ -881,6 +887,95 @@ Advanced users might need granular access to the physics loop and direct access
881
887
  Allows you to run code after the physics simulation is stepped.
882
888
  🧩 See [useAfterPhysicsStep docs](https://pmndrs.github.io/react-three-rapier/functions/useAfterPhysicsStep.html) for more information.
883
889
 
890
+ ### Physics Hooks (Collision Filtering)
891
+
892
+ You can implement advanced collision behaviors like one-way platforms by using physics hooks. These hooks allow you to filter collision and intersection pairs during the physics step.
893
+
894
+ `r3/rapier` provides two hooks for collision filtering:
895
+ - `useFilterContactPair` - Filter collision pairs and control solver behavior
896
+ - `useFilterIntersectionPair` - Filter intersection pairs for sensors
897
+
898
+ #### Filter Contact Pairs
899
+
900
+ `useFilterContactPair` allows you to control how collisions are processed. The callback should return:
901
+ - `SolverFlags.COMPUTE_IMPULSE` (1) - Process the collision normally
902
+ - `SolverFlags.EMPTY` (0) - Ignore the collision
903
+ - `null` - Let other hooks decide, or use default behavior
904
+
905
+ #### Filter Intersection Pairs
906
+
907
+ `useFilterIntersectionPair` controls which sensor intersections are detected. The callback should return:
908
+ - `true` - Allow the intersection to be detected
909
+ - `false` - Block the intersection
910
+
911
+ If multiple hooks are registered:
912
+ - For contact pairs, the **first hook that returns non-null wins**
913
+ - For intersection pairs, the **first hook that returns false blocks** the intersection
914
+
915
+ **Important:** To avoid Rust aliasing errors, you **cannot** access rigid body properties (like `translation()` or `linvel()`) directly during the physics step. Instead, cache the needed state before the step using `useBeforePhysicsStep`.
916
+
917
+ 🧩 See [useFilterContactPair docs](https://pmndrs.github.io/react-three-rapier/functions/useFilterContactPair.html) and [useFilterIntersectionPair docs](https://pmndrs.github.io/react-three-rapier/functions/useFilterIntersectionPair.html) for more information.
918
+
919
+ ```tsx
920
+ import {
921
+ useRapier,
922
+ useBeforePhysicsStep,
923
+ useFilterContactPair
924
+ } from "@react-three/rapier";
925
+
926
+ const OneWayPlatform = () => {
927
+ const platformRef = useRef<RapierRigidBody>(null);
928
+ const ballRef = useRef<RapierRigidBody>(null);
929
+ const colliderRef = useRef<RapierCollider>(null);
930
+
931
+ // Cache for storing body states before physics step
932
+ const bodyStateCache = useRef(new Map());
933
+
934
+ const { rapier } = useRapier();
935
+
936
+ // Cache body states BEFORE the physics step
937
+ useBeforePhysicsStep(() => {
938
+ if (platformRef.current && ballRef.current) {
939
+ const ballPos = ballRef.current.translation();
940
+ const ballVel = ballRef.current.linvel();
941
+
942
+ bodyStateCache.current.set(ballRef.current.handle, {
943
+ position: ballPos,
944
+ velocity: ballVel
945
+ });
946
+ }
947
+ });
948
+
949
+ // Filter collisions using cached data
950
+ useFilterContactPair((collider1, collider2, body1, body2) => {
951
+ const ballState = bodyStateCache.current.get(body1);
952
+ if (!ballState) return null; // Let other hooks or default behavior handle it
953
+
954
+ // Allow collision only if ball is moving down and above platform
955
+ if (ballState.velocity.y < 0 && ballState.position.y > 0) {
956
+ return rapier.SolverFlags.COMPUTE_IMPULSE; // Process collision
957
+ }
958
+ return rapier.SolverFlags.EMPTY; // Ignore collision
959
+ });
960
+
961
+ useEffect(() => {
962
+ // Enable active hooks on the collider (required for filtering)
963
+ colliderRef.current?.setActiveHooks(rapier.ActiveHooks.FILTER_CONTACT_PAIRS);
964
+ }, []);
965
+
966
+ return (
967
+ <>
968
+ <RigidBody ref={platformRef} type="fixed">
969
+ <CuboidCollider ref={colliderRef} args={[5, 0.1, 5]} />
970
+ </RigidBody>
971
+ <RigidBody ref={ballRef} position={[0, 3, 0]}>
972
+ <CuboidCollider args={[1, 1, 1]} />
973
+ </RigidBody>
974
+ </>
975
+ );
976
+ };
977
+ ```
978
+
884
979
  ### Manual stepping
885
980
 
886
981
  You can manually step the physics simulation by calling the `step` method from the `useRapier` hook.
@@ -939,4 +1034,4 @@ const SnapshottingComponent = () => {
939
1034
  <Rigidbody>...</RigidBody>
940
1035
  </>
941
1036
  }
942
- ```
1037
+ ```