昔作ったActionScript 3.0向けの物理エンジンであるOimoPhysicsを全て書き直し、機能を大幅強化してJavaScriptから使えるようにしました。ようやく機能的にもAmmo.jsに対抗できるレベルになってきたかと思います。
デモは文字をクリックするかキー入力することで操作できます。Q
, E
キーで前後のデモと切り替えます。
>> Launch Demo (Click text on the left or press keys to control)
>> Open OimoPhysics GitHub page
物理エンジンとは
場面に合わせた物理シミュレーションをしてくれるライブラリです。最近のリアルなゲームには欠かせないものになりつつあります。オープンソースの有名どころでは Box2D, Bullet Physics, Open Dynamics Engine などがあります。ちなみにUnityでは標準でPhysXという物理エンジンを使うことができます。
JavaScriptで使える物理エンジンでは box2d.js※1Box2DをJavaScriptに変換したものです。, Matter.js, Ammo.js※2Bullet PhysicsをJavaScriptに変換したものです。, Oimo.js※3ActionScript 3.0版のOimoPhysicsをJavaScriptに移植したものです。, Cannon.js などがあります。Emscriptenの台頭により、C/C++で書かれたライブラリをasm.jsに変換したものが目立ちます。
OimoPhysicsについて
2011年頃から私が個人で開発を続けている軽量和製3D物理エンジンです。高速化に力を入れており、当時比較的遅いと言われてきたActionScript 3.0 (以下AS3) で実用的な3次元物理シミュレーションを可能にしました。
2016年から開発言語をAS3からHaxeに切り替え、1からコードを書き直しました。Haxeを採用した理由は、
・AS3, JavaScript, Java 等複数の言語に変換できる
・AS3やJavaScriptと同じECMAScript派生であり※4元々AS3がJavaScriptと同じECMAScript派生であり、HaxeがAS3から派生したという経緯があります。、文法が似ていて書きやすい
・静的型付けである
・強力なマクロが利用できる
などです。特に最後の2つが強力な理由です。Haxeのマクロについてはshohei909さんの「Haxe 実践マクロ」が最高に分かりやすいです。これがなければ開発が数年遅れていた可能性さえあります。ありがとうございました。
JavaScriptライブラリとしての利用
Haxeが書き出したJavaScriptファイルと、それをClosure Compilerで圧縮したものがGitHubのリポジトリに置いてあります。.jsファイルをダウンロードして普通のライブラリと同じように使うことができます。
機能と特長
他ライブラリとの独立性
3Dエンジンやその他ライブラリと完全に独立しています。好きなライブラリを組み合わせて使うことができますが、逆に言うと単体では描画機能を持ちません。
高速な衝突判定
以前より改善された広域衝突判定アルゴリズムにより、多数の剛体が動き回っていても快適に動作します。また、剛体の動きが小さい場合にはさらに判定が高速になります。
様々な図形のサポート
衝突形状として、
・球
・箱
・カプセル
・円柱
・円錐
・頂点群の凸包
が利用できます。これらを複数組み合わせて使うこともできます。
様々なジョイントのサポート
剛体同士をつなぐジョイントは
・球面ジョイント(Spherical Joint)
・回転ジョイント(Revolute Joint)
・直動ジョイント(Prismatic Joint)
・円筒ジョイント(Cylindrical Joint)
・ユニバーサルジョイント(Universal Joint)
・ラグドールジョイント(Ragdoll Joint)
をサポートしています。また、これらのうち多くは可動範囲の制限とモーター、可動範囲外に動いたときのバネ・ダンパを設定することができます。
ワールドに対するクエリ
物理演算ワールドに対して、AABBクエリ、レイキャスト、凸形状キャストを O(logN) で行うことができます。
その他諸々
・動かない剛体のスリープ
・衝突フィルタリング・コールバック
・回転方向の制限
・破壊可能なジョイント
・Projected Gauss-Seidelソルバによる安定したスタッキング
3Dライブラリとの連携
cx20さんがThree.jsなどの3DライブラリでOimoPhysicsを使うデモを作成されています。ありがとうございます!
このページ上で見るとテクスチャが読み込めなくて真っ黒になってしまうので、ぜひ元ページ上でご覧ください…… 追記:修正していただきました。このページ上でもちゃんと見られるようになりました。
Hello, OimoPhysics!
実際に使ってみたい方のために、HTML5のCanvasを使って簡単なシミュレーションを2Dで表示するサンプルを作ってみます。
準備
まずはCanvasを用意し、メインループが実行されるようにJavaScriptを書きます。ついでにこの記事を書いている段階での最新バージョン1.1.2のOimoPhysicsをインポートするスクリプトも書いておきます。
OimoPhysicsのクラスは全て window.OIMO
変数にexposeされているので、物理演算のためのワールドを new OIMO.World()
で生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<script src="https://rawgit.com/saharan/OimoPhysics/262eb59f22af6c0371051bb2b7c0e95c1e2ce1d1/bin/js/OimoPhysics.min.js"></script> <canvas id="canvas"></canvas> <script> var width = 600; var height = 400; var canvas; var g; var world; // 初期化 function init() { canvas = document.getElementById("canvas"); canvas.width = width; canvas.height = height; g = canvas.getContext("2d"); // Worldを生成する world = new OIMO.World(); setInterval(frame, 16); // ※注:この書き方は本当はあまりよくない } // フレーム毎の処理 function frame() { // 1/60秒時間を進める world.step(1 / 60); g.fillStyle = "rgb(240,240,240)"; g.fillRect(0, 0, width, height); } // 起動 init(); </script> |
以降はscriptタグの内部のみ見ていきます。
剛体を追加する
剛体のワールドへの追加は次のような手順で行います※5Box2Dを使ったことがある方は、Box2Dにおける Shape
, Fixture
, Body
, *Def
がそれぞれOimoPhysicsにおける Geometry
, Shape
, RigidBody
, *Config
であるという理解をしておけば問題ありません。。
ShapeConfig
を用意するShapeConfig
に衝突ジオメトリを設定するShapeConfig
を使ってShape
を生成するRigidBodyConfig
のインスタンスを用意するRigidBodyConfig
に位置や速度、剛体のタイプなどを設定するRigidBodyConfig
を使ってRigidBody
を生成するRigidBody
にShape
を追加するRigidBody
をワールドに追加する
恐らく「剛体一つ追加するだけでどんだけ長いねん!」という気持ちになったことでしょう。しかし1つ1つのステップは簡単ですので、覚えてしまえばそこまで大変ではないはずです。
まずは Shape
の生成です。Shape
には設定できる項目が多いので、ShapeConfig
という設定をまとめたオブジェクトをコンストラクタに渡してやります。ShapeConfig
で設定できるものは
・衝突図形(Geometry
)
・密度
・摩擦係数
・反発係数
・衝突フィルタリング
などです。最低限必要なものは図形だけなので、図形を設定した ShapeConfig
を使って Shape
を生成します。
1 2 3 4 5 6 |
// Shapeの設定を生成する var sconf = new OIMO.ShapeConfig(); // Shapeの図形を「幅1m、高さ1m、奥行き1mの箱」に設定する sconf.geometry = new OIMO.BoxGeometry(new OIMO.Vec3(0.5, 0.5, 0.5)); // Shapeを生成する var shape = new OIMO.Shape(sconf); |
ちなみに Geometry
と Shape
の違いですが、Geometry
が「幾何的な情報」のみ持っているのに対し、Shape
の方は回転や平行移動を含めた「物理的な性質の情報」も持っています。また、Geometry
は複数の Shape
の間で使い回すことができます。
続いて剛体の生成です。剛体の生成にも RigidBodyConfig
という設定オブジェクトを使います。生成した剛体に先程の Shape
を追加し、ワールドに剛体を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// RigidBodyの設定を生成する var rconf = new OIMO.RigidBodyConfig(); // 位置を原点の5m上に設定する rconf.position.init(0, 5, 0); // タイプをDYNAMICにする rconf.type = OIMO.RigidBodyType.DYNAMIC; // RigidBodyを生成する var rbody = new OIMO.RigidBody(rconf); // Shapeを追加する rbody.addShape(shape); // Worldに追加する world.addRigidBody(rbody); |
剛体には DYNAMIC
, STATIC
, KINEMATIC
の3種類のタイプがあります。通常は動く剛体に DYNAMIC
、壁などの動かない剛体に STATIC
を指定しておけば大丈夫です。
以上をまとめて追加したコードが以下になります。これでワールドに剛体を追加することができました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
var width = 600; var height = 400; var canvas; var g; var world; // 初期化 function init() { canvas = document.getElementById("canvas"); canvas.width = width; canvas.height = height; g = canvas.getContext("2d"); // Worldを生成する world = new OIMO.World(); // Shapeの設定を生成する var sconf = new OIMO.ShapeConfig(); // Shapeの図形を「幅1m、高さ1m、奥行き1mの箱」に設定する sconf.geometry = new OIMO.BoxGeometry(new OIMO.Vec3(0.5, 0.5, 0.5)); // Shapeを生成する var shape = new OIMO.Shape(sconf); // RigidBodyの設定を生成する var rconf = new OIMO.RigidBodyConfig(); // 位置を原点の5m上に設定する rconf.position.init(0, 5, 0); // タイプをDYNAMICにする rconf.type = OIMO.RigidBodyType.DYNAMIC; // RigidBodyを生成する var rbody = new OIMO.RigidBody(rconf); // Shapeを追加する rbody.addShape(shape); // Worldに追加する world.addRigidBody(rbody); setInterval(frame, 16); // ※注:この書き方は本当はあまりよくない } // フレーム毎の処理 function frame() { // 1/60秒時間を進める world.step(1 / 60); g.fillStyle = "rgb(240,240,240)"; g.fillRect(0, 0, width, height); } // 起動 init(); |
実行すると何も表示されませんが、内部では確かに箱が高さ5mの地点から落下しています。
箱を表示する
何も表示されないのは寂しいので、落下する箱を表示されましょう。OimoPhysicsにはデバッグ用の表示補助機能があるので、今回はそれを使います。DebugDraw
オブジェクトを用意し、line
メソッドを上書きします※6本当は point
メソッドと triangle
メソッドも上書きする必要がありますが、とりあえず線だけ描画することにします。。このとき、座標の単位がメートルであることと、Y軸が鉛直上向きであることに注意します。最後にワールドに DebugDraw
オブジェクトを設定すれば完了です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// DebugDrawを生成 var debugDraw = new OIMO.DebugDraw(); // ワイヤーフレームモードにして、面の描画は行わない debugDraw.wireframe = true; // 線の描画関数を上書きする debugDraw.line = (v1, v2, color) => { // 1m = 20px に拡大して表示する var scale = 20; // z座標を無視して2次元に正射影 var x1 = v1.x * scale + width * 0.5; var y1 = -v1.y * scale + height * 0.5; var x2 = v2.x * scale + width * 0.5; var y2 = -v2.y * scale + height * 0.5; // 線を引く g.strokeStyle = "rgb(" + (color.x * 255 | 0) + "," + (color.y * 255 | 0) + "," + (color.z * 255 | 0) + ")"; g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); }; // Worldに設定する world.setDebugDraw(debugDraw); |
ワールドを描画する際は、World.debugDraw
メソッドを呼び出します。
1 2 |
// Worldを描画する world.debugDraw(); |
以上のコードを init
と frame
に追加すると……
落下する箱が表示されました!
箱を増やす
このままでは箱は奈落の底へ落ちてしまいますので、床を作りましょう。ついでに箱を増やして、球体も追加してみましょう。まず、 Geometry
と座標、剛体のタイプを受け取って剛体を生成する関数を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function createRigidBody(geom, position, type) { // Shapeの設定を生成する var sconf = new OIMO.ShapeConfig(); // Shapeの図形を設定する sconf.geometry = geom; // Shapeを生成する var shape = new OIMO.Shape(sconf); // RigidBodyの設定を生成する var rconf = new OIMO.RigidBodyConfig(); // 位置を設定する rconf.position = position; // タイプを設定する rconf.type = type; // RigidBodyを生成する var rbody = new OIMO.RigidBody(rconf); // Shapeを追加する rbody.addShape(shape); return rbody; } |
適当な場所に床を配置し、箱と球体を生成するコードを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 床を生成する var floor = createRigidBody( new OIMO.BoxGeometry(new OIMO.Vec3(5, 0.5, 5)), // 幅奥行き10m、高さ1mの箱 new OIMO.Vec3(0, -5, 0), // 原点から5m下 OIMO.RigidBodyType.STATIC // 固定 ); world.addRigidBody(floor); // 箱を生成する for (var i = 0; i < 8; i++) { var box = createRigidBody( new OIMO.BoxGeometry(new OIMO.Vec3(0.5, 0.5, 0.5)), // 1m四方の箱 new OIMO.Vec3(0, -3 + i * 2, 0), // 原点の3m下から2mずつ間を空けて配置 OIMO.RigidBodyType.DYNAMIC // 動く ); world.addRigidBody(box); } // 球体を生成する var sphere = createRigidBody( new OIMO.SphereGeometry(1), // 直径2mの球体 new OIMO.Vec3(-0.5, 15, 0), // 原点から左に0.5m、上に15m OIMO.RigidBodyType.DYNAMIC // 動く ); world.addRigidBody(sphere); |
これを init
内に書いて実行します。
崩れる箱のシミュレーションが完成しました!
DebugDraw
で上書きしたのは線分を書くメソッドだけですが、球体もちゃんと表示されています。これは内部で図形の描画を線分の描画に帰着させるコードが動いているためです。point
と triangle
も実装すればポリゴンも描画されますが、陰面消去が使えないと前後関係がおかしくなってしまいます。
🍠 おわり 🍠
物理エンジンを使うと意外と簡単にシミュレーションを作成することができます。ライブラリの詳しい仕様は OimoPhysics API Documentation から見ることができます。他にも色々な機能があるので試してみてください。
また、今回のサンプルはこちらに置いてあります。
OimoPhysicsに関してバグ報告や質問等があれば、GitHubにissueを投げるかTwitterでリプライを送ると対応できると思います(多分Twitterの方が反応が早いです)。使ってみた報告もお待ちしています……!
Contact me
Send me a reply or create an issue on GitHub if you have any question or bug report. Consider using Twitter if you want a quicker response. I welcome English messages :)
注釈
1. | ↑ | Box2DをJavaScriptに変換したものです。 |
2. | ↑ | Bullet PhysicsをJavaScriptに変換したものです。 |
3. | ↑ | ActionScript 3.0版のOimoPhysicsをJavaScriptに移植したものです。 |
4. | ↑ | 元々AS3がJavaScriptと同じECMAScript派生であり、HaxeがAS3から派生したという経緯があります。 |
5. | ↑ | Box2Dを使ったことがある方は、Box2Dにおける Shape , Fixture , Body , *Def がそれぞれOimoPhysicsにおける Geometry , Shape , RigidBody , *Config であるという理解をしておけば問題ありません。 |
6. | ↑ | 本当は point メソッドと triangle メソッドも上書きする必要がありますが、とりあえず線だけ描画することにします。 |