react-three-rapier

repository·main·Indexed 23 days ago

https://github.com/pmndrs/react-three-rapier

A React wrapper for the Rapier physics engine designed for Three.js and React Three Fiber. It provides declarative components like <Physics />, <RigidBody />, and <InstancedRigidBodies /> to build physics-based 3D scenes. Features include automatic and compound collider generation, collision and state event handling, and an Attractor component for simulating gravity with static, linear, or newtonian models.

Tokens
15.2K
Snippets
25
Records
76
Agent score
80%

What's inside react-three-rapier

  1. Use sensors for intersection detection

    main

    A Collider can be set as a sensor. Sensors do not generate contact points and are not affected by forces. They are used to detect when objects overlap without physical interaction.

    Use onIntersectionEnter and onIntersectionExit to handle these events.

    <RigidBody>
      <GoalPosts />
    
      <CuboidCollider
        args={[5, 5, 1]}
        sensor
        onIntersectionEnter={() => console.log("Goal!")}
      />
    </RigidBody>
  2. How the RigidBody component works

    main

    The <RigidBody /> component adds a mesh (or multiple meshes) into the physics simulation. By default, it automatically generates colliders based on the shape of its children.

    To ensure stable simulation, it is recommended to create RigidBodies where the center of gravity is at the center of the object. RigidBodies can be placed inside transformed objects (like <group />), and the simulation will correctly handle the transformation from world space to local space.

    const RigidBodyMesh = () => (
      <RigidBody>
        <mesh />
      </RigidBody>
    );
  3. Implement collision filtering with useFilterContactPair and useFilterIntersectionPair

    main

    To implement advanced behaviors like one-way platforms or custom sensor logic, use physics hooks to filter collision and intersection pairs.

    Filter Contact Pairs

    useFilterContactPair controls how collisions are processed. The callback must return:

    • rapier.SolverFlags.COMPUTE_IMPULSE (1): Process the collision normally.
    • rapier.SolverFlags.EMPTY (0): Ignore the collision.
    • null: Let other hooks or default behavior decide.

    Note: The first hook that returns a non-null value wins.

    Filter Intersection Pairs

    useFilterIntersectionPair controls sensor intersections. The callback must return:

    • true: Allow the intersection.
    • false: Block the intersection.

    Note: The first hook that returns false blocks the intersection.

    Critical Constraint: Avoid Aliasing Errors

    You cannot access rigid body properties (e.g., translation(), linvel()) directly inside these hooks because they run during the physics step. You must cache the required state in useBeforePhysicsStep and access the cached values within the filter hooks.

    Implementation Requirements

    You must explicitly enable the hooks on the collider using collider.setActiveHooks(rapier.ActiveHooks.FILTER_CONTACT_PAIRS) or FILTER_INTERSECTION_PAIRS.

    import { 
      useRapier, 
      useBeforePhysicsStep,
      useFilterContactPair 
    } from "@react-three/rapier";
    
    const OneWayPlatform = () => {
      const platformRef = useRef<RapierRigidBody>(null);
      const ballRef = useRef<RapierRigidBody>(null);
      const colliderRef = useRef<RapierCollider>(null);
      
      const bodyStateCache = useRef(new Map());
      const { rapier } = useRapier();
    
      // 1. Cache state BEFORE the step to avoid Rust aliasing errors
      useBeforePhysicsStep(() => {
        if (platformRef.current && ballRef.current) {
          bodyStateCache.current.set(ballRef.current.handle, {
            position: ballRef.current.translation(),
            velocity: ballRef.current.linvel()
          });
        }
      });
    
      // 2. Use cached data to filter collisions
      useFilterContactPair((collider1, collider2, body1, body2) => {
        const ballState = bodyStateCache.current.get(body1);
        if (!ballState) return null;
    
        if (ballState.velocity.y < 0 && ballState.position.y > 0) {
          return rapier.SolverFlags.COMPUTE_IMPULSE;
        }
        return rapier.SolverFlags.EMPTY;
      });
    
      // 3. Enable hooks on the collider
      useEffect(() => {
        colliderRef.current?.setActiveHooks(rapier.ActiveHooks.FILTER_CONTACT_PAIRS);
      }, []);
    
      return (
        <>
          <RigidBody ref={platformRef} type="fixed">
            <CuboidCollider ref={colliderRef} args={[5, 0.1, 5]} />
          </RigidBody>
          <RigidBody ref={ballRef} position={[0, 3, 0]}>
            <CuboidCollider args={[1, 1, 1]} />
          </RigidBody>
        </>
      );
    };
  4. Configure Attractor gravity types

    main

    The Attractor component supports three different gravity calculation models via the type prop:

    • static (Default): Applies the same constant strength to all rigid-bodies within the range, regardless of their distance from the attractor.
    • linear: The force is linearly interpolated based on distance: strength * distance / range. The force decreases as the object moves further from the attractor.
    • newtonian: Uses the Newtonian gravity formula: F = gravitationalConstant * mass1 * mass2 / Math.pow(distance, 2).
      • mass1 is the strength property.
      • mass2 is the mass of the target RigidBody.
      • You can customize the gravitationalConstant (defaults to 6.673e-11).
  5. How the Physics component works

    main

    The <Physics /> component is the root of your physics world. It manages the simulation and creates the physics world. Because it lazily initiates the Rapier WASM engine, it must be wrapped in a <Suspense /> component.

    Common props include:

    • gravity: An array (e.g., [0, -9.81, 0]) defining the world gravity.
    • interpolation: Boolean to enable/disable simulation interpolation.
    • colliders: Sets the default automatic collider type for all RigidBodies in the world if not specified locally.
    • debug: Enables a live visual representation of all colliders.
    const Scene = () => {
      return (
        <Canvas>
          <Suspense>
            <Physics gravity={[0, 1, 0]} interpolation={false} colliders={false}>
              ...
            </Physics>
          </Suspense>
        </Canvas>
      );
    };
  6. How joints work in react-three-rapier

    main

    Joints are used to restrict the motion of one RigidBody in relation to another. In react-three-rapier, joints are implemented as hooks that take two RigidBody refs and a configuration array. Each joint hook returns a RefObject containing the raw reference to the underlying Rapier joint instance, allowing you to interact with it directly (e.g., configuring motors).

    Available joint types include:

    • Fixed: Keeps two bodies fixed together.
    • Spherical: A ball-and-socket connection (e.g., for arms or chains).
    • Revolute: A hinge connection (e.g., for doors or wheels).
    • Prismatic: A sliding connection (e.g., for pistons).
    • Rope: Limits the maximum distance between two bodies.
    • Spring: Applies a force proportional to the distance between two bodies.
    const WheelJoint = ({ bodyA, bodyB }) => {
      const joint = useRevoluteJoint(bodyA, bodyB, [
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]
      ]);
    
      useFrame(() => {
        if (joint.current) {
          joint.current.configureMotorVelocity(10, 2);
        }
      }, []);
    
      return null;
    };
  7. Quickstart: Basic Usage of @react-three/rapier

    main

    To use @react-three/rapier, wrap your scene in a <Physics /> component inside a <Suspense /> block. You can then add physics-enabled objects using the <RigidBody /> component.

    Note: If you are using React 18, you must use @react-three/rapier v1. For React 19 and @react-three/fiber v9, use @react-three/rapier v2.

    import { Box, Torus } from "@react-three/drei";
    import { Canvas } from "@react-three/fiber";
    import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
    import { Suspense } from "react";
    
    const App = () => {
      return (
        <Canvas>
          <Suspense>
            <Physics debug>
              <RigidBody colliders={"hull"} restitution={2}>
                <Torus />
              </RigidBody>
    
              <CuboidCollider position={[0, -2, 0]} args={[20, 0.5, 20]} />
            </Physics>
          </Suspense>
        </Canvas>
      );
    };
  8. Convert Rapier types to Three.js types

    main

    Rapier's return types (vectors and quaternions) are not directly compatible with Three.js. @react-three/rapier provides helper functions vec3, quat, and euler to perform these conversions quickly.

    While reading values requires these helpers, you can set values (like translation or rotation) directly using Three.js types.

    import { RapierRigidBody, quat, vec3, euler } from "@react-three/rapier";
    import { useRef, useEffect } from "react";
    
    const Scene = () => {
      const rigidBody = useRef<RapierRigidBody>(null);
    
      useEffect(() => {
        if (rigidBody.current) {
          const position = vec3(rigidBody.current.translation());
          const quaternion = quat(rigidBody.current.rotation());
          const eulerRot = euler().setFromQuaternion(
            quat(rigidBody.current.rotation())
          );
    
          // While Rapier's return types need conversion, setting values can be done directly with Three.js types
          rigidBody.current.setTranslation(position, true);
          rigidBody.current.setRotation(quaternion, true);
          rigidBody.current.setAngvel({ x: 0, y: 2, z: 0 }, true);
        }
      }, []);
    
      return (
        <RigidBody ref={rigidBody}>
          <mesh>
            <boxBufferGeometry />
            <meshStandardMaterial />
          </mesh>
        </RigidBody>
      );
    };
  9. Manipulate RigidBodies using refs

    main

    You can perform direct physics operations on a RigidBody by accessing its underlying Rapier instance via a ref. The library exports RapierRigidBody and RapierCollider as type aliases for the underlying Rapier objects.

    Common operations include:

    • applyImpulse: A one-off push.
    • addForce: A continuous force.
    • applyTorqueImpulse: A one-off torque rotation.
    • addTorque: A continuous torque.

    For a full list of methods, refer to the Rapier documentation.

    import { RigidBody, RapierRigidBody } from "@react-three/rapier";
    import { useRef, useEffect } from "react";
    
    const Scene = () => {
      const rigidBody = useRef<RapierRigidBody>(null);
    
      useEffect(() => {
        if (rigidBody.current) {
          // A one-off "push"
          rigidBody.current.applyImpulse({ x: 0, y: 10, z: 0 }, true);
    
          // A continuous force
          rigidBody.current.addForce({ x: 0, y: 10, z: 0 }, true);
    
          // A one-off torque rotation
          rigidBody.current.applyTorqueImpulse({ x: 0, y: 10, z: 0 }, true);
    
          // A continuous torque
          rigidBody.current.addTorque({ x: 0, y: 10, z: 0 }, true);
        }
      }, []);
    
      return (
        <RigidBody ref={rigidBody}>
          <mesh>
            <boxBufferGeometry />
            <meshStandardMaterial />
          </mesh>
        </RigidBody>
      );
    };
  10. Create compound colliders manually

    main

    To create complex shapes or optimize performance, you can manually add collider components (like <BallCollider />, <CuboidCollider />, or <MeshCollider />) as children of a <RigidBody />. This creates a compound collider.

    For <MeshCollider />, you can specify the type as either "trimesh" or "hull".

    const Scene = () => (
      <RigidBody position={[0, 10, 0]}>
        <Sphere />
        {/* Compound shape: Sphere + two BallColliders */}
        <BallCollider args={[0.5]} />
        <BallCollider args={[0.5]} position={[1, 0, 0]} />
      </RigidBody>
    );
  11. Use the Attractor component to simulate gravity

    main

    The Attractor component simulates a source of gravity that pulls (attracts) or pushes (repels) RigidBody components within a specified range.

    • Attraction: Set strength to a positive value.
    • Repulsion: Set strength to a negative value.

    To affect specific objects, you can use the collisionGroups prop with interactionGroups to filter which RigidBody colliders are influenced by the attractor.

    import { Attractor } from "@react-three/rapier-addons"
    
    // Standard attractor
    <Attractor range={10} strength={5} type="linear" position={[5, -5, 0]} />
    
    // An attractor with negative strength, repels RigidBodies
    <Attractor range={10} strength={-5} position={[5, -5, 0]} />
    
    // An attractor belonging to group 0 only affecting bodies in group 2 and 3
    <Attractor range={10} strength={10} position={[5, -5, 0]} collisionGroups={interactionGroups(0, [2,3])} />
  12. Use InstancedRigidBodies for high-performance physics

    main

    When you need many identical physics objects, use <InstancedRigidBodies />. This component wraps a single Three.InstancedMesh and attaches an individual RigidBody to each instance.

    Key features:

    • Accessing instances: Use a ref to get an array of RapierRigidBody objects. You can then call methods like .applyImpulse() on specific indices or loop through all of them.
    • Initial state: Provide an array of InstancedRigidBodyProps to the instances prop to set initial positions, rotations, and scales.
    • Compound shapes: Use the colliderNodes prop to pass an array of collider components (e.g., [<BoxCollider />, <SphereCollider />]) that will be applied to every instance.
    import { InstancedRigidBodies, RapierRigidBody, BoxCollider, SphereCollider } from "@react-three/rapier";
    
    const COUNT = 500;
    
    const Scene = () => {
      const rigidBodies = useRef<RapierRigidBody[]>(null);
    
      const instances = useMemo(() => {
        const items: InstancedRigidBodyProps[] = [];
        for (let i = 0; i < COUNT; i++) {
          items.push({
            key: "instance_" + Math.random(),
            position: [Math.random() * 10, Math.random() * 10, Math.random() * 10],
            rotation: [Math.random(), Math.random(), Math.random()]
          });
        }
        return items;
      }, []);
    
      return (
        <InstancedRigidBodies
          ref={rigidBodies}
          instances={instances}
          colliders="ball"
          colliderNodes={[
            <BoxCollider args={[0.5, 0.5, 0.5]} />,
            <SphereCollider args={[0.5]} />
          ]}
        >
          <instancedMesh args={[undefined, undefined, COUNT]} count={COUNT} />
        </InstancedRigidBodies>
      );
    };