勉強会のためにVTuberシステムっぽい何かを作った話

#作ろうと思ったきっかけ
自分の後輩が学校で何度か自主的な勉強会を開催しており、自分も講師として何度か参加している。
そして今回、新一年生を加えた2回目のC4D+AE勉強会（[詳細](http://twipla.jp/events/313443)）において、事前に下記の問題を把握していた。

■重要度：大
・部屋のキャパに対して希望者数が多くなってしまった
■重要度：中
・過去のアーカイブが欲しいという在校生からの要望がそれなりにあった
・学校のパソコンに使用したいプラグインが無い
■重要度：低
・講演用の自身のノートPCが貧弱

■その他
・なんかVR空間で勉強会の講師みたいなことをやってみたかった

#各種検証と自作に至るまで

上記問題解決のために、まず思いついたのがCluster.を使って配信を行うことだった。
最近アップデートが入りVRMのモデルをアップロードができるようになったり、youtubeのコメントストリーミングが可能など、度々噂には聞いていたので、開催場所として可能か検証してみたところ、以下の問題が発覚した。

・デスクトップ画面の共有（或いはスクリーンに映像ソースの流し込み）ができない
・アップロード可能なメディアに映像が再生できるものがない
・ポインターがない
・マイクを持たないと全体に音声を流せない（？）

など、本当はもうちょっと細々したところが（もちろん良いところもあったが）あり、今回の運用が難しそうだったのでCluster.経由での講師中継を断念した（15日時点）
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">せむ、Cluster.に立つ <a href="https://t.co/c7kA85Gq1C">pic.twitter.com/c7kA85Gq1C</a></p>&mdash; せむ (@exemoss) <a href="https://twitter.com/exemoss/status/996406478348021760?ref_src=twsrc%5Etfw">2018年5月15日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
ちなみにVRM経由でCluster.にキャラクターを持っていく場合はオプションボーンも含めた全部のリグで構成されていないとIKが動かないようで、Tポーズのアバターの腕を動かすのに3日もかかってしまった。

というわけで、一度は上記の要領で挫折し、VRChatも一瞬検討したがVRChatも確かデスクトップの共有はできなかった記憶があり、あとはネットワーク周りのフラグ管理も含めたワールド開発を今から行える気がしなかったので見送り、ワンチャンオフラインでならなんとかなるのではないかとUnity上での開発を始めた。

#使用したソフトとアセット

以下開発環境と使用したアセット群

・Unity 2017.2.0.f3
・SteamVR plugin
・uDesktopDuplication-v1.5.2
・UniVRM-0.35
・Mecanim Example Scenes

Unityは元々入ってたやつをそのまま使用。
ViveとUnityで開発するならSteamVRがいいということでSteamVRを導入。
デスクトップを写すために凹みさんの[uDesktoppDuplication](http://tips.hecomi.com/entry/2016/12/04/125641)を導入。
Cluster.にアバターをアップロードするためにアバターをVRM化していたのでVRMを導入。
このままでは腕が動かないのでIKを導入するために[こちらの記事](http://onoty3d.hatenablog.com/entry/2015/03/16/183650)を参考にMecanim Example Scenesを導入。

#作成したシーン
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/0f2eb4ea-7f7f-195c-38bc-21b862bd1491.png)

##アセット以外で自作したもの
・床のグリッド画像
・右下にいるアバター
・skydomeオブジェクト（天球画像は[++skies;](https://aokcub.net/cg/incskies/)さんから）

それら以外はアセットで完結しています。

##作った流れ

・とりあえず上記4つのアセットをインポート（Mecanimは複数ファイルが展開されるので最初にインポートしてフォルダにまとめるのが良さそう）

・IK.csをアバターの一番親(＝Animatorコンポーネントがアタッチされているオブジェクト、画像の場合はfigure_rigging_exemoss)にアタッチ

・空のGameObjectを作ってそれぞれリネームし、IK.csのそれぞれ該当するObjにアタッチ。
(もしかしたらDCC側で事前に作っておいたほうが楽だったのかもしれない)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/024584b9-6087-5fd3-5e24-f39dbde04591.png)

・Assets>Create>Animator Controllerから新しいAnimatorを作成し、Window>Animatorで編集画面を開き、Base Layerの歯車からIK Passにチェックを入れる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/be55af3b-7b58-102e-fa56-f44edd990fa6.png)

・作ったAnimator ControllerをVRMアバターのAnimatorのControllerに突っ込む（画像のStayってやつがそれ）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/57127c84-16a8-1052-2674-9c9899713fd1.png)

・それぞれのIK Controller（少し上の方で作った、リネームしてIK.csに登録したオブジェクト）を、それぞれの階層へ移動させて、**必要に応じて位置を調整する**（この部分細かいので割愛）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/b736df0b-c741-d61e-b7fa-add3faae8240.png)

・次にProjectパネルからAssets>uDesktopDupulication>Models>uDD_BoardをSceneに突っ込んでいい感じの大きさに調整する。

・planeを作ってグリッド画像をアタッチする

・法線を内側に向けたskydomeオブジェクトをプロジェクトに突っ込んで天球画像をアタッチする

だいたいこんな感じになった
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/86a9cd2f-d610-ab09-a7b0-a7468aac2c28.png)

#書いたスクリプト

　上記工程でほぼほぼ完成したと思ってたら、ボディがフルトラッキングじゃないため回転ができないという割りと致命的な不具合があることが分かったので、仕方なくスクリプトである程度の改善を試みた。

　作ったのは下の4つ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/8896736c-4e7c-3ef7-970c-305cc3cd967c.png)


* Controller_Input.cs
* PointerTrigger.cs

```C#:Controller_Input.cs
using UnityEngine;
using System.Collections;

public class Controller_Input : MonoBehaviour
{
    public GameObject rotateObj;
    public GameObject stayObj;
    private SteamVR_TrackedObject trackedObj;
    private float RotY = 0;

    private void Awake()
    {
        trackedObj = GetComponent<SteamVR_TrackedObject>();
    }

    private void Update()
    {
        var device = SteamVR_Controller.Input((int)trackedObj.index);


        if (device.GetPressDown(SteamVR_Controller.ButtonMask.Touchpad) && device.GetAxis().x <= -0.7 )
        {

            Debug.Log("Push Left");
            RotY = RotY + 15;
        }
        if (device.GetPressDown(SteamVR_Controller.ButtonMask.Touchpad) && device.GetAxis().x >= 0.7)
        {
            Debug.Log("Push Right");
            RotY = RotY - 15;
        }
        rotateObj.transform.eulerAngles = new Vector3(0, RotY, 0);
    }
}
```
```C#:PointerTrigger.cs
using UnityEngine;

public class PointerTrigger : MonoBehaviour {
    private SteamVR_TrackedObject trackedObj;

    private void Awake()
    {
        trackedObj = GetComponent<SteamVR_TrackedObject>();
    }

    void Update()
    {
        var device = SteamVR_Controller.Input((int)trackedObj.index);

        if (device.GetPressDown(SteamVR_Controller.ButtonMask.Trigger))
        {
            this.GetComponent<LineRenderer>().enabled = true;
        }
        if(device.GetPressUp(SteamVR_Controller.ButtonMask.Trigger))
        {
            this.GetComponent<LineRenderer>().enabled = false;
        }
        
    }
}
```

上記2つは、それぞれコントローラー向けに書いた。
左手にController_Inputsを、右手にPointerTriggerをアタッチした。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/fcd2f539-e164-964c-838e-413adf2c8fc8.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/57e58cdf-0df0-a7fa-b6be-e3c9774414cc.png)

コントローラーのキーアサインに関しては[この記事](http://cycling.hateblo.jp/entry/2016/03/26/010232)と[この記事](https://framesynthesis.jp/tech/unity/htcvive/)を参考に、
レーザーポインターに関しては[ここの記事](https://news.mynavi.jp/itsearch/article/devsoft/1540)を参考にした。

* XYpos_sync.cs

```C#:XYpos_sync.cs
using UnityEngine;

public class XYpos_sync : MonoBehaviour {
    public GameObject ParentObj;
    public GameObject SyncObj;
    private float XPos;
    private float ZPos;

	void Start () {
		
	}
	
	void Update () {
        Vector3 ParentVector = ParentObj.transform.position;
        SyncObj.transform.position = new Vector3(ParentVector.x, 0, ParentVector.z);
	}
}
```

カメラのXZ位置(スクリプトが誤字ってるけど直してない)とアバターのXZ位置を同期させるために書いた。

*Cam_Yrotate_Sync.cs

```C#:Cam_Yrotate_Sync.cs
using UnityEngine;

public class Cam_Yrotate_Sync : MonoBehaviour {
    public GameObject ParentObj;
    public GameObject Body;

	// Update is called once per frame
	void Update () {
        Vector3 ParentPos = ParentObj.transform.eulerAngles;
        Body.transform.eulerAngles = new Vector3(0 ,ParentPos.y ,0);
	}
}
```

HMDのY回転とアバターのY回転を同期させるために書いた。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/2dfc5722-d648-f15c-cd9a-7b90e79ca724.png)

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">せむ、Unityに立つ <a href="https://t.co/CNF3AqN4Se">pic.twitter.com/CNF3AqN4Se</a></p>&mdash; せむ (@exemoss) <a href="https://twitter.com/exemoss/status/997537495746162689?ref_src=twsrc%5Etfw">2018年5月18日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#作ってみた感想
日曜大工みたいで面白かった（小並感）
前々からVTuber自体はそれなりに追ってたり、VTuberハッカソンにも見学として参加したりしていたがなかなか自分から動くきっかけがなかったので、自分で手を動かす機会としてはちょうどよかったと思う。
移動ができない、足が動かない、回転すると何故かアバターのアンカーじゃなくてCameraRigのアンカーで回転しちゃう、Lineがオブジェクトを貫通したままとかいろいろ完成度はアレだけど2,3日でわりとそれっぽいのができて割と満足感がある。
そしてここまで作ってておおよそ似たことがVirtualCastでできると知ったので夜検証してみて使えそうだったら明日それを使おうと思います……。

#追記：運用後の反省と今後

VirtualCastは結局検証できなかったので上記のものをそのまま運用。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152735/0bc47d6a-ba57-ad1b-95b4-d99ba53d93d2.png)
(https://www.youtube.com/watch?v=qodJt_-ufiI)

■運用上良かったこと
- 会場のウケはそれなりによかったらしい
- ポインターがあると、やりやすさがある
- アーカイブの運用が超絶楽
- 非参加者からもコメントやレスポンスがもらえる

■運用上良くなかったこと
- 配信環境の音周りをAG03とSM58で整えた結果OBSの音声出力をモノラルにしてなかったせいでL側からしか聞こえないというミスを起こした（テスト及び会場(ステレオも強制モノラル出力)では問題なかった）
- 会場側にこちらへの音声を流す方法を用意していなかったため、アーカイブに会場の質疑応答やこちら側でレスポンスを確認することが難しかった。
- 最終的にHMDは上にずらして喋っていた（左右で同期ズレがおきていたのでモニター2で映していたyoutubeやtwitterのコメントが拾えなかった）

■今後
次は会場にVive持ち込んで他の人のとかもアーカイブ化したいなあと思っています。