ハロウィンなのでJINS MEMEを使ってVTuberの仮装をしてみた

# できたもの
https://twitter.com/i/status/1056448648274305024


# きっかけ
毎年、会社でハロウィンパーティーがあり、多くの人が仮装をします。
私はたいてい、その年にドハマリしたものの仮装（というよりコスプレ）をしていますが
今はVTuberにドハマリしていて、シロちゃんが大好きなのです。
シロちゃんになるかーとも思ったが、今年はちょっと趣向を変えてみようと思っていました。
（なお、一昨年はまどマギのさやか、去年はけもフレのサーバル）

そこでふと思い出したのがこの記事
 [【朝起きてからバズるまで】 世界一(?)スマートにVTuberになるシステムをマッハ開発した](https://qiita.com/kenta71/items/f232f498522198a17930)

JINS MEME で VTuber になってみるというもので、ちょうど私は JINS MEME を持っていたのだ。
そこで、今回思いついたのが、
**自分はばあちゃるの仮装をし、iPad を持ってそこにシロちゃんを登場させ、自分の頭の動きとリンクさせる** というもの

Anime Japan 2018 の「禍つヴァールハイト」のステージで、
ばあちゃるがリアルに出てきて、シロちゃんがディスプレイで登場（[参考](https://panora.tokyo/57145/)）
というのが元々の発想

# 使ったもの
### ハードウェア
[JINS MEME ES](https://jins-meme.com/ja/products/es/)
iPad Pro 12.9（第1世代）iOS 12.0

### ソフトウェア
Unity 2018.2.13f1
[JINS MEME SDK version 1.1.3](https://developers.jins.com/ja/sdks/ios/)
[JINS-MEME-Unity-Plugin](https://github.com/SystemFriend/JINS-MEME-Unity-Plugin)
[MMD4Mecanim](http://stereoarts.jp/)
[MMD4MecanimFaciem](http://mebiustos.hatenablog.com/entry/2015/08/03/192115)

### お借りしたモデル
[電脳少女シロ（冬服 白Ver.）](https://3d.nicovideo.jp/works/td31882)
[ハロウィンステージ](http://seiga.nicovideo.jp/seiga/im3501758)
[手を振るだけモーション](https://bowlroll.net/file/79547)

# 導入
まずは元の JINS MEME で VTuber になる記事を参考に JINS MEME の入力を受け付けるところを作っていきました。
元記事ではユニティちゃんを動かしていますが、今回私がやりたいのはシロちゃんを動かすことなので
シロちゃんのモデルが使えるMMDをUnityで動かせるようにしなくてはなりません。
そこで使うのが MMD4Mecanim です。導入方法はこちらの[記事](https://qiita.com/yasuimi1456dN/items/a750d67e8f719c054ad2)を参考に。

ここまでで、以下ができるようになっています
1. JINS MEME の入力を受け付けること
2. シロちゃんのモデルを Unity で使えるようにすること

<img width="1421" alt="スクリーンショット 2018-10-28 14.30.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/265518/9f2af9b3-dd98-c627-ed3a-6a1a4fd5e585.png">


# JINS MEME の入力から、シロちゃんを動かす
今回いれた機能は以下です
1. 注視方向を動かす
2. まばたきをする
3. 視線を動かす
4. 手を振りながらあいさつをする（おまけ ※ JINS MEME 関係なし）

シロちゃんの Animator Controller は UnityChanLocomotions にしています。

## 1. 注視方向を動かす
JINS MEME から送られてくるデータの pitch, yaw, roll を利用します
pitch は首を前後に振る動き。通常の前を向いた姿勢で 0、下を向くと、+180、上を向くと -180 で -180 ~ +180 の値を取る
yaw は左右に振る回転。0 ~ 360 の値をとり、左方向がマイナス、右方向がプラス
roll は首を左右に傾ける動き。今回は使っていません

```c#:LookAt
    void Start() {
	    GameObject mainCamera = GameObject.FindGameObjectWithTag ("MainCamera");
	    cameraPos = mainCamera.transform.localPosition;
	    targetPos = cameraPos;
	    
        this.animator = GetComponent<Animator>();
        
        // JINS MEME から data が来たら
        EyeMotionProxy.Instance.dataStream.Subscribe(data => {
	        // 最初の角度は0がきてしまうので、初めて数字が入ってきたときのものをベースにする
	        if (existBaseMemeRadius == false && !Mathf.Approximately(data.pitch, 0.0f) && !Mathf.Approximately(data.yaw, 0.0f) && !Mathf.Approximately(data.roll, 0.0f) ) {
		        existBaseMemeRadius = true;
		        baseMemeRadius = new Vector3(data.pitch, data.yaw, data.roll);
	        }

	        float pitchDif = baseMemeRadius.x - data.pitch;
	        float yawDif = baseMemeRadius.y - data.yaw;
	        float rollDif = baseMemeRadius.z - data.roll;
	        
	        // yaw の境界線の検知
	        if (Math.Abs(baseMemeRadius.y - data.yaw) > 180.0f) {
		        if (data.yaw > 180.0f) {
			        yawDif = baseMemeRadius.y + (360.0f - data.yaw);
		        } else {
			        yawDif = - (360.0f - baseMemeRadius.y + data.yaw);
		        }
	        }
	        
            PlayLookAt(pitchDif, yawDif, rollDif);
        }).AddTo(this);
    }

    void PlayLookAt(float pitchDif, float yawDif, float rollDif) {
	    float xPos = 0.0f;
	    float yPos = 0.0f;
	    float zPos = 0.0f;
	    if (yawDif < -120.0f) {
		    xPos = -1.5f;
	    } else if (yawDif > 120.0f) {
		    xPos = 1.5f;
	    } else {
		    xPos = yawDif / 90.0f;
	    }
	    if (pitchDif < -120.0f) {
		    yPos = -1.5f;
	    } else if (pitchDif > 120.0f) {
		    yPos = 1.5f;
	    } else {
		    yPos = pitchDif / 90.0f;
	    }
	    Vector3 movePos = new Vector3(xPos, yPos, 0.0f);
	    targetPos = cameraPos + movePos;
    }

	private void OnAnimatorIK(int layerIndex) {
		animator.SetLookAtWeight(1.0f, 0.2f, 1.0f, 0.2f, 0f);
		animator.SetLookAtPosition(targetPos);
	}
```

起動して、はじめて入ってきた姿勢角の値を基準にして注視方向を動かします
最初の基準値からどれくらい動いたかで、注視方向を決定しています
yaw は 0度と360度の境界を頻繁に超えるため、それを検知して調整しています
もっと自然な方法があるような気がしますが…

実はこのままでは、自分の体全体が向く方向が変わるとシロちゃんもその方向を向いたままという問題が発生します。
初期状態から、自分が体全体を左に移動させた場合に、シロちゃんは左を向いたままになってしまいます。
通常 VTuber をやる場合であれば、正面しか見ないのでこれで問題ないのですが
今回はオフィスを歩き回るため、これをなんとかしなければなりません。
応急処置として、画面左下にリセットボタンを設置し、姿勢角の基準値を今の値にするという処理を実行するようにしました。
なので、移動するたびにリセットボタンを連打していました。

## 2. まばたきをする
ユニティちゃんのときのように、Eye Obj L/R とかがないので
MMD4MecanimFaciem を利用して、表情を変えていきます
<img width="616" alt="スクリーンショット 2018-10-28 15.08.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/265518/501ddd77-2ddf-899b-e8ba-e8b1540fa112.png">

Faciem Database で、目を閉じた状態の表情を作り保存しておきます
まばたきを検知したら、現在の表情を覚えておき、目を閉じる表情をセットします。
そして、0.1秒後にもとの表情に戻します。

```c#:Blink
    void Start() {
        // JINS MEME から data が来たら
        EyeMotionProxy.Instance.dataStream.Subscribe(data => {
            FaciemController faciemController = GetComponent<FaciemController>();
            string currentFaceName = faciemController.GetCurrentFaceName();
            // 瞬きの強さが50以上かつ、まばたき中じゃなければまばたき実行
            if (data.blinkStrength >= 50 && !currentFaceName.Equals("目を閉じる")) {
                // まばたきアニメーション実行
                faciemController.SetFace("目を閉じる");
                StartCoroutine (blinkBack(faciemController, currentFaceName));
            }
        }).AddTo(this);
    }

    IEnumerator blinkBack(FaciemController faciemController, string currentFaceName){
        while (true) {
            yield return new WaitForSeconds (0.1f);
            faciemController.SetFace(currentFaceName);
            break;
        }
    }
```

## 3. 視線を動かす
2 のまばたきをすると同様に MMD4MecanimFaciem を利用して表情を変えます
あまり意図した動きを作れず妥協したところです

```c#:EyeDirection
    void Start() {
        // JINS MEME から data が来たら
        EyeMotionProxy.Instance.dataStream.Subscribe(data => {
            PlayEyeDirection((int)data.eyeMoveUp, (int)data.eyeMoveDown, (int)data.eyeMoveLeft, (int)data.eyeMoveRight);
        }).AddTo(this);
    }
    
    void PlayEyeDirection(int up, int down, int left, int right) {
        FaciemController faciemController = GetComponent<FaciemController>();

//        0：なし
//        1：移動検知ー小
//        2：移動検知ー中
//        3：逆移動検知ー大
        if (up + down + left + right > 0 && isMove == false) {
            isMove = true;

            if (up > 0) {
                faciemController.SetFace("目線上");
                StartCoroutine (eyeMoveBack(faciemController));
            } else if (down > 0) {
                faciemController.SetFace("目線下");
                StartCoroutine (eyeMoveBack(faciemController));
            } else if (left > 0) {
                faciemController.SetFace("目線左");
                StartCoroutine (eyeMoveBack(faciemController));
            } else if (right > 0) {
                faciemController.SetFace("目線右");
                StartCoroutine (eyeMoveBack(faciemController));
            } else {
                isMove = false;
            }
        }
    }
    
    IEnumerator eyeMoveBack(FaciemController faciemController){
        while (true) {
            yield return new WaitForSeconds (0.1f);
            faciemController.SetFace("デフォルト");
            isMove = false;
            break;
        }
    }
```
目玉の細かな位置を指定するというより、方向とその強さが3段階でわかるくらいなので
なかなか自然な目の動きを再現することができませんでした。
MMD4MecanimFaciem で設定した表情ではなく、直接数字を指定して表情を作るといった方法がありそうですが、そこまで手が回りませんでした。

## 4. 手を振りながらあいさつをする
これは、JINS MEME と関係ありませんが、自分が欲しいと思ったのでいれました

```c#:Touch
    void Update() {
        if (Input.touchCount > 0) {
            Touch touch = Input.GetTouch(0);
            Vector2 touchVec = new Vector2(touch.position.x, Screen.height - touch.position.y);

            FaciemController faciemController = GetComponent<FaciemController>();
            audioSource = gameObject.GetComponent<AudioSource>();
            
            float screenWidth = Screen.width;
            float screenHeight = Screen.height;
            if (touchVec.x > screenWidth / 2) {
                if (touchVec.y > screenHeight / 2) {
                    faciemController.SetFace("おほー");
                    audioSource.clip = audioClip2;
                    audioSource.Play();
                } else {
                    faciemController.SetFace("あいさつ");
                    audioSource.clip = audioClip1;
                    audioSource.Play();
                    Animator animator = GetComponent<Animator>();
                    animator.SetTrigger("hand");
                }
            } else {
                if (touchVec.y > screenHeight / 2) {
                    faciemController.SetFace("デフォルト");
                } else {
                    faciemController.SetFace("ねえ");
                    audioSource.clip = audioClip3;
                    audioSource.Play();
                }
            }
        }
    }
```

画面の特定の箇所をタッチしたら手を振りながらあいさつをするようになっています。
手を振るモーションは UnityChanLocomotions に追加し、hand という Trigger を設定します。
これで、「こんにちは、シロです！」という声とともに、手を振ってくれます。
<img width="1325" alt="スクリーンショット 2018-10-28 15.37.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/265518/da450cde-45e3-398b-9655-606e75ab137f.png">

他にも、「おほー（しいたけ目）」と「ね゛え゛え゛」もしゃべってくれます

# で、反応はどうだったの？
ばあちゃるとシロちゃんを知っていなくても、見た目的にはかなりインパクトのある仮装なのですが
私の仮装であるばあちゃるよりもシロちゃんの可愛さにみな目が行っていました。
さすが、**誰が見てもかわいい** だけあります。
そして、ほぼ全員がシロちゃんの動きが自分の動きとリンクしていることに気が付きませんでした。
私がわかりやすく頭を振ってはじめて、「え？連動してるの！？」と気づき驚いていました。
エンジニアの人たちはどうやって検知しているのかみな聞いてきました。
馬のかぶりものをしていたので余計不思議に見えたようです。

# さいごに
思いついたのが3週間前とかで実質的に土日しか作業する時間がなかったですが
実質2日くらいでほぼほぼ出来上がったので、先駆者の方々に感謝です。

作ったもののクオリティはさておき、
自分がやりたいものを作って、人に見せて反応を見るのは楽しいし大事だなぁと思いました。
こういう目的がなければなかなか土日に頑張れないなぁと。

普段の業務は全然違うことをしているんですが
VR/AR あたりが今一番楽しいので、今後もなにかやっていきたいですね。