2012/06/13

Three.jsとPhysijsで3D物理演算

three.jsという3Dのライブラリなどを使って、簡単に3Dの物理演算を行うための記事が本当に簡単だったので、翻訳させてもらいました。

著者のJerome Etienneさんはブログ「Learning Three.js」でさらにThree.jsやphysijs他色々な記事を公開されてます。記事で使用するThree.jsのエクステンションtQueryの開発者でもあります。

元の記事: 3D Physics With Three.js and Physijs


これは物理演算に関するさらに別の記事になる。この記事は、physijsというChandler Prall氏によるライブラリについての記事だ。このphysijsでは、ammo.jsthree.js を簡単に連携させることができる。以下で、それを使ったサンプルのコードを見てみることにする。3Dグラフィックスにおいて、物理演算はいつも重要なものだ。物理演算をすると、空間がよりリアルなものになる。ただ何かのオブジェクトをもってくるだけで、素敵に、リアルに動いてくれるだろう。なので一つ一つの動きを調整するなんてことに時間を費やす必要が少なくなる。とてもズボラな筆者には、ここは大事なところなんだ。ちなみに、screencast は、デモ(今回の記事で書くもの)のほか、ammo.jsを使って何ができるかがわかる様々なサンプルを公開しているよ。

物理演算エンジンについて

前回までに、three.js と物理演算についてみてきた。marblesoccerのミニゲームにmicrophysics.jsを使うというものだった。microphysics.jsは、3D物理演算を実装している非常に小さなライブラリだ。それについては、“Lets Make a 3D Game: microphysics.js”“Lets Make a 3D Game: microphysics.js, Even Easier”の記事でほんのちょっとふれたとおりである。microphysics.jsは、たった500行のライブラリなんだよ!ただ残念なことに、このきわめて少ない行数はいくつかの制限を生んでしまっている。

今日は、Chandler Prall氏のphysijsを使う予定だ。これは非常に素敵なライブラリで、three.jsとともにammo.jsを使うのを簡単なことにしてくれる。ammo.jsは、Bullet物理演算エンジンをEmscripten を使ってJavaScriptへ直接移植したもので、そのソースコードは自動でJavaScriptに変換されているため、機能的にはオリジナルのBulletとまったく同じものなのだ。

bulletは、3Dの世界ではよくしられた本格的な物理演算エンジンだ。ドキュメントをみるとわかるのだが、できることは多い。そしてammo.jsは、高機能な3D物理演算エンジンに期待される機能をすべて実装している。@ccliffe氏ことCharles J.Cliffe氏は、ammo.jsを使ったいくつかのデモをみせてくれている。一つは、heighfieldだ。あとstunt trackを見てほしい!なお、この二つは@ccliffe氏のライブラリであるcubicvrを使っている。

physijsはパフォーマンスを考慮して書かれている。ammo.jswebworkerの内部で実行されるので、だいたいの場合はマルチコアCPUを生かせるだろう。つまり二倍以上のCPU処理能力を作成するjavascriptにもたせることができるということなんだ。3Dの座標のデータは使用できるようになると、所有権の譲渡ができるオブジェクト(transferable objects)に交換される。「所有権の譲渡ができるオブジェクト」とは、メインスレッドとwebworkerの間で、データをコピーすることなくデータを移すことができるオブジェクトの特別な型のことだ。ということは、データがどんなに巨大であっても、待ち時間は非常に少なくなるだろう。

はじめよう

今日紹介するコードは、physijsに付属している衝突デモのサンプルをコピーしたものになる。あるプログラムを、同じ方法で実行できると確かにするために再び書き直してみるのは、役に立つこともある:) お試しあれ。というわけで、オブジェクトが地面に落ち、そこでわずかに反発するようにした。それじゃ、やってみよう。

まず典型的なところからさわろう。tQuery.Worldを作成すると、scene(空間)、レンダラ、カメラとカメラをコントロールするものといったオブジェクトがすぐに使えるようになる。これらには、適当な初期値が最初から設定されている。.boilerplate()とすると、“three.jsのひな型(boilerplate)”が追加される。これは去年記事にした。カメラコントロール(cameraControls)は使えないようにする。そして、レンダラの描画ループを動かすため.start()を実行する。

var world   = tQuery.createWorld().boilerplate({cameraControls: false}).start();

さて、sceneの中央から少し離れたところにカメラをセットする。そうすると、さらに大きなオブジェクトをおいてもちゃんと見られるようになった。

world.tCamera().position.set( 70, 40, 70 );
world.tCamera().lookAt( world.tScene().position );

さて、影付け処理が必要であるとレンダラに伝える必要がある。それは以下のシンプルな何行かのコードで実現できる。詳しくは“Casting Shadows”という記事で見たとおりだ。

world.tRenderer().shadowMapEnabled  = true;
world.tRenderer().shadowMapSoft     = true;
world.tRenderer().setClearColorHex( 0xffffff, 1 );

この空間に物理演算を適用することができるようになった。ここは重要なところだ。たった今から、この空間にある物理演算をするよう設定したオブジェクトは、リアルな物理法則にしたがって動くことになる。

world.enablePhysics();

ライトをつけよう

次はライトの設定をしよう。話を簡単にするために、一方向ライトのみを使用しよう。まず好みにあわせて配置位置と色を変える。感覚をつかむためにパラメーターを色々いじってみるといい。では、影についての設定を調整する。場合によっては難しいかもしれない。詳細は、"Casting Shadow"という記事で書いてあるので、それを見てもらってもいい。シャドウカメラ(訳注:カメラのように視野角などで設定できる影付けの方法がある)を可視化するときに便利だ。それには.shadowCameraVisible(true)を使う。

tQuery.createDirectionalLight().addTo(world)
    .position(20, 40, -15).color(0xffffff)
    .castShadow(true).shadowMap(512*2,512*2)
    .shadowCamera(60, -60, 60, -60, 20, 200)
    .shadowDarkness(0.7).shadowBias(.002)

地面を作ろう

まず地面にはるテクスチャを作ろう。岩の感じをだすためにrocks.jpgを使おう。面の上でテクスチャを繰り返すには.RepeatWrappingを使う。それから、適切な大きさにしよう。

var texture = THREE.ImageUtils.loadTexture( "images/rocks.jpg" );
texture.wrapS   = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 3, 3 );

まあ実際には、地面は広くて平坦な立方体だ。昔の人が信じていた地球平面説とちょっと似ている:) それはいまや普通のことだね。さてここまでで、配置位置、材質、影について設定できた。

var ground  = tQuery.createCube(100, 1, 100).addTo(world)
    .position(0, -10, 0)
    .setLambertMaterial().map(texture).back()
    .receiveShadow(true)

さて、地面の物理演算について設定をしないといけない。これは.enablePhysics()で設定できる。引数massで、この物理空間でのこのオブジェクトの重さを定義している。ちなみにデフォルトでは、オブジェクトの大きさから自動的に計算される。ここでmassを0にしているのは、特別な意味があり、「常に重さは無限大であり、動くこともない(静的オブジェクトである)」ことを意味している。

ground.enablePhysics({
    mass    : 0
});

3D空間でオブジェクトを生成する

この空間でオブジェクトを生成するのはいつも、デリケートな問題だ。しかしまず、生成するオブジェクトのテクスチャを読み込んでしまおう。これはオブジェクトを生成するループの外で読み込むので、データが一度だけメモリに読み込まれ、残りは再利用される。よく知られているように、すべてのオブジェクトに対し、データは一度だけGPUへ送られる。パフォーマンス性を考えると、ここは重要なポイントだ。去年、"Performance: Caching Material"という記事でこれに関する話題をちょうどやった。

var cTexture    = THREE.ImageUtils.loadTexture( "images/plywood.jpg" );

さて、オブジェクトを生成する関数としてspawnObject()を宣言した。立方体を作り、配置や回転している角度・材質を設定した。いい感じだよ。あっ、シャドウマッピングが使えるように.castShadow()を使うのを忘れないようにしよう。

var spawnObject = function(){
    var object  = tQuery.createCube(4,4,4).addTo(world)
        .rotation(Math.random()*Math.PI*2, Math.random()*Math.PI*2, Math.random()*Math.PI*2)
        .position(Math.random()*15-7.5, 25, Math.random()*15-7.5)
        .setLambertMaterial().map(cTexture).back()
        .castShadow(true)

オブジェクトに対し物理演算を適用してみる。たった今から、この空間はすべての動きをコントロールできるようになる。独自に設定したパラメーターを使うと、それは物理の法則に反するだろう。以下のfriction(摩擦)は、二つのオブジェクトがお互いにこすれあうときにおこる反発の力だ。次のrestitution(反発)は、オブジェクトがどう跳ね返るかのパラメーター。学術的な定義は、本で見てね:)

object.enablePhysics({
        friction    : 0.4,
        restitution : 0.6
    });

さて、次は「衝突」のイベントをいじってみようと思う。Physijsはあるオブジェクトが別のオブジェクトに衝突したときに教えてくれる。それにはただ.addEventListener()を「衝突」のときに使えばいい。ここでは、何かに衝突した回数によってオブジェクトの色を変えてみることにする。

var nCollisions = 0;
    object.physics().addEventListener('collision', function(){
        var colliColors = [0xcc8855, 0xbb9955, 0xaaaa55, 0x99bb55, 0x88cc55, 0x77dd55];
        if( ++nCollisions < colliColors.length ){
            var color   = colliColors[nCollisions];
            object.get(0).material.color.setHex( color );
        }
    })
}

さて、一秒毎にオブジェクトを生成するのには、シンプルにsetInterval()を使うことにしよう。

setInterval(spawnObject, 1000);

これで終わり!リアルな3D物理演算をすることができた!やあ、悪くないね:)

まとめ

そんなわけで、今はphysijsがあるので、three.jstQueryを使えば、本格的でリアルな物理演算をすることができる。それは空間をよりリアルにするのには単純な方法だ。使ってみるのもいいだろう。また何かこの話題について書くと思う。

どうもありがとう。それじゃお楽しみください。:)


誤訳かな?と思われたら@ikaflower3ikaflowerあっとcrna.sakura.ne.jpまで教えていただけると大変嬉しいです。

three.jsのサンプルを求めてこられる方が多いので追記

three.jsのサンプルはこちらもオススメです。

http://yomotsu.github.com/threejs-examples/