中級 20 min 16 / 25
ライブデモ
チュートリアル 15: 物理 — 剛体、ジョイント、衝突イベント
レベル: 中級 時間: 20 分 学ぶこと:
@joroya/physicsが任意の OroyaScene上でcannon-esを駆動する方法、衝突イベントをノードに配線する方法、ジョイントで剛体を拘束する方法。
インストール
npm install @joroya/core @joroya/renderer-three @joroya/physics three
ステップ 1 — 床と落下する立方体
RigidBody + Collider は通常の Oroya ノードに付けるコンポーネントです。PhysicsSystem はフレームごとにシーンを走査し、両方を持つノードに対応する cannon ボディを生成し、シミュレートされた姿勢を node.transform に書き戻します。
import {
Scene, Node, Camera, CameraType, Light, LightType, createBox, createPlane, Material,
RigidBody, RigidBodyType,
Collider, ColliderShape,
} from "@joroya/core";
import { PhysicsSystem } from "@joroya/physics";
import { ThreeRenderer } from "@joroya/renderer-three";
const scene = new Scene();
const cam = new Node("camera");
cam.addComponent(new Camera({ type: CameraType.Perspective, fov: 50, aspect: 16 / 9, near: 0.1, far: 100 }));
cam.transform.setPosition({ x: 6, y: 6, z: 10 });
cam.transform.lookAt({ x: 0, y: 0, z: 0 });
scene.add(cam);
const ambient = new Node("ambient");
ambient.addComponent(new Light({ type: LightType.Ambient, intensity: 1 }));
scene.add(ambient);
const floor = new Node("floor");
floor.addComponent(createPlane(20, 20, 1, 1, { receiveShadow: true }));
floor.addComponent(new Material({ color: { r: 0.25, g: 0.27, b: 0.32 } }));
floor.transform.rotation = { x: -Math.SQRT1_2, y: 0, z: 0, w: Math.SQRT1_2 };
floor.addComponent(new RigidBody({ type: RigidBodyType.Static }));
floor.addComponent(new Collider({
shape: ColliderShape.Box,
halfExtents: { x: 10, y: 0.1, z: 10 },
}));
scene.add(floor);
const cube = new Node("cube");
cube.addComponent(createBox(1, 1, 1, { castShadow: true }));
cube.addComponent(new Material({ color: { r: 1, g: 0.55, b: 0.2 } }));
cube.transform.position = { x: 0, y: 5, z: 0 };
cube.transform.updateLocalMatrix();
cube.addComponent(new RigidBody({ type: RigidBodyType.Dynamic, mass: 1 }));
cube.addComponent(new Collider({
shape: ColliderShape.Box,
halfExtents: { x: 0.5, y: 0.5, z: 0.5 },
restitution: 0.4,
}));
scene.add(cube);
ステップ 2 — レンダーループで物理を実行
const renderer = new ThreeRenderer({
canvas: document.getElementById("canvas") as HTMLCanvasElement,
width: window.innerWidth, height: window.innerHeight,
});
renderer.mount(scene);
const physics = new PhysicsSystem({ gravity: { x: 0, y: -9.82, z: 0 } });
let last = performance.now();
function frame(now: number) {
const dt = Math.max(0, Math.min((now - last) / 1000, 0.1));
last = now;
physics.update(dt, scene);
renderer.render(dt);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
ステップ 3 — 衝突に反応
PhysicsSystem はノードの EventEmitter(クリック / ホバーイベントと同じチャンネル)で衝突イベントを発火します:
cube.events.on("collide-begin", (e) => {
console.log(`cube が ${e.other.name} に衝撃 ${e.impulse?.toFixed(2)} で衝突`);
});
6 つのイベント名:
collide-begin/collide/collide-end固体接触trigger-enter/trigger-stay/trigger-exitセンサーコライダー (isTrigger: true)
ステップ 4 — ジョイントを追加
physics.update(1 / 60, scene); // 拘束前にボディを生成
physics.addHingeConstraint(anchor, pendulum, {
pivotA: { x: 0, y: 0, z: 0 },
pivotB: { x: 0, y: 1, z: 0 },
axisA: { x: 0, y: 0, z: 1 },
});
他のジョイント: addPointToPointConstraint, addDistanceConstraint。
ステップ 5 — 物理レイキャスト
const hit = physics.raycast({ x: 0, y: 10, z: 0 }, { x: 0, y: 0, z: 0 });
if (hit) console.log(`${hit.node.name} に距離 ${hit.distance} でヒット`);
次のステップ
- センサー: Collider に
isTrigger: trueを付ける — 物体は通過するがtrigger-enter/trigger-exitが発火。 - 衝突フィルター:
collisionGroupとcollisionMask(ビットマスク)でレイヤー。 - 車両:
Vehicleラッパーは cannon-es のRaycastVehicleをカバー。