@react-three/rapier 2.1.0 → 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.
- package/dist/declarations/src/components/Physics.d.ts +19 -9
- package/dist/declarations/src/hooks/hooks.d.ts +68 -0
- package/dist/declarations/src/index.d.ts +2 -2
- package/dist/react-three-rapier.cjs.dev.js +118 -5
- package/dist/react-three-rapier.cjs.prod.js +118 -5
- package/dist/react-three-rapier.esm.js +117 -6
- package/package.json +2 -2
- package/readme.md +96 -1
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
+
```
|