LoginSignup
14
19

More than 5 years have passed since last update.

Live2DのWebGL版をコードリーディング

Last updated at Posted at 2015-02-27

自分が作ったものが2本腕が表示されるバグがあったので、WebGLのSampleApp1プロジェクトもコードリーディングしてみました。
ちなみにWebGL部分のソース解説はないですが、もう少し情報整理できたらそのうち書こうと思います。

SampleApp1のクラス関連図

基本はUnityと同じクラス名ですが、Frameworkのクラスが1つにまとめられてます

WebGL_SampleApp1.png

解析した結果、pose.jsonというポーズを決めるファイルが読めてなかった...。
読み込ませて無事に解決しました。
haru_mod_1.png

ついでにmodel.jsonとフォルダパスを指定するだけでLive2Dモデルを表示し、モーションチェンジするものを作りました(SDK付属のSimpleプロジェクトから作りました)
haru_res.png

フォルダ構成

フォルダ構成はこんな感じでPlatformManager.jsとLAppLive2DManager.jsは、SampleApp1から持ってきました

フォルダ構成
Live2D SDK
 ├─framework
 │  Live2DFramework.js
 ├─lib
 │  live2d.min.js
 └─sample
   └─simple_custom
     │  simple.html
     ├─assets
     │  └─haru
     │   │  haru.moc
     │   │  haru.model.json
     │   │  haru.physics.json
     │   │  haru.pose.json
     │   │ 
     │   ├─expressions
     │   ├─haru_01.1024
     │   ├─haru_02.1024
     │   ├─motions
     │   └─sounds
     └─src
        Simple.js
        LAppLive2DManager.js
        PlatformManager.js

ソースコード

HTMLは選択ボタンからmotionChange()イベントを呼びます。

simple.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
            <title>Live2D Simple</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=4.0">
    </head>
    <body onload="Simple()">
        <!-- 選択ボタン -->
        <span style="font-size:14px;">モーション選択</span>
        <select id="selectmenu" onchange="motionChange()"></select>
        <br>
        <canvas id="glcanvas" style="border:dashed 1px #CCC"></canvas>
        <!-- Live2D Library -->
        <script src="../../lib/live2d.min.js"></script>
        <!-- Live2Dの便利機能が入ったクラス群 -->
        <script src="../../framework/Live2DFramework.js"></script>
        <!-- Live2Dモデルやテクスチャロード管理クラス -->
        <script src="src/PlatformManager.js"></script>
        <!-- Live2Dモデルの生成やチェンジを行うクラス -->
        <script src="src/LAppLive2DManager.js"></script>
        <!-- メイン処理クラス -->
        <script src="src/Simple.js"></script>
    </body>
</html>

model.jsonは通常のjson読込で、pose.jsonやphysics.json、mocやmtnはバイナリ読込です。
定数部分を変更すれば他のLive2Dモデルでも使いまわせます。
model.jsonから各ファイルパスを取得し、jsonを読み込みLive2Dを生成し、最後にselectボタンにモーションファイル数分の値をセットしてます。

Simple.js
/**
*    定数
*/
// モデルパス
var MODEL_PATH = 'assets/haru/';
// モデル定義ファイル
var MODEL_JSON = "haru.model.json";
// canvasの幅と高さ
var CANVAS_WIDTH  = 512;
var CANVAS_HEIGHT = 512;
// canvasのID
var CANVAS_ID ='glcanvas';
var glCanvas = null;


// ロード時
window.onload = function(){
    // model.jsonからLive2Dモデル情報取得し生成
    jsonloader(MODEL_PATH + MODEL_JSON);
}

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


/**
* ファイルを配列としてロードする
*/
function jsonloader(filepath){
    var request = new XMLHttpRequest();
    request.open("GET", filepath, true);
    request.onreadystatechange = function(){
        if(request.readyState == 4 && request.status == 200){
            // model.jsonから取得
            var jsondata = JSON.parse(request.responseText);
            // 引数はCanvasID, json, 表示スケール(省略可)
            glCanvas = new Simple(CANVAS_ID, jsondata , 2.0);
            // モーション分の選択ボタンを作る
            make_optionbtn();
        }
    }
    request.send(null);
}


/****************************************
* Simpleを拡張したクラス
****************************************/
var Simple = function(canvasid, json, modelscale) {
    // optional
    if(modelscale == null) modelscale = 2.0;
    // Live2Dモデル管理クラスのインスタンス化
    this.live2DMgr = new LAppLive2DManager();
    // Live2Dモデルのインスタンス
    this.live2DModel = null;
    // アニメーションを停止するためのID
    this.requestID = null;
    // モデルのロードが完了したら true
    this.loadLive2DCompleted = false;
    // モデルの初期化が完了したら true
    this.initLive2DCompleted = false;
    // WebGL Image型オブジェクトの配列
    this.loadedImages = [];
    // モーション
    this.motions = [];
    // モーション管理マネジャー
    this.motionMgr = null;
    // モーション番号
    this.motionnm = 0;
    // モーションフラグ
    this.motionflg = false;
    // サウンド
    this.sounds = [];
    // サウンド番号
    this.soundnm = 0;
    // 前に流したサウンド
    this.beforesound = 0;

    // Live2D モデル設定
    this.modelDef = json;
    // Live2DモデルのOpenGL表示サイズ
    this.modelscale = modelscale;
    // フェードイン
    this.fadeines = [];
    // フェードアウト
    this.fadeoutes = [];
    // ポーズ
    this.pose = null;
    // 物理演算
    this.physics = null;

    // Live2Dの初期化
    Live2D.init();

    // canvasオブジェクトを取得
    this.canvas = document.getElementById(canvasid);
    this.canvas.width = CANVAS_WIDTH;
    this.canvas.height = CANVAS_HEIGHT;

    // コンテキストを失ったとき
    this.canvas.addEventListener("webglcontextlost", function(e) {
        console.log("context lost");
        this.loadLive2DCompleted = false;
        this.initLive2DCompleted = false;

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

        e.preventDefault();
    }, false);

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

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


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

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

    //------------ Live2Dの初期化 ------------
    // コールバック対策用
    var that = this;
    // mocファイルからLive2Dモデルのインスタンスを生成
    this.loadBytes(MODEL_PATH + that.modelDef.model, function(buf){
        that.live2DModel = Live2DModelWebGL.loadModel(buf);
    });

    // テクスチャの読み込み
    var loadCount = 0;
    for(var i = 0; i < that.modelDef.textures.length; i++){
        (function ( tno ){// 即時関数で i の値を tno に固定する(onerror用)
            that.loadedImages[tno] = new Image();
            that.loadedImages[tno].src = MODEL_PATH + that.modelDef.textures[tno];
            that.loadedImages[tno].onload = function(){
                if((++loadCount) == that.modelDef.textures.length) {
                    that.loadLive2DCompleted = true;//全て読み終わった
                }
            }
            that.loadedImages[tno].onerror = function() {
                console.log("Failed to load image : " + that.modelDef.textures[tno]);
            }
        })( i );
    }

    var motion_keys = [];   // モーションキー配列
    var mtn_tag = 0;        // モーションタグ
    var mtn_num = 0;        // モーションカウント
    // keyを取得
    for(var key in that.modelDef.motions){
        // moitons配下のキーを取得
        motion_keys[mtn_tag] = key;
        // 読み込むモーションファイル数を取得
        mtn_num += that.modelDef.motions[motion_keys[mtn_tag]].length;
        mtn_tag++;
    }

    // モーションタグ分ループ
    for(var mtnkey in motion_keys){
        // モーションとサウンドを読み込む(motions配下のタグを読み込む)
        for(var j = 0; j < that.modelDef.motions[motion_keys[mtnkey]].length; j++){
            // モーションの数だけロード
            that.loadBytes(MODEL_PATH + that.modelDef.motions[motion_keys[mtnkey]][j].file, function(buf){
                that.motions.push(Live2DMotion.loadMotion(buf));
            });
            // サウンドの数だけロード
            if(that.modelDef.motions[motion_keys[mtnkey]][j].sound == null){
                that.sounds.push("");
            }else{
                that.sounds.push(new Sound(MODEL_PATH + that.modelDef.motions[motion_keys[mtnkey]][j].sound));
            }
            // フェードイン
            if(that.modelDef.motions[motion_keys[mtnkey]][j].fade_in == null){
                that.fadeines.push("");
            }else{
                that.fadeines.push(that.modelDef.motions[motion_keys[mtnkey]][j].fade_in);
            }
            // フェードアウト
            if(that.modelDef.motions[motion_keys[mtnkey]][j].fade_out == null){
                that.fadeoutes.push("");
            }else{
                that.fadeoutes.push(that.modelDef.motions[motion_keys[mtnkey]][j].fade_out);
            }
        }
    }

    // モーションマネジャーのインスタンス化
    that.motionMgr = new L2DMotionManager();

    // ポーズのロード(json内のposeがあるかチェック)
    if(that.modelDef.pose !== void 0){
        that.loadBytes(MODEL_PATH + that.modelDef.pose, function(buf){
            // ポースクラスのロード
            that.pose = L2DPose.load(buf);
        });
    }

    // 物理演算のロード(json内のphysicsがあるかチェック)
    if(that.modelDef.physics !== void 0){
        that.loadBytes(MODEL_PATH + that.modelDef.physics, function(buf){
            // 物理演算クラスのロード
            that.physics = L2DPhysics.load(buf);
        });
    }

    //------------ 描画ループ ------------
    (function tick() {
        that.draw(gl, that); // 1回分描画

        var requestAnimationFrame =
            window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
        that.requestID = requestAnimationFrame( tick , that.canvas );// 一定時間後に自身を呼び出す
    })();
};


/**
* Live2Dの描画処理
*/
Simple.prototype.draw = function(gl/*WebGLコンテキスト*/, that)
{
    // Canvasをクリアする
    gl.clear(gl.COLOR_BUFFER_BIT);

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

    // ロード完了後に初回のみ初期化する
    if( ! that.initLive2DCompleted ){
        that.initLive2DCompleted = true;
        // 画像からWebGLテクスチャを生成し、モデルに登録
        for( var i = 0; i < that.loadedImages.length; i++ ){
            //Image型オブジェクトからテクスチャを生成
            var texName = that.createTexture(gl, that.loadedImages[i]);

            that.live2DModel.setTexture(i, texName); //モデルにテクスチャをセット
        }

        // テクスチャの元画像の参照をクリア
        that.loadedImages = null;
        // OpenGLのコンテキストをセット
        that.live2DModel.setGL(gl);
        // 表示位置を指定するための行列を定義する
        var s = that.modelscale / that.live2DModel.getCanvasWidth();
        var matrix4x4 = [
             s, 0, 0, 0,
             0,-s, 0, 0,
             0, 0, 1, 0,
            -that.modelscale/2, that.modelscale/2, 0, 1
        ];
        that.live2DModel.setMatrix(matrix4x4);
    }

    // アイドルモーション以外の場合(フラグと優先度で判定する)
    if(that.motionflg == true && that.motionMgr.getCurrentPriority() == 0){
        // フェードインの設定
        that.motions[that.motionnm].setFadeIn(that.fadeines[that.motionnm]);
        // フェードアウトの設定
        that.motions[that.motionnm].setFadeOut(that.fadeoutes[that.motionnm]);
        // アイドルモーションよりも優先度を高く再生する
        that.motionMgr.startMotion(that.motions[that.motionnm], 1);
        that.motionflg = false;
        // 音声ファイルもあれば再生
        if(that.sounds[that.motionnm]){
            // 前回の音声があれば停止する
            if(that.sounds[that.beforesound] != ""){
                that.sounds[that.beforesound].stop();
            }
            // 音声を再生
            that.sounds[that.motionnm].play();
            // 途中で停止できるように格納する
            that.beforesound = that.motionnm;
        }
    }

    // モーションが終了していたらアイドルモーションの再生
    if(that.motionMgr.isFinished() && that.motionnm != null){
        // フェードインの設定
        that.motions[that.motionnm].setFadeIn(that.fadeines[that.motionnm]);
        // フェードアウトの設定
        that.motions[that.motionnm].setFadeOut(that.fadeoutes[that.motionnm]);
        // 優先度は低めでモーション再生
        that.motionMgr.startMotion(that.motions[that.motionnm], 0);
        // 音声ファイルもあれば再生
        if(that.sounds[that.motionnm]){
            // 前回の音声があれば停止する
            if(that.sounds[that.beforesound] != ""){
                that.sounds[that.beforesound].stop();
            }
            // 音声を再生
            that.sounds[that.motionnm].play();
            // 途中で停止できるように格納する
            that.beforesound = that.motionnm;
        }
    }
    // モーション指定されていない場合は何も再生しない
    if(that.motionnm != null){
        // モーションパラメータの更新
        that.motionMgr.updateParam(that.live2DModel);
    }

    // ポーズパラメータの更新
    if(that.pose != null)that.pose.updateParam(that.live2DModel);

    // 物理演算パラメータの更新
    if(that.physics != null)that.physics.updateParam(that.live2DModel);

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


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


/**
* Image型オブジェクトからテクスチャを生成
*/
Simple.prototype.createTexture = function(gl/*WebGLコンテキスト*/, image/*WebGL Image*/)
{
    var texture = gl.createTexture(); //テクスチャオブジェクトを作成する
    if ( !texture ){
        console.log("Failed to generate gl texture name.");
        return -1;
    }
    // 乗算アルファ対応
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1); 
    // imageを上下反転
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
    // テクスチャのユニットを指定する
    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.prototype.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:
            console.log( "Failed to load (" + request.status + ") : " + path );
            break;
        }
    }
    request.send(null);
};


/****************************************
* サウンドクラス
****************************************/
var Sound = function(path   /*音声ファイルパス*/) {
    this.snd = document.createElement("audio");
    this.snd.src = path;
};

/**
* 音声再生
*/
Sound.prototype.play = function() {
    this.snd.play();
};

/**
* 音声停止
*/
Sound.prototype.stop = function() {
    this.snd.pause();
    this.snd.currentTime = 0;
};


/****************************************
* ボタンイベント処理
****************************************/
// モーションファイル名取得
var mtnfilenames = [];
// モーションボタン作る場所
var selectmenu = document.getElementById('selectmenu');

// モーション分の選択ボタンを作る
function make_optionbtn(){
    // モーションファイル名を取得する
    if(glCanvas != null && mtnfilenames.length == 0){
        for(var key in glCanvas.modelDef.motions){
            console.log(key);
            for(j = 0; j < glCanvas.modelDef.motions[key].length; j++){
                // 余分なパスをカット
                var strfilenm = glCanvas.modelDef.motions[key][j].file.split("/");
                // 読み込むモーションファイル名を取得
                mtnfilenames.push(strfilenm[1]);
                // オプションボタンを追加
                var option = document.createElement('option');
                option.value = strfilenm[1];
                option.appendChild(document.createTextNode(strfilenm[1]));
                selectmenu.appendChild(option);
            }
        }
    }
}


// モーション切り替え
function motionChange(){
    // 選択ボタンの値を取得
    var mtnfilenm = selectmenu.value;

    var cnt = 0;
    // ファイル名からファイル番号を取り出す
    for(var k = 0; k < mtnfilenames.length; k++){
        if(mtnfilenm == mtnfilenames[k]){
            break;
        }
        cnt++;
    }
    // Live2Dモデルに渡す
    glCanvas.motionnm = cnt;
    glCanvas.motionflg = true;
}

その他ハマッたこと

・WebGL版の場合、motionファイルが多いとロードに時間がかかり、モーション切替で違うモーションが再生される
・jsonファイルがBOM付きだとバイナリ読込でエラーとなるので、BOM削除した
・IEとmobilesafariだと透明部分が黒くなる時がある。しかもiPhone Simulatorと実機のsafariで挙動が違う
症例.png

Stackoverflowで透過部分が2px必要?の記事見て、透過部分を増やすとmobilesafariは解決したけど、IEは解決しなかった...。
結局、専用のツールをカスタマイズしてもらって対応しました(テクスチャゴミ取りツール

参考URL:Live2D Document - WebGL版で境界線が出る

14
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
19