物理シミュレーション

重力・衝突・拘束。R3F では @react-three/rapier(推奨)または @react-three/cannon を使う。 Rapier は Rust 実装で速く、現代的な API。新規は Rapier。

選択肢

@react-three/rapier@react-three/cannon
実装Rust → WASM(Rapier)JS(cannon-es)
速度速い普通
API宣言的、現代的古め
機能剛体・関節・キャラコン etc 充実剛体・関節
新規開発これ一択

以下は Rapier の使い方を中心に。

導入

npm install @react-three/rapier

基本構造

全体を <Physics> で囲み、その中に <RigidBody> で物体を包む。 RigidBody の中の <mesh> が物体の見た目。

import { Canvas } from "@react-three/fiber"
import { Physics, RigidBody } from "@react-three/rapier"

export default function App() {
  return (
    <Canvas shadows>
      <ambientLight intensity={0.4} />
      <directionalLight position={[5, 10, 5]} castShadow />

      <Physics gravity={[0, -9.81, 0]}>
        {/* 落ちてくるボール */}
        <RigidBody position={[0, 5, 0]} colliders="ball">
          <mesh castShadow>
            <sphereGeometry />
            <meshStandardMaterial color="hotpink" />
          </mesh>
        </RigidBody>

        {/* 床(type="fixed" で動かない) */}
        <RigidBody type="fixed">
          <mesh receiveShadow rotation-x={-Math.PI / 2}>
            <planeGeometry args={[20, 20]} />
            <meshStandardMaterial color="#1f2430" />
          </mesh>
        </RigidBody>
      </Physics>
    </Canvas>
  )
}

これだけでピンクのボールが床に落ちて跳ねる

<Physics> の主な props

prop役割
gravity[x, y, z] 重力ベクトル(既定 [0, -9.81, 0]
timeStep固定 timestep(既定 1/60
pausedシミュレーションを止める
debug当たり判定形状を可視化
colliders子の RigidBody に対する既定の collider タイプ
interpolateレンダーフレームと物理ステップの補間

<RigidBody>

type

colliders

中の mesh から自動生成される当たり判定形状。

主な props

prop意味
position / rotation初期位置・回転
mass質量
linearDamping並進の摩擦
angularDamping回転の摩擦
restitution反発係数(0..1)
friction摩擦係数
gravityScale個別の重力倍率
lockTranslations / lockRotations軸を固定
enabledRotations / enabledTranslations軸別の有効化
onCollisionEnter / onCollisionExit衝突イベント
onIntersectionEnter / onIntersectionExitセンサー(trigger)イベント

力を加える

useRefRigidBody 参照を取って、API を呼ぶ。

import { RigidBody, RapierRigidBody } from "@react-three/rapier"

function Ball() {
  const ref = useRef<RapierRigidBody>(null)

  function jump() {
    ref.current?.applyImpulse({ x: 0, y: 5, z: 0 }, true)
  }

  return (
    <RigidBody ref={ref} colliders="ball" position={[0, 5, 0]}>
      <mesh onClick={jump} castShadow>
        <sphereGeometry />
        <meshStandardMaterial color="hotpink" />
      </mesh>
    </RigidBody>
  )
}

API メソッド

衝突イベント

<RigidBody
  onCollisionEnter={({ other }) => {
    console.log("hit:", other.rigidBodyObject?.name)
  }}
  onCollisionExit={() => { /* 離れた */ }}
>
  <mesh> ... </mesh>
</RigidBody>

センサー(Trigger)

通り抜けるが「入った/出た」だけ知りたい時。sensor prop。

<RigidBody
  type="fixed"
  sensor
  onIntersectionEnter={() => console.log("プレイヤーがゾーンに入った")}
>
  <mesh>
    <boxGeometry args={[3, 3, 3]} />
    <meshBasicMaterial color="red" wireframe />
  </mesh>
</RigidBody>

明示的に Collider を置く

見た目と当たり判定を別にしたい時。

import { RigidBody, CuboidCollider, BallCollider } from "@react-three/rapier"

<RigidBody type="dynamic" colliders={false}>
  <mesh castShadow>
    <boxGeometry args={[1, 2, 1]} />
    <meshStandardMaterial />
  </mesh>
  <CuboidCollider args={[0.5, 1, 0.5]} /> {/* 半サイズ */}
</RigidBody>

主な Collider 種別

キネマティック(自分で位置を制御)

プレイヤーやエレベータなど「物理に流されず自分で動かす」物体。

function MovingPlatform() {
  const ref = useRef<RapierRigidBody>(null)

  useFrame(({ clock }) => {
    if (!ref.current) return
    const t = clock.getElapsedTime()
    ref.current.setNextKinematicTranslation({
      x: Math.sin(t) * 3,
      y: 1,
      z: 0,
    })
  })

  return (
    <RigidBody ref={ref} type="kinematicPosition">
      <mesh>
        <boxGeometry args={[2, 0.2, 2]} />
        <meshStandardMaterial color="#888" />
      </mesh>
    </RigidBody>
  )
}

キャラクターコントローラ

Rapier のキャラコンは「壁にめり込まない、階段を登れる」物理キャラクター。 useRapier で世界 API にアクセス:

import { useRapier } from "@react-three/rapier"

function Player() {
  const { world, rapier } = useRapier()
  const ref = useRef<RapierRigidBody>(null)

  useEffect(() => {
    const cc = world.createCharacterController(0.01)
    cc.setMaxSlopeClimbAngle(Math.PI / 4)
    cc.enableAutostep(0.5, 0.2, true)
    cc.enableSnapToGround(0.5)
    // 移動に使う...
  }, [world])

  return (
    <RigidBody ref={ref} type="kinematicPosition" colliders={false}>
      <CapsuleCollider args={[0.5, 0.5]} />
      <mesh> ... </mesh>
    </RigidBody>
  )
}

Joint(関節)

2つの剛体を繋ぐ。振り子・布・乗り物などに。

import { useSphericalJoint, useRevoluteJoint, useFixedJoint } from "@react-three/rapier"

function Pendulum() {
  const anchor = useRef<RapierRigidBody>(null)
  const ball = useRef<RapierRigidBody>(null)

  // ボールリンクを anchor に紐づけて振り子に
  useSphericalJoint(anchor, ball, [
    [0, 0, 0],   // anchor 側のローカル位置
    [0, 1, 0],   // ball 側のローカル位置
  ])

  return (
    <>
      <RigidBody ref={anchor} type="fixed" position={[0, 4, 0]}> ... </RigidBody>
      <RigidBody ref={ball} colliders="ball" position={[0, 3, 0]}> ... </RigidBody>
    </>
  )
}

Joint の種類

InstancedRigidBodies — 大量の物理物体

100個以上の同形物体を効率良くシミュレーションする。

import { InstancedRigidBodies } from "@react-three/rapier"

function Cubes({ count = 200 }) {
  const instances = useMemo(() => (
    Array.from({ length: count }, (_, i) => ({
      key: "cube-" + i,
      position: [
        (Math.random() - 0.5) * 5,
        Math.random() * 10 + 5,
        (Math.random() - 0.5) * 5,
      ] as [number, number, number],
      rotation: [Math.random(), Math.random(), Math.random()] as [number, number, number],
    }))
  ), [count])

  return (
    <InstancedRigidBodies instances={instances} colliders="cuboid">
      <instancedMesh args={[undefined, undefined, count]} castShadow>
        <boxGeometry />
        <meshStandardMaterial color="#7dd3fc" />
      </instancedMesh>
    </InstancedRigidBodies>
  )
}

デバッグ可視化

<Physics debug>
  ...
</Physics>

当たり判定の形状(緑の線)が描画される。見た目と判定がずれてないかのチェックに必須。

典型的なゲームループ

function ControlledPlayer() {
  const ref = useRef<RapierRigidBody>(null)
  const [, get] = useKeyboardControls()
  const SPEED = 5

  useFrame(() => {
    if (!ref.current) return
    const { forward, back, left, right, jump } = get()
    const vel = ref.current.linvel()
    const dir = new THREE.Vector3(
      Number(right) - Number(left),
      0,
      Number(back) - Number(forward),
    ).normalize().multiplyScalar(SPEED)

    ref.current.setLinvel({ x: dir.x, y: vel.y, z: dir.z }, true)

    if (jump && Math.abs(vel.y) < 0.05) {
      ref.current.applyImpulse({ x: 0, y: 7, z: 0 }, true)
    }
  })

  return (
    <RigidBody
      ref={ref}
      colliders={false}
      enabledRotations={[false, false, false]}
      position={[0, 1, 0]}
    >
      <CapsuleCollider args={[0.5, 0.5]} />
      <mesh> ... </mesh>
    </RigidBody>
  )
}

パフォーマンスのコツ

判断ガイド

やりたいこと選び方
転がる・落ちる・跳ねるRigidBody(dynamic)
動かない壁・床RigidBody(fixed)
プレイヤーキャラkinematicPosition + CharacterController
動くプラットフォームkinematicPosition
当たった時のイベントだけsensor
関節・布・チェーンJoints
1000個以上の物体InstancedRigidBodies
軽いパーティクル演出物理使わず useFrame
参考

Rapier 公式 / react-three-rapier
物理はパラメータ調整が大半。debug=true で見ながら mass/friction/restitution を試行錯誤するのが早い。