Live2DをJavascriptで動かす

Live2D楽しそうだなあと思っていたところに、以下の書籍が発売されたので、買ってみました。
紙の書籍だと、腰を落ち着けて読んだり、読み返しながらじっくりと読むことができそうです。
<b>
　「Live2Dの教科書　静止画イラストからつくる本格アニメーション」
　株式会社Live2D　監修/株式会社サイドランチ　著/Crico株式会社 癸のずみ　著
</b>

せっかく、理解が進んだので、Webページを作ってみました。
Live2Dで作成したキャラクタとアニメーションを、Javascript＋Vueで表示させる、というものです。

デスクトップPCで作業されている方はたいていデュアルディスプレイですよね。
だったら、サブディスプレイの方に、Live2Dで作成したキャラクタが動いていたら、なんかうれしいんじゃないかと思いました。

こんな感じです。(念のためモザイクかけてます)

![aaaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/da3e17c3-4268-b4a6-6acb-89bc7d6ff1b1.png)
[モーションGIFはこちら](https://github.com/poruruba/media/blob/master/index.gif)

# 必要なもの

・Live2D Cubism Editor v4

以下のページから、Live2Dの Cubism Editor をインストールしておきます。(Free版で大丈夫です)
　https://www.live2d.com/ 

そして、以下の書籍を購入しましょう。なぜならば、さきほどのモーションGIFに示したかわいらしいキャラクタのモデルとアニメーションがダウンロードできるためです。

・書籍「Live2Dの教科書　静止画イラストからつくる本格アニメーション」
　https://books.mdn.co.jp/books/3218303029/ 

そして、書籍に記載のURLから本紙解説ダウンロードファイルをダウンロードしておきます。

次は、Live2DをWebページで表示するためのSDKです。

・Cubism SDK For JavaScript Components
　https://github.com/Live2D/CubismJsComponents 


# 必要ファイルの抽出

Cubism SDK For JavaScript Components から、以下のファイルを抽出します。

・live2dcubismframework.js
・live2dcubismpixi.js

CubismJsComponents-master\CubismJsComponents-master\example\wwwroot\js にあります。


# Live2Dファイルの用意

以下のファイルを用意します。

* mocファイル(拡張子.moc3)
* テクスチャファイル(拡張子.png)
* モーションファイル(拡張子.motion3.json)


上記は以下の手順で作成します。
本紙解説ダウンロードファイルを使います。

Live2D_kyoukasho_2\Live2D_kyoukasho_2\Chapter07\7-6-02-finish.can3
をダブルクリックして、Cubism Editorを開きます。途中、7-6-01.cmo3が見つからないといわれますが、同じフォルダにあるので、「置き換え」を押して、そのファイルを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/1da991f3-abf3-7013-e751-4ca737fcdce5.png)

これから、シーン のところにある12個程度あるシーンを出力します。ファイル→組込み用ファイル書き出し→モーションファイル書き出し を選択します。ダイアログが表示されますので、「全シーン」を出力を選択してOKボタンを押下します。
これで、motion3.jsonファイルがたくさん出力されたかと思います。

次に、左上の「Animation」となっているところを「Model」に変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/1b1ee35b-c1f6-4f37-8fc0-8cb2b28ed238.png)

次に、左上にある「7-6-02-finish」を展開すると、「7-6-01.cmo3」が現れますので、それをダブルクリックします。
そして、ファイル→組込み用ファイル書き出し→moc3ファイル書き出しを選択します。ダイアログが表示されますので、OKボタンを押下します。そうすると、 mocファイルとテクスチャファイルが出力されます。


# 背景画像の撮影

Live2Dのキャラクタを表示するページの背景画像を用意します。
これからサブディスプレイにキャラクタを映すので、そこにいるかのように思わせるため、サブディスプレイの後ろや横のあたりを写真に撮っておきましょう。ファイル名を、「bgimage.jpg」とでもしておきます。

# Webページの作成

ソースコードを載せておきます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
    <meta name="format-detection" content="telephone=no">
    <meta name="msapplication-tap-highlight" content="no">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <!-- Optional theme -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

    <title>Template : Simple</title>

    <script src="https://unpkg.com/vue"></script>

    <!-- Include Pixi. -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.8/pixi.js"></script>

    <!-- Include Cubism Core. -->
    <!--By including below library in your project you agree to http://live2d.com/eula/live2d-proprietary-software-license-agreement_en.html-->
    <script src="https://s3-ap-northeast-1.amazonaws.com/cubism3.live2d.com/sdk/js_eap/live2dcubismcore.min.js"></script>

    <!-- Include Cubism Components. -->
    <script src="./dist/js/live2dcubismframework.js"></script>
    <script src="./dist/js/live2dcubismpixi.js"></script>
</head>
<body style="background: url('./img/bgimage.jpg'); background-size: 100%;">
    <div id="top" class="container">
        <div class="row">
            <div id="live2d" v-on:click="live2d_clicked()" style="margin:0 auto;"> 
        </div>
    </div>

    <!-- <div id="live2d" style="width:500px; position: absolute; left: 300px; top:100px;"> -->

    <script src="js/start.js"></script>
</body>
```

```js:start.js
'use strict';

const IDLE_MOTION_DURATION = 60000;

const contents = {
    moc: './assets/Tsumugi/Tsumugi.moc3',
    texture: './assets/Tsumugi/Tsumugi.2048/texture_00.png',
    idle_motions: [
        { name: "normal_idle", file: './assets/Tsumugi/normal_idle.motion3.json' },
        { name: "angry_idle", file: './assets/Tsumugi/angry_idle.motion3.json' },
        { name: "sad_idle", file: './assets/Tsumugi/sad_idle.motion3.json' },
        { name: "happy_idle", file: './assets/Tsumugi/happy_idle.motion3.json' },
        { name: "shy_idle", file: './assets/Tsumugi/shy_idle.motion3.json' },
    ],
    onetime_motions: [
        { name: "angry", file: './assets/Tsumugi/angry.motion3.json' },
        { name: "comical1", file: './assets/Tsumugi/comical1.motion3.json' },
        { name: "comical2", file: './assets/Tsumugi/comical2.motion3.json' },
        { name: "happy", file: './assets/Tsumugi/happy.motion3.json' },
        { name: "sad", file: './assets/Tsumugi/sad.motion3.json' },
        { name: "shy", file: './assets/Tsumugi/shy.motion3.json' },
        { name: "surprise", file: './assets/Tsumugi/surprise.motion3.json' },
    ],
};

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

    },
    computed: {
    },
    methods: {
        live2d_clicked: function(){
            var index = Math.floor(Math.random() * contents.onetime_motions.length);
            live2d_set_onetime_motion(contents.onetime_motions[index].name);
        },
    },
    created: function(){
    },
    mounted: async function(){
        try{
            await live2d_init("#live2d", contents, 'normal_idle');

            setInterval(()=>{
                var index = Math.floor(Math.random() * contents.idle_motions.length);
                live2d_set_idle_motion(contents.idle_motions[index].name);
            }, IDLE_MOTION_DURATION);
        }catch(error){
            console.log(error);
            alert(error);
        }
    }
};
var vue = new Vue( vue_options );

var live2d_model = null;
var live2d_loader = null;
var live2d_motion = null;
var live2d_motion_onetime = false;

function live2d_set_onetime_motion(name){
    if( live2d_model == null )
        return;

    var motion = live2d_model.animator.getLayer("base");
    motion.stop();
    live2d_motion_onetime = LIVE2DCUBISMFRAMEWORK.Animation.fromMotion3Json(live2d_loader.resources[name].data);
    motion.play(live2d_motion_onetime);
    live2d_motion_onetime = true;
}

function live2d_set_idle_motion(name){
    if( live2d_model == null )
        return;

    var motion = live2d_model.animator.getLayer("base");
    motion.stop();
    if( name ){
        live2d_motion_onetime = false;
        live2d_motion = LIVE2DCUBISMFRAMEWORK.Animation.fromMotion3Json(live2d_loader.resources[name].data);
        motion.play(live2d_motion);
    }else{
        live2d_motion = null;
    }
}

function live2d_init(target, contents, first_motion){
    var loader = new PIXI.loaders.Loader();
    loader
        .add('moc', contents.moc, { xhrType: PIXI.loaders.Resource.XHR_RESPONSE_TYPE.BUFFER })
        .add('texture', contents.texture);
    for( var i = 0 ; i < contents.idle_motions.length ; i++ )
        loader.add(contents.idle_motions[i].name, contents.idle_motions[i].file, { xhrType: PIXI.loaders.Resource.XHR_RESPONSE_TYPE.JSON });
    for( var i = 0 ; i < contents.onetime_motions.length ; i++ )
        loader.add(contents.onetime_motions[i].name, contents.onetime_motions[i].file, { xhrType: PIXI.loaders.Resource.XHR_RESPONSE_TYPE.JSON });

    return new Promise((resolve, reject) =>{
        loader.load((loader, resources) => {
            try{
                var moc = Live2DCubismCore.Moc.fromArrayBuffer(resources['moc'].data);
                var model = new LIVE2DCUBISMPIXI.ModelBuilder()
                    .setMoc(moc)
                    .setTimeScale(1)
                    .addTexture(0, resources['texture'].texture)
                    .addAnimatorLayer("base", LIVE2DCUBISMFRAMEWORK.BuiltinAnimationBlenders.OVERRIDE, 1)
                    .build();

                var app = new PIXI.Application({ transparent: true, /* backgroundColor: 0x1099bb */ });

//                $(target).append('<hr>');
                $(target).append(app.view);
//                $(target).append('<hr>');
                app.stage.addChild(model);
                app.stage.addChild(model.masks);

                var motion_name = first_motion ? first_motion : contents.idle_motions[Math.floor(Math.random() * contents.idle_motions.length)].name;
                live2d_motion = LIVE2DCUBISMFRAMEWORK.Animation.fromMotion3Json(resources[motion_name].data);
                model.animator.getLayer("base").play(live2d_motion);

                app.ticker.add(function (deltaTime) {
                    if(live2d_motion_onetime){
                        var motion = model.animator.getLayer("base");
                        if( motion.currentTime >= motion.currentAnimation.duration){
                            motion.stop();
                            live2d_motion_onetime = false;
                            if( live2d_motion )
                                motion.play(live2d_motion);
                        }
                    }
                    model.update(deltaTime);
                    model.masks.update(app.renderer);
                });
                
                var onResize = (event) => {
                    var size = rect_resize( $(target).width(), $(target).height(), model.canvasinfo.CanvasWidth, model.canvasinfo.CanvasHeight, true);
                    app.renderer.resize(size.valid_width, size.valid_height);
                    model.position = new PIXI.Point(size.valid_width * 0.5, size.valid_height * 0.5);
                    model.scale = new PIXI.Point(size.valid_width * 1, size.valid_width * 1);
                    model.masks.resize(size.valid_width, size.valid_height);
                };
                onResize();
                window.onresize = onResize;

                live2d_model = model;
                live2d_loader = loader;

                return resolve();
            }catch(error){
                return reject(error);
            }
        });
    });
}

function rect_resize( disp_width, disp_height, image_width, image_height, height_expand ){
    var scale = disp_width / image_width;
    var adjust = "width";
    if( !height_expand ){
        var height_scale = disp_height / image_height;
        if( height_scale < scale ){
            scale = height_scale;
            adjust = "height";
        }
    }else{
        disp_height = Math.floor(image_height * scale);
    }

    var valid_width = image_width * scale;
    var valid_height = image_height * scale;

    return {
        valid_width: Math.floor(valid_width),
        valid_height: Math.floor(valid_height),
        disp_width: disp_width,
        disp_height: disp_height,
        image_width: image_width,
        image_height: image_height,
        scale: scale,
        adjust: adjust
    };
}
```

ちょっと解説します。

function live2d_init(target, contents, first_motion)

Live2DのSDKを初期化し、HTMLにLive2D表示用のElementを配置します。
targetには、html内のidを指定します。このidで指定されたElementの下に配置されることになります。
contentsは、mocファイル等の場所を示したものです。
first_motionは、最初に動くアニメーションを指定します。指定しなければ、ランダムに選ばれます。
ただし、IDLE_MOTION_DURATIONで指定した時間(msec)が経過するたびに、live2d_set_idle_motion()を呼び出して、アニメーションを切り替えています。

function live2d_set_onetime_motion(name)

通常はアニメーションはループしているのですが、この関数を呼び出すと、1回だけアニメーションを起動して、それが終わると、元のループしているアニメーションに戻ります。
マウスでクリックしたときに、この関数を呼ぶようにしていますので、気になったら、クリックしてあげてください。


# 抽出ファイルの配置

start.jsのconst contentsに、Live2Dファイルの配置場所を記載しています。assetsフォルダの下に置いてある想定です。

SDKのファイルも、index.htmlを参考に、./dist/js/ フォルダに置いておきます。それから、先ほど撮影した写真も、./img/ フォルダに置いておきます。

# ブラウザで表示

それでは、さっそくブラウザで表示しましょう。
いつも使っているブラウザではなく、あまり使っていないブラウザにしましょう。私は通常Chromeを使っているので、Microsoft EdgeやFirefoxなどを使いました。

キャラクタが表示されましたでしょうか。
そして、ブラウザをサブディスプレイにもっていって、F11 キーを押下します。
そうすると、サブディスプレイに全画面で表示されたはずです。

メインディスプレイをメインに使っている時、サブディスプレイでキャラクタがアニメーションで動いてます。
サブディスプレイに、別のウィンドウを持って行っても、背景にキャラクタが動いたままになっているかと思います（だけど、最小化してしまうと戻ってしまうのが寂しい）。

おわり。。。

（わからないことだらけなので、まちがってたらすみません）

以上