Help us understand the problem. What is going on with this article?

Live2D WebGLをES6に書き直し

More than 1 year has passed since last update.

今更ながらES6を勉強したので、Live2D WebGL版をES6にかき直してみた。
screenshot.png

開発環境

Live2D WebGL SDK2.1のSimpleプロジェクト

今回使ったものは以下の通り。まだ勉強中なので色々と間違ってるかも。
それにしてもソースがすっきりして素晴らしいっ!

・const/let
・class構文
・Promise
・arrow関数
・Fetch API

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>
<canvas id="glcanvas" style="border:dashed 1px #CCC"></canvas>
<!-- Live2D Library -->
<script src="../../lib/live2d.min.js"></script>
<!-- User's Script -->
<script src="src/Simple.js"></script>
</body>
</html>

javascript部分

Simple.js
'use strict';

const CANVAS_SIZE = 512;        // canvasサイズ
const CANVAS_ID = 'glcanvas';   // canvasID
// Live2D モデル設定
const MODEL_DEF = {
    "type":"Live2D Model Setting",
    "name":"haru",
    "model":"assets/haru/haru_02.moc",
    "textures":[
        "assets/haru/haru_02.1024/texture_00.png",
        "assets/haru/haru_02.1024/texture_01.png",
        "assets/haru/haru_02.1024/texture_02.png",
    ]
};


window.onload = () => {
    let Live2DModel = new Simple(CANVAS_SIZE, CANVAS_ID, MODEL_DEF);
};


class Simple{
    /*
     * コンストラクタ
     * @param {number} canSize
     * @param {string} canId
     */
    constructor(canSize, canId, modelDef){
        // Live2Dモデルのインスタンス
        this.live2DModel = null;
        // アニメーションを停止するためのID
        this.requestID = null;
        // モデルの初期化が完了したら true
        this.initLive2DCompleted = false;
        // WebGL Image型オブジェクトの配列
        this.loadedImages = [];
        // Live2D モデル設定
        this.modelDef = modelDef;
        // Live2Dの初期化
        Live2D.init();
        // canvasオブジェクトを取得
        this.canvas = document.getElementById(canId);
        this.canvas.width = this.canvas.height = canSize;
        // コンテキストを失ったとき
        this.canvas.addEventListener("webglcontextlost", (e) => {
            console.error("context lost");
            this.initLive2DCompleted = false;
            // アニメーションを停止
            let cancelAnimationFrame =
                window.cancelAnimationFrame ||
                window.mozCancelAnimationFrame;
            cancelAnimationFrame(this.requestID);
            e.preventDefault();
        }, false);
        // コンテキストが復元されたとき
        this.canvas.addEventListener("webglcontextrestored", (e) => {
            console.error("webglcontext restored");
            this.initLoop(this.canvas);
        }, false);
        // Init and start Loop
        this.initLoop(this.canvas);
    }

    /***
     * WebGLコンテキストを取得・初期化。
     * Live2Dの初期化、描画ループを開始。
     * @param {canvas} canvas
     */
    initLoop(canvas){
        // WebGLのコンテキストを取得する
        let para = {
            premultipliedAlpha : true,
    //        alpha : false
        };
        this.gl = this.getWebGLContext(canvas, para);
        if (!this.gl) {
            console.error("Failed to create WebGL context.");
            return;
        }
        // OpenGLのコンテキストをセット
        Live2D.setGL(this.gl);
        // mocファイルからLive2Dモデルのインスタンスを生成
        const modelBuffer = fetch(this.modelDef.model).then(res => {
            return res.arrayBuffer();
        }).then(arrayBuffer => {
            this.live2DModel = Live2DModelWebGL.loadModel(arrayBuffer);
        });
        // テクスチャの読み込み
        let promises = [];
        for(let i = 0; i < this.modelDef.textures.length; i++){
            promises[i] = this.loadTextureImage(i);
        }
        // 全部テクスチャロードしたら次の処理へ
        Promise.all(promises).then(() => {
            this.tick();
        });
    }

    /***
     * テクスチャを読み込みPromiseを返します。
     * @param {Number} i
     * @returns {Promise}
     */
    loadTextureImage(i) {
        this.loadedImages[i] = new Image();
        return new Promise((resolve, reject) => {
            this.loadedImages[i].addEventListener('load', (e) => {
                resolve(this.loadedImages[i]);
            });
            this.loadedImages[i].src = this.modelDef.textures[i];
        });
    }

    /***
     * ループ処理
     */
    tick(){
        this.draw(); // 1回分描画
        let requestAnimationFrame =
            window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
            this.requestID = requestAnimationFrame(this.tick.bind(this));
    }

    /***
     * 描画処理
     */
    draw(){
        // 描画エリアをクリア
        this.gl.clearColor( 0.0 , 0.0 , 0.0 , 0.0 );
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);
        // Live2D初期化
        if(!this.live2DModel)
            return; //ロードが完了していないので何もしないで返る
        // ロード完了後に初回のみ初期化する
        if(!this.initLive2DCompleted){
            this.initLive2DCompleted = true;
            // 画像からWebGLテクスチャを生成し、モデルに登録
            for(let i = 0; i < this.loadedImages.length; i++){
                //Image型オブジェクトからテクスチャを生成
                let texName = this.createTexture(this.gl, this.loadedImages[i]);
                this.live2DModel.setTexture(i, texName); //モデルにテクスチャをセット
            }
            // テクスチャの元画像の参照をクリア
            this.loadedImages = null;

            // 表示位置を指定するための行列を定義する
            let s = 2.0 / this.live2DModel.getCanvasWidth(); //canvasの横幅を-1..1区間に収める
            let matrix4x4 = [
                s, 0, 0, 0,
                0,-s, 0, 0,
                0, 0, 1, 0,
               -1, 1, 0, 1
            ];
            this.live2DModel.setMatrix(matrix4x4);
        }

        // キャラクターのパラメータを適当に更新
        let t = UtSystem.getUserTimeMSec() * 0.001 * 2 * Math.PI; //1秒ごとに2π(1周期)増える
        let cycle = 3.0; //パラメータが一周する時間(秒)
        // PARAM_ANGLE_Xのパラメータが[cycle]秒ごとに-30から30まで変化する
        this.live2DModel.setParamFloat("PARAM_ANGLE_X", 30 * Math.sin(t/cycle));
        this.live2DModel.setParamFloat("PARAM_EYE_R_OPEN", 1 * Math.sin(t/cycle));
        this.live2DModel.setParamFloat("PARAM_EYE_L_OPEN", 1 * Math.sin(t/cycle));
        // Live2Dモデルを更新して描画
        this.live2DModel.update(); // 現在のパラメータに合わせて頂点等を計算
        this.live2DModel.draw();   // 描画
    }

    /***
     * WebGLのコンテキストを取得する
     * @param {canvas} canvas
     */
    getWebGLContext(canvas){
        let NAMES = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
        let param = {
            alpha : true,
            premultipliedAlpha : true
        };

        for(let i = 0; i < NAMES.length; i++){
            try{
                let ctx = canvas.getContext(NAMES[i], param);
                if(ctx) return ctx;
            }
            catch(e){}
        }
        return null;
    }

    /***
     * Image型オブジェクトからテクスチャを生成
     * @param {gl} gl
     * @param {Image} image
     * @returns {Texture}
     */
    createTexture(gl, image){
        let texture = gl.createTexture(); //テクスチャオブジェクトを作成する
        if (!texture){
            console.error("Failed to generate gl texture name.");
            return -1;
        }

        if(this.live2DModel.isPremultipliedAlpha() == false){
            // 乗算済アルファテクスチャ以外の場合
            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;
    }
};

参考URL

お疲れさまXMLHttpRequest、こんにちはfetch

モーション再生版も修正

==== 2017/02/26追記 ====
ちなみに以下記事のモーション切り替え版も修正してみた。
モーションファイル読み込みもfetchさせたら上手くいきました。
Live2D WebGL版のモーション切替する

simple.js
'use strict';

const CANVAS_SIZE = 512;        // canvasサイズ
const CANVAS_ID = 'glcanvas';   // canvasID
var MODEL_PATH = "assets/haru/";
var MODEL_DEFINE = {
    "type":"Live2D Model Setting",
    "name":"haru",
    "model": MODEL_PATH + "haru_01.moc",
    "textures":[
        MODEL_PATH + "haru_01.1024/texture_00.png",
        MODEL_PATH + "haru_01.1024/texture_01.png",
        MODEL_PATH + "haru_01.1024/texture_02.png",
    ],
    "motions":[
        MODEL_PATH + "motions/idle_00.mtn",
        MODEL_PATH + "motions/shake_00.mtn",
        MODEL_PATH + "motions/tapBody_05.mtn",
    ],
};

window.onload = () => {
    let Live2DModel = new Simple();
};


class Simple{
    /*
     * コンストラクタ
     */
    constructor(){
        // Live2Dモデルのインスタンス
        this.live2DModel = null;
        // アニメーションを停止するためのID
        this.requestID = null;
        // モデルの初期化が完了したら true
        this.initLive2DCompleted = false;
        // WebGL Image型オブジェクトの配列
        this.loadedImages = [];
        // モーション
        this.motions = [];
        // モーション管理マネジャー
        this.motionMgr = null;
        // モーション番号
        this.motionnm = 0;
        // モーションチェンジ
        this.motionchange = false;
        // Live2D モデル設定
        this.modelDef = MODEL_DEFINE;

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

        // canvasオブジェクトを取得
        this.canvas = document.getElementById(CANVAS_ID);
        this.canvas.width = this.canvas.height = CANVAS_SIZE;
        // コンテキストを失ったとき
        this.canvas.addEventListener("webglcontextlost", (e) => {
            console.error("context lost");
            this.initLive2DCompleted = false;
            // アニメーションを停止
            let cancelAnimationFrame =
                window.cancelAnimationFrame ||
                window.mozCancelAnimationFrame;
            cancelAnimationFrame(this.requestID);
            e.preventDefault();
        }, false);
        // コンテキストが復元されたとき
        this.canvas.addEventListener("webglcontextrestored", (e) => {
            console.error("webglcontext restored");
            this.initLoop(this.canvas);
        }, false);
        // Init and start Loop
        this.initLoop(this.canvas);
    }

    /***
     * WebGLコンテキストを取得・初期化。
     * Live2Dの初期化、描画ループを開始。
     * @param {canvas} canvas
     */
    initLoop(canvas){
        // WebGLのコンテキストを取得する
        let para = {
            premultipliedAlpha : true,
    //        alpha : false
        };
        this.gl = this.getWebGLContext(canvas, para);
        if (!this.gl) {
            console.error("Failed to create WebGL context.");
            return;
        }
        // OpenGLのコンテキストをセット
        Live2D.setGL(this.gl);

        // mocファイルからLive2Dモデルのインスタンスを生成
        const modelBuffer = fetch(this.modelDef.model).then(res => {
            return res.arrayBuffer();
        }).then(arrayBuffer => {
            this.live2DModel = Live2DModelWebGL.loadModel(arrayBuffer);
        });
        // テクスチャの読み込み
        let promises = [];
        for(let i = 0; i < this.modelDef.textures.length; i++){
            promises[i] = this.loadTextureImage(i);
        }
        // 全部テクスチャロードしたら次の処理へ
        Promise.all(promises).then(() => {
            this.loadMotionFile();
            this.tick();
        });
    }

    /***
     * テクスチャを読み込みPromiseを返します。
     * @param {Number} i
     * @returns {Promise}
     */
    loadTextureImage(i) {
        this.loadedImages[i] = new Image();
        return new Promise((resolve, reject) => {
            this.loadedImages[i].addEventListener('load', (e) => {
                resolve(this.loadedImages[i]);
            });
            this.loadedImages[i].src = this.modelDef.textures[i];
        });
    }

    /***
     * モーションを読み込みます
     */
    loadMotionFile(){
        // モーションのロード
        for(var i = 0; i < this.modelDef.motions.length; i++){
            fetch(this.modelDef.motions[i]).then(res => {
                return res.arrayBuffer();
            }).then(arrayBuffer => {
                this.motions.push(Live2DMotion.loadMotion(arrayBuffer));
            });
        }
        // モーションマネジャーのインスタンス化
       this.motionMgr = new L2DMotionManager();

        // マウスクリックイベント
        this.canvas.addEventListener("click", e => {
            this.motionchange = true;
            if(this.motions.length - 1  > this.motionnm){
                this.motionnm++;
            }else{
                this.motionnm = 0;
            }
        }, false);
    }

    /***
     * ループ処理
     */
    tick(){
        this.draw(); // 1回分描画
        let requestAnimationFrame =
            window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
            this.requestID = requestAnimationFrame(this.tick.bind(this));
    }

    /***
     * 描画処理
     */
    draw(){
        // 描画エリアをクリア
        this.gl.clearColor( 0.0 , 0.0 , 0.0 , 0.0 );
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);
        // Live2D初期化
        if(!this.live2DModel)
            return; //ロードが完了していないので何もしないで返る
        // ロード完了後に初回のみ初期化する
        if(!this.initLive2DCompleted){
            this.initLive2DCompleted = true;
            // 画像からWebGLテクスチャを生成し、モデルに登録
            for(let i = 0; i < this.loadedImages.length; i++){
                //Image型オブジェクトからテクスチャを生成
                let texName = this.createTexture(this.gl, this.loadedImages[i]);
                this.live2DModel.setTexture(i, texName); //モデルにテクスチャをセット
            }
            // テクスチャの元画像の参照をクリア
            this.loadedImages = null;

            // 表示位置を指定するための行列を定義する
            let s = 2.0 / this.live2DModel.getCanvasWidth(); //canvasの横幅を-1..1区間に収める
            let matrix4x4 = [
                s, 0, 0, 0,
                0,-s, 0, 0,
                0, 0, 1, 0,
               -1, 1, 0, 1
            ];
            this.live2DModel.setMatrix(matrix4x4);
        }

        // モーションが終了していたら再生する
        if(this.motionMgr.isFinished() || this.motionchange == true ){
            this.motionMgr.startMotion(this.motions[this.motionnm], 0);
            this.motionchange = false;
            console.info("motion:" + this.motionnm);
        }
        // モーション指定されていない場合は何も再生しない
        if(this.motionnm != null){
            // モーションパラメータの更新
            this.motionMgr.updateParam(this.live2DModel);
        }

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

    /***
     * WebGLのコンテキストを取得する
     * @param {canvas} canvas
     */
    getWebGLContext(canvas){
        let NAMES = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
        let param = {
            alpha : true,
            premultipliedAlpha : true
        };

        for(let i = 0; i < NAMES.length; i++){
            try{
                let ctx = canvas.getContext(NAMES[i], param);
                if(ctx) return ctx;
            }
            catch(e){}
        }
        return null;
    }

    /***
     * Image型オブジェクトからテクスチャを生成
     * @param {gl} gl
     * @param {Image} image
     * @returns {Texture}
     */
    createTexture(gl, image){
        let texture = gl.createTexture(); //テクスチャオブジェクトを作成する
        if (!texture){
            console.error("Failed to generate gl texture name.");
            return -1;
        }

        if(this.live2DModel.isPremultipliedAlpha() == false){
            // 乗算済アルファテクスチャ以外の場合
            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;
    }
};
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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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