mcpField
T
threejs-impl-physics

threejs-impl-physics

github.com/impertio-studio開発23134 回インストール30 週間前に更新
リポジトリ

threejs-impl-physics について

threejs-impl-physics は、Three.js 開発者向けの物理実装解説スキルです。cannon-es と Rapier の選択、剛体やコライダーの設定、シーンとの同期を説明します。

主な機能

  1. 01cannon-es と Rapier の比較
  2. 02剛体、コライダー、拘束の設定
  3. 03物理ステップと衝突イベントの処理
  4. 04物理状態と Three.js メッシュの同期
  5. 05React Three Fiber との連携
  6. 06レイキャストとデバッグ描画の例

活用シーン

  1. 01Three.js シーンへの重力と衝突の追加
  2. 02シーン要件に合う物理エンジンの選択
  3. 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 と一緒にインストールされる参考資料やスクリプト。

.gitkeep
references/
anti-patterns.md
examples.md
methods.md

詳細

threejs-impl-physics

クイックリファレンス

エンジン選択の判断ツリー
基準cannon-esRapier
シーンの規模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.STATIC0動かず、質量は無限大です
CANNON.Body.KINEMATIC0プログラムで移動し、動的ボディを押します
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);
形状の種類
形状コンストラクター備考
Boxnew CANNON.Box(halfExtents: Vec3)軸に沿ったボックス
Spherenew CANNON.Sphere(radius)最も計算コストの低い衝突形状
Cylindernew CANNON.Cylinder(rTop, rBottom, height, segments)円柱
Planenew CANNON.Plane()無限の地面平面
ConvexPolyhedronnew CANNON.ConvexPolyhedron({vertices, faces})カスタム凸包
Trimeshnew CANNON.Trimesh(vertices, indices)静的なボディ専用
Heightfieldnew CANNON.Heightfield(data, {elementSize})地形
Particlenew 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-esRapier
言語JavaScriptWASM にコンパイルされた Rust
リアルタイムの剛体数~1,000~10,000+
バンドルサイズ~100 KB~300–600 KB
CCD制限あり完全にサポート
決定論的いいえはい(クロスプラットフォーム)
デバッグ描画手動組み込みの world.debugRender()
API のスタイルコンストラクターベースビルダーパターン

参考リンク

公式ソース

コメント