[UIデザイナーが学ぶ]Three.jsの物理演算の話
はじめに
この記事はthreejs journeyを利用しての学習メモです。
このthreejs journeyはthree.jsを学ぶのに非常に有用ですので、是非視聴してみてください。おすすめです!!
今回はWebglでの物理演算をする方法について少し記載します。
学習の備忘録てきに書いているので、詳しくは上記リンクのthreejs journeyを御覧ください。
物理演算ライブラリ
3Dの物理演算用のライブラリはいくつかあります。
代表的なものは以下になります。
1. CANNON.js
メリット:
軽量で使いやすい
剛体力学の基本機能をサポート
衝突判定、摩擦、反発のシミュレーションが可能
デメリット:
高度な物理シミュレーション(柔体、流体など)はサポートされていない
開発が活発ではない
2. Ammo.js
メリット:
高精度でリアルなシミュレーションが可能
剛体、柔体、車両などの高度な物理シミュレーションをサポート
Bullet Physics Engineの移植版で信頼性が高い
デメリット:
ライブラリが重く、学習コストが高い
初心者にはやや複雑
3. Oimo.js
メリット:
軽量で高速
基本的な剛体シミュレーションに適している
シンプルなAPIで使いやすい
デメリット:
高度な物理シミュレーションはサポートされていない
機能が限定的
4. Physijs
メリット:
Three.jsとの統合が容易
Three.jsのオブジェクトに物理特性を簡単に追加可能
シンプルなAPIで使いやすい
デメリット:
開発が活発ではない
他のライブラリに比べて機能が限定的
5. Matter.js
メリット:
シンプルで使いやすいAPI
豊富なドキュメントとサンプル
2D物理エンジンとして非常に優れている
デメリット:
3D物理シミュレーションには対応していない
3Dプロジェクトには不向き
準備
まずは、物理演算用のライブラリを追加。
今回はcannon.jsを利用します。
npm i cannon
次に、追加したライブラリをimportして、物理演算用のworldを作成します。
また、worldに対して重力追加します。
...中略
import CANNON from "cannon"
...中略
// worldを作成
const world = new CANNON.World()
// 重力を設定
// 地球の重力加速度(重力)は 9.8 m/s2
world.gravity.set(0, -9.82, 0)
物理シュミレーションをする為のインスタンスを作成します。
これのインスタンスと、Three.jsのインスタンスを紐づけることで物理シュミレーションを行います。
const sphereShape = new CANNON.Sphere(0.5);
const sphereBody = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: sphereShape,
});
world.addBody(sphereBody);
bodyに設定できる主なプロパティは以下です。
mass:ボディの質量を指定します。質量が0の場合、ボディは固定され動きません。
position: ボディの位置を示すCANNON.Vec3オブジェクトです。
velocity: ボディの速度を示すCANNON.Vec3オブジェクトです。
quaternion: ボディの回転を示すCANNON.Quaternionオブジェクトです。
angularVelocity: ボディの角速度を示すCANNON.Vec3オブジェクトです。
shape: ボディの形状を指定します。CANNON.Shapeのインスタンスを使用します。
material: ボディの材質を指定します。CANNON.Materialのインスタンスを使用します。
linearDamping: ボディの線形減衰係数を指定します。値が大きいほど、ボディの速度が速く減衰します。
angularDamping: ボディの角減衰係数を指定します。値が大きいほど、ボディの回転が速く減衰します.
worldをupdate
cannon.jsのworld.stepメソッドは、物理シミュレーションのステップを進めるために使用されます。world.stepの引数は以下の通りです。
timeStep:シミュレーションの1ステップの時間(秒単位)。通常は固定値(例:1/60)。
deltaTime:前回のステップからの経過時間(秒単位)。
maxSubSteps:最大サブステップ数。シミュレーションが遅れている場合に、シミュレーションを追いつかせるために使用されます。
const clock = new THREE.Clock();
let oldElapsedTime = 0;
const tick = () => {
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - oldElapsedTime;
oldElapsedTime = elapsedTime;
// Update controls
controls.update();
// Update physics world
world.step(1 / 60, deltaTime, 3);
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
sphereBodyの値を利用してsphereの座標を更新します。
const tick = () => {
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - oldElapsedTime;
oldElapsedTime = elapsedTime;
// Update controls
controls.update();
// Update physics world
world.step(1 / 60, deltaTime, 3);
sphere.position.copy(sphereBody.position);
// ↑ 以下と同じ
// sphere.position.x = sphereBody.position.x;
// sphere.position.y = sphereBody.position.y;
// sphere.position.z = sphereBody.position.z;
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
球体がsphereBodyの座標に合わせて落下(移動)するようになります。
ただし、地面の設定がないので落下し続けます。
地面を追加
地面を追加すると、球体の動きがおかしくなります。
// Floor
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body({
mass: 0,
shape: floorShape,
});
world.addBody(floorBody);
理由としては、Planeオブジェクトの初期表示は以下のような表示になっているからです。
CANNON.Planeも同様に回転させてあげる必要があります。
Planeオブジェクトを回転させます。
回転にはquaternionを利用します。
quaternionをCANNON.Bodyのプロパティとして直接設定することはできないので、インスタンスを作成した後に設定します。
// Floor
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body({
mass: 0,
shape: floorShape,
});
// 回転
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5);
正しく動作するようになりました。
マテリアルを指定する
物理演算用のマテリアルを追加します。
マテリアルによって、物体同士がぶつかった場合などの表現が変わってきます。
設定にはMaterial、ContactMaterialを利用します。
// Material
const concreteMaterial = new CANNON.Material("concrete");
const plasticMaterial = new CANNON.Material("plastic");
const concretePlasticContactMaterial = new CANNON.ContactMaterial(
concreteMaterial,
plasticMaterial,
{
friction: 0.1,
restitution: 0.7,
}
);
world.addContactMaterial(concretePlasticContactMaterial)
◎マテリアルの追加方法
材質の作成
CANNON.Materialを使用して、異なる材質(例:コンクリートとプラスチック)を作成します。接触材質の作成
CANNON.ContactMaterialを使用して、第1、2引数で接触する2つの材質を指定し、第3引数で摩擦係数と反発係数などの設定をします。物理ワールドに接触材質を追加
world.addContactMaterialでワールドに追加します
マテリアルの設定を追加したら、各オブジェクトのbodyにマテリアルを追加します。
...中略
const sphereBody = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: sphereShape,
material: plasticMaterial, // 追加
});
...中略
const floorBody = new CANNON.Body({
mass: 0,
shape: floorShape,
material: concreteMaterial,
});
アニメーションにマテリアルの設定が反映されました。
個別のマテリアルを指定するのが面倒な場合、
world.defaultContactMaterialを指定するだけで。全てのオブジェクトにマテリアルが設定されます。
const defaultMaterial = new CANNON.Material("default");
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1,
restitution: 0.7,
}
);
// これが重要
world.defaultContactMaterial = defaultContactMaterial;
力を加える
これまでは、オブジェクトが重力によって落下するだけでしたが、
次は、オブジェクトに力を加えて動かしてみます。
applyForceを利用することで、物理ボディに対して力を加えることができます。
// force 加える力のベクトル : Vec3
// worldPoint 力を加える位置をワールド座標系 : Vec3
body.applyForce(force, worldPoint);
以下を設定してみます。
sphereBody.applyForce(new CANNON.Vec3(100, 0, 0), sphereBody.position);
オブジェクトがX軸方向に力が加わり押し出されます。
フレームアニメーション内で、applyForceを利用して力を加えてみます。
常に吹いている風のようなイメージです。
const tick = () => {
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - oldElapsedTime;
oldElapsedTime = elapsedTime;
// Update controls
controls.update();
// Update physics world
world.step(1 / 60, deltaTime, 3);
// 追加 ↓↓↓↓↓↓↓↓
sphereBody.applyForce(new CANNON.Vec3(-0.5, 0, 0), sphereBody.position);
sphere.position.copy(sphereBody.position);
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
パフォーマンス対応
cannon.worldでは、全てのオブジェクトで衝突判定が行われます。
その為、大量のオブジェクトを追加した場合に、パフォーマンスに影響が出る場合があります。
その為の対策がいくつか用意されています。
world.broadphase
broadphaseは物理ワールドの広域衝突判定アルゴリズムを設定するプロパティです。
物理シミュレーションで衝突判定を効率的に行うことができます。
const world = new CANNON.World();
world.broadphase = new CANNON.SAPBroadphase(world);
◎設定できる主な広域衝突判定アルゴリズム
SAPBroadphase (Sweep and Prune)
オブジェクトの位置を軸に沿ってソートし、衝突の可能性があるオブジェクトのペアを効率的に見つけます。
大規模なシーンに適しており、一般的に最も効率的です。NaiveBroadphase
最も単純なアルゴリズムで、すべてのオブジェクトペアをチェックします。
小規模なシーンに適していますが、大規模なシーンでは非効率です。GridBroadphase
空間をグリッドに分割し、各グリッドセル内のオブジェクトのみをチェックします。
中規模のシーンに適しています。
world.allowSleep
world.allowSleepは、物理シミュレーションにおいてオブジェクトが「スリープ」状態になることを許可する設定です。
この設定により、動かないオブジェクトがスリープ状態になり、計算から除外されるため、シミュレーションのパフォーマンスが向上します。
const world = new CANNON.World();
world.allowSleep = true
衝突判定を利用
bodyにイベントリスナーを追加することで、衝突判定のイベントを取得できます。
const body = new CANNON.Body({
mass: 1,
position,
shape,
material: defaultMaterial,
});
body.position.copy(position);
body.addEventListener("collide", (event) => console.log(event))
world.addBody(body);
他のjsでの処理と同じですが、不要になったら、イベントを削除するのを忘れないようにしてください。
body.removeEventListener("collide", (event) => console.log(event))
衝突判定を利用して、オブジェクトがぶつかった際に音を出してみます。
... 中略
const sound = new Audio("sound.mp3");
const playSound = (event) => {
// 衝撃の強さを取得
const impactStrength = event.contact.getImpactVelocityAlongNormal();
// 衝撃が大きい場合のみ音をだす
if (impactStrength > 1.5) {
sound.currentTime = 0;
sound.play();
}
};
// オブジェクトを追加する関数
const createSphere = (radius, position) => {
const mesh = new THREE.Mesh(SphereGeometry, SphereMaterial);
mesh.scale.set(radius, radius, radius);
mesh.castShadow = true;
mesh.position.copy(position);
scene.add(mesh);
const shape = new CANNON.Sphere(radius);
const body = new CANNON.Body({
mass: 1,
position,
shape,
material: defaultMaterial,
});
body.position.copy(position);
// 追加
body.addEventListener("collide", playSound);
world.addBody(body);
objectsToUpdate.push({ mesh, body });
};
...省略
まとめ
3Dをやったら、絶対にやりたくなるのが物理演算だと思います。
ライブラリを使うことで複雑な計算をしなくても物理演算を取り入れることができて楽しいです。