HTML5のcanvas2dでテクスチャーをはったポリゴン描画

HTML5のCanvas2dではポリゴンや三角形に自由にテクスチャーをはって
描画するという機能はなさそうに見えましたが、
工夫次第ではできるのではないかと思い、ちょっとためしに実装してみました。
ちょっと調べた感じだとすぐには見つからなかったというのもあります。
Canvas3Dはまだ開発版でしか使えないのと、OpenGL2.0相当くらいの
グラフィックスボードが必要とされているっぽいので
インテルオンボード名環境とかでもちょっとテクスチャーをはった多角形を描けると、
いいこともあるんじゃないかと思っています。
ばねたいとかよさそうです。
http://d.hatena.ne.jp/Dycoon/20081109


以下に動作するものを置きました。
http://www.rmake.net/dycoon/files/polygonTest2/polygonCanvasTest800x600.html
FireFoxChromeで動作するんじゃないかと思います。
Internet Explorer 8ではHTML5に対応していないので、動きません。
"change mode"というボタンを押すと描画の方法が変わります。


デフォルトの描画に関するスクリーンショットを以下に置きました。


まず、Mozilla FireFox 3.6.6のスクリーンショット


次に、Google Chrome 6.0.435.1のスクリーンショット


やっていることは、三角形ポリゴンを2つ組み合わせて
飛行機のテクスチャーを張った四角形を欠いています。
アフィン変換した四角形なら元から描画可能なのであまり適切な例ではありませんが、
とりあえず動作確認をするためには使えるかと思います。


まず、FireFoxではバイリニアフィルタがかかっていますが
Chromeではかかっていません。
ここら辺を調整する関数はないと思いますので、ブラウザの実装依存かと思います。


つぎに、どちらの画面でも飛行機の左上から右下にかけてなにやら線が入ってしまっています。
これはポリゴンの境目に相当するところで発生しています。
これは、テクスチャーをはった三角形描画の方法が原因で起こってしまっているのではないかと思いますので
実現方法から説明してみたいと思います。


HTML5ではパスに模様をつけて描画することと、それをアフィン変換で変換して表示することができます。
このプログラムではテクスチャーの座標の位置に頂点を置き、
その頂点を画面上の位置に変換するようなアフィン変換の行列を求めることで実現しています。


しかし、そうすると本来同じ位置にあるはずの頂点が変換による誤差で
同じ位置にならず三角形の間に隙間が生じてしまうようです。
(ただ、これが原因だともっと違う表示のされ方がするような気もしますので、
もしかしたら別の原因もあるのかもしれません。)


"change mode"を1回押しますと、strokeにテクスチャーを使うようになります。
これにより隙間の線は消えますが、バイリニアフィルタで半透明になっている部分が
隙間のあった位置で濃くなっているのと、描画の範囲自体が広がってしまうということが起こります。
半透明のテクスチャーを使った場合は三角形が隣接している部分が濃くなるというようなことが起こるかと思います。


"change mode"をもう1回押しますと境界の線を表示するようになります。
どんな三角形を描いているか確認するのにご利用ください。


この実現方法だとどうしても問題が残ってしまいました。
この手のことを考えている方の参考にでもなればということで、
以下にソースを貼り付けておこうかと思います。


それでは


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<title>jstest</title>

<script>//<![CDATA[

var Vector2D = function(){

};

Vector2D.prototype = {
    x : 0.0,
    y : 0.0,

    add : function(v){
        this.x += v.x;
        this.y += v.y;
    },

    sub : function(v){
        this.x -= v.x;
        this.y -= v.y;
    },

    mul : function(s){
        this.x *= s;
        this.y *= s;
    },

    copy : function(v){
        v.x = this.x;
        v.y = this.y;
    },

    init : function(x, y){
        this.x = x;
        this.y = y;
    }

};

//
var AffineMatrix2D = function(){

};

AffineMatrix2D.prototype = {
    a : 1.0,
    b : 0.0,
    c : 0.0,
    d : 1.0,
    tx : 0.0,
    ty : 0.0,

    inverse : function(dst){
        var t;

        t = this.a * this.d - this.b * this.c;
        if(Math.abs(t) <= 0.0){
            return false;
        }

        t = 1.0 / t;

        dst.a = this.d * t;
        dst.b = -this.b * t;
        dst.c = -this.c * t;
        dst.d = this.a * t;
        dst.tx = -(this.tx * dst.a + this.ty * dst.c);
        dst.ty = -(this.tx * dst.b + this.ty * dst.d);

        return true;
    },

    mul : function(m1, dst){
        dst.a = this.a * m1.a + this.b * m1.c;
        dst.b = this.a * m1.b + this.b * m1.d;

        dst.c = this.c * m1.a + this.d * m1.c;
        dst.d = this.c * m1.b + this.d * m1.d;

        dst.tx = this.tx * m1.a + this.ty * m1.c + m1.tx;
        dst.ty = this.tx * m1.b + this.ty * m1.d + m1.ty;
    },

    mulVec : function(v, dv){
        dv.x = v.x * this.a + v.y * this.c + this.tx;
        dv.y = v.x * this.b + v.y * this.d + this.ty;
    },

    init : function(a, b, c, d, tx, ty){
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
        this.tx = tx;
        this.ty = ty;
    }
};

var Vertex = function(){
    this.p = new Vector2D();
    this.t = new Vector2D();
};

Vertex.prototype = {
    p : new Vector2D(),
    t : new Vector2D(),

    init : function(x, y, u, v){
        this.p.x = x;
        this.p.y = y;
        this.t.x = u;
        this.t.y = v;
    }
};

var Face = function(){
    this.v0 = new Vertex();
    this.v1 = new Vertex();
    this.v2 = new Vertex();
    this.invMat = new AffineMatrix2D();
    this.posMat = new AffineMatrix2D();
    this.transMat = new AffineMatrix2D();

    this.p0 = new Vector2D();
    this.p1 = new Vector2D();
    this.p2 = new Vector2D();

    this.tp0 = new Vector2D();
    this.td1 = new Vector2D();
    this.td2 = new Vector2D();
    this.m = new AffineMatrix2D();
};

Face.prototype = {
    v0 : new Vertex(),
    v1 : new Vertex(),
    v2 : new Vertex(),
    invMat : new AffineMatrix2D(),
    posMat : new AffineMatrix2D(),
    transMat : new AffineMatrix2D(),

    p0 : new Vector2D(),
    p1 : new Vector2D(),
    p2 : new Vector2D(),

    tp0 : new Vector2D(),
    td1 : new Vector2D(),
    td2 : new Vector2D(),
    m : new AffineMatrix2D(),

    calcInv : function(img){

        var w = img.width;
        var h = img.height;

        this.v0.t.copy(this.tp0);

        this.v1.t.copy(this.td1);
        this.td1.sub(this.tp0);

        this.v2.t.copy(this.td2);
        this.td2.sub(this.tp0);

        this.tp0.x *= w;
        this.tp0.y *= h;

        this.td1.x *= w;
        this.td1.y *= h;

        this.td2.x *= w;
        this.td2.y *= h;

        this.p0.x = this.tp0.x;
        this.p0.y = this.tp0.y;

        this.p1.x = this.td1.x + this.tp0.x;
        this.p1.y = this.td1.y + this.tp0.y;

        this.p2.x = this.td2.x + this.tp0.x;
        this.p2.y = this.td2.y + this.tp0.y;

        this.m.init(this.td1.x, this.td1.y, this.td2.x, this.td2.y, this.tp0.x, this.tp0.y);

        this.m.inverse(this.invMat);

    },

    calcPos : function(){
        this.v0.p.copy(this.tp0);

        this.v1.p.copy(this.td1);
        this.td1.sub(this.tp0);

        this.v2.p.copy(this.td2);
        this.td2.sub(this.tp0);

        this.posMat.init(this.td1.x, this.td1.y, this.td2.x, this.td2.y, this.tp0.x, this.tp0.y);
    },

    getTrans : function(){
        this.invMat.mul(this.posMat, this.transMat);
        return this.transMat;
    }
};


var timerID;
var tm = new Date();
var count = 0;

var loadCount = 2;
var img0 = null;
var img1 = null;

var pat = null;

var f = [];

var logArea = null;

function init(){

    logArea = document.getElementById("logArea");

    img0 = new Image();
    img0.src = "CA340034.png";

    img0.onload = function(){
        loadCount -= 1;
    }

    img1 = new Image();
    img1.src = "fighter.png";

    img1.onload = function(){
        var canvas = document.getElementById("canvas1");
        c = canvas.getContext("2d");
        pat = c.createPattern(img1, "repeat");
        loadCount -= 1;
    }


    timerID = setInterval("loop()",1);

}

var mode = 0;

function changeMode(){
    mode += 1;
    mode %= 3;
}

function createFace(){

    var w = img1.width;
    var h = img1.height;

    f.length = 2;

    isz = f.length;
    for(i = 0 ; i < isz ; i++){
        f[i] = new Face();
    }

    f[0].v0.init(200, 100, 0.5 + 0.5 / w, 0.75 + 0.5 / h);
    f[0].v1.init(200, 500, 0.5 + 0.5 / w, 1.0 - 0.5 / h);
    f[0].v2.init(600, 500, 1.0 - 0.5 / w, 1.0 - 0.5 / h);

    f[1].v0.init(200, 100, 0.5 + 0.5 / w, 0.75 + 0.5 / h);
    f[1].v1.init(600, 500, 1.0 - 0.5 / w, 1.0 - 0.5 / h);
    f[1].v2.init(600, 100, 1.0 - 0.5 / w, 0.75 + 0.5 / h);

    var i;
    var isz;
    var v = new Vector2D();
    var str = "";

    isz = f.length;
    for(i = 0 ; i < isz ; i++){
        f[i].calcInv(img1);

        f[i].invMat.mulVec(f[i].v0.t, v);
        str += "v0t = (" + v.x + ", " + v.y + ")\n";

        f[i].invMat.mulVec(f[i].v1.t, v);
        str += "v1t = (" + v.x + ", " + v.y + ")\n";

        f[i].invMat.mulVec(f[i].v2.t, v);
        str += "v2t = (" + v.x + ", " + v.y + ")\n";
    }

    //logArea.innerHTML = "<pre>" + str + "</pre>";
}

var first = true;
var t = 0.0;

function loop(){

    //

    if(loadCount <= 0){

        var canvas = document.getElementById("canvas1");
        canvasW = canvas.width;
        canvasH = canvas.height;
        c = canvas.getContext("2d");

        if(first){
            createFace();

            first = false;
        }

        var isz;
        var i;
        var m;

        //
        c.setTransform(1.0, 0, 0, 1.0, 0, 0);
        c.drawImage(img0, 0.0, 0.0);

        //
        isz = f.length;

        f[0].v0.p.x = 200 + Math.cos(t) * 50.0;
        f[0].v0.p.y = 100 + Math.sin(t) * 50.0;

        f[0].v1.p.x = 200 + Math.cos(t + Math.PI * 0.5) * 50.0;
        f[0].v1.p.y = 500 + Math.sin(t + Math.PI * 0.5) * 50.0;

        f[0].v2.p.x = 600 + Math.cos(t + Math.PI * 1.0) * 50.0;
        f[0].v2.p.y = 500 + Math.sin(t + Math.PI * 1.0) * 50.0;

        f[1].v0.p.x = f[0].v0.p.x;
        f[1].v0.p.y = f[0].v0.p.y;

        f[1].v1.p.x = f[0].v2.p.x;
        f[1].v1.p.y = f[0].v2.p.y;

        f[1].v2.p.x = 600 + Math.cos(t + Math.PI * 1.5) * 50.0;
        f[1].v2.p.y = 100 + Math.sin(t + Math.PI * 1.5) * 50.0;

        for(i = 0 ; i < isz ; i++){

            f[i].calcPos();
            m = f[i].getTrans();
            c.setTransform(m.a, m.b, m.c, m.d, m.tx, m.ty);

            c.beginPath();
            c.fillStyle = pat;
            if(mode == 1){
                c.strokeStyle = pat;
            }
            else{
                c.strokeStyle = "#000000";
            }

            //c.arc(16, 16, 32, 0, 2 * Math.PI, false);
            //c.rect(0.0, 0.0, 32.0, 32.0);
            c.moveTo(f[i].p0.x, f[i].p0.y);
            c.lineTo(f[i].p1.x, f[i].p1.y);
            c.lineTo(f[i].p2.x, f[i].p2.y);
            c.closePath();

            if(mode == 1 || mode == 2){
                c.stroke();
            }
            
            c.fill();
        }

        t += 0.05;


    }



    //

    var fpsArea = document.getElementById("fpsArea");
    var now = new Date();

    count += 1;

    if(now.getTime() - tm.getTime() > 1000){
        tm = now;
        fpsArea.innerHTML = "fps = " + count;
        count = 0;
    }

}


//]]></script>
</head>
<body onload="init()">

<canvas id="canvas1" width="800" height="600"></canvas><br>
<input type="button" value="change mode" onClick="changeMode()"></input>
<div id="fpsArea"></div><br />
<div id="logArea"></div>

</body>
</html>