Hatena::Groupjavascript

JavaScriptで遊ぶよ

 | 

2011-12-13

ammo.jsとbullet.jsを使ってみた(ammo.js編)

03:53

JavaScript Advent Calendar 2011 WebGL駅伝14日目です。

前に紹介だけしたbullet.jsとammo.jsを試してみたので使い心地を書いてみます。


まずこんな感じでThree.jsを使ったコードがあるとします。まだ動きません。一回描画するだけです。

f:id:edvakf:20111214025452p:image:w500:right

window.addEventListener("load", function(){
  var width = 800;
  var height = 600;

  var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
  camera.position.x = -200;
  camera.position.y = 200;
  camera.position.z = 1000;
  camera.lookAt(new THREE.Vector3(0, 0, 0));

  var scene = new THREE.Scene();

  var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
  directionalLight.position.z = 3;
  scene.add( directionalLight );

  var geometry = new THREE.CubeGeometry(400, 400, 400);
  var material = new THREE.MeshLambertMaterial({color: 0x660000});
  var ground = new THREE.Mesh(geometry, material);
  scene.add(ground);
  ground.position.y -= 250;

  for (var i = 0; i < 20; i++) {
    var geometry = new THREE.SphereGeometry(20);
    var material = new THREE.MeshLambertMaterial({color: 0x666666});
    var ball = new THREE.Mesh(geometry, material);
    ball.position.y += i * 40;
    scene.add(ball);
  }

  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  document.body.appendChild(renderer.domElement);

  renderer.render(scene, camera);

}, false);

これを動かしてみたいと思います。

今日はammo.jsだけ。明日bullet.jsのほうをやりたいと思います。


剛体を作る

基本的にはThree.jsのオブジェクト一つにつきbulletのオブジェクト(剛体)を一つ作ることになるので、面倒なので両方を束ねるオブジェクトを作ります。

まずはボールのオブジェクトから。

function Ball(x, y, z, r, m) {
  this.x = x;
  this.y = y;
  this.z = z;
  this.r = r;
  this.bulletObj = null;
  this.threeObj = null;
  this._trans = new Ammo.btTransform();
  this.initThreeObj();
  this.initBulletObj(m);
}

Ball.prototype.destructor = function() {
  Ammo.destroy(this.bulletObj);
  Ammo.destroy(this._trans);
};

こんな感じです。rは半径、mは質量です。destructorってなんで必要なの?ってのはあとで説明します。

initThreeObjはほぼ上で書いたコードのコピペです。

Ball.prototype.initThreeObj = function() {
  var geometry = new THREE.SphereGeometry(this.r);
  var material = new THREE.MeshLambertMaterial({color: 0x666666});
  var ball = new THREE.Mesh(geometry, material);
  ball.position.x = this.x;
  ball.position.y = this.y;
  ball.position.z = this.z;

  this.threeObj = ball;
};

こっからが本番。bulletのオブジェクトを作ります。

Ball.prototype.initBulletObj = function(m) {
  var startTransform = new Ammo.btTransform();
  startTransform.setIdentity();
  var origin = startTransform.getOrigin();
  origin.setX(this.x);
  origin.setY(this.y);
  origin.setZ(this.z);

  var shape = new Ammo.btSphereShape(this.r);
  var localInertia = new Ammo.btVector3(0, 0, 0);
  shape.calculateLocalInertia(m, localInertia);

  var motionState = new Ammo.btDefaultMotionState(startTransform);
  var rbInfo = new Ammo.btRigidBodyConstructionInfo(m, motionState, shape, localInertia);
  rbInfo.set_m_restitution(1);
  var body = new Ammo.btRigidBody(rbInfo);

  Ammo.destroy(startTransform);
  //Ammo.destroy(shape);
  Ammo.destroy(localInertia);
  //Ammo.destroy(motionState);
  Ammo.destroy(rbInfo);

  this.bulletObj = body;
};

めんどくさいですね。startTransformというのは初期位置を表します。bulletでは剛体の座標から世界の座標への変換(ベクトルとクォータニオン)という形で剛体の位置やら回転を表します。btSphereShapeで形を作って、それを元にbtRigidBodyConstructionInfoで剛体を生成するための情報を作って、btRigidBodyでようやく剛体を作ります。

Ammo.destoryのことはあとで説明します。

で、ボールを移動させた後にbulletのオブジェクトから移動後の位置や回転を読んでThree.jsのオブジェクトを動かしてやります。

Ball.prototype.move = function() {
  this.bulletObj.getMotionState().getWorldTransform(this._trans);
  var origin = this._trans.getOrigin();
  this.x = this.threeObj.position.x = origin.x();
  this.y = this.threeObj.position.y = origin.y();
  this.z = this.threeObj.position.z = origin.z();
  var quaternion = this._trans.getRotation();
  var x = quaternion.x();
  var y = quaternion.y();
  var z = quaternion.z();
  var w = quaternion.w();
  this.threeObj.rotation.x = Math.atan2(2*(x*y + w*z), w*w + x*x - y*y - z*z); // roll
  this.threeObj.rotation.y = Math.atan2(2*(y*z + w*x), w*w - x*x - y*y + z*z); // pitch
  this.threeObj.rotation.z = Math.asin(-2*(x*z - w*y)); // yaw
};

地面のほうはまあほぼ同じなのでいいと思います。地面は質量にゼロを与えることで「動かない」(質量無限大の)オブジェクトが作れます。それから、Three.jsのオブジェクトを作るときに1辺が400の箱をこうやって作ったのですが、

  var geometry = new THREE.CubeGeometry(400, 400, 400);

bulletの引数は1辺の半分の長さになります。

  var tmpVec = new Ammo.btVector3(s / 2, s / 2, s / 2);
  var shape = new Ammo.btBoxShape(tmpVec);
  Ammo.destroy(tmpVec);

物理演算の世界を作る

剛体を作るのも面倒でしたが、世界を作るのも面倒です。

function initPhysicsWorld() {
  var gravity = new Ammo.btVector3(0, -200, 0);

  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(gravity);

  return dynamicsWorld;
}

重力はいいとして、他のやつはよくわかりません。どっかのデモコードからコピペしました。こういう作法みたいです。


動かす

準備完了しました。あとは動かすだけです。初期化の部分は一番上のコードとほとんど同じです。

window.addEventListener("load", function(){
  var width = 800;
  var height = 600;
  var deltaT = 30;

  var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
  camera.position.x = -200;
  camera.position.y = 200;
  camera.position.z = 1000;
  camera.lookAt(new THREE.Vector3(0, 0, 0));

  var scene = new THREE.Scene();

  var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
  directionalLight.position.z = 3;
  scene.add(directionalLight);

  var dynamicsWorld = initPhysicsWorld();

  var ground = new Box(0, -250, 0, 400, 0);
  scene.add(ground.threeObj);
  dynamicsWorld.addRigidBody(ground.bulletObj);

  var numballs = 0;
  var balls = [];
  for (var i = 0; i < 20; i++) {
    var ball = new Ball(Math.random(), i * 40, Math.random(), 20, 10);
    scene.add(ball.threeObj);
    dynamicsWorld.addRigidBody(ball.bulletObj);
    balls.push(ball);
    numballs++;
  }

  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  document.body.appendChild(renderer.domElement);

  var n = 0;
  function rendering() {
    dynamicsWorld.stepSimulation(deltaT / 1000);

    for (var i = numballs; i--; ) {
      var ball = balls[i];
      ball.move();

      if (ball.y < -1000) {
        scene.remove(ball.threeObj);
        dynamicsWorld.removeRigidBody(ball.bulletObj);
        ball.destructor();
        balls.splice(i, 1);
        numballs--;
      }
    }

    if (numballs < 20) {
      var ball = new Ball(Math.random() * 10, Math.random() * 20 * 40, Math.random() * 10, 20, 10);
      scene.add(ball.threeObj);
      dynamicsWorld.addRigidBody(ball.bulletObj);
      balls.push(ball);
      numballs++;
    }

    renderer.render(scene, camera);
    setTimeout(rendering, deltaT);
  }

  rendering();

}, false);

rendering関数の最初にあるdynamicsWorld.stepSimulationが物理演算をちょこっと進めるメソッドです。単位は秒なので、1000で割ってます。

その次は、ボールごとにball.moveを呼んで、もしy座標が-1000より小さくなったら取り除いています。さらに、ボールの数が20個を下回ったら順次新しいボールを足しています。


まとめ

デモ↓Chromeで開くと軽くブラクラになります。Firefoxで開いて下さい。

結論から言うと、ammo.jsはもうちょっとすれば良くなりそうという感じです。最大の難点が、ChromeのJSエンジンと相性が悪いので、ammo.jsをパースするのに10秒もかかってしまうということです。Firefoxなら一瞬です。あとChromeのGCが数百msに一回走るため、そこで動きがカク、カク、となってしまいます。(そういえばChromiumに新しいGCが入ったとかどうとか…期待!)

f:id:edvakf:20111210160808p:image:w800

↑Three.jsとammo.jsを比べてみてください。↓内訳。何やらいっぱいGCしています。

f:id:edvakf:20111210162239p:image:w700


それからammo.jsのオブジェクトは必ずnewで作らなければいけません。そしてnewしたものは手動でdestroyしなければいけません。

なんだC++と同じじゃん、と思う無かれ、↓こういうふうにはできません。

  var shape = new Ammo.btBoxShape(Ammo.btVector3(s / 2, s / 2, s / 2));

↓こうしないとメモリリークします。(その前にbtVector3new無しでは呼べません)

  var tmpVec = new Ammo.btVector3(s / 2, s / 2, s / 2);
  var shape = new Ammo.btBoxShape(tmpVec);
  Ammo.destroy(tmpVec);

オブジェクトは全部ヒープへ。スタックにはポインタしか積めないプログラミングが味わえます。

注意して全部解放しようと思ったのですが、↓この2つだけはエラーが出ました。どうしてなんでしょうか。

  Ammo.destroy(shape);
  Ammo.destroy(motionState);

それで、上のデモを走らせ続けると案の定メモリを食い続けます。まあメモリリークと言ってもタブを閉じると解放される類のものですけど。このデモみたいにオブジェクトを追加し続けるようなものには向かないでしょう。

最後に、Readmeにも書いてありますが、ammo.jsは高速化のために全部のオブジェクトをグローバルに宣言します。クロージャとか使わないで。これだけで50%も速くなるそうです。今のところAmmowindowと同じです。これを付けずに使うこともできますが、将来はもしかしたらクロージャにするかもしれないのでつけたほうがいいそうです。もしくはworkerにしたほうがいいでしょう。

以上です。明日はbullet.jsをやります。


余談ですがこれのデモのために初めてThree.jsを使いました。簡単すぎて泣きそうでした。

 |