物理シミュレーション
重力・衝突・拘束。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
"dynamic"(既定) — 重力・力で動く"fixed"— 動かない(床・壁)"kinematicPosition"— 自分でポジションを設定して動かす(プレイヤー等)"kinematicVelocity"— 自分で速度を設定して動かす
colliders
中の mesh から自動生成される当たり判定形状。
"ball"— 球"cuboid"— 直方体"hull"— 凸包(複雑な形状向け、軽い)"trimesh"— 三角形メッシュ(精密だが重い、dynamic では使えない)false— 自動生成しない(手動で<CuboidCollider>等を入れる)
主な props
| prop | 意味 |
|---|---|
position / rotation | 初期位置・回転 |
mass | 質量 |
linearDamping | 並進の摩擦 |
angularDamping | 回転の摩擦 |
restitution | 反発係数(0..1) |
friction | 摩擦係数 |
gravityScale | 個別の重力倍率 |
lockTranslations / lockRotations | 軸を固定 |
enabledRotations / enabledTranslations | 軸別の有効化 |
onCollisionEnter / onCollisionExit | 衝突イベント |
onIntersectionEnter / onIntersectionExit | センサー(trigger)イベント |
力を加える
useRef で RigidBody 参照を取って、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 メソッド
applyImpulse(vec, wake)— 瞬間的な力(速度を一気に変える)addForce(vec, wake)— 継続的な力(毎フレームの applyForce)applyTorqueImpulse(vec, wake)— 瞬間的なトルクsetLinvel(vec, wake)— 速度を直接設定setTranslation(vec, wake)— 位置を直接設定setRotation(quat, wake)— 回転を直接設定linvel()— 速度を取得translation()— 位置を取得
衝突イベント
<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 種別
<CuboidCollider args={[hx, hy, hz]} />— 直方体(半サイズ)<BallCollider args={[r]} />— 球<CylinderCollider args={[hh, r]} />— 円柱(半高さ・半径)<CapsuleCollider args={[hh, r]} />— カプセル<ConeCollider args={[hh, r]} />— 円錐<TrimeshCollider args={[verts, indices]} />— 任意の三角形メッシュ(fixed のみ推奨)<ConvexHullCollider args={[verts]} />— 凸包<HeightfieldCollider />— 地形
キネマティック(自分で位置を制御)
プレイヤーやエレベータなど「物理に流されず自分で動かす」物体。
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 の種類
useFixedJoint— 完全固定useSphericalJoint— 球面関節(点で繋いで回転自由)useRevoluteJoint— ヒンジ(1軸回転)usePrismaticJoint— スライダ(1軸並進)
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>
)
}
パフォーマンスのコツ
- trimesh は dynamic に使わない(動かない床のような fixed のみ)
- 大量の同形物体は
InstancedRigidBodies - 遠くで動く必要がない物体は
type="fixed"に切り替える(distance based culling 風) linearDamping/angularDampingを入れて無限に動き続けるのを防ぐ- シミュレーション規模が大きい時は
timeStepを見直す - パーティクル的な軽い演出には物理を使わず、useFrame で自前計算が速い
判断ガイド
| やりたいこと | 選び方 |
|---|---|
| 転がる・落ちる・跳ねる | RigidBody(dynamic) |
| 動かない壁・床 | RigidBody(fixed) |
| プレイヤーキャラ | kinematicPosition + CharacterController |
| 動くプラットフォーム | kinematicPosition |
| 当たった時のイベントだけ | sensor |
| 関節・布・チェーン | Joints |
| 1000個以上の物体 | InstancedRigidBodies |
| 軽いパーティクル演出 | 物理使わず useFrame |
Rapier 公式 / react-three-rapier。
物理はパラメータ調整が大半。debug=true で見ながら mass/friction/restitution を試行錯誤するのが早い。