• このエントリーをはてなブックマークに追加

今なら、継続入会で月額会員費が1ヶ月分無料!

Three.jsに挫折した人のための、ブラウザでグリグリ動く3Dゲームを秒速で作る! gl.enchant.jsの使い方
閉じる
閉じる

新しい記事を投稿しました。シェアして読者に伝えましょう

×

Three.jsに挫折した人のための、ブラウザでグリグリ動く3Dゲームを秒速で作る! gl.enchant.jsの使い方

2014-06-29 10:59
  • 1
新しいMacOS、そしてiOSで、ついにWebGLが正式サポートされました。
これは地味に夢が広がりんぐです!

WebGLといえばThree.jsが有名ですが、Three.jsはガチの3Dライブラリのため、
ゲームをつくるにはかなり沢山のことを勉強しなければなりません。

そこで世界で最も簡単に(当社比)3Dゲームが作れるgl.enchant.jsの使い方を紹介したいと思います



■WebGLってなんだ?
WebGLはChronosグループが制定しているOpenGLのサブセットのひとつです。
その名の通りWebで動作するOpenGLです。WebGLに対応したライブラリはthree.jsが有名です。
three.jsも素晴らしいライブラリなのですが、機能が高度なので使いこなすのに時間が掛かります。
その点、gl.enchant.jsは3Dゲーム開発に特化しているので非常に簡単に3Dプログラミングをすることができます。
ただし、注意しなくてはならないのは現状、WebGLに対応しているブラウザはFirefoxとChrome、そしてSafariだけだという点です。
InternetExplorerは残念ながら対応していません。
また、Safariであっても、「開発モード」にして「開発」のメニューから「WebGLを有効にする」を選ばなくてはなりません。
また、ローカルでテストする場合はさらに「ローカルファイルの制限を無効にする」のチェックも必要です。
■enchant.jsからgl.enchant.jsへのステップアップ
ではさっそく、gl.enchant.jsについて見てみましょう。
enchant.jsでよく使うクラスはgl.enchant.jsではどう置き換えられているか以下に示します。
enchant.js     gl.enchant.js
Sprite        → Sprite3D     キャラクターを操るスプライトクラス
Scene        → Scene3D      シーンクラス
                      Mesh           3Dの形状クラス
                      Texture        3Dの質感や素材を決めるクラス
                      Camera       3D空間内を見て回るカメラのクラス
                      Light            3D空間内を照らす光源のクラス
基本はこれだけです。
やはり3Dなだけあってクラスの数も増加しています。
けれども、ひとつずつ見ていけばそんなに難しくありません。
次節から順番にみていきましょう。
 
■gl.enchant.jsを使うための準備
gl.enchant.jsを使うにはいくつかのファイルが必要です。
enchant.jsのパッケージに含まれる以下のファイルを同じディレクトリにコピーしましょう。
 
plugins/libs/gl-matrix-min.js
plugins/gl.enchant.js
plugins/primitive.gl.enchant.js
 
 
それから、index.htmlを以下のようにします。
 
<html>
<head>
<title>noname</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="gl-matrix-min.js"></script>
<script type="text/javascript" src="enchant.js"></script>
<script type="text/javascript" src="gl.enchant.js"></script>
<script type="text/javascript" src="primitive.gl.enchant.js"></script>
<script type="text/javascript" src="main.js"></script><style type="text/css">
    body {
        margin: 0;
    }
</style>
</head>
<body>
</body>
</html>


これで準備OK。プログラムの本体はいつものようにmain.jsに書きます。
まずはシンプルに立方体(cube)を表示するところからやってみましょう。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.onload = function() {
        scene = new Scene3D(); //3Dのシーンを作る
        obj = new Cube(); //立方体を作る
        scene.addChild(obj); //3Dシーンに立方体を追加
    };
    game.start();
};
これだけです。
なんと簡単なのでしょう。
実行すると非常にシンプルな画面が表示されます。

d941b8b967fba461af48bf6e7b4aaca9457c1730
Scene3Dを作ると背景は自動的に黒になります。
しかし立方体が表示されているはずなのに、ただの四角形に見えてしまいます。
これは立方体を真正面から見ているせいです。
ちょっと横にずらしてみましょう。
横にずらすには二次元のスプライトと同じようにx、y、そしてzのどれかを変更するだけでOKです。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cube();
          obj.x=-1; // x座標を-1に
        scene.addChild(obj);
    };
    game.start();
};

f3a25c048411b33d93d87ddec31bb8dd3be2b190
ちょっと動かしただけで立方体の側面が見えてきましたね。
3Dの世界では2Dの世界と違って1の違いが凄く大きな違いになります。
だからほんの少しだけ動かしたい場合などには小数を使います。
せっかくですからZ座標もいじってみましょう。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cube();
          obj.x=-1;
          obj.z= 4;
        scene.addChild(obj);
    };
    game.start();
};

274da8f25f4dc5c5c4b556ec61c62ca281696408
だいぶ近づきましたね。
デフォルトではZ座標は大きいほどカメラに近づきます。
次に、立方体を回転させてみましょう。
gl.enchant.jsのスプライトはrotatePitch、rotateYawrotateRollで回転させることができます。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cube();
          obj.rotateYaw(0.5);  //立方体を回転
        scene.addChild(obj);
    };
    game.start();
};

rotatePitch、rotateYawrotateRoll、はそれぞれ、X軸、Y軸、Z軸の回転を行います。
たとえばrotatePitchで回転すると下図のようになります
 


12f272c0b220e1680cb312206f2a2c46b2ff9468
rotateRollは下図になります
 

 
三次元空間は立体ですから回転させる軸も三つ用意されているわけです。
 
Sprite3Dでは、この三つの軸で回転させることができるようになっています。
Sprite3D.rotateYawは、ヨー角、Sprite3D.rotateRollはロール角、Sprite3D.rotatePitchはピッチ角をそれぞれ指定します。
こうした角度の呼び方は、航空力学から来ています。
aaad0f6626aced4f63d8f25ef78a51e182178de2

 
これと別に、好きな軸で回転させるためにクォータニオン(Quaternion)を与えることもできます。
rotatePitch、rotateYawrotateRollにはラジアン角を与えます。
ラジアン角とは、一周を2πとする角度で、90度なら0.5πです。
JavaScriptにはMath.PIが用意されていて、円周率はMath.PIで表せます。
 
たとえば90度回転させたかったら Math.PI*0.5となります。
我々が普通に使っている360度を一周とする角度dからラジアン角rへの変換は以下の式で行う事が出来ます。
 
 
r = d*Math.PI/180;
これは3Dプログラミングの世界では「お約束」なので覚えて下さい。
ラジアン角は最初直感的に解りにくいのですが、コツさえ掴めば簡単です。
■アニメーションさせてみよう
enchant.jsの他のエンティティと同じようにアニメーションさせることができます。
まずはenterframeイベントでやってみましょう。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cube();

          obj.addEventListener('enterframe',function(){
               this.rotateYaw(0.1); //0.1ラジアンづつYaw回転
               this.rotatePitch(0.1); //0.1ラジアンづつPitch回転
          });

        scene.addChild(obj);
    };
    game.start();
};
■色を変えてみよう。
色がグレーでは面白くありません。
色を変えるのはちょっとしたコツがあります。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cube();
         
          //ここで色を設定する
          obj.mesh.texture.diffuse      =[1.0,  0.0,  0.0,  1.0];  //ディフューズ
          obj.mesh.texture.specular   =[1.0,  1.0,  1.0,  1.0];//スペキュラー
          obj.mesh.texture.ambient    =[0.1,  0.1,  0.1,  1.0];//アンビエント
          obj.mesh.texture.shininess=1;
          obj.mesh.texture.emmission=[0.0,  0.0,  0.0,  1.0];//自発光
         
          obj.addEventListener('enterframe',function(){
               this.rotateYaw(0.05); //0.1ラジアンづつYaw回転
               this.rotatePitch(0.1); //0.1ラジアンづつPitch回転
          });
        scene.addChild(obj);
    };
    game.start();
};

67035cfd00690fdc32c71a5eddc5eeec6e291570
3Dの物体の色を決定するには一般的に三つの要素があります。
一つは拡散光(diffuse)、これは光が物体の表面に当たったときの色です。
もう一つは鏡面反射光(specular)、これは光が物体の表面に当たって、直接視野に入って来る光です。
最後のひとつは環境光(ambient)です。
さらにもうひとつ、自発光する色(emission)も指定することができますが通常ここは真っ黒にします(発光していないため)。
shininessは鏡面反射係数で、スペキュラーの係数として使われます。数が大きいほど鋭い鏡面反射になります。
それぞれの色を変えてもいいのですが、一般的にスペキュラーは白で、いわゆる「色」はディフューズで設定します。
色を指定するには配列を渡していますが、これは0番目の要素から順に、赤、緑、青、αとなっています。αは透明度で、1.0で不透明を意味します。通常は1.0を指定します。
■テクスチャを貼り付けよう
gl.enchant.jsでテクスチャを貼り付けるのは色を細かく指定するよりもずっと簡単です。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.preload('enchant.png');  //テクスチャを読み込む
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cube();
         
          obj.mesh.texture.src = game.assets['enchant.png']; //テクスチャを使う
          obj.addEventListener('enterframe',function(){
               this.rotateYaw(0.05);
               this.rotatePitch(0.1);
          });
        scene.addChild(obj);
    };
    game.start();
};

注意しなくてはならないのは、WebGLで利用可能なテクスチャは2の乗数のサイズで、正方形でなければならないということです。
2の乗数とは、2,4,8,16,32….と進んで行くやつで、一般的には128x128や512x512といったサイズのテクスチャが用いられます。

c1dfd8ff838d983ed5eecaf0202264a444506cd3

色とテクスチャは両方指定することができます。

enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.preload('enchant.png');
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cube();
         
          obj.mesh.texture.src = game.assets['enchant.png'];
          obj.mesh.texture.diffuse = [1.0, 0.0, 0.0, 1.0]; //テクスチャに赤いディフューズを適用
          obj.addEventListener('enterframe',function(){
               this.rotateYaw(0.05);
               this.rotatePitch(0.1);
          });
        scene.addChild(obj);
    };
    game.start();
};
e15253d290b29dcc59600e7b1e39b020f8f78f72
■いろいろな形を出してみよう
enchant.jsには立方体以外にもいろいろな形状が用意されています。
例えば球を出してみましょう。
球を出すにはCubeをSphereに変えるだけです。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.preload('enchant.png');
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Sphere(); //球
         
          obj.mesh.texture.src = game.assets['enchant.png'];
          obj.addEventListener('enterframe',function(){
               this.rotateYaw(0.05);
               this.rotatePitch(0.1);
          });
        scene.addChild(obj);
    };
    game.start();
};
0f0e9d7eb3cc6fc7d7964fb3714afe63e69cbd8f
簡単ですね。
円筒も簡単に出す事が出来ます。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.preload('enchant.png');
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Cylinder(); //円筒
         
          obj.mesh.texture.src = game.assets['images/enchant.png'];
          obj.addEventListener('enterframe',function(){
               this.rotateYaw(0.05);
               this.rotatePitch(0.1);
          });
        scene.addChild(obj);
    };
    game.start();
};


45fca463248d91604ee45495cf78ffbbf6dffea6
変わったところで、板を出してみましょうか。
enchant();
var game;
window.onload = function() {
    game = new Game(640, 640);
    game.preload('enchant.png');
    game.onload = function() {
        var scene = new Scene3D();
          var obj = new Plane(); //板
         
          obj.mesh.texture.src = game.assets['enchant.png'];
          obj.addEventListener('enterframe',function(){
               this.rotateYaw(0.05);
               this.rotatePitch(0.1);
          });
        scene.addChild(obj);
    };
    game.start();
};
b93157bc694db3b9f51a1ab319e722a63ad45903

他にも、XZ平面の板を出すPlaneXZや、XY平面の板を出すPlaneXY、YZ平面の板を出すPlaneYZなどがあります。
■ドラッグで操作してみよう
ここまでが基本です。ではgl.enchant.jsで実際にインタラクティブな3Dプログラムを書いてみましょう。
ためしに球をマウスドラッグで回転させるプログラムを書いてみましょう。
gl.enchant.jsであっても、ふつうにenchant.jsのgame.rootSceneは使えるので、こちらを
使ってマウスの動きを読み取ることにします
enchant();
window.onload = function(){
  game = new Game(320,320);
  game.onload = function(){
       var scene = new Scene3D(); //3Dシーンを初期化
       var ball = new Sphere(); //球を作り出す
       ball.mesh.texture = new Texture("../../images/enchant-sphere.png"); //テクスチャを設定する
       ball.z=1;                        //球のz座標を設定
       game.rootScene.addEventListener('touchstart',function(e){ //rootSceneのtouchstartイベントを設定
             tempEv = e;
       });
       game.rootScene.addEventListener('touchmove',function(e){ //rootSceneのtouchmoveイベントを設定
             ball.rotateYaw( (e.x - tempEv.x)*0.01 ); // ヨー軸まわりの回転を行う
             ball.rotatePitch( (e.y - tempEv.y)*0.01 ); // ピッチ軸まわりの回転を行う
             tempEv = e;
       });
       scene.addChild(ball);     //球をシーンに追加する
  }
  game.start();
};




ここではtempEvという変数を使って、マウスの直前の位置と現在の位置の差分を出してそれを角度に反映させているのがポイントです。
そのままだとあまりにも数値が大きいので0.01を掛けて小さい値に調整しています。
これだけでボールはマウス操作に反応して回転するはずです。
c79c6cc4caebc8282f2e02082049bc01a1e47531


■球をドラッグで移動させてみよう
もちろん移動もおなじようにできます。
さっきのプログラムを改造してみましょう。

enchant();
window.onload = function(){
  game = new Game(320,320);
  game.onload = function(){
       var scene = new Scene3D(); //3Dシーンを初期化
       var ball = new Sphere(); //球を作り出す
       ball.mesh.texture = new Texture("../../images/enchant-sphere.png"); //テクスチャを設定する
       ball.z=1;                        //球のz座標を設定

       game.rootScene.addEventListener('touchstart',function(e){ //rootSceneのtouchstartイベントを設定
             tempEv = e;
       });
       game.rootScene.addEventListener('touchmove',function(e){ //rootSceneのtouchmoveイベントを設定
            ball.x+=(e.x - tempEv.x)*0.1; // X軸の移動を行う
            ball.z+=(e.y - tempEv.y)*0.1; // Z軸の移動を行う
             tempEv = e;
       });
       scene.addChild(ball);     //球をシーンに追加する
  }
  game.start();
};

ちょっと細工して、マウスを縦方向に動かしたらY軸ではなくZ軸方向に移動するようにしてみました。
実際に操作してみると、なるほど画面の奥に三次元的な空間があるなあ、という感じがしてきませんか?

b8c20af048c743135878c9b6df834fad06ef1264

■立方体も表示させてみよう

球だけでなく立方体も表示させることが出来ます。

enchant();
window.onload = function(){
  game = new Game(320,320);
  game.onload = function(){
       var scene = new Scene3D(); //3Dシーンを初期化

       var cube = new Cube(); //立方体を作り出す
       cube.mesh.texture = new Texture("../../images/enchant.png"); //テクスチャを設定する
       cube.z=1;                        //z座標を設定


       game.rootScene.addEventListener('touchstart',function(e){ //rootSceneのtouchstartイベントを設定
             tempEv = e;
       });

       game.rootScene.addEventListener('touchmove',function(e){ //rootSceneのtouchmoveイベントを設定

            cube.rotateYaw((e.x - tempEv.x)*0.1); // X軸の移動を行う
            cube.rotatePitch((e.y - tempEv.y)*0.1); // X軸の移動を行う

             tempEv = e;
       });
       scene.addChild(cube);     //立方体をシーンに追加する
  }
  game.start();
};




■3D空間を動き回ってみよう
さて、実際に本格的な3Dゲームを作るためには、空間を動き回るようなプログラムを書く必要があります。
まず、プレイヤーの操作に応じて空間を動き回るようなキャラクターを作ってみましょう。
enchant();
window.onload = function(){
  game = new Game(320,320);
  game.onload = function(){
       var scene = new Scene3D(); //3Dシーンを初期化

       //3D空間上にボールをランダムに配置
       for(i=0;i<50;i++){
             var ball = new Sphere(); //球を作り出す
             ball.x=Math.random()*50-25;      //球のx座標をランダムに設定
             ball.z=Math.random()*50-25;      //球のz座標をランダムに 設定
             ball.scale(0.2,0.2,0.2);
             scene.addChild(ball);     //球をシーンに追加する
       }

       //プレイヤーキャラクター
       var hero = new Cube();
       hero.z=-5;
       scene.addChild(hero);
       game.rootScene.addEventListener('touchstart',function(e){ //rootSceneのtouchstartイベントを設定
             tempEv = e;
       });

       game.rootScene.addEventListener('touchmove',function(e){ //rootSceneのtouchmoveイベントを設定
            hero.rotateYaw((e.x - tempEv.x)*0.01 ); // 方向転換する
            hero.forward((e.y - tempEv.y)*0.1); // 前進する
             tempEv = e;
       });
  }
  game.start();
};

7b80d40536323209e8fee8ac1c910fd208ba5653
ポイントは、Sprite3D.forwardを使っているところです。
これはスプライトが向いている方向に前進させるための命令です。もちろん、マイナスの値を入れるとバックします。
ここでは、マウスのy軸の動きで前進・後退をできるようになっています。
こうすると、立方対をプレイヤーが操作して、まるでラジコンのように3D空間上を走り回るようになります。
ぐっとゲームっぽくなりましたね。あと一息です。

■カメラを使いこなそう
gl.enchant.jsの世界は、いわばひとつの映画の舞台のようなものです。
Sprite3Dが俳優だとすれば、それを撮影するカメラが必要です。
プログラミングを簡単にするために、デフォルトでカメラが設定されています。
しかし優れたカメラワークこそがゲームを盛り上げるコツです。
さきほどのサンプルではすぐに自機である立方体を見失ってしまいました。
まずはこれを追いかけるようにしてみましょう。

Camera3D.lookAtという関数を使います。
これは特定のスプライトを見続けるための命令です。

          camera = scene.getCamera(); //デフォルトで設定されているカメラを取得
          game.rootScene.addEventListener('enterframe',function(){
                   camera.lookAt(hero); // heroの方を向く
          });  

カメラが常に画面の中心にheroを捕らえつづけるようになりましたね?
このままだとカメラは固定されています。カメラ自身もheroを追跡するようにしてみましょう。
それにはCamera3D.chaseという関数を使います。


          camera = scene.getCamera(); //デフォルトで設定されているカメラを取得
          game.rootScene.addEventListener('enterframe',function(){
                   camera.lookAt(hero);
                   camera.chase(hero,30,30); // heroを追跡する
          });  

このとき、chaseの第二引数はheroとカメラの距離、第三引数はスピードを決めるパラメータです。
ただし、第三引数は数値が小さいほど速く追い掛けます。

ぐっとゲームっぽくなったのが解るでしょうか。


■カメラのもう少し詳しい使い方
ゲームで使うカメラワークの大半はchaseとlookAtで済んでしまいます。
もっと凝ったことをすることもできます。
カメラには、カメラの位置とカメラの向いてる位置、そしてカメラの上方向を示すベクトルと、大きくわけて三つのパラメータがあります。
けれども、たいていはカメラの位置とカメラの向いてる位置、二つを決めるだけでだいたいゲームを作るには充分です。
Camera3Dにはx,y,zというカメラの位置を決めるプロパティと、centerX,centerY,centerZという、カメラの見る向きの中心を意味するプロパティがあります。
まずは見る場所を固定してカメラの位置をマウスドラッグで動かすプログラムを書いてみましょう。
          camera = scene.getCamera(); //デフォルトで設定されているカメラを取得
          game.rootScene.addEventListener('touchstart',function(e){
                   tmpEv = e; // 最初のマウス位置を記録
          });
          game.rootScene.addEventListener('touchmove',function(e){
                   camera.x  +=  (e.x - tmpEv.x) *0.1; //カメラの位置xを動かす
                   camera.y  +=  (e.y - tmpEv.y) *0.1;//カメラの位置yを動かす
                   tmpEv = e;
          });

実際にこのプログラムを実行すると、ある一点を見つめたままカメラが動くのが実感できるでしょう。
次に、逆にカメラの位置は固定したまま見る方向を変えてみましょう。
          camera = scene.getCamera(); //デフォルトで設定されているカメラを取得
          game.rootScene.addEventListener('touchstart',function(e){
                   tmpEv = e; // 最初のマウス位置を記録
          });
          game.rootScene.addEventListener('touchmove',function(e){
                   camera.centerX  +=  (e.x - tmpEv.x) *0.1; //カメラの見る方向の中心xを動かす
                   camera.centerY  +=  (e.y - tmpEv.y) *0.1;//カメラの 見る方向の中心 yを動かす
                   tmpEv = e;
          });

二種類プロパティの違いがなんとなくわかってきましたか?
もちろんzを移動させてもかまいません。
          camera = scene.getCamera(); //デフォルトで設定されているカメラを取得
          game.rootScene.addEventListener('touchstart',function(e){
                   tmpEv = e; // 最初のマウス位置を記録
          });
          game.rootScene.addEventListener('touchmove',function(e){
                   camera.centerX  +=  (e.x - tmpEv.x) *0.1; //カメラの見る方向の中心xを動かす
                   camera.centerZ  +=  (e.y - tmpEv.y) *0.1;//カメラの 見る方向の中心 zを動かす
                   tmpEv = e;
          });

centerZの場合、もともと前を見ているので変化がわかりにくいですね。
では、カメラの位置とカメラの見る方向の中心を同時に動かしたらどうなるでしょうか。



          camera = scene.getCamera(); //デフォルトで設定されているカメラを取得
          game.rootScene.addEventListener('touchstart',function(e){
                   tmpEv = e; // 最初のマウス位置を記録
          });
          game.rootScene.addEventListener('touchmove',function(e){
                   camera.x  +=  (e.x - tmpEv.x) *0.1; //カメラの位置xを動かす
                   camera.z  +=  (e.y - tmpEv.y) *0.1;//カメラの位置zを動かす
                   camera.centerX  +=  (e.x - tmpEv.x) *0.1; //カメラの見る方向の中心xを動かす
                   camera.centerZ  +=  (e.y - tmpEv.y) *0.1;//カメラの 見る方向の中心 zを動かす
                   tmpEv = e;
          });

カメラ全体が動くようになりましたね。
こんなふうにたいていの場合はカメラの位置とカメラの見る方向、この二つでカメラワークを制御します。
■いろいろな基本図形を表示してみよう。
立方体や円柱など、基本的な図形はprimitives.gl.enchant.jsに入っています。
他にはどんなものがあるか試してみましょう。

まずは円柱です。

       for(i=0;i<50;i++){
             var obj = new Cylinder(); //円柱を作り出す
             obj.x=Math.random()*50-25;      //円柱のx座標をランダムに設定
             obj.z=Math.random()*50-25;      //円柱のz座標をランダムに 設定
             obj.scale(0.2,1,0.2);      //円柱の大きさを指定する
             scene.addChild(obj);     //円柱をシーンに追加する
       }

35ac7eac8e6ca785d0980b7d0cbb66df0999168e

好みの円柱を作るためには、Sprite3D.scaleで縦横比を調整するといいでしょう。
変わったところでは平面があります。
まずは平面を出してみましょう。

       for(i=0;i<50;i++){
             var obj = new Plane(); //平面を作り出す
             obj.x=Math.random()*50-25;      //x座標をランダムに設定
             obj.z=Math.random()*50-25;      //z座標をランダムに 設定
             obj.scale(1,1,1);      //大きさを指定する
             scene.addChild(obj);     //シーンに追加する
       }

04699a66e0207d700c7c390cbc87852b1a1a996a

これでxy平面に平行な平面が作られます。
PlaneのかわりにPlaneXYを呼び出しても同じ結果が得られます。
また、床を作る時に便利なxz平面もあります。


       for(i=0;i<50;i++){
             var obj = new PlaneXZ(); //XZ平面を作り出す
             obj.x=Math.random()*50-25;      //x座標をランダムに設定
             obj.y = Math.random()*4-2;       //y座標をランダムに 設定
             obj.z=Math.random()*50-25;      //z座標をランダムに 設定
             obj.scale(1,1,1);      //大きさを指定する
             scene.addChild(obj);     //シーンに追加する
       }

ff0ed85ee4357b6a46ff42c8f65500d1c4e2dd1b

さらに変わったところでは、常にカメラの方を向いている平面を作ってみましょう。
これはビルボード(看板)と呼ばれています。

       for(i=0;i<50;i++){
             var obj = new Billboard (); //ビルボードを作り出す
             obj.mesh.texture = new Texture("../../images/enchant.png"); //ビルボードに画像を設定
             obj.x=Math.random()*50-25;      //x座標をランダムに設定
             obj.z=Math.random()*50-25;      //z座標をランダムに 設定
             obj.scale(1,1,1);      //大きさを指定する
             scene.addChild(obj);     //シーンに追加する
       }

5609f6de0e730557a0d18cdd60ff72ee29bda3ba

ビルボードは簡単なアイテムの表示や、エフェクトなどで多用されます。
エフェクトとしてビルボードを使う場合のために、アニメーションができるビルボード、BillboardAnimationもあります。


■3Dモデルを読み込ませてみよう
立方体や円柱など、基本的なモデルは表示できるようになりました。
ではもう少し複雑なモデルを読み込ませてみましょう。

enchant.jsには、COLLADAフォーマットを読み込む機能がついています。
COLLADAフォーマットとは、OpenGLやWebGLの策定団体であるクロノスグループが策定した汎用的な3Dグラフィックスフォーマットで、多くの3Dグラフィックスソフトが対応を表明しているXML形式の3Dグラフィックスデータです。XML形式なのでJavaScriptでも扱い易いのです。拡張子は.daeとなります。

ただし、COLLADAの書き方にはグラフィックスソフトによって方言があり、enchant.jsが全てのCOLLADAフォーマットに対応しているわけでは残念ながらありません。個別に対応しているのが現状です。現在はオープンソースのグラフィックスソフトウェアであるBlenderで出力したCOLLADAフォーマットを読み込むことが出来ます。

gl.enchant.jsでCOLLADAフォーマットの読み込みをさせるには、collada.gl.enchant.jsをindex.htmlで読み込むようにする必要があります。


実際に読み込むには、enchant.jsの作法に従ってpreloadで読み込みます。
examples/gl/の下にあるcolladaフォルダの中にサンプルがあるので紹介しましょう。

enchant();
window.onload = function(){
    game = new Game(320, 320);
    game.fps = 120;
    game.preload('../../../images/droid.dae'); //.daeファイルを読み込む
    game.onload = function(){
        scene = new Scene3D();
        scene.setDirectionalLight(new DirectionalLight());
        scene.setCamera(new Camera3D());
        var droid = Sprite3D();  //スプライト3Dを作成
        droid.set(game.assets['../../../images/droid.dae']);  //読み込んだ.daeファイルをセットする
        droid.y= -1;
        droid.z=-3;
        scene.addChild(droid);
    };
    game.start();
};
ef0e8b7d069bd2a9e5bba69515c60a2d0a283b59

Colladaフォーマットのデータは、BlenderやLightwave、Shade3Dなどで作ることが出来ます。

こうして作ったデータは立方体や球と同じように扱うことが出来ます。

■オブジェクト同士の当たり判定

3Dの当たり判定はかなり複雑ですが、gl.enchant.jsではとても簡単です。
intersectメソッドを使えば、容易に当たり判定が実装できます

enchant();
window.onload = function(){
  game = new Game(320,320);
  game.onload = function(){
       var scene = new Scene3D(); //3Dシーンを初期化
       var cube = new Cube();    //立方体を作り出す
       cube.z=-10;                        //立方体 のz座標を設定
       scene.addChild(cube);     //立方体 をシーンに追加する

       var ball = new Sphere(); //球を作り出す
       ball.z=0;                        //球のz座標を設定
       ball.scale(0.3,0.3,0.3);    //球をちょっと小さくする
       ball.addEventListener('enterframe',function(){ //ballのtouchendイベントを設定
            this.z--;
            if(this.intersect(cube)){ //あたり判定をする
                       //立方体に当たった!
                       this.z=0;
            }
    });
       scene.addChild(ball);     //球をシーンに追加する
  }
  game.start();
};




■3Dゲームを作ってみよう。

さて、一通りの作り方がわかったところで、ひとつゲームを作ってみましょう。
あんまり複雑なものを作るのも大変なので、ここまでに学んだ知識を生かしたものにしましょうか。
主人公が走りながら画面上に存在する赤い立方体を時間内にできるだけたくさん取るようなものにします。

7e6f03477d0387d9022b0cae375efe4e7039e268

今回はクラスを使ってみたいと思います。
まずはプレイヤーのクラスHeroを作りましょう。

//プレイヤークラスを定義する
Hero = Class.create(Sprite3D,{
     initialize:function(x,y,z){
          Sprite3D.call(this);
          this.set(game.assets['droid.dae']);
          this.noToon=false;
          this.x = x;
          this.y = y;
          this.z = z;
          scene.addChild(this);         
     }
});

Heroは今回は立方体ではなくドロイドのモデルにします。
Heroの制御はgame.rootSceneのイベントリスナで行うことにします。
多少行儀が悪いのですが、画面全体からイベントを拾うためです。

              
              hero = new Hero(0,-0.5,0);  //heroを作る
              hero.v = 1; //初期の速度を指定
             
              game.rootScene.addEventListener('touchstart',function(e){
                   tmpEv = e;
              });
              game.rootScene.addEventListener('touchmove',function(e){
                   hero.rotateYaw( -(e.x - tmpEv.x)*0.02);      //フリックのX移動量に応じて方向転換
                   hero.v += -(e.y - tmpEv.y)*0.1;                  //フリックのY移動量に応じて速度を増す
                   if(hero.v>5)hero.v=5;
                   if(hero.v<0.1)hero.v=0.1;
                  tmpEv = e;
              });
             
              camera = scene.getCamera();
              camera.y=1;
              game.rootScene.addEventListener('enterframe',function(e){
                   hero.forward(hero.v*0.07);     //heroを前進させる
                  
                   //キーボード操作にも対応
                   if(game.input.left){     //左キー
                        hero.rotateYaw( 0.05);
                   }
                   if(game.input.right){   //右キー
                        hero.rotateYaw( -0.05);
                   }
                   if(game.input.up){ //上キー
                        hero.v += 1;
                        if(hero.v>5)hero.v=5;
                   }
                   if(game.input.down){ //下キー
                        hero.v*=0.8;
                   }
                  
                   //フィールドの端に達したら方向転換
                   if(hero.x<-30)hero.rotateYaw(Math.PI*0.25);
                   if(hero.x> 30)hero.rotateYaw(Math.PI*0.25);
                   if(hero.z<-30)hero.rotateYaw(Math.PI*0.25);
                   if(hero.z> 30)hero.rotateYaw(Math.PI*0.52);
                  
                   camera.chase(hero,-20,10);      //heroをカメラが追いかける
                   camera.lookAt(hero);                //heroをカメラが見る
                   camera.y=4;
                  
              });
              


次に、画面上に障害物を配置してみましょう。
障害物はとりあえずボールにしたいと思います。

//ボールクラスを定義する
Ball = Class.create(Sphere,{
     initialize:function(x,y,z){
          Sphere.call(this);
          this.x = x;
          this.y = y;
          this.z = z;
          this.scale(0.5,0.5,0.5);
          this.mesh.setBaseColor([0.2, 1.0, 0.2,1.0]);   //緑色に指定
          scene.addChild(this);
          this.noToon=false;
     },
     onenterframe:function(){  //enterframeイベント
          if(this.intersect(hero)){ //もしheroに当たったら
               hero.forward(-hero.v*0.5);  // heroをバックさせる
               hero.v=0;
          }
     }
});


enchant.jsでクラスを定義する際、onenterframeのように、onのあとにイベント名を書いておけば自動的にイベントリスナーとして登録されるようになっています。

わざわざAddEventListenerと書かなくてもすっきりと記述できてなかなか便利です。

onenterframeのなかで、フレームごとに当たり判定をしています。
この障害物のように、当たり判定の対象がheroひとつなら、わざわざ別ループをまわすよりもオブジェクトごとのenterframeで処理したほうがラクです。

Sprit3D.forwardにマイナスの値を入れると、バックします。
また、hero.vに0を代入して速度を殺しています。


実際にフィールドにボールを配置するには、game.onloadに以下の処理を追加します。

              for(i=0;i<60;i++){
                   var ball = new Ball(Math.random()*60-30, 0,Math.random()*60-30);
               }

ボールだけでは寂しいので、ステージを追加しましょう。
今回は60x60のフィールドを作っています。

              stage = new PlaneXZ();
              stage.y=-0.5;
              stage.mesh.texture = new Texture("tile.png");
              stage.scale(60,1,60);
              scene.addChild(stage);

ここではtile.pngというテクスチャを用意しました。

bb00c922c53512a88c4803bc1babd197a54f28ad

こういうチェック模様のテクスチャにすると、空間の広がりやスピード感がわかりやすくなります。

次に、アイテムを作りましょう。
アイテムもクラスを作った方がなにかと便利です。

b5a85b8e93ebd423dfb89c9837efa7f62f4628c0

ここではゲームの伝統的なアイテムであるフルーツを出すことにします。
フルーツの画像はenchant.jsに付属のicon0.gifを使いますが、このままだとテクスチャにできないので、下図のように256x256ドットにサイズを拡張したicon0.pngを使います。


score=0;

//フルーツクラスを定義する
Fruits = Class.create(BillboardAnimation,{ //ビルボードを使用する
     initialize:function(x,y,z){
          BillboardAnimation.call(this,16,0.5); //16x16で大きさが0.5のビルボードを作成
          this.mesh.texture = new Texture('icon0.png');
          this.mesh.texture.specular=[0,0,0,0];             //輝かないようにする
          this.kind  = Math.floor(Math.random()*4)+1;    //種類をランダムに選ぶ
          this.frame=15 + this.kind; //フレームを指定
          this.x = x;
          this.y = y;
          this.z = z;
          this.mesh.setBaseColor([1.0, 1.0, 1.0,1.0]);
          this.mesh.texture.emmision=[1.0,1.0,1.0,1.0];
          this.mesh.texture.ambient=[1.0,1.0,1.0,1.0];
          scene.addChild(this);
     },
     onenterframe:function(){ //enterframeイベント
          if(this.intersect(hero)){ //heroに当たった?
                    score+=1; //スコアを加算
                    scene.removeChild(this); //自分を消す
               }
          }
     }
});

これでアイテムが出るようになりました。
onenterframeで障害物と同じようにintersectでheroとの当たり判定をとり、当たった場合はスコアを加算するようにしています。

BillboardAnimationは、2DのSpriteと同じように使うことが出来ます。
frameに欲しいフレーム番号を与えると、Spriteと同じように左上から順番に割り当てられたフレーム番号のスプライトが表示されます。

これは当然ながら、アニメーションに便利です。
ただし、Spriteとちがって大きさは正方形に限定されているので縦横比の違うものはビルボードとしては使えません。

さて、ここまででゲームの基本要素はあらかた揃いました。
しかし肝心なものがありません。

そう、スコアです。

そこでスコア表示を追加します。
game.onloadの後ろの方に以下のような部分を追加してください。

        score=0;
        scoreBoard = new MutableText(170,8, game.width, "SCORE:0");
        scoreBoard.addEventListener('enterframe',function(){
             this.setText("SCORE:"+score);
        });
        game.rootScene.addChild(scoreBoard);


ここでは、nakamura001さんが開発したプラグインdraw.text.jsを使っています。
draw.text.jsは手軽にゲームフォントっぽい画像のフォントを表示することが出来ます。

ここではスコアを表示するためにScoreBoardという変数を使っています。
これはMutableTextです。

MutableTextクラスは、draw.text.jsで定義されているクラスで、座標、画面幅、そして初期表示する文字列(英数字のみ)の順番で指定します。

これをenterframeで毎フレーム更新すると、簡単にスコアの表示更新ができます。

最後にこれをgame.rootSceneにaddChildしています。
gl.enchant.jsを使っている最中でも、game.rootsSceneは有効です。
これを利用して2Dと3Dを組み合わせた表示にすることができます。

最後に、タイム表示も入れましょう。

        timeBoard = new MutableText(16,8, game.width, "0.0sec");
        time = 60*30;
        timeBoard.addEventListener('enterframe',function(){
             this.setText(Math.floor(time/30)+"."+Math.floor(time/3%10)+"sec");
             time--;
                 if(time==0){
                  game.end(score,"Score:"+score);
             }
        });
        game.rootScene.addChild(timeBoard);

こちらもMutableTextを使っています。
このゲームは30fpsで動作させているので、時間の初期値である60秒に30を乗じたものを毎フレーム減算しています。
timeが0になったらゲーム終了。

nineleap.enchant.jsプラグインを読み込んでおくと、game.endでゲーム終了時にスコアなどを登録させることができます。
ゲームの全体的な処理は以上です。

実際のゲームはここで紹介したコードに少し味付けをすると、9leapにアップロードされている「ドロイド君のフルーツ狩り」の出来上がりです。



■物理シミュレーションもやってみる

enchant.jsはなんと物理シミュレーションにも対応しています。
少し前なら博士論文のネタになってもおかしくないものがこんなに手軽に扱えてしまうなんて、レイ・カーツワイルの収穫加速の法則を実感する今日この頃です。

enchant.jsでは2D/3Dどちらの物理シミュレーションもサポートしています。


□2Dの物理シミュレーション

まずは2Dの方から紹介しましょう。

2Dの物理シミュレーションは、Box2DJSというオープンソースソフトウェアの恩恵を受けています。
とはいえ、そのままでは使いにくいので、kassy709さんがenchant.jsのSpriteから使い易いようにしたbox2d.enchant.jsというものを使います。

enchant.jsを使ったことがある人には、box2d.enchant.jsを使ったソースは非常に解り易いと思います。

box2d.enchant.jsで物理シミュレーションするスプライトを作るには、まず、物理シミュレーションの世界を作ります。


       //物理シミュレーションの世界を作る
       world = new PhysicsWorld(0, 9.8);


ふたつのパラメータはx,y軸それぞれの重力加速度です。
ここでは、Y軸に対して重力加速度9.8の世界ができました。

次に、スプライトを配置します。
物理シミュレーション可能なスプライトは、PhyBoxSpriteとPhyCircleSpriteの二種類あります。

PhyBoxSpriteは、その名の通り物理演算(Physics)する四角形(Box)のスプライト(Sprite)を創りだします。
パラメータが若干多いのはその物理的特性故です。

         PhyBoxSprite(width, height, staticOrDynamic, density, friction, restitution, isSleeping );
         - [width] Spriteの横幅.
         - [height] Spriteの高さ.
         - [staticOrDynamic] 静止するか動くか.
         - [density] Spriteの密度.
         - [friction] Spriteの摩擦.
         - [restitution] Spriteの反発.
         - [isSleeping] Spriteが初めから物理演算を行うか.


まあ慣れてしまえばそう難しくもありません。
staticOrDynamicは、その物体が固定されているか、それとも動くか、ということを指定します。
これはenchant.box2d.DYNAMIC_SPRITEまたはnchant.box2d.STATIC_SPRITEのどちらかの値を採ります。

そうして例えば以下のようにするとします。

          for(i=0;i<40;i++){
               var box = new PhyBoxSprite(16,16,enchant.box2d.DYNAMIC_SPRITE,
                                                       1.0,0.5,0.3, true);;
               box.image = game.assets['icon0.png'];
               box.frame=36+Math.floor(Math.random()*6); //サイコロの1から6をランダムに表示
               box.x = Math.random()*320;
               box.y = Math.random()*40;
               game.rootScene.addChild(box);
          }


すると40個のサイコロがランダムに画面の上の方に出現します。
これだけではまだ動きません。


          game.rootScene.addEventListener('enterframe',function(){
              world.step(game.fps);
         });


これで世界が動き出すことになります。
world.stepにフレームレートを与えると、そのぶん少しだけ世界が動くようになっているのです。

さらに静的なスプライトも使ってみましょう。

          for(i=0;i<10;i++){
               var circle = new PhyCircleSprite(8,enchant.box2d.STATIC_SPRITE,
                                                       1.0,0.2,0.9, true);;
               circle.image = game.assets['icon0.png'];
               circle.frame=20;
               circle.x = Math.random()*320;
               circle.y = Math.random()*200;
               root.addChild(circle);
          }


今回はもうひとつの要素である円形の物理スプライトを使っています。
PhyCircleSpriteというクラスで、PhyBoxSpriteとの違いは、幅と高さを指定するべきところが半径の指定になっているところです。

今回は16x16のアイコンなので半径8の円を想定してみました。

こうすると、実行するだけでまるでパチンコのようにサイコロが転がって行く物理シミュレーションを作ることが出来ます。
PhyBoxSpriteの縦横比は自由に変えることが出来ます。

どうです?簡単でしょう。

2Dの物理シミュレーションのサンプルを以下に示します。

//2D物理シミュレーションサンプル
enchant();
window.onload=function(){
     game = new Game(320,320);
     game.preload('icon0.png');
     game.onload=function(){
          world = new PhysicsWorld(0, 9.8);
          root = game.rootScene;
          for(i=0;i<40;i++){
               var box = new PhyBoxSprite(16,16,enchant.box2d.DYNAMIC_SPRITE,
                                                       1.0,0.5,0.3, true);;
               box.image = game.assets['icon0.png'];
               box.frame=36+Math.floor(Math.random()*6);
               box.x = Math.random()*320;
               box.y = Math.random()*40;
               root.addChild(box);
          }
          for(i=0;i<15;i++){
               var circle = new PhyCircleSprite(8,enchant.box2d.STATIC_SPRITE,
                                                       1.0,0.5,0.3, true);;
               circle.image = game.assets['icon0.png'];
               circle.frame=20;
               circle.x = i*16+50;
               circle.y = 250;
               root.addChild(circle);
          }
          for(i=0;i<10;i++){
               var circle = new PhyCircleSprite(8,enchant.box2d.STATIC_SPRITE,
                                                       1.0,0.2,0.9, true);;
               circle.image = game.assets['icon0.png'];
               circle.frame=20;
               circle.x = Math.random()*320;
               circle.y = Math.random()*200;
               root.addChild(circle);
          }
         
          root.addEventListener('enterframe',function(){
              world.step(game.fps);
         });
     }
     game.start();
}

b3a351e0c6e3aa07e5c64b94b0ea4008aa90d796

□3Dの物理シミュレーション

そしてなんと、enchant.jsは3Dの物理シミュレーションにまで対応しているのです。ただしまだβ版です。
ここではversion 0.3.2をベースに話をします。

内部的にはBulletというC/C++で書かれた物理シミュレーションエンジンをJavaScriptにコンバートしたammo.jsを使っています。
ただ、ammo.jsはC言語を無理矢理JavaScriptで動かしているのでかなり特殊な内部構造になっていて、正直使い易いとは言えません。そこでそれをうまーくgl.enchant.jsと組み合わせて使えるようにしたプラグインが、physics.gl.enchant.jsです。


これも中身が3Dになっただけで、box2d.enchant.jsと使い方はあまり変わりません。
ただ、box2dと作者が違うので、ちょっと呼び出し方が違います。このあたり、将来的には呼び出し方は統合していきたいところですね。

physics.gl.enchant.jsでは、Scene3DのかわりにPhyScene3Dを使います。

        var scene = new PhyScene3D();  // 物理シミュレーションシーンを生成
        scene.timeStep = 1 / game.fps;   // シーンのタイムステップを設定


プリミティブも用意されています。やはりプリミティブの名前の前にPhyが付きます。

        var plane = new PhyPlane(0, 1, 0, 0);
        scene.addChild(plane);


パラメータはβ版なのでまだかなり適当です。
x,y,zで面の法線ベクトルを与え、最後のパラメータで原典からの距離を与えています。

円(Circle)ではなく球(Sphere)もあります。

            var ball = new PhySphere(0.2, 0.35); // 半径0.2、質量0.35の球を作る
            scene.addChild(ball);


他にもPhyCylinder(円柱)、PhyCapsule(両端が半球の円柱)、PhyContainer(上蓋の開いた箱)、PhyBox(閉じた箱)があります。

そして最後に

        scene.play();

で実際に物理シミュレーションを開始することができます。
このあたり、βじゃなくなる頃にはbox2dとphysics.gl.enchant.jsのインターフェースにもう少し統一感を持たせて欲しいですね。

しかし、たったこれだけで3D物理シミュレーションまで扱えるとはただただ驚きです。

完全なソースコードを以下に示します。

//3D物理シミュレーションのサンプル
enchant();
var game;

window.onload = function(){
    game = new Game(640, 640);
    game.onload = function(){
        var ballsNum = 10;
        var playing = true;
        var sum = 0;

        var boxtex = new Texture("enchant.png");
        boxtex.ambient = [ 0.6, 0.6, 0.7, 1.0 ];
        boxtex.diffuse = [ 0.4, 0.4, 0.3, 1.0 ];
        boxtex.specular = [ 0.1, 0.1, 0.2, 1.0 ];

        var spheretex = new Texture("enchant-sphere.png");
        spheretex.ambient = [ 0.6, 0.6, 0.7, 1.0 ];
        spheretex.diffuse = [ 0.4, 0.4, 0.3, 1.0 ];
        spheretex.specular = [ 0.1, 0.1, 0.2, 1.0 ];

        var scene = new PhyScene3D();
        scene.timeStep = 1 / 60;
        camera = scene.getCamera();
        camera.y = 16;
        camera.z = 5;

        game.rootScene.addEventListener('touchstart',function(e){
             tempE = e;
        });
       
        game.rootScene.addEventListener('touchmove',function(e){
             camera.z +=  (e.y - tempE.y)*0.1;
             camera.x +=  (e.x - tempE.x)*0.1;
             tempE = e;            
        });

        var container = new PhyContainer(1, 10);
        container.mesh.texture = boxtex;
        container.translate(0, 1, 0);
        container.rotationSet(new Quat(0, 1, 0, Math.PI/16));
        scene.addChild(container);

        for (var i = 0; i < ballsNum; i++) {
            var x = Math.random() * 0.5 - 0.25;
            var y = i * 1.0 + 5;
            var z = Math.random() * 0.5 - 0.25;
            var ball = new PhySphere(0.2, 0.35);
            ball.mesh.texture = spheretex;
            ball.translate(x, y, z);
            scene.addChild(ball);
        }

        scene.play();
    };
    game.start();
};

acc4e33705c63fd39dd054142ba897a35f381733

チャンネル会員ならもっと楽しめる!
  • 会員限定の新着記事が読み放題!※1
  • 動画や生放送などの追加コンテンツが見放題!※2
    • ※1、入会月以降の記事が対象になります。
    • ※2、チャンネルによって、見放題になるコンテンツは異なります。
ブログイメージ
電脳ヒッチハイクガイド
更新頻度: 毎週月曜日
最終更新日:
チャンネル月額: ¥1,080 (税込)

チャンネルに入会して購読

コメントを書く
コメントをするには、
ログインして下さい。