Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

WebGLとCanvas2Dを併用してみる

More than 5 years have passed since last update.

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

・Canvasリファレンス
sc001.png

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

WebGL描画をCanvas2Dに描画

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

// 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
あとはWebGLのcanvasをCSSでdisplay:noneして消してやればOK!

canvas2Dでパーティクル表示

せっかくなので、WebGL描画の上にCanvas2Dでパーティクル加えてみました。
つまり、この方法を使えばCanvas2Dのスキルを活かしつつ、WebGLも活用できますね!
sc003.gif

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

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でパーティクルっぽいのを作ってみる

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枚重ねたサンプル

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ライブラリなどを組み合わせて描画するのは手間なので、これで簡単に組み合わせられます!

naotaro0123
Webエンジニアです。BlenderとWebGLと漫画が大好きです ◆ソース http://github.com/naotaro0123
http://ameblo.jp/chicktack123/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away