threejs-impl-physics について
threejs-impl-physics は、Three.js 開発者向けの物理実装解説スキルです。cannon-es と Rapier の選択、剛体やコライダーの設定、シーンとの同期を説明します。
主な機能
- 01cannon-es と Rapier の比較
- 02剛体、コライダー、拘束の設定
- 03物理ステップと衝突イベントの処理
- 04物理状態と Three.js メッシュの同期
- 05React Three Fiber との連携
- 06レイキャストとデバッグ描画の例
活用シーン
- 01Three.js シーンへの重力と衝突の追加
- 02シーン要件に合う物理エンジンの選択
- 03React Three Fiber での物理オブジェクト設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ impertio-studio/ three.js-claude-skill-package --skill threejs-impl-physicsインストール後、Claude Code で/threejs-impl-physicsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.gitkeepreferences/anti-patterns.mdexamples.mdmethods.md詳細
threejs-impl-physics
クイックリファレンス
エンジン選択の判断ツリー
| 基準 | cannon-es | Rapier |
|---|---|---|
| シーンの規模 | 100 ボディ未満 | 100~10,000 以上のボディ |
| 決定性が必要か | いいえ | はい(クロスプラットフォーム) |
| CCD(高速オブジェクト) | 限定的 | 完全対応 |
| バンドルサイズの予算 | 約 100 KB | 約 300~600 KB(WASM) |
| 初期化 | 同期 | 非同期(必ず await init() を実行) |
| プロトタイピングの速度 | 高速(よりシンプルな API) | 低速(ビルダーパターン) |
| R3F との統合 | @react-three/cannon | @react-three/rapier |
ルール: 決定性、CCD、または 100 を超えるボディが必要な本番アプリケーションでは、必ず Rapier を使用してください。プロトタイピングやシンプルなシーンには cannon-es を使用してください。
重要な警告
アニメーションループ内で world.step() を呼び出すのを絶対に忘れないでください。呼び出さないと、物理ボディは動きません。
Rapier で mesh.position.copy(body.position) を絶対に使用しないでください。Rapier は CANNON.Vec3 ではなく、プレーンな {x, y, z} オブジェクトを返します。必ず mesh.position.set(pos.x, pos.y, pos.z) を使用してください。
await RAPIER.init() が完了する前に Rapier API を絶対に使用しないでください。WASM の読み込みが完了するまで、すべての Rapier クラスは未定義です。
動的ボディに Trimesh/trimesh コライダーを絶対に使用しないでください。両エンジンにおいて、三角形メッシュは静的なボディにのみ使用できます。動的な凹形状のジオメトリには ConvexPolyhedron/convexHull を使用してください。
意図的に不可視のコライダーを作成する場合を除き、対応する Three.js メッシュなしで物理ボディを絶対に作成しないでください。孤立したボディはシミュレーションの予算を無駄にします。
cannon-es
ワールドのセットアップ
import * as CANNON from 'cannon-es';
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world);
world.solver.iterations = 10;
world.allowSleep = true;
必ず world.allowSleep = true を設定してください。スリープ中のボディはシミュレーションをスキップするため、パフォーマンスが大幅に向上します。
20 を超えるボディを含むシーンでは、必ず SAPBroadphase を使用してください。NaiveBroadphase の計算量は O(n^2) です。
ボディの種類
| 種類 | 質量 | 挙動 |
|---|---|---|
CANNON.Body.DYNAMIC | > 0 | 力と衝突の影響を受けます |
CANNON.Body.STATIC | 0 | 動かず、質量は無限大です |
CANNON.Body.KINEMATIC | 0 | プログラムで移動し、動的ボディを押します |
const body = new CANNON.Body({
mass: 5,
position: new CANNON.Vec3(0, 10, 0),
shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1)), // half-extents
linearDamping: 0.01,
angularDamping: 0.01,
});
world.addBody(body);
形状の種類
| 形状 | コンストラクター | 備考 |
|---|---|---|
Box | new CANNON.Box(halfExtents: Vec3) | 軸に沿ったボックス |
Sphere | new CANNON.Sphere(radius) | 最も計算コストの低い衝突形状 |
Cylinder | new CANNON.Cylinder(rTop, rBottom, height, segments) | 円柱 |
Plane | new CANNON.Plane() | 無限の地面平面 |
ConvexPolyhedron | new CANNON.ConvexPolyhedron({vertices, faces}) | カスタム凸包 |
Trimesh | new CANNON.Trimesh(vertices, indices) | 静的なボディ専用 |
Heightfield | new CANNON.Heightfield(data, {elementSize}) | 地形 |
Particle | new CANNON.Particle() | 点粒子 |
マテリアルと接触
const groundMat = new CANNON.Material('ground');
const ballMat = new CANNON.Material('ball');
const contact = new CANNON.ContactMaterial(groundMat, ballMat, {
friction: 0.4,
restitution: 0.6,
});
world.addContactMaterial(contact);
// Assign materials to bodies
groundBody.material = groundMat;
ballBody.material = ballMat;
ContactMaterial を作成した後、必ずマテリアルをボディに割り当ててください。割り当てないと、ContactMaterial はまったく効果がありません。
拘束
| 拘束 | コンストラクター | 用途 |
|---|---|---|
PointToPointConstraint | (bodyA, pivotA, bodyB, pivotB) | ボールジョイント |
DistanceConstraint | (bodyA, bodyB, distance) | 固定距離のロッド |
HingeConstraint | (bodyA, bodyB, {pivotA, axisA, pivotB, axisB}) | ドアのヒンジ |
LockConstraint | (bodyA, bodyB) | 剛体ロック |
ConeTwistConstraint | (bodyA, bodyB, options) | ラグドールのジョイント |
Spring | (bodyA, bodyB, options) | 減衰スプリング |
const hinge = new CANNON.HingeConstraint(bodyA, bodyB, {
pivotA: new CANNON.Vec3(0, 0, 0),
axisA: new CANNON.Vec3(0, 1, 0),
pivotB: new CANNON.Vec3(-2, 0, 0),
axisB: new CANNON.Vec3(0, 1, 0),
});
world.addConstraint(hinge);
イベント
body.addEventListener('collide', (event) => {
const { contact } = event;
// contact.ni = contact normal
// contact.ri = contact point relative to bodyA
// contact.rj = contact point relative to bodyB
});
body.addEventListener('sleep', () => { /* body went to sleep */ });
body.addEventListener('wakeup', () => { /* body woke up */ });
ステップ実行
// ALWAYS use three-argument step for deterministic simulation
const fixedTimeStep = 1 / 60;
const maxSubSteps = 3;
function animate() {
const delta = clock.getDelta();
world.step(fixedTimeStep, delta, maxSubSteps);
}
Rapier
WASM の初期化
import RAPIER from '@dimforge/rapier3d-compat';
await RAPIER.init(); // MUST await — ALL APIs undefined before this resolves
const gravity = { x: 0.0, y: -9.81, z: 0.0 };
const world = new RAPIER.World(gravity);
必ず Rapier の使用を非同期関数またはトップレベルの await で囲んでください。init() が完了する前に Rapier のコンストラクターを呼び出すと、実行時エラーが発生します。
RigidBody の作成(ビルダーパターン)
// Dynamic body
const bodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(0, 10, 0)
.setLinvel(0, 0, 0)
.setAngvel({ x: 0, y: 0, z: 0 })
.setLinearDamping(0.01)
.setAngularDamping(0.01)
.setCcdEnabled(true);
const rigidBody = world.createRigidBody(bodyDesc);
// Static body
const staticDesc = RAPIER.RigidBodyDesc.fixed().setTranslation(0, 0, 0);
const staticBody = world.createRigidBody(staticDesc);
// Kinematic (position-based)
const kinDesc = RAPIER.RigidBodyDesc.kinematicPositionBased();
const kinBody = world.createRigidBody(kinDesc);
// Kinematic (velocity-based)
const kinVelDesc = RAPIER.RigidBodyDesc.kinematicVelocityBased();
コライダーの形状
const colliderDesc = RAPIER.ColliderDesc.cuboid(1, 1, 1)
.setRestitution(0.5)
.setFriction(0.7);
world.createCollider(colliderDesc, rigidBody);
| 形状 | コンストラクター | 備考 |
|---|---|---|
ColliderDesc.cuboid(hx, hy, hz) | ボックスの半径 | 最も一般的 |
ColliderDesc.ball(radius) | 球体 | 最も軽量な形状 |
ColliderDesc.capsule(halfHeight, radius) | カプセル | キャラクターに適しています |
ColliderDesc.cylinder(halfHeight, radius) | 円柱 | |
ColliderDesc.cone(halfHeight, radius) | 円錐 | |
ColliderDesc.convexHull(vertices) | 凸包 | Float32Array から作成 |
ColliderDesc.trimesh(vertices, indices) | 三角形メッシュ | STATIC のみ |
ColliderDesc.heightfield(nrows, ncols, heights, scale) | 地形 | |
ColliderDesc.roundCuboid(hx, hy, hz, borderRadius) | 角丸ボックス |
レイキャスト
const ray = new RAPIER.Ray({ x: 0, y: 10, z: 0 }, { x: 0, y: -1, z: 0 });
const hit = world.castRay(ray, 100, true);
if (hit) {
const hitPoint = ray.pointAt(hit.timeOfImpact);
const hitCollider = world.getCollider(hit.colliderHandle);
}
CCD(連続衝突検出)
薄いジオメトリをすり抜けるトンネリングを防ぐため、高速で動く剛体では必ず CCD を有効にしてください。
const desc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true);
衝突イベント
const eventQueue = new RAPIER.EventQueue(true);
world.step(eventQueue);
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
// started === true: collision began
// started === false: collision ended
});
eventQueue.drainContactForceEvents((event) => {
const force = event.totalForceMagnitude();
});
デバッグ描画
const { vertices, colors } = world.debugRender();
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 4));
const material = new THREE.LineBasicMaterial({ vertexColors: true });
const lines = new THREE.LineSegments(geometry, material);
scene.add(lines);
Three.js 同期パターン
cannon-es の同期
// Store body-mesh pairs
const pairs = [];
function addPhysicsObject(mesh, body) {
scene.add(mesh);
world.addBody(body);
pairs.push({ mesh, body });
}
function updatePhysics(delta) {
world.step(1 / 60, delta, 3);
for (const { mesh, body } of pairs) {
mesh.position.copy(body.position); // Vec3 → Vector3 (compatible)
mesh.quaternion.copy(body.quaternion); // Quaternion → Quaternion (compatible)
}
}
cannon-es の Vec3 と Quaternion は、Three.js の .copy() と直接互換性があります。
Rapier の同期
const pairs = [];
function addPhysicsObject(mesh, rigidBody) {
scene.add(mesh);
pairs.push({ mesh, rigidBody });
}
function updatePhysics() {
world.step();
for (const { mesh, rigidBody } of pairs) {
const pos = rigidBody.translation(); // returns {x, y, z}
const rot = rigidBody.rotation(); // returns {x, y, z, w}
mesh.position.set(pos.x, pos.y, pos.z);
mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w);
}
}
Rapier の戻り値はプレーンオブジェクトであり、Three.js の型ではないため、.copy() は絶対に使用しないでください。
React Three Fiber との統合
@react-three/rapier
import { Physics, RigidBody } from '@react-three/rapier';
function Scene() {
return (
<Physics gravity={[0, -9.81, 0]}>
<RigidBody type="fixed">
<mesh position={[0, -1, 0]}>
<boxGeometry args={[20, 1, 20]} />
<meshStandardMaterial />
</mesh>
</RigidBody>
<RigidBody>
<mesh position={[0, 5, 0]}>
<sphereGeometry args={[1]} />
<meshStandardMaterial />
</mesh>
</RigidBody>
</Physics>
);
}
@react-three/cannon
import { Physics, useBox, useSphere } from '@react-three/cannon';
function Floor() {
const [ref] = useBox(() => ({ mass: 0, args: [20, 1, 20], position: [0, -1, 0] }));
return (
<mesh ref={ref}>
<boxGeometry args={[20, 1, 20]} />
<meshStandardMaterial />
</mesh>
);
}
パフォーマンスの比較
| 機能 | cannon-es | Rapier |
|---|---|---|
| 言語 | JavaScript | WASM にコンパイルされた Rust |
| リアルタイムの剛体数 | ~1,000 | ~10,000+ |
| バンドルサイズ | ~100 KB | ~300–600 KB |
| CCD | 制限あり | 完全にサポート |
| 決定論的 | いいえ | はい(クロスプラットフォーム) |
| デバッグ描画 | 手動 | 組み込みの world.debugRender() |
| API のスタイル | コンストラクターベース | ビルダーパターン |
参考リンク
- references/methods.md — cannon-es と Rapier の API シグネチャ
- references/examples.md — 動作する統合例
- references/anti-patterns.md — よくある物理演算の誤りと修正方法