LoginSignup
70

More than 5 years have passed since last update.

【忙しい人向け】JavaScriptで3D物理エンジン動かしてみる (three.js + ammo.js)

Last updated at Posted at 2014-08-31

きっかけ

Blenderでアペンドミクさん表示したい。Blenderにpmxを読み込みレンダリングまで

Unityでアペンドミクさん動かしたい。MMD4Mecanimを使用してMMDモデルにUnityで踊って頂いてみた。

WEBブラウザ上でMMD動くの!?MMD on three.js / three.jsで遊んでみる(29)

とりあえずWEBGLと物理エンジン試してみよう ←イマココ

この記事で行うこと

  • 下のアニメーションが動く所まで。

ammo_threejs2.gif

  • ammo.js (JavaScriptの3D物理エンジン) の簡単な使い方
  • three.js (JavaScriptの3D描画ライブラリ) の簡単な使い方

この記事で行わないこと

  • ベクトルとか物理数学関係の計算
  • three.jsと物理エンジンのラッパーライブラリ (Physi.jsとか)の使い方

使用環境

  • Windows7 + Google Chrome(Ver 37)
  • Mac(OS X10.9.4) + Google Chrome(Ver 37)

1.まずは物理エンジンのみで動かしてみる

JavaScript版の物理エンジン、ammo.jsのみを使って、まずは動作を確認しましょう。
ammo.jsをダウンロードしてください。

やりたいことは、以下のとおりです。

  • 物理エンジンを初期化して重力を設定する。
  • 地面と球を作る。
  • 球を地面の上空10mから落とす。
  • 物理エンジンにかけると球が地面に衝突して動きが変わるはず。

3D描画はとりあえず置いておいて、ログを出力して座標を確認する形で実験してみます。
今回は忙しい人に優しく、htmlをファイルに保存して、ブラウザにhtmlファイルをドラッグ&ドロップするだけで動く仕様になっています。

hello_ammo.html
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>hello_ammo</title>
        <!-- https://raw.githubusercontent.com/kripken/ammo.js/52e7061924d2f1a4728f3cb4550794d25a0074af/builds/ammo.js -->
        <script src="ammo.js"></script>
    </head>
    <body>
        <script>
            // htmlファイルを読み込んだら実行
            window.addEventListener("DOMContentLoaded", function(){
                // 物理エンジンの初期化
                function init() {
                    // 物理エンジンの初期化部分はお約束なので気になる方は個別にお調べください
                    var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
                    var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
                    var overlappingPairCache = new Ammo.btDbvtBroadphase();
                    var solver = new Ammo.btSequentialImpulseConstraintSolver();
                    // 物理エンジンの世界(ワールド)を作成。剛体を全てここに追加していく
                    var dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(
                        dispatcher, 
                        overlappingPairCache, 
                        solver, 
                        collisionConfiguration
                    );
                    // 重力の設定(Y軸に対して設定)
                    dynamicsWorld.setGravity(new Ammo.btVector3(0, -10, 0));
                    return dynamicsWorld;
                }

                // 地面の設定
                function make_ground(size, pos) {
                    // 初期状態を設定する
                    var form = new Ammo.btTransform();
                    // 初期化する
                    form.setIdentity();
                    // 初期座標を設定する
                    form.setOrigin(pos);
                    // 剛体を作成する。
                    return new Ammo.btRigidBody(
                        // 剛体を設定する
                        new Ammo.btRigidBodyConstructionInfo(
                            0,  // 質量を0にすることで、質量無限大=動かなくなる
                            new Ammo.btDefaultMotionState(form),  // 初期状態
                            new Ammo.btBoxShape(size), // 箱の形状。一辺が指定値の2倍になる
                            new Ammo.btVector3(0, 0, 0) // 慣性モーメント=物体の回転のしにくさ。地面は動かないので全部0
                        )
                    );
                }

                // 球の設定
                function make_sphere(r, mass, pos) {
                    var form = new Ammo.btTransform();
                    form.setIdentity();
                    form.setOrigin(pos);
                    // 半径rの球を設定
                    var shpere = new Ammo.btSphereShape(r);
                    // 質量massで慣性モーメントを設定。質量が設定された剛体は以下2行で値を設定するのがお約束
                    var localInertia = new Ammo.btVector3(0, 0, 0);
                    shpere.calculateLocalInertia(mass,localInertia);
                    return new Ammo.btRigidBody(
                        new Ammo.btRigidBodyConstructionInfo(
                            mass, // 質量
                            new Ammo.btDefaultMotionState(form),   // 初期状態
                            shpere, // 球の形状
                            localInertia // 慣性モーメント
                        )
                    );
                }

                // 初期化。ワールドを取得
                var world = init();
                // 地面の設定。大きさと位置を指定。(原点が重心となるため、Y軸を-1ずらすことで、Y=0に地面が設定される)
                var ground = make_ground(
                    new Ammo.btVector3(5, 1, 5), 
                    new Ammo.btVector3(0, -1, 0)
                );
                // 地面をワールドに登録
                world.addRigidBody(ground);
                // 球の設定。半径1、重量1、位置は地面に対して、10高く設定
                var sphere = make_sphere(1, 1, new Ammo.btVector3(0, 10, 0));
                // 球をワールドに登録
                world.addRigidBody(sphere);

                // 物理エンジン実行後の座標を入れるためのオブジェクトを生成
                var trans = new Ammo.btTransform();

                // 60fpsで100フレーム分まわす
                for (var i = 0; i < 100; i++) {
                    // 1フレーム分演算
                    world.stepSimulation(1/60, 0);
                    // 演算後の球の位置がtransに入る
                    sphere.getMotionState().getWorldTransform(trans);
                    // 座標を出力
                    console.log("sphere pos = " + 
                        [trans.getOrigin().x().toFixed(2), 
                         trans.getOrigin().y().toFixed(2), 
                         trans.getOrigin().z().toFixed(2)]
                    );
                }

            }, false);
        </script>
    </body>
</html>

3行で済ませるとかは無理で、それなりに設定が必要です。
物理エンジン特有のキーワードを説明しておきます。

  • 剛体:物理エンジンで扱う地面や球などを物理エンジンでは剛体と呼んでいます。他のロープや旗等の柔軟体もありますが、今回の記事では扱いません。プログラムのオブジェクトと区別するために使います。
  • ワールド:物理エンジンの世界。この世界に登録された情報を使って計算します。
  • 慣性モーメント:とりあえず引数の一つなのでコメントとして書きましたが、興味があれば調べればいいレベルだと思います。

さて、上記htmlをWEBブラウザに貼り付けると何が起こるかというと、真っ白画面になります。
慌てずにWEBブラウザ上の画面を右クリックして、「要素を検証」をクリック、consoleをタブを表示すると結果が出ています。

sphere pos = 0.00,10.00,0.00
sphere pos = 0.00,9.99,0.00
sphere pos = 0.00,9.98,0.00
sphere pos = 0.00,9.97,0.00
:
sphere pos = 0.00,1.22,0.00
sphere pos = 0.00,1.00,0.00

という結果になりました。座標は、x,y,zの順に並んでいます。Y座標が10.00から1.00まで変化して、そこからは変わらないのがわかりますね。物理エンジンは動いているようです。

2.3D描画付きで物理エンジンを動かしてみる

物理エンジンが動いているのはわかりましたが、ログに表示するだけだと面白くないので、3Dで描画してみます。
three.jsを合わせて使います。r68のthree.min.jsをダウンロードしてください。
まずは1の動きを描画で忠実に再現します。

作業内容は、1の内容に加えて、

  • 3Dエンジンを初期化してカメラとライトを設定する。
  • 描画用の地面と球を作る。
  • 球を地面から落とした際に、球の座標を書き換えて再描画する。

となります。

hello_ammo_with_threejs1.html
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>hello_ammo</title>
        <script src="three.min.r68.js"></script>
        <!-- https://raw.githubusercontent.com/kripken/ammo.js/52e7061924d2f1a4728f3cb4550794d25a0074af/builds/ammo.js -->
        <script src="ammo.js"></script>
    </head>
    <body>
        <script>

            window.addEventListener("DOMContentLoaded", function(){

                // 初期化(物理エンジン、3Dエンジン両方)
                function init(width, height, angle, near, far, camerapos, lightdir) {
                    // 3Dエンジンのシーン(物理エンジンのワールドに相当するもの)を生成
                    var scene = new THREE.Scene();
                    return {
                        ammo: init_ammo(), // 物理エンジンのワールドを生成して返す
                        three_renderer: init_three(),  // 3Dエンジンの画像描画オブジェクトを生成して返す
                        three_scene: scene,  // 3Dエンジンのシーンを返す
                        three_camera: init_camera_and_light_three() // 3Dエンジンのカメラとライトを生成してカメラを返す
                    };

                    // 物理エンジンの初期化
                    function init_ammo() {
                        var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
                        var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
                        var overlappingPairCache = new Ammo.btDbvtBroadphase();
                        var solver = new Ammo.btSequentialImpulseConstraintSolver();
                        var dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(
                            dispatcher, 
                            overlappingPairCache, 
                            solver, 
                            collisionConfiguration
                        );
                        dynamicsWorld.setGravity(new Ammo.btVector3(0, -10, 0));
                        return dynamicsWorld;
                    }

                    // 3Dエンジンの初期化
                    function init_three() {
                        // 画像描画オブジェクト(レンダラ)を生成。WEBGLを利用する
                        var renderer = new THREE.WebGLRenderer({antialias: true});
                        // 描画サイズを設定
                        renderer.setSize(width, height);
                        // 描画しない部分を空色に設定。2番目の引数はアルファ値
                        renderer.setClearColor(0x90D7EC, 1);
                        // 画像描画オブジェクトをDOM上に設定する(body直下)
                        document.body.appendChild(renderer.domElement);
                        // 画像描画オブジェクトを返す
                        return renderer;
                    }

                    // 3Dエンジンのカメラとライトを設定
                    function init_camera_and_light_three() {
                        // パースペクティブ(透視投影)カメラを設定。引数は、画角、アスペクト比、ニアクリップ、ファークリップの順。
                        // カメラの参考資料:http://www56.atwiki.jp/threejs/pages/70.html
                        var camera = new THREE.PerspectiveCamera( angle, width / height, near, far );
                        // 平行投影カメラの場合は下記記述
                        // var scale = 0.1;
                        // var camera = new THREE.OrthographicCamera(
                        //  -width/2*scale, width/2*scale, height/2*scale, -height/2*scale, 1, 1000);

                        // カメラの位置をセット
                        camera.position.set(camerapos.x, camerapos.y, camerapos.z);
                        // カメラの向きをセット。この場合原点となる。
                        camera.lookAt( scene.position );
                        // シーンに追加
                        scene.add( camera );

                        // 平行光源(ライト)を設定。白色、強さを指定
                        var directionalLight = new THREE.DirectionalLight( 0xffffff, 5 );
                        // カメラの向きを指定(平行光源なので、位置は無関係)
                        console.log(lightdir.x);
                        directionalLight.position.set(lightdir.x, lightdir.y, lightdir.z);
                        // シーンに追加
                        scene.add( directionalLight );
                        // カメラを返す
                        return camera;
                    }
                }

                // 地面の設定
                function make_ground(size, pos) {
                    return {
                        ammo: make_ground_ammo(), // 物理エンジンで使う地面を返す
                        three: make_ground_three()  // 3Dエンジンで使う地面を返す
                    };

                    // 物理エンジンの地面設定
                    function make_ground_ammo() {
                        var form = new Ammo.btTransform();
                        form.setIdentity();
                        form.setOrigin(pos);
                        var ground = new Ammo.btRigidBody(
                            new Ammo.btRigidBodyConstructionInfo(
                                0, 
                                new Ammo.btDefaultMotionState(form), 
                                new Ammo.btBoxShape(size),
                                new Ammo.btVector3(0, 0, 0)
                            )
                        );
                        // ワールドに設定
                        world.ammo.addRigidBody(ground);
                        return ground;
                    }

                    // 3Dエンジンの地面設定
                    function make_ground_three() {
                        // 地面ポリゴンを作成
                        var ground = new THREE.Mesh(
                            // 箱のポリゴンの頂点情報を設定
                            // 物理エンジンのサイズに合わせるためそれぞれ2倍する
                            new THREE.BoxGeometry(size.x()*2, size.y()*2, size.z()*2), 
                            // 表面の材質の指定。ここではランバート反射。色は灰色とする
                            // 参考:http://sawanoya.blogspot.jp/2012/06/blog-post_29.html
                            new THREE.MeshLambertMaterial({color: 0x999999})
                        );
                        // 地面の位置指定
                        ground.position.set(pos.x(),pos.y(),pos.z());
                        // 地面をシーンに追加
                        world.three_scene.add(ground);
                        // 地面を返す
                        return ground;
                    }
                }

                // 球の設定
                function make_sphere(r, mass, pos) {
                    return {
                        ammo: make_sphere_ammo(),  // 物理エンジンで使う球を返す
                        three: make_sphere_three()  // 3Dエンジンで使う球を返す
                    };

                    // 物理エンジンの球設定
                    function make_sphere_ammo() {
                        var form = new Ammo.btTransform();
                        form.setIdentity();
                        form.setOrigin(pos);
                        var shpere = new Ammo.btSphereShape(r)
                        var localInertia = new Ammo.btVector3(0, 0, 0);
                        shpere.calculateLocalInertia(mass,localInertia);
                        var spherebody = new Ammo.btRigidBody(
                            new Ammo.btRigidBodyConstructionInfo(
                                mass, 
                                new Ammo.btDefaultMotionState(form), 
                                shpere, 
                                localInertia
                            )
                        );
                        // ワールドに設定
                        world.ammo.addRigidBody(spherebody);
                        return spherebody;
                    }

                    // 3Dエンジンの球設定
                    function make_sphere_three() {
                        // 球のポリゴンの頂点情報を指定。半径rで、緯度経度の分割数を指定する。(数字が大きいほど細かくなる)
                        var sphereGeometry = new THREE.SphereGeometry( r, 16,16);
                        // 表面の材質の指定。ここではランバート反射。色は赤色とする
                        var sphereMaterial = new THREE.MeshLambertMaterial( { color: 0xff0000,
                            // ワイヤーフレームにする。フレームの線の太さが指定できる
                            wireframe: true,
                            wireframeLinewidth: 0.2 
                        });
                        // 球オブジェクトを作成
                        var sphereMesh = new THREE.Mesh( sphereGeometry, sphereMaterial );
                        // 初期位置を指定
                        sphereMesh.position.set(pos.x(), pos.y(), pos.z() );
                        // 球をシーンに追加
                        world.three_scene.add( sphereMesh );
                        // 球を返す
                        return sphereMesh;
                    }
                }

                // 画面に描画する
                function rendering() {
                    // 描画の際はシーンとカメラを指定する
                    world.three_renderer.render( world.three_scene, world.three_camera );
                }

                // アニメーションを行う
                function animate(){
                    // カウント回数分以下の処理を行う
                    if (count >= 0) {
                        // 物理演算及び画面描画を行う
                        update();
                        count--;
                        // requestAnimationFrameは、ブラウザ任せで次に呼び出す関数を登録
                        // 参考:http://lealog.hateblo.jp/entry/2013/10/01/235736
                        window.requestAnimationFrame( animate );
                    }
                }

                //  物理演算及び画面描画
                function update() {
                    // 1/60間隔で物理演算
                    world.ammo.stepSimulation(1/60, 0);
                    // 球の位置情報を取得
                    sphere.ammo.getMotionState().getWorldTransform(update_trans);
                    // 3Dエンジン側の球に位置をセット
                    sphere.three.position.set(
                        update_trans.getOrigin().x(),
                        update_trans.getOrigin().y(),
                        update_trans.getOrigin().z()
                    );

                    // コンソール上に座標を表示
                    console.log(" count:" + count + " sphere pos = " + 
                        [update_trans.getOrigin().x().toFixed(2), 
                         update_trans.getOrigin().y().toFixed(2), 
                         update_trans.getOrigin().z().toFixed(2)]
                    );
                    // 画面描画
                    rendering();
                }

                // 初期化
                var world = init(
                    window.innerWidth, // WEBブラウザタブ内の幅を指定
                    window.innerHeight, // WEBブラウザタブ内の高さを指定
                    35, // 画角(広角気味)
                    1, // ニアクリップ
                    1000, // ファークリップ
                    {x:20, y:20, z:20}, // カメラ位置
                    {x:5, y:1, z:2} // ライト向き
                );

                // 地面設定
                var ground = make_ground(
                    new Ammo.btVector3(5,1,5), // 地面サイズ
                    new Ammo.btVector3(0, -1, 0) // 地面位置(高さをY=0地点にしたいので-1)
                );

                // 球設定
                var sphere = make_sphere(
                    1, // 球半径
                    1, // 球質量
                    new Ammo.btVector3(0, 10, 0) // 球位置(地面に対して10垂直に離れている)
                );

                // 初期設定値でまず1回描画
                rendering();

                // 100回分アニメーションさせる
                var count = 100;
                // ループ内で毎回newするとメモリ使うので
                var update_trans = new Ammo.btTransform();
                // アニメーション開始
                animate();
            }, false);
        </script>
    </body>
</html>

ドスンと地面に落ちる球が表現できていれば完成です。
ammo_threejs3.gif

3.拡張してみる

とりあえず物理エンジンの結果を画面に表現するところまでは出来ました。
ただ、いくつか気に入らない所があります。

  • 地面に落ちても全く跳ねない
  • 転がせたい

物理エンジンはパラメータをちゃんと設定しないと、物理のテストでよく見かける「ただし摩擦はないものとする」というような状態になります。摩擦無しで滑らせるとどこまでも滑っていきます。

プログラムに追加するのは以下です。

  • 物理エンジンワールドの地面に反発係数と摩擦係数追加
  • 物理エンジンワールドの球に反発係数、摩擦係数、減衰率、回転制限、滑り制限追加
  • 物理エンジンワールドの球に重力以外の加速度を追加(Z軸方向に1)
  • 3D描画の原点にXYZ軸を追加
  • 球の転がり具合を表現できるように追加
  • 床を薄く、球の高さを少し高くする
  • アニメーションカウントを増やす

物理エンジンの理屈はまだよくわかってないのですが、動かした限りだと、

  • 反発係数が0だと跳ねない(反発係数は衝突した剛体同士の反発係数を掛け算する)
  • 摩擦係数が0だと滑り続ける(摩擦係数は接触している剛体同士の摩擦係数を掛け算する)
  • 滑るのと転がるのは完全に別っぽい
  • 減衰率は滑るのと転がるのとそれぞれ設定できる。
  • 減衰率が0だと、転がりが止まらない(完全な球は点で地面と接しているので回転し続ける)

です。ちなみに今回設定した係数は適当です(地面の係数は1固定)

hello_ammo_with_threejs2.html
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>hello_ammo</title>
        <script src="three.min.r68.js"></script>
        <!-- https://raw.githubusercontent.com/kripken/ammo.js/52e7061924d2f1a4728f3cb4550794d25a0074af/builds/ammo.js -->
        <script src="ammo.js"></script>
    </head>
    <body>
        <script>

            window.addEventListener("DOMContentLoaded", function(){

                // 初期化(物理エンジン、3Dエンジン両方)
                function init(width, height, angle, near, far, camerapos, lightdir) {
                    // 3Dエンジンのシーン(物理エンジンのワールドに相当するもの)を生成
                    var scene = new THREE.Scene();
                    return {
                        ammo: init_ammo(), // 物理エンジンのワールドを生成して返す
                        three_renderer: init_three(),  // 3Dエンジンの画像描画オブジェクトを生成して返す
                        three_scene: scene,  // 3Dエンジンのシーンを返す
                        three_camera: init_camera_and_light_three() // 3Dエンジンのカメラとライトを生成してカメラを返す
                    };

                    // 物理エンジンの初期化
                    function init_ammo() {
                        var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
                        var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
                        var overlappingPairCache = new Ammo.btDbvtBroadphase();
                        var solver = new Ammo.btSequentialImpulseConstraintSolver();
                        var dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(
                            dispatcher, 
                            overlappingPairCache, 
                            solver, 
                            collisionConfiguration
                        );
                        dynamicsWorld.setGravity(new Ammo.btVector3(0, -10, 0));
                        return dynamicsWorld;
                    }

                    // 3Dエンジンの初期化
                    function init_three() {
                        // 画像描画オブジェクト(レンダラ)を生成。WEBGLを利用する
                        var renderer = new THREE.WebGLRenderer({antialias: true});
                        // 描画サイズを設定
                        renderer.setSize(width, height);
                        // 描画しない部分を空色に設定。2番目の引数はアルファ値
                        renderer.setClearColor(0x90D7EC, 1);
                        // 画像描画オブジェクトをDOM上に設定する(body直下)
                        document.body.appendChild(renderer.domElement);
                        // 画像描画オブジェクトを返す
                        return renderer;
                    }

                    // 3Dエンジンのカメラとライトを設定
                    function init_camera_and_light_three() {
                        // パースペクティブ(透視投影)カメラを設定。引数は、画角、アスペクト比、ニアクリップ、ファークリップの順。
                        // カメラの参考資料:http://www56.atwiki.jp/threejs/pages/70.html
                        var camera = new THREE.PerspectiveCamera( angle, width / height, near, far );
                        // 平行投影カメラの場合は下記記述
                        // var scale = 0.1;
                        // var camera = new THREE.OrthographicCamera(
                        //  -width/2*scale, width/2*scale, height/2*scale, -height/2*scale, 1, 1000);

                        // カメラの位置をセット
                        camera.position.set(camerapos.x, camerapos.y, camerapos.z);
                        // カメラの向きをセット。この場合原点となる。
                        camera.lookAt( scene.position );
                        // シーンに追加
                        scene.add( camera );

                        // 平行光源(ライト)を設定。白色、強さを指定
                        var directionalLight = new THREE.DirectionalLight( 0xffffff, 5 );
                        // カメラの向きを指定(平行光源なので、位置は無関係)
                        console.log(lightdir.x);
                        directionalLight.position.set(lightdir.x, lightdir.y, lightdir.z);
                        // シーンに追加
                        scene.add( directionalLight );

                        // 軸の追加  X軸が赤色、Y軸が緑色、Z軸が青色
                        var axis = new THREE.AxisHelper(500);
                        // 原点にセット
                        axis.position.set(0,0,0);
                        // シーンに追加
                        scene.add(axis);

                        // カメラを返す
                        return camera;
                    }
                }

                // 地面の設定
                function make_ground(size, pos) {
                    return {
                        ammo: make_ground_ammo(), // 物理エンジンで使う地面を返す
                        three: make_ground_three()  // 3Dエンジンで使う地面を返す
                    };

                    // 物理エンジンの地面設定
                    function make_ground_ammo() {
                        var form = new Ammo.btTransform();
                        form.setIdentity();
                        form.setOrigin(pos);
                        var ground = new Ammo.btRigidBody(
                            new Ammo.btRigidBodyConstructionInfo(
                                0, 
                                new Ammo.btDefaultMotionState(form), 
                                new Ammo.btBoxShape(size),
                                new Ammo.btVector3(0, 0, 0)
                            )
                        );
                        // 反発係数を設定
                        ground.setRestitution(1);
                        // 摩擦係数を設定
                        ground.setFriction(1);
                        // ワールドに設定
                        world.ammo.addRigidBody(ground);
                        return ground;
                    }

                    // 3Dエンジンの地面設定
                    function make_ground_three() {
                        // 地面ポリゴンを作成
                        var ground = new THREE.Mesh(
                            // 箱のポリゴンの頂点情報を設定
                            // 物理エンジンのサイズに合わせるためそれぞれ2倍する
                            new THREE.BoxGeometry(size.x()*2, size.y()*2, size.z()*2), 
                            // 表面の材質の指定。ここではランバート反射。色は灰色とする
                            // 参考:http://sawanoya.blogspot.jp/2012/06/blog-post_29.html
                            new THREE.MeshLambertMaterial({color: 0x999999})
                        );
                        // 地面の位置指定
                        ground.position.set(pos.x(),pos.y(),pos.z());
                        // 地面をシーンに追加
                        world.three_scene.add(ground);
                        // 地面を返す
                        return ground;
                    }
                }

                // 球の設定
                function make_sphere(r, mass, pos) {
                    return {
                        ammo: make_sphere_ammo(),  // 物理エンジンで使う球を返す
                        three: make_sphere_three()  // 3Dエンジンで使う球を返す
                    };

                    // 物理エンジンの球設定
                    function make_sphere_ammo() {
                        var form = new Ammo.btTransform();
                        form.setIdentity();
                        form.setOrigin(pos);
                        var shpere = new Ammo.btSphereShape(r)
                        var localInertia = new Ammo.btVector3(0, 0, 0);
                        shpere.calculateLocalInertia(mass,localInertia);
                        var spherebody = new Ammo.btRigidBody(
                            new Ammo.btRigidBodyConstructionInfo(
                                mass, 
                                new Ammo.btDefaultMotionState(form), 
                                shpere, 
                                localInertia
                            )
                        );

                        // Z軸に加速度(1)を追加
                        spherebody.setLinearVelocity(new Ammo.btVector3(0,0,1));
                        // 反発係数を設定
                        spherebody.setRestitution(0.6);
                        // 摩擦係数を設定
                        spherebody.setFriction(0.1);
                        // 減衰率を設定
                        spherebody.setDamping(0, 0.05);
                        // 回転制限(1の軸でしか回転しない)
                        spherebody.setAngularFactor(new Ammo.btVector3(1, 1, 1));
                        // 滑り制限(1の軸でしか動かない。初期値には適用されない)
                        spherebody.setLinearFactor(new Ammo.btVector3(1, 1, 1 ));
                        // ワールドに設定
                        world.ammo.addRigidBody(spherebody);
                        return spherebody;
                    }

                    // 3Dエンジンの球設定
                    function make_sphere_three() {
                        // 球のポリゴンの頂点情報を指定。半径rで、緯度経度の分割数を指定する。(数字が大きいほど細かくなる)
                        var sphereGeometry = new THREE.SphereGeometry( r, 16,16);
                        // 表面の材質の指定。ここではランバート反射。色は赤色とする
                        var sphereMaterial = new THREE.MeshLambertMaterial( { color: 0xff0000,
                            // ワイヤーフレームにする。フレームの線の太さが指定できる
                            wireframe: true,
                            wireframeLinewidth: 0.2 
                        });
                        // 球オブジェクトを作成
                        var sphereMesh = new THREE.Mesh( sphereGeometry, sphereMaterial );
                        // 初期位置を指定
                        sphereMesh.position.set(pos.x(), pos.y(), pos.z() );
                        // 球をシーンに追加
                        world.three_scene.add( sphereMesh );
                        // 球を返す
                        return sphereMesh;
                    }
                }

                // 画面に描画する
                function rendering() {
                    // 描画の際はシーンとカメラを指定する
                    world.three_renderer.render( world.three_scene, world.three_camera );
                }

                // アニメーションを行う
                function animate(){
                    // カウント回数分以下の処理を行う
                    if (count >= 0) {
                        // 物理演算及び画面描画を行う
                        update();
                        count--;
                        // requestAnimationFrameは、ブラウザ任せで次に呼び出す関数を登録
                        // 参考:http://lealog.hateblo.jp/entry/2013/10/01/235736
                        window.requestAnimationFrame( animate );
                    }
                }

                //  物理演算及び画面描画
                function update() {
                    // 1/60間隔で物理演算
                    world.ammo.stepSimulation(1/60, 0);
                    // 球の位置情報を取得
                    sphere.ammo.getMotionState().getWorldTransform(update_trans);
                    // 3Dエンジン側の球に位置をセット
                    sphere.three.position.set(
                        update_trans.getOrigin().x(),
                        update_trans.getOrigin().y(),
                        update_trans.getOrigin().z()
                    );
                    // 3Dエンジン側の球の転がり具合をセット
                    sphere.three.quaternion.set(
                        update_trans.getRotation().x(),
                        update_trans.getRotation().y(),
                        update_trans.getRotation().z(),
                        update_trans.getRotation().w()
                    );


                    // コンソール上に座標を表示
                    console.log(" count:" + count + " sphere pos = " + 
                        [update_trans.getOrigin().x().toFixed(2), 
                         update_trans.getOrigin().y().toFixed(2), 
                         update_trans.getOrigin().z().toFixed(2)]
                    );

                    // 画面描画
                    rendering();
                }

                // 初期化
                var world = init(
                    window.innerWidth, // WEBブラウザタブ内の幅を指定
                    window.innerHeight, // WEBブラウザタブ内の高さを指定
                    35, // 画角(広角気味)
                    1, // ニアクリップ
                    1000, // ファークリップ
                    {x:20, y:20, z:20}, // カメラ位置
                    {x:5, y:1, z:2} // ライト向き
                );

                // 地面設定
                var ground = make_ground(
                    new Ammo.btVector3(5,0.5,5), // 地面サイズ
                    new Ammo.btVector3(0, -0.5, 0) // 地面位置(高さをY=0地点にしたいので-0.5)
                );

                // 球設定
                var sphere = make_sphere(
                    1, // 球半径
                    1, // 球質量
                    new Ammo.btVector3(0, 15, 0) // 球位置(地面に対して15垂直に離れている)
                );

                // 初期設定値でまず1回描画
                rendering();

                // 600回分アニメーションさせる
                var count = 600;
                // ループ内で毎回newするとメモリ使うので
                var update_trans = new Ammo.btTransform();
                // アニメーション開始
                animate();
            }, false);
        </script>
    </body>
</html>

冒頭のアニメーションと同じになってますね。
ammo_threejs2.gif

今回原点に軸を設定して動作をわかりやすくしました。X軸=赤、Y軸=緑、Z軸=青です。
Z軸方向に加速度を設定したので、真ん中ではなくZ軸方向に少し寄った位置に落下しています。

おまけ.バージョンが変わると普通に動かなくなる

WEBや本を見て、写経しようとしたり動かそうとしたりするとエラーが出るという現象に悩まされます。初心者にとっては何が悪いのかがわからないので、結構困ります。
原因としては、メソッドの改廃が多いのと、古い関数名を使い続けられるようにしないので、古いサンプルは古いライブラリでしか動かなくなることが多いかなと。このあたりは追いかけるしかないというのが現状です。

  • three.jsの場合
  • ammo.jsの場合
    • ammo.jsはbulletというC++の物理エンジンをEmscriptenというツールを使ってコンバートしているんだけど、Bullet Ver 2.82から変換したタイミングで、btStaticPlaneShapeという関数がなくなっていて、地面をこの関数で作っているサンプルは全滅
    • 今のサンプルがどうなっているかというと、btStaticPlaneShapeの代わりに、btBoxShapeを利用している。(汎用性考えるとそのほうがいい気がする)
    • 自動コンバートだからかどうかわからないのですが、ammo.jsはバージョン取得する方法がありません。なので今回のサンプルでは、githubの具体的な場所を注釈で記述しています。もうちょっといい方法があればいいのですけど。

おまけ2. ammo.jsでnewしたオブジェクトはdestroyしないといけない

ammo.jsでnewしたオブジェクトは全部destroyしないとメモリリークするよ、とサンプルに記述がありました。

実験のようにすぐ終わるものだと問題ないかもしれませんが、途中で物理エンジンをリセットして再度使うような処理を入れる場合は、注意してください。

var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
var overlappingPairCache = new Ammo.btDbvtBroadphase();
var solver = new Ammo.btSequentialImpulseConstraintSolver();
var dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(
    dispatcher, 
    overlappingPairCache, 
    solver, 
    collisionConfiguration
);
:
// いろいろ処理する
:
// 全部削除する
Ammo.destroy( dynamicsWorld );
Ammo.destroy( solver );
Ammo.destroy( collisionConfiguration );
Ammo.destroy( dispatcher );
Ammo.destroy( broadphase );

参考文献

  • Bullet Physicsではじめよう 3Dモーションシミュレーション 橋本洋志・佐々木智典 共著 オーム社

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
70