Skip to content

Canvasで3Dワイヤーフレームに挑戦!

こんにちは。久しぶり! shi3zだ。
今日はenchant.jsで3Dグラフィックスに挑戦してみたよ。

3Dグラフィックスはとっても奥が深くて楽しいんだ。
その中でも入門に最適なのはなんといっても、ワイヤーフレーム。

Wireframe Cube with enchant.js and canvas – jsdo.it – share JavaScript, HTML5 and CSS

こんなふうに骨組みだけでできたものを「ワイヤーフレーム」って呼ぶんだ。
今回はせっかくなのでjsdo.itで作ってみた。

みんながこれをチョコっと変えて動かしたりすることもできるからね。

Wireframe Cube with enchant.js and canvas – jsdo.it – share JavaScript, HTML5 and CSS

こっちがソースコード。

今回はenchant.js上で実装したんだけど、いくつか実装のポイントを紹介するよ

Vector = new enchant.Class.create({
        initialize:function(x,y,z,w){
            this.x=0;
            this.y=0;
            this.z=0;
            this.w=1;
           
            if(arguments.length>1){
                this.x =x;
                this.y =y;
                this.z =z;
                if(arguments.length>3)
                    this.w =w;
            }
            if(arguments.length==1){
                this.x = x.x;
                this.y = x.y;
                this.z = x.z;
                this.w = x.w;
            }
           
    },

まずVectorクラスの宣言

まあたいていの3Dプログラミングにはベクトルは必須だからね。
今回は「同次座標」という、三次元のx,y,zにもうひとつwを加えた四次元のベクトルとして定義してみたよ。

まあこのVectorクラスは今回のサンプルに必要な最低限のメソッドしかないから、気が向いたら自分でいろいろ書き換えてみてね。

このクラスのinitializeでポイントになってるのは、パラメータかな。
コンストラクタに渡したパラメータが、ひとつなら、他のVectorインスタンスを渡されたと解釈してコピーを作っている。こうしないとJavaScriptでは参照がコピーされるだけになってしまうからね。

二つ以上だったら、パラメータをx,y,zと個別に指定していると解釈して、代入している。4つだったら、wも指定していると解釈するわけだ。ただし、三次元空間では常にwは1だ。

このwというのがまあクセモノに見えると思うんだけど、これがあることで非常に計算がシンプルかつ便利になるんだよね。

今回、Vectorクラスに用意したメソッドはコンストラクタ以外にaddとscale、そしてtoStringの三つだけ。本当は内積(dotProduct)とか外積(crossProduct)とか、正規化(normalize)もサポートしたいところだったけど、今回は使わなかったから省略したんだ。

そして3Dグラフィックスといえばもうひとつ、書かせないのが行列だ。
今回は同次座標を使うので当然、4×4行列を扱うクラスを書いたよ

Matrix = new enchant.Class.create({
    initialize:function(m){
        this.matrix = [     [1,0,0,0],
                        [0,1,0,0],
                        [0,0,1,0],
                        [0,0,0,1]   ];
        if(arguments.length==1){
            if(m.matrix != undefined ){
                for(var i=0;i<4;i++)
                    for(var j=0;j<4;j++)
                        this.matrix[i][j] = m.matrix[i][j];
            }else{
                this.matrix = m;
            }
        }
    },

こちらもVectorと同じく、パラメータとして何を渡したかによって行列の内容を設定しているのか、他のMatrixをコピーしているのか判定するようになっている。

Matrixクラスに用意したメソッドは、他にapplyとcomposition、そしてrotateX、rotateY,rotateZがある。
applyはVectorを適用(乗算)するメソッド、compositionは行列を合成(乗算)するメソッドだ。

rotateX、rotateY,rotateZの三つのメソッドは、与えられた角度T(ラジアン)に対する、回転行列を返すユーティリティメソッドだ。

そしてもうひとつ、頂点配列を扱うVertexTableクラス

VertexTable = new enchant.Class.create(Array,{
    initialize:function(vt){
        Array.call(this);
        if(vt != undefined){
            for(var i=0;i<vt.length;i++)
                this.push(new Vector(vt[i]));
        }
    },

これは組込みクラスのArray(配列)を継承して作った頂点配列専用クラスだ。
組込みクラスをいきなり継承できるというのもJavaScriptの面白いところだね。

次に、透視投影に使う行列をgame.onloadで定義している。

        var minZ =1,maxZ=10;
        var S = 1/Math.tan(45*3.14159/180);
        var Sz = maxZ/(maxZ-minZ);
        var projMat = new Matrix([  [  S, 0, 0, 0],
                            [  0, S, 0, 0],
                            [  0, 0, Sz, 1],
                            [  0, 0,-Sz*minZ, 0] ]);

回転とか平行移動とかを単にひとつの行列で表せるから同次座標は便利なんだよね。

さらに、画面上の座標を出すためのスクリーン変換にも同次座標行列は使える

        var screenMat = new Matrix([    [160,   0,0,160],
                                [  0,-160,0,160],
                                [  0,   0,1,  0],
                                [  0,   0,0,  1] ]);

こうすると、x,yをそれぞれ160倍、-160倍(y軸は画面と数学上で上下が反転するため)して、さらにxとyに160を足す、という意味の行列になる。

enchant.jsのスクリーンサイズは320×320だから、160はちょうど画面の半分のサイズ
x,yに160を足すと、画面の中心が原点(0,0)になるというワケ。

        var movMat =new Matrix([    [ 1, 0, 0, 0],
                            [ 0, 1, 0, 0],
                            [ 0, 0, 1, 50],
                            [ 0, 0, 0, 1]]);

立方体は原点を中心に作っているので、ちょっと離さないと目にめり込んで表示が崩れてしまう。
そこで、この移動行列movMatを作って、z軸方向に50だけずらしている。

行列が便利なのは、合成することで行列計算をまとめて行えてしまうこと。

        convMat =screenMat.composition(projMat);

このconvMat(変換行列、の意味)は、スクリーン座標変換をするscreenMat行列に透視投影をするprojMatを合成した結果を持たせる。

こうすることで、毎回必ず行う透視投影とスクリーン座標変換をまとめておけるわけだね。

        var vt =new VertexTable();
        vt.push(new Vector( -10, -10, -10) );
        vt.push(new Vector( -10,  10, -10) );
        vt.push(new Vector(  10,  10, -10) );
        vt.push(new Vector(  10, -10, -10) );
        vt.push(new Vector( -10, -10, -10) );
        vt.push(new Vector( -10, -10,  10) );
        vt.push(new Vector(  10, -10,  10) );
        vt.push(new Vector(  10,  10,  10) );
        vt.push(new Vector( -10,  10,  10) );
        vt.push(new Vector( -10, -10,  10) );
        vt.push(new Vector(  10, -10,  10) );
        vt.push(new Vector(  10, -10, -10) );
        vt.push(new Vector(  10,  10, -10) );
        vt.push(new Vector(  10,  10,  10) );
        vt.push(new Vector( -10,  10,  10) );
        vt.push(new Vector( -10,  10, -10) );

これは立方体のワイヤーフレームデータ。
といっても、線を描く順番に頂点を並べただけのもの。
立方体は本来、8頂点しかないのに、16頂点も掛かっているのは、立方体が一筆書きできないから。

同じ線を二回くらい描いてるんだよね。
ま、これは当然、もっとエレガントな方法もあるけど、恐ろしいことに今のコンピュータってこれくらい雑でも平気なんだよね。

僕が小学生の頃は、こんな雑な書き方したらリアルタイムでアニメーションさせるなんてとてもできなかったのにねー。時代は進むんだなー。

さあ、そしてそのアニメーションの部分だけど、これはもちろん’enterframe’の中で行う。

        var screen = new Sprite(320,320);
        var surface = new Surface(320,320);
        screen.image=surface;
        game.rootScene.addChild(screen);
        var context = surface.context;

        game.rootScene.addEventListener('enterframe', function(e) {
            context.fillStyle  = '#fff';
            context.fillRect(0,0,320,320);

                context.beginPath();
                context.strokeStyle='#00f';

まずスクリーンサイズと同じ大きさのスプライトとサーフェスを用意して、rootSceneに配置。
スプライトのimageに作成したサーフェスを設定し、contextにsurface.contextを代入して準備完了。

あとはenterfameの中で、ず白く描画領域全体を塗りつぶす。画面クリアしてるわけだね。
それから、beginPathで線を描き始める、と。

実際の座標変換はこんなふうにやってる

            t++;
            var rotMatY = m.rotateY(t*3.14159/180);
            var rotMatX = m.rotateX(t*3.14159/180);
           
            rotMat =rotMatX.composition(rotMatY);
            rotMat =movMat.composition(rotMat);
            rotMat =convMat.composition(rotMat);
           
            rvt =vt.apply(rotMat);

tは連続的に変化する変数で、ここでは回転角を与えている。
m.rotateYとm.rotateXは、それぞれTに対する回転行列を返すようになっているから、ちょうどフレームごとに1度ずつY軸とX軸まわりに回転していることになる。

t*3.14159/180というのは、普通の角度(degree)からラジアン角への変換という、いつものお約束をやっているだけ。

rotMat =rotMatX.composition(rotMatY); で、X軸まわりの回転行列とY軸まわりの回転行列を合成している。

ちなみに行列は、合成する順序によって結果が変化する。
この場合は、Y軸周りの回転行列(rotMatY)で回転したあとにX軸周りの回転行列(rotMatX)で回転することになる。

試しにrotMatXとrotMatYを入れ替えたりしてみると、感覚が掴めるかもしれない。

また、そのあとrotMat =movMat.composition(rotMat);で、回転した行列を移動行列(movMat)を合成することで移動させている。

これも回転と移動の順序を入れ替えると面白いことになるから試してみるといいだろう。

最後にあらかじめ合成してあった透視射影スクリーン変換行列convMatを合成して、変換行列rotMatのできあがり。

            rvt =vt.apply(rotMat);

            rvt[0].x=rvt[0].x/rvt[0].w;
            rvt[0].y=rvt[0].y/rvt[0].w;
            context.moveTo(rvt[0].x,rvt[0].y);
            for(var i=1;i<rvt.length;i++){
                rvt[i].x=rvt[i].x/rvt[i].w;
                rvt[i].y=rvt[i].y/rvt[i].w;
                context.lineTo(rvt[i].x,rvt[i].y);
            }
            context.stroke();

変換行列ができたら、もう終わったも同然だ。
立方体のデータが入っている頂点配列vtに回転行列を適用(apply)して、全ての頂点を変換し、変換した結果の頂点配列をrvtに格納する。

rvtには、変換されたあとの同次座標が入っている。
いろいろな変換をするため、wの値も変動している。

しかし、前述したように、三次元空間では常にwは1だ。wが1以外の値だったら、それは四次元の値ということになる。

ではどうするか?
全ての値をwで割ると、正しい三次元空間の値になる。w/w=1だからね。

この、トンチみたいな処理によって、無事、全ての座標変換が終わり、最後のcontext.stroke()で画面には立方体が表示される。

おめでとう。立方体が表示できたぞ。

3Dグラフィックスも基本を抑えればすごく簡単に表現できることがわかってもらえたと思う。

数学がニガテな人も、回転行列の順番を変えたり、今回は使っていなかったZ軸の回転を加えたりして遊んでみると感覚が掴めてくるかもしれないよ。

練習問題
1) X軸まわりの回転のかわりにZ軸まわりの回転をさせてみよう
2) Y軸まわりの回転の速度だけ二倍にしてみよう
3) movMatをenterFrameの中で再定義するように改造して、次第に遠ざかって行くように改造してみよう
4) 三角錐など、立方体ではない物体を表示させてみよう

さあできるかな?

このエントリーをはてなブックマークに追加
はてなブックマーク - Canvasで3Dワイヤーフレームに挑戦!
Post to Google Buzz
Share on GREE

Related posts:

  1. [投稿]JavaScriptでBASICっぽい言語をつくる
  2. JavaScriptでMMOGをつくってみよう その7 JSON-RPC風通信
  3. [enchant.js]shi3z式ゲームプログラミング #7 タイムアタックは神!
  4. かんたんプログラミング #1:亀の子グラフィック
  5. shi3z式ゲームプログラミング入門 #8 enchant.jsでプチ物理シミュレースゲームを作ってみた

Facebook comments:

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*