LoginSignup
16
16

More than 5 years have passed since last update.

簡易Live2DViewer作ってみた

Last updated at Posted at 2015-08-06

Live2D SDK WebGL版で簡易Live2DViewer作ってみました。
Live2Dモデルのファイルをサーバーにアップロードさせたくなかったので、全てHTML5の機能で実装してます。
ファイル選択とファイルのドラッグ&ドロップに対応しています
(テクスチャは複数選択してドラッグ&ドロップして下さい)

簡易Live2DViewer
schreenshot.png

フォルダ構成

Live2D SDKのSimpleプロジェクトを元にカスタムしています。

Live2D_DragDrop

  index.html

├─framework
    Live2DFramework.js・・・SDK付属のフレームワーク
    
├─lib
    live2d.min.js・・・Live2Dのコアライブラリ
    
└─src
      Simple.js

ソース

既存のファイル選択ボタンだとダサいので、ダミーボタンを表示してファイル選択処理を呼んでいます

index.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>
    <style>
        /* 選択したファイルのサムネイル表示 */
      .thumb {
        height: 75px;
        border: 1px solid #000;
        margin: 10px 5px 0 0;
      }
      .drop_area{
          border:dashed 2px #ccc;
          width:512px;
          height:80px;
          margin:4px;
          background-color:#efefef;
      }
      .moc_area, .texture_area, .motion_area{
          padding-top:10px;
          font-size: 18px;
          color:blue;
          text-align: center;
      }
      #select_moc, #select_texture, #select_motion{
          display:none;
      }
      #dummy_moc_btn, #dummy_texture_btn, #dummy_motion_btn{
          margin-top:10px;
          margin-left:150px;
          font-size: 12px;
          text-decoration: none;
          color:#fff;
          background-color: #49a9d4;
          border:none;
          border-radius: 5px;
          padding:2px 16px 2px 16px;
      }
      #dummy_moc_file, #dummy_motion_file{
          width:200px;
          height: 20px;
          font-size: 14px;
          border:none;
          background-color: #e6e6e6;
      }
      #displaybtn{
          width: 160px;
          height:40px;
          margin-left:4px;
          margin-bottom:4px;
          font-size:20px;
          font-weight: bold;
          color:#fff;
          background-color: #49a9d4;
          border:none;
          border-radius: 5px;
          padding:2px 16px 2px 16px;
          box-shadow:2px 2px #1a6ea0;
          text-shadow:1px 1px #555;
      }
      #displaybtn:hover{
          color:#fff;
          background-color: #0F7DAE;
          box-shadow: 2px 2px #073E5F;
      }
      .live2d_area{
          position: absolute;
          top:4px;
          left:528px;
      }
      #glcanvas{
          border:dashed 2px #CCC;
          margin:4px;
      }
    </style>
    </head>
    <body>
        <!-- mocファイル -->
        <div class="drop_area" id="drop_moc">
            <div class="moc_area">mocをドロップ</div>        
            <input type="file" id="select_moc" name="files" />
            <button id="dummy_moc_btn" onclick="dummy_moc_click()">mocファイル選択</button>
            <input type="text" id="dummy_moc_file" readonly>
        </div>
        <!-- textureファイル -->
        <div class="drop_area" id="drop_texture">
            <div class="texture_area">texutureをドロップ</div>        
            <input type="file" id="select_texture" name="files[]" multiple />            
            <button id="dummy_texture_btn" onclick="dummy_texture_click()">textureファイル選択</button>
        </div>
        <!-- 選択したしたテクスチャを表示する部分 -->
        <output id="texture_list"></output>
        <br>
        <!-- motionファイル -->
        <div class="drop_area" id="drop_motion">
            <div class="motion_area">motionをドロップ</div>        
            <input type="file" id="select_motion" name="files" />
            <button id="dummy_motion_btn" onclick="dummy_motion_click()">motionファイル選択</button>
            <input type="text" id="dummy_motion_file" readonly>
        </div>
        <!-- モデル表示ボタン -->
        <button id="displaybtn" onclick="Simple()">モデル表示</button>
        <div class="live2d_area">
            <!-- Live2Dモデル -->
            <canvas id="glcanvas" width = "512px" height="512px"></canvas>
            <div id="myconsole" style="color:#BBB">---- Log ----</div>                    
        </div>
        <!-- Live2D Library -->
        <script src="lib/live2d.min.js"></script>        
        <script src="framework/Live2DFramework.js"></script>
        <!-- User's Script -->
        <script src="src/Simple.js"></script>
    </body>
</html>

元々はLive2Dモデルファイル(.moc、.mtn)はXMLHttpRequestのarraybuffer形式でロードしてました。
今回はファイル選択形式なので、FileReaderでBase64形式にしてロードしてます。
Base64は小さい画像のみ有効かと思ったら、1024px x 1024pxのテクスチャ5枚とかもさくっとロードしてくれました。

Simple.js
/******************** mocファイル ********************/
var binarymoc = null;
// mocファイルのファイル選択ボタン(非表示でダミーボタンで装飾)
var select_moc = document.getElementById('select_moc');
select_moc.addEventListener('change', mocSelect, false);
// mocファイル選択名
var dummy_moc_file = document.getElementById('dummy_moc_file');
// mocファイルのドロップ領域
var drop_moc = document.getElementById('drop_moc');
drop_moc.addEventListener('drop', mocDrop);
drop_moc.addEventListener('dragover', mocDragOver);
drop_moc.addEventListener('dragleave', mocDragLeave);

// dummy用のmocファイル選択ボタンからイベント呼び出す
function dummy_moc_click(){
    select_moc.click();
}
// mocファイルドラッグオーバー時
function mocDragOver(event){
    drop_moc.style.backgroundColor = "#C1C1C1"; // hover色
    event.preventDefault();
}
// mocファイルドラッグリーブ時
function mocDragLeave(event){
    drop_moc.style.backgroundColor = "#efefef"; // 元の色
    event.preventDefault();    
}
// mocファイルドロップ時
function mocDrop(evt){
    // ドロップしたファイル情報
    var files = evt.dataTransfer.files;
    moc_load(files);
    evt.preventDefault();
    drop_moc.style.backgroundColor = "#efefef"; // 元の色
}
// mocファイル選択時
function mocSelect(evt) {
    // 選択したファイル情報
    var files = evt.target.files;
    moc_load(files);
}
// mocファイルロード処理
function moc_load(files){
    // 初期化処理
    binarymoc = null;
    dummy_moc_file.value = "";
    for (var i = 0, f; f = files[i]; i++) {
        var reader = new FileReader();
        // 選択したファイル名をセット
        dummy_moc_file.value = files[i].name;
        reader.onload = (function(theFile) {
            return function(e) {
                // moc中身をセット(Base64形式)
                binarymoc = e.target.result;
            };
        })(f);
        // arrayBuffer形式
        reader.readAsArrayBuffer(f);
    }    
}

/******************** textureファイル ********************/
var binarytextures = [];
// textureファイルの選択ボタン(非表示でダミーボタンで装飾)
var select_texture = document.getElementById('select_texture');
select_texture.addEventListener('change', TextureSelect, false);
// textureファイルのドロップ領域
var drop_texture = document.getElementById('drop_texture');
drop_texture.addEventListener('drop', textureDrop);
drop_texture.addEventListener('dragover', textureDragOver);
drop_texture.addEventListener('dragleave', textureDragLeave);

// textureファイル選択ボタンからイベント呼び出す
function dummy_texture_click(){
    select_texture.click();
}
// textureファイルドラッグオーバー時
function textureDragOver(event){
    drop_texture.style.backgroundColor = "#C1C1C1"; // hover色
    event.preventDefault();
}
// textureファイルドラッグリーブ時
function textureDragLeave(event){
    drop_texture.style.backgroundColor = "#efefef"; // 元の色
    event.preventDefault();    
}
// textureファイルドロップ時
function textureDrop(evt){
    // ドロップしたファイル情報
    var files = evt.dataTransfer.files;
    texture_load(files);
    evt.preventDefault();
    drop_texture.style.backgroundColor = "#efefef"; // 元の色
}
// textureファイル選択時
function TextureSelect(evt) {
    // 選択したファイル情報
    var files = evt.target.files;
    texture_load(files);
}

// textureファイルロード処理
function texture_load(files){
    // 配列をクリアしておく
    binarytextures = [];
    // 前回のサムネイルを削除する
    var output = document.getElementById('texture_list');
    while(output.firstChild){
        output.removeChild(output.firstChild);
    }

    for (var i = 0, f; f = files[i]; i++) {
        var reader = new FileReader();
        // 非同期処理のため、ファイル番号を保持しておく
        reader.filenm = i;
        reader.onload = (function(theFile) {
            return function(e) {
                // ロードしたテクスチャのサムネイル表示する
                var span = document.createElement('span');
                span.innerHTML = ['<img class="thumb" src="', e.target.result,
                                    '" title="', escape(theFile.name), '"/>'].join('');
                document.getElementById('texture_list').insertBefore(span, null);
                // 変数にBase64エンコード形式で保持
                binarytextures[e.target.filenm] = e.target.result;
            };
        })(f);
        // 画像はこちらをつかう
        reader.readAsDataURL(f);
    }    
}

/******************** motionファイル ********************/
var binarymotion = null;
// motionファイルのファイル選択ボタン(非表示でダミーボタンで装飾)
var select_motion = document.getElementById('select_motion');
select_motion.addEventListener('change', motionSelect, false);
// motionファイル選択名
var dummy_motion_file = document.getElementById('dummy_motion_file');
// motionファイルのドロップ領域
var drop_motion = document.getElementById('drop_motion');
drop_motion.addEventListener('drop', motionDrop);
drop_motion.addEventListener('dragover', motionDragOver);
drop_motion.addEventListener('dragleave', motionDragLeave);

// dummy用のmotionファイル選択ボタンからイベント呼び出す
function dummy_motion_click(){
    select_motion.click();
}
// motionファイルドラッグオーバー時
function motionDragOver(event){
    drop_motion.style.backgroundColor = "#C1C1C1"; // hover色
    event.preventDefault();
}
// motionファイルドラッグリーブ時
function motionDragLeave(event){
    drop_motion.style.backgroundColor = "#efefef"; // 元の色
    event.preventDefault();    
}
// motionファイルドロップ時
function motionDrop(evt){
    // ドロップしたファイル情報
    var files = evt.dataTransfer.files;
    motion_load(files);
    evt.preventDefault();
    drop_motion.style.backgroundColor = "#efefef"; // 元の色
}
// motionファイル選択時
function motionSelect(evt) {
    // 選択したファイル情報
    var files = evt.target.files;
    motion_load(files);
}
// motionファイルロード処理
function motion_load(files){
    // 初期化処理
    binarymotion = null;
    dummy_motion_file.value = "";
    for (var i = 0, f; f = files[i]; i++) {
        var reader = new FileReader();
        // 選択したファイル名をセット
        dummy_motion_file.value = files[i].name;
        reader.onload = (function(theFile) {
            return function(e) {
                // motion中身をセット(Base64形式)
                binarymotion = e.target.result;
                Simple();
            };
        })(f);
        // arrayBuffer形式
        reader.readAsArrayBuffer(f);
    }    
}


// 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"
//        ]
//    };
    this.motion = null;     // モーション
    this.motionMgr = null;  // モーションマネジャー

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


    // canvasオブジェクトを取得
    var canvas = document.getElementById("glcanvas");

    // コンテキストを失ったとき

    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);
//  });
        live2DModel = Live2DModelWebGL.loadModel(binarymoc);

    // テクスチャの読み込み
    var loadCount = 0;
//  for(var i = 0; i < modelDef.textures.length; i++){
    for(var i = 0; i < binarytextures.length; i++){
        (function ( tno ){// 即時関数で i の値を tno に固定する(onerror用)
            loadedImages[tno] = new Image();
//          loadedImages[tno].src = modelDef.textures[tno];
            loadedImages[tno].src = binarytextures[i];
            loadedImages[tno].onload = function(){
//              if((++loadCount) == modelDef.textures.length) {
                if((++loadCount) == binarytextures.length) {
                    loadLive2DCompleted = true;//全て読み終わった
                }
            }
//          loadedImages[tno].onerror = function() { 
//              Simple.myerror("Failed to load image : " + modelDef.textures[tno]); 
//          }
        })( i );
    }
        // モーションのロード
        motion = new Live2DMotion.loadMotion(binarymotion);
        // モーションマネージャのインスタンス化
        motionMgr = new L2DMotionManager();

    //------------ 描画ループ ------------

    (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));
    // モーションが終了していたらモーションの再生
    if(motionMgr.isFinished()){
        motionMgr.startMotion(motion);
    }
    motionMgr.updateParam(live2DModel);

    // 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>");
};

課題

テクスチャのロードは、0番から正しく順番に読み込まれないと表示が崩れます。

あと、Live2DViewerだとmocやmtnファイルパスを書いたmodel.json1つロードするだけでモデルが表示できます。
HTML5だとセキュリティ上、ファイルを直接選択する必要があるのでmodel.jsonからロードはできない…。
(結局はphpでファイルアップロードさせる方がいいかも)

ただ、Live2DとWebGLで作ったポストエフェクトなどはユーザが作ったモデルで試せるサイトが作れるので、何か面白いもの作ったらこれで公開していこうと思います。

16
16
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
16
16