WebGLとCanvas2Dを併用してみる

WebGLとCanvas2Dを併用できないかな〜とぼんやり考えてたら、以下のAPIを見つけました。

[・Canvasリファレンス](http://www.htmq.com/canvas/drawImage_s.shtml)
![sc001.png](https://qiita-image-store.s3.amazonaws.com/0/15028/625866d8-1488-57fa-7d42-53719939b67b.png)

なんと第1引数のimageにimg要素、canvas要素、video要素が指定できるとっ！
これで描画して更新すれば、canvas2DにWebGL描画できそうなのでやってみました。

# WebGL描画をCanvas2Dに描画
ソースはこんな感じです。
ここには書いてないですが、別途WebGLのcanvasをgetElementByIdし、変数canvasを渡しておきdrawImageの第1引数に指定します

```js
// WebGLのcanvasとは別に、2D用のcanvasタグをHTML側に作って参照する
var can_2d = document.getElementById("2dcanvas");
var ctx_2d = can_2d.getContext("2d");

// WebGLの描画処理後に呼び出す
function draw_2DCanvas(){
    // canvasクリア
    ctx_2d.clearRect(0,0, can_2d.width, can_2d.height);
    // webglのcanvasを指定
    ctx_2d.drawImage(canvas, 0, 0);
    // 繰り返し
    requestAnimationFrame(draw_2DCanvas);
}
```

これをLive2D WebGL SDKに適用してみました。
左がWebGL、右がCanvas2D。ちゃんと毎フレーム描画されています
![sc002.gif](https://qiita-image-store.s3.amazonaws.com/0/15028/73122c00-d0b8-5180-6a00-2ad005f35f7d.gif)
あとはWebGLのcanvasをCSSでdisplay:noneして消してやればOK！

# canvas2Dでパーティクル表示
せっかくなので、WebGL描画の上にCanvas2Dでパーティクル加えてみました。
つまり、この方法を使えばCanvas2Dのスキルを活かしつつ、WebGLも活用できますね！
![sc003.gif](https://qiita-image-store.s3.amazonaws.com/0/15028/d5a76aea-ad44-47d9-9017-f04affd10029.gif)

Live2D SDKのSimpleプロジェクトを元にカスタムしました。
HTML側はcanvas2D用のタグを1行追加しています。

```html:simple.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
            <title>Live2D Simple</title>
        </meta>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=4.0">
        </meta>
    </head>
    <body onload="Simple(); draw_2DCanvas();">
        <canvas id="glcanvas" style="border:dashed 1px #CCC;display:none;"></canvas>
       <canvas id="2dcanvas" style="border:dashed 1px #CCC"></canvas>
        <div id="myconsole" style="color:#BBB">---- Log ----</div>
        <!-- Live2D Library -->
        <script src="lib/live2d.min.js"></script>
        <!-- User's Script -->
        <script src="src/Simple.js"></script>
    </body>
</html>
```

js側は、258行目辺りからのcanvas2D描画を追加しました。
パーティクルは以下のサイトを参考にさせていただきました。
　→　[HTML5+Canvasでパーティクルっぽいのを作ってみる](http://www.webopixel.net/javascript/1003.html)

```js:Simple.js
var CANVAS_SIZE = 300;
var canvas;

// JavaScriptで発生したエラーを取得
window.onerror = function(msg, url, line, col, error) {
    var errmsg = "file:" + url + "<br>line:" + line + " " + msg;
    Simple.myerror(errmsg);
}

var Simple = function() {
    // Live2Dモデルのインスタンス
    this.live2DModel = null;    
    // アニメーションを停止するためのID
    this.requestID = null;    
    // モデルのロードが完了したら true
    this.loadLive2DCompleted = false;
    // モデルの初期化が完了したら true
    this.initLive2DCompleted = false;
    // WebGL Image型オブジェクトの配列
    this.loadedImages = [];
    // Live2D モデル設定。
    this.modelDef = {
        
        "type":"Live2D Model Setting",
        "name":"haru",
        "model":"assets/haru/haru.moc",
        "textures":[
            "assets/haru/haru.1024/texture_00.png",
            "assets/haru/haru.1024/texture_01.png",
            "assets/haru/haru.1024/texture_02.png"
        ]
    };    
    // Live2Dの初期化
    Live2D.init();    
    // canvasオブジェクトを取得
    canvas = document.getElementById("glcanvas");
    canvas.width = canvas.height = CANVAS_SIZE;
    // コンテキストを失ったとき
    canvas.addEventListener("webglcontextlost", function(e) {
        Simple.myerror("context lost");
        loadLive2DCompleted = false;
        initLive2DCompleted = false;

        var cancelAnimationFrame = 
            window.cancelAnimationFrame || 
            window.mozCancelAnimationFrame;
        cancelAnimationFrame(requestID); //アニメーションを停止

        e.preventDefault(); 
    }, false);
    
    // コンテキストが復元されたとき
    canvas.addEventListener("webglcontextrestored" , function(e){
        Simple.myerror("webglcontext restored");
        Simple.initLoop(canvas); 
    }, false);

    // Init and start Loop
    Simple.initLoop(canvas);
};

/*
* WebGLコンテキストを取得・初期化。
* Live2Dの初期化、描画ループを開始。
*/
Simple.initLoop = function(canvas/*HTML5 canvasオブジェクト*/) 
{
    //------------ WebGLの初期化 ------------
    
    // WebGLのコンテキストを取得する
    var para = {
        premultipliedAlpha : true,
//        alpha : false
    };
    var gl = Simple.getWebGLContext(canvas, para);
    if (!gl) {
        Simple.myerror("Failed to create WebGL context.");
        return;
    }

    // 描画エリアを白でクリア
    gl.clearColor( 0.0 , 0.0 , 0.0 , 0.0 );	 

    //------------ Live2Dの初期化 ------------
    
    // mocファイルからLive2Dモデルのインスタンスを生成
    Simple.loadBytes(modelDef.model, function(buf){
            live2DModel = Live2DModelWebGL.loadModel(buf);
    });

    // テクスチャの読み込み
    var loadCount = 0;
    for(var i = 0; i < modelDef.textures.length; i++){
        (function ( tno ){// 即時関数で i の値を tno に固定する（onerror用)
            loadedImages[tno] = new Image();
            loadedImages[tno].src = modelDef.textures[tno];
            loadedImages[tno].onload = function(){
                if((++loadCount) == modelDef.textures.length) {
                    loadLive2DCompleted = true;//全て読み終わった
                }
            }
            loadedImages[tno].onerror = function() { 
                Simple.myerror("Failed to load image : " + modelDef.textures[tno]); 
            }
        })( i );
    }
    
    //------------ 描画ループ ------------    
    (function tick() {
        Simple.draw(gl); // 1回分描画
        
        var requestAnimationFrame = 
            window.requestAnimationFrame || 
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame || 
            window.msRequestAnimationFrame;
		requestID = requestAnimationFrame( tick , canvas );// 一定時間後に自身を呼び出す
    })();
};


Simple.draw = function(gl/*WebGLコンテキスト*/)
{
    // Canvasをクリアする
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Live2D初期化
    if( ! live2DModel || ! loadLive2DCompleted ) 
    return; //ロードが完了していないので何もしないで返る

    // ロード完了後に初回のみ初期化する
    if( ! initLive2DCompleted ){
        initLive2DCompleted = true;

        // 画像からWebGLテクスチャを生成し、モデルに登録
        for( var i = 0; i < loadedImages.length; i++ ){
            //Image型オブジェクトからテクスチャを生成
            var texName = Simple.createTexture(gl, loadedImages[i]);
            live2DModel.setTexture(i, texName); //モデルにテクスチャをセット
        }

        // テクスチャの元画像の参照をクリア
        loadedImages = null;

        // OpenGLのコンテキストをセット
        live2DModel.setGL(gl);

        // 表示位置を指定するための行列を定義する
        var s = 2.0 / live2DModel.getCanvasWidth(); //canvasの横幅を-1..1区間に収める
        var matrix4x4 = [ s,0,0,0 , 0,-s,0,0 , 0,0,1,0 , -1.0,1,0,1 ];
        live2DModel.setMatrix(matrix4x4);
    }
    
	// キャラクターのパラメータを適当に更新
    var t = UtSystem.getTimeMSec() * 0.001 * 2 * Math.PI; //1秒ごとに2π(1周期)増える
    var cycle = 3.0; //パラメータが一周する時間(秒)
    // PARAM_ANGLE_Xのパラメータが[cycle]秒ごとに-30から30まで変化する
    live2DModel.setParamFloat("PARAM_ANGLE_X", 30 * Math.sin(t/cycle));

    // Live2Dモデルを更新して描画
    live2DModel.update(); // 現在のパラメータに合わせて頂点等を計算
    live2DModel.draw();	// 描画
};


/*
* WebGLのコンテキストを取得する
*/
Simple.getWebGLContext = function(canvas/*HTML5 canvasオブジェクト*/)
{
    var NAMES = [ "webgl" , "experimental-webgl" , "webkit-3d" , "moz-webgl"];
	
    var param = {
        alpha : true,
        premultipliedAlpha : true
    };
    
    for( var i = 0; i < NAMES.length; i++ ){
            try{
                    var ctx = canvas.getContext( NAMES[i], param );
                    if( ctx ) return ctx;
            } 
            catch(e){}
    }
    return null;
};


/*
* Image型オブジェクトからテクスチャを生成
*/
Simple.createTexture = function(gl/*WebGLコンテキスト*/, image/*WebGL Image*/) 
{
    var texture = gl.createTexture(); //テクスチャオブジェクトを作成する
    if ( !texture ){
        mylog("Failed to generate gl texture name.");
        return -1;
    }
    
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);	//imageを上下反転
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
        
    gl.generateMipmap(gl.TEXTURE_2D);
    gl.bindTexture( gl.TEXTURE_2D , null );
    
    return texture;
};


/*
* ファイルをバイト配列としてロードする
*/
Simple.loadBytes = function(path , callback)
{
    var request = new XMLHttpRequest();
    request.open("GET", path , true);
    request.responseType = "arraybuffer";
    request.onload = function(){
            switch( request.status ){
            case 200:
                callback( request.response );
                break;
            default:
                Simple.myerror( "Failed to load (" + request.status + ") : " + path );
                break;
            }
    }
    
    request.send(null); 
};


/*
* 画面ログを出力
*/
Simple.mylog = function(msg/*string*/)
{
    var myconsole = document.getElementById("myconsole");
    myconsole.innerHTML = myconsole.innerHTML + "<br>" + msg;
    console.log(msg);
};

/*
* 画面エラーを出力
*/
Simple.myerror = function(msg/*string*/)
{
    console.error(msg);
    Simple.mylog( "<span style='color:red'>" + msg + "</span>");
};

/*
 * Canvas2D描画 
 * http://www.webopixel.net/javascript/1003.html
 */
var can_2d = document.getElementById("2dcanvas"); 
can_2d.width = can_2d.height = CANVAS_SIZE;
var ctx_2d = can_2d.getContext("2d");

var Particle = function(scale, color, speed) {
    this.scale = scale; //大きさ
    this.color = color; //色
    this.speed = speed; //速度
    this.position = {   // 位置
        x: 100,
        y: 100
    };
};

Particle.prototype.draw = function() {
    ctx_2d.beginPath();
    ctx_2d.arc(this.position.x, this.position.y, this.scale, 0, 2*Math.PI, false);
    ctx_2d.fillStyle = this.color;
    ctx_2d.fill();
};

var density = 40;  //パーティクルの密度
var particles = []; //パーティクルをまとめる配列
var colors = ['#D0A000', '#6DD0A5', '#E084C5'];

for (var i=0; i<density; i++) {
    var color = colors[~~(Math.random()*3)];
    var scale = ~~(Math.random()*(8-3))+3;
    particles[i] = new Particle(scale, color, scale/2);
    particles[i].position.x = Math.random()*can_2d.width;
    particles[i].position.y = Math.random()*can_2d.height;
    particles[i].draw();
}

function draw_2DCanvas(){
    // canvasクリア
    ctx_2d.clearRect(0,0, can_2d.width, can_2d.height);
    // webglのcanvasを指定
    ctx_2d.drawImage(canvas, 0, 0);
    // パーティクルを描画する
    draw_Particle();
    // 繰り返し
    requestAnimationFrame(draw_2DCanvas);
}

function draw_Particle() {
    for (var i=0; i<density; i++) {
        particles[i].position.x += particles[i].speed;
        particles[i].draw();
        if (particles[i].position.x > can_2d.width) particles[i].position.x = -30;
    }
}
```

# CSSで描画順を制御する
ちなみに上記やり方でなくても、CanvasをCSSで重ねてやる方法もあります。
WebGLのcanvas3枚重ねた例ですが、z-indexとabsoluteを使っていけます。

以前作ったものですが、こちらが参考になるかと思います。
　→　[WebGLのcanvas3枚重ねたサンプル](http://simplecode.jp/live2d_sdk_webgl_demo/sample/simple_canvas3/simple.html)

```html:simple.html
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Test</title>
	<style>
    .waku{
        position: relative;
        margin:0 auto;
        width:500px;
        height:auto;
    }
    #frontcanvas{
        position:absolute;
        z-index: 50;
        left:0px;
        top:0px;
    }
    #glcanvas{
    position:absolute;
    z-index: 1;
    left:0px;
    top:0px;
    }
    #Live2Dcanvas{
        position:absolute;
        z-index: 10;
        left:0px;
        top:0px;
    }
	</style>
</head>
<body>
<div class="waku">
   <canvas id="frontcanvas"></canvas>
   <canvas id="Live2Dcanvas"></canvas>
   <canvas id="glcanvas"></canvas>
</div>
</body>
</html>
```

WebGLのjsライブラリなどを組み合わせて描画するのは手間なので、これで簡単に組み合わせられます！