UnityでVRコスプレするときに詰まったところの覚え書　その3

この記事は[前回の記事](https://qiita.com/_karukaru_/items/f7cdbffff591d0f0fb9b)の続きです。また自分用メモのように使用しているため、記事内容のブラッシュアップをしていません。

# モーフコントローラでエラー（アウトオブレンジ）
以前書いた MorphController だと、相手のモーフインデックスを自分のモデルに適応しようとしてアウトオブレンジのエラーを吐いていました。
またAutoBlinkの処理もゲストの数だけ実行されるようになっていたバグもあったため、全面的に改修することにしました。

```VRMMorphController.cs
using System.Collections;
using UnityEngine;
using VRM;

public class VRMMorphController : MonoBehaviour
{
    public VRMBlendShapeProxy vRMBlendShapeProxy;
    public bool isBlink = true;

    [Tooltip("半分瞬きの重み")]
    public float HalfBlinkWeight = 0.75f;

    [Tooltip("目パチの時間")]
    public float MaxBlinkingTime = 0.2f;
    public float MinBlinkingTime = 0.05f;

    [Tooltip("ランダム判定の閾値")]
    public float Threshold = 0.3f;

    [Tooltip("ランダム判定のインターバル")]
    public float MaxInterval = 3.5f;
    public float MinInterval = 2.5f;


    //モーフステート保存用変数
    private BlendShapePreset OldMorphState;
    private BlendShapePreset _MorphState = BlendShapePreset.Neutral;
    public BlendShapePreset MorphState
    {
        get
        {
            return _MorphState;
        }

        set
        {
            OldMorphState = _MorphState;
            _MorphState = value;

            //モーフステートが Neutral なら瞬きをする
            if (_MorphState == BlendShapePreset.Neutral)
                isBlink = true;
            //モーフステートが Neutral 以外なら瞬きをしない
            else
                isBlink = false;

            if (photonView == null)
            {
                Debug.LogError("Photon NULL!");
            }


            photonView.RPC("SetMorph", PhotonTargets.All, _MorphState, OldMorphState);
        }
    }

    //PhotonView保存用変数
    private PhotonView photonView;

    //初期化
    void Reset()
    {
        photonView = GetComponent<PhotonView>();
        vRMBlendShapeProxy = GetComponent<VRMBlendShapeProxy>();
    }

    void Awake()
    {
        GetComponent<VRMFirstPerson>().Setup();
    }

    // Use this for initialization
    void Start()
    {
        //各種コンポーネントを取得
        photonView = photonView ?? GetComponent<PhotonView>();
        vRMBlendShapeProxy = vRMBlendShapeProxy ?? GetComponent<VRMBlendShapeProxy>();

        StartCoroutine(AutoBlinkCoroutine(vRMBlendShapeProxy));
    }

    // Update is called once per frame
    void Update()
    {

    }

    //表情変化
    [PunRPC]
    IEnumerator SetMorph(BlendShapePreset morphState, BlendShapePreset oldMorphState)
    {
        //同じ表情なら何もしない
        if (oldMorphState == morphState)
        {
            yield break;
        }

        //モーフの値が 0になったらループから脱出
        bool _whileFlag = true;
        while (_whileFlag)
        {
            //ウェイトを取得して-0.5する
            float _oldWeight = vRMBlendShapeProxy.GetValue(oldMorphState) - 0.5f;
            float _weight = vRMBlendShapeProxy.GetValue(morphState) + 0.5f;

            //もしウェイトの値が 0以下なら 0にセットしてループを終了
            if (_oldWeight <= 0.0f && _weight >= 1.0f)
            {
                _oldWeight = 0.0f;
                _weight = 1.0f;
                _whileFlag = false;
            }

            //ウェイトを設定
            vRMBlendShapeProxy.SetValue(oldMorphState, _oldWeight, false);
            vRMBlendShapeProxy.SetValue(morphState, _weight, false);
            vRMBlendShapeProxy.Apply();

            //0.01秒待機
            yield return new WaitForSecondsRealtime(0.01f);
        }

        yield break;
    }

    //瞬きするコルーチン
    IEnumerator AutoBlinkCoroutine(VRMBlendShapeProxy vrmBlendShapeProxy)
    {
        while (true)
        {
            if (!isBlink)
            {
                yield return null;
                continue;
            }

            float _seed = Random.Range(0.0f, 1.0f);
            if (_seed < Threshold)
            {
                vrmBlendShapeProxy.SetValue(BlendShapePreset.Blink, HalfBlinkWeight);
                yield return new WaitForSecondsRealtime(MinBlinkingTime);

                vrmBlendShapeProxy.SetValue(BlendShapePreset.Blink, 1.0f);
                float _timeBlink = Random.Range(MinBlinkingTime, MaxBlinkingTime);
                yield return new WaitForSecondsRealtime(_timeBlink);

                vrmBlendShapeProxy.SetValue(BlendShapePreset.Blink, HalfBlinkWeight);
                yield return new WaitForSecondsRealtime(MinBlinkingTime);

                vrmBlendShapeProxy.SetValue(BlendShapePreset.Blink, 0.0f);
            }

            float _interval = Random.Range(MinInterval, MaxInterval);
            yield return new WaitForSecondsRealtime(_interval);
        }
    }
}
```

# テレポート時にローカル内で別ユーザの高さまでアジャストしてしまう
自分がテレポートしたときに自分のモデルだけ高さをアジャストしたかったのに、ゲストのモデルまでアジャストしてしまって変なことになっていました。
ModelPositionAdjuster.enabled を最初は false にしておいて、Photonにログイン後自分のモデルだけ true に設定することで解決しました。

# 同期設定が『動いたら同期する』になっている
ユーザがログインしたときにボタンなどが初期配置になっているため事故が起こりやすい。
これは全ての PhotonView の同期設定を UnreliableOnChange から Unreliable にすることで解決できます。

# ゲスト切断がうまくいってない？
なぜかゲストを切断する関数が削除されていました。改めて処理を書き直すことで解決。

# ゆかりさんとVR交流会
5/16にニコ生で放送しました。
タイムシフトはこちら：http://live.nicovideo.jp/gate/lv313194173
~~盛大に事故った放送だったので動画には残しません。~~ 残しました。
<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm33518120/script?w=640&h=360"></script><noscript><a href="http://www.nicovideo.jp/watch/sm33518120">【VR配信】ゆかりさんとVR交流会　2018/05/16【あーかいぶ】</a></noscript>

## 問題
いつもこの項目でかなりの行数を持ってかれてしまっていたので、今後はTrelloで管理することにしました。URLはコチラです。
https://trello.com/b/qE36194K

# やっぱりWebCamTexture重い
最適化作業を続けていったら UnityCam がメチャクチャ重いことにムカついたので削除しました。
画面上には左目の映像が流れるようになりますが、UnityEngine.VR.VRSettings.showDeviceView を false にすることで通常のカメラ映像を見れるようになります。

```VRSettingsManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VRSettingsManager : MonoBehaviour
{
    public bool isShowDeviceView;

    void Awake()
    {
        UnityEngine.VR.VRSettings.showDeviceView = isShowDeviceView;
        Debug.Log("UnityEngine.VR.VRSettings.showDeviceView : " + isShowDeviceView);
    }
}
```
これを Camera (eye) オブジェクトにアタッチすればOKです。
![スクリーンショット (186).png](https://qiita-image-store.s3.amazonaws.com/0/213883/a9ec82fc-b625-6724-716b-43b5f9420131.png)

# ゲストが入室したときに無限ループしてる
別ユーザが入室したときに動かしている処理で無限ループしていました。この処理がかなりスパゲッティ化しているので改善したいですね。

```PhotonManager.cs
/*--- 前略 ---*/

    //ゲストオブジェクトを整理するコルーチン
    IEnumerator GuestParentOrganizeCoroutine()
    {
        //Root直下にあるGuestオブジェクトのリスト
        List<Transform> _rootGuest;

        //整理されたゲストオブジェクトの数を取得
        int _organizedGuestCount = GameObject.FindGameObjectsWithTag("Guest").Length;

        //root直下に "Guest"タグのオブジェクトが生成されるまで待機
        while (true)
        {
            GameObject[] _rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
            yield return _rootObjects;
            //Debug.LogFormat("_rootObjects.Count : {0}, PhotonNetwork.playerList.Length - _organizedGuestCount : {1}, _organizedGuestCount : {2}", _rootObjects.Count(root => root.tag == "Guest"), PhotonNetwork.otherPlayers.Length - _organizedGuestCount, _organizedGuestCount);
            if (_rootObjects.Count(root => root.tag == "Guest") == PhotonNetwork.otherPlayers.Length - _organizedGuestCount)
            {
                //Root直下にあるGuestオブジェクトを取得
                _rootGuest = _rootObjects.Where(root => root.tag == "Guest").Select(root => root.transform).ToList();
                yield return _rootGuest;
                break;
            }
            //Debug.Log("loop");
            yield return null;
        }

        //Root直下にあるGuestオブジェクトの初期化
        _rootGuest.Select(transform =>
        {
            //親を設定
            transform.SetParent(GuestsTransform);
            //アクティブ（true）にする
            transform.gameObject.SetActive(true);
            return transform;
        }).ToList();

        yield break;
    }

/*--- 後略 ---*/
```

# ボイスチャットが音割れ？ブツブツ？する
色々と調査・修正・改造を __1ヵ月__ してきましたが、PhotonVoice と OVRLipSync の相性がメチャクチャに悪いということしかわかりませんでした。
OVRLipSync の代わりに __[SALSA With RandomEyes](https://assetstore.unity.com/packages/tools/animation/salsa-with-randomeyes-16944)__ を導入することにしました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">SALSA Lip-Sync with PhotonVoice (demonstration) <a href="https://t.co/TM7wbPlk6u">https://t.co/TM7wbPlk6u</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>より<br>このアセットを買えば何も考えずにPhotonVoiceとLipSyncができる…？(脳死)</p>&mdash; karukaru (@_karukaru_) <a href="https://twitter.com/_karukaru_/status/997187115891834880?ref_src=twsrc%5Etfw">2018年5月17日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
大体この動画通りにセットアップすれば問題ないです。
自分の画面で自分がリップシンクできないですが、PhotonVoice.DebugEchoMode を true にすれば自分の声が PhotonVoice経由で返ってくるので、そこからリップシンクすれば問題ないです。

```PhotonVoiceRecorder.cs
/*--- 前略 ---*/

    IEnumerator Start()
    {
        yield return Application.RequestUserAuthorization(UserAuthorization.Microphone);
        if (!Application.HasUserAuthorization(UserAuthorization.Microphone))
        {
            yield break;
        }

        if (photonView.isMine)
        {
            switch (this.TypeConvert)
            {
                case SampleTypeConv.Short:
                    forceShort = true;
                    Debug.LogFormat("PUNVoice: Type Convertion set to Short. Audio samples will be converted if source samples type differs.");
                    break;
                case SampleTypeConv.ShortAuto:
                    var speex = gameObject.GetComponent<SpeexDSP>();
                    if (speex != null && speex.Active)
                    {
                        if (PhotonVoiceSettings.Instance.DebugInfo)
                        {
                            Debug.LogFormat("PUNVoice: Type Convertion set to ShortAuto. SpeexDSP found. Audio samples will be converted if source samples type differs.");
                        }
                        forceShort = true;
                    }
                    break;
            }
            this.voice = createLocalVoiceAudioAndSource();

            //DebugEchoMode を True にすることで自分の声を聴くことができる 	
            if (photonManager.loginMode != PhotonManager.LoginMode.DesktopGuest)
            {
                this.voice.DebugEchoMode = true;
            }
            else
            {
                this.voice.DebugEchoMode = false;
            }

            this.VoiceDetector.On = PhotonVoiceSettings.Instance.VoiceDetection;
            this.VoiceDetector.Threshold = PhotonVoiceSettings.Instance.VoiceDetectionThreshold;
            if (this.voice != Voice.LocalVoiceAudio.Dummy)
            {
                this.voice.Transmit = PhotonVoiceSettings.Instance.AutoTransmit;
            }
            else if (PhotonVoiceSettings.Instance.AutoTransmit)
            {
                Debug.LogWarning("PUNVoice: Cannot Transmit.");
            }
            sendPhotonVoiceCreatedMessage();
        }
    }

/*--- 後略 ---*/
```

SALSAでのリップシンクですが、動作内容的に音量から口パクを生成している感じがするので自作することもできそうです。
逆に言うと正確なリップシンクはできません。それっぽく動かしてくれるだけです。
モーフの登録は以下のようにすれば大体それっぽくなります。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">めも<br>SALSA のリップシンク<br>Small -&gt; ii<br>Medium -&gt; ee<br>Large -&gt; oo</p>&mdash; karukaru (@_karukaru_) <a href="https://twitter.com/_karukaru_/status/999215352742494208?ref_src=twsrc%5Etfw">2018年5月23日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
![スクリーンショット (187).png](https://qiita-image-store.s3.amazonaws.com/0/213883/40cf38f5-4d2b-558b-4773-8e477a97734f.png)

# ゲストがマイク入力先を選択できるようにする
同じく PhotonVoiceRecorder を改造すれば任意のマイク入力をボイスチャットに乗せることができます。

```PhotonVoiceRecorder.cs
/*--- 前略 ---*/

                    //var micDev = this.MicrophoneDevice != null ? this.MicrophoneDevice : PhotonVoiceNetwork.MicrophoneDevice;

                    string micDev = null;
                    if (photonManager.loginMode == PhotonManager.LoginMode.Master)
                    {
                        micDev = Microphone.devices.First(deviceName => deviceName.StartsWith("Yamaha"));
                    }
                    else
                    {
                        micDev = PhotonManager.MicrophoneName;
                    }

                    if (PhotonVoiceSettings.Instance.DebugInfo)
                    {
                        Debug.LogFormat("PUNVoice: Setting recorder's source to microphone device {0}", micDev);
                    }
                    // mic can ignore passed sampling rate and set it's own
                    var mic = new MicWrapper(micDev, (int)pvs.SamplingRate);

/*--- 後略 ---*/
```

# ゆかりさんとVR交流会　第2回
5/26にニコ生で放送しました。
タイムシフトはこちら：http://live.nicovideo.jp/gate/lv313408052
<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm33518434/script?w=640&h=360"></script><noscript><a href="http://www.nicovideo.jp/watch/sm33518434">【VR配信】ゆかりさんとVR交流会　2018/05/26【あーかいぶ】</a></noscript>

## 問題
Trello に移行しました。
https://trello.com/b/qE36194K

# VRゲストが別ユーザから動いてない
VRゲストのVRIKをマスター側でも有効にしていたのが原因でした。
初期設定でVRIKを無効化しておき、VRゲスト入室時に有効化することで解決しました。

# VRゲストの頭が表示されない
こちらもVRゲストの VRMFirstPerson.Setup をマスター側でも実行していたのが問題でした。
同じようにVRゲスト入室時に実行することで解決しました。

# 落物とボタンが接触しても反応してしまう
Physics の設定からレイヤーの物理判定を選択することで解決しました。
![スクリーンショット (190).png](https://qiita-image-store.s3.amazonaws.com/0/213883/deca7d62-15d2-ca17-beb2-0005e7f63f09.png)

# ゲスト側で落物が移動して見える
検証したところ、どうも Rigidbody がついているオブジェクト同士が接触すると不安定になるようです。
根本的な解決方法はないと思うので、Photon の同期方法と補間方法を変更することで誤魔化そうと思います。
![スクリーンショット (191).png](https://qiita-image-store.s3.amazonaws.com/0/213883/a0c1d218-4318-bdd4-f9df-4518c1ecbab5.png)

# ダンスステージに変更するとモデルが暗くて見えない
モデルはUnlitなシェーダを使っているのですが、これは太陽光のみ反映して環境光は無視します。一方ダンスステージでは暗い演出をするために太陽が沈んでいる状態にしていました。
この状態ではモデルに光が当たらず、影の中に埋もれてしまって見栄えが悪いです。
AssetStoreから適当な夜空のSkyboxを持ってきて夜っぽい演出をしつつ、太陽を直上に移動させることで光源を確保しました。

# カメラを持つとブレる
VirtualCast のカメラの挙動を見てたら線形補間っぽかったので、これならできそうだと思って[ココ](https://gametukurikata.com/basic/smoothmove)を参考に書いてみました。

```ShakeCorrection.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShakeCorrection : MonoBehaviour
{
    public GameObject TargetCollider;
    public GameObject TargetRenderer;

    public enum InterpolateMode
    {
        None,
        Lerp,
        Slerp
    };

    //補間モード
    public InterpolateMode interpolateMode;

    //移動スピード
    public float MoveSpeed = 5f;

    //レンダラーのRigidbody
    private Rigidbody RendererRigidbody;

    //コルーチンチェック用
    private bool isCoroutineRunning = false;

    // Use this for initialization
    void Start()
    {
        RendererRigidbody = TargetRenderer.GetComponent<Rigidbody>();

        //Rigidbody が null なら実行しない
        if (RendererRigidbody != null)
        {
            StartCoroutine(UpdateCoroutine());
        }
    }

    IEnumerator UpdateCoroutine()
    {
        while (true)
        {
            if (isCoroutineRunning)
            {
                yield return null;
                continue;
            }

            Debug.Log("InterpolateMode : " + interpolateMode);
            switch (interpolateMode)
            {
                case InterpolateMode.Lerp:
                    StartCoroutine(LerpCoroutine());
                    break;

                case InterpolateMode.Slerp:
                    StartCoroutine(SlerpCoroutine());
                    break;

                default:
                    StartCoroutine(NoneCoroutine());
                    break;
            }

            yield return null;
        }
    }

    IEnumerator NoneCoroutine()
    {
        isCoroutineRunning = true;

        while (interpolateMode == InterpolateMode.None)
        {
            RendererRigidbody.position = TargetCollider.transform.position;
            RendererRigidbody.rotation = TargetCollider.transform.rotation;

            yield return null;
        }

        isCoroutineRunning = false;
        yield break;
    }

    IEnumerator LerpCoroutine()
    {
        isCoroutineRunning = true;

        Vector3 _oldPos = RendererRigidbody.position;
        Quaternion _oldRot = RendererRigidbody.rotation;
        while (interpolateMode == InterpolateMode.Lerp)
        {
            Vector3 _newPos = TargetCollider.transform.position;
            Quaternion _newRot = TargetCollider.transform.rotation;

            RendererRigidbody.position = Vector3.Lerp(_oldPos, _newPos, MoveSpeed * Time.deltaTime);
            RendererRigidbody.rotation = Quaternion.Lerp(_oldRot, _newRot, MoveSpeed * Time.deltaTime);

            _oldPos = RendererRigidbody.position;
            _oldRot = RendererRigidbody.rotation;

            yield return null;
        }

        isCoroutineRunning = false;
        yield break;
    }

    IEnumerator SlerpCoroutine()
    {
        isCoroutineRunning = true;

        Vector3 _oldPos = RendererRigidbody.position;
        Quaternion _oldRot = RendererRigidbody.rotation;
        while (interpolateMode == InterpolateMode.Slerp)
        {
            Vector3 _newPos = TargetCollider.transform.position;
            Quaternion _newRot = TargetCollider.transform.rotation;

            RendererRigidbody.position = Vector3.Slerp(_oldPos, _newPos, MoveSpeed * Time.deltaTime);
            RendererRigidbody.rotation = Quaternion.Slerp(_oldRot, _newRot, MoveSpeed * Time.deltaTime);

            _oldPos = RendererRigidbody.position;
            _oldRot = RendererRigidbody.rotation;

            yield return null;
        }

        isCoroutineRunning = false;
        yield break;
    }
}
```
TargetCollider にコライダと VRTK 一式がアタッチされた GameObject を、TargetRenderer に Rigidbody がアタッチされた手振れ補正をかけたい Renderer がある GameObject を登録してください。
![スクリーンショット (194).png](https://qiita-image-store.s3.amazonaws.com/0/213883/30736a5b-9b2f-e6c2-811a-882ae9d4e8f5.png)

# ゲストクライアントにコメントを落とす
この項目でやりたいことはPhotonの同期オブジェクトを動的に生成することなので、これについては長くなってしまうので別記事に~~書きます。~~ 書きました。
[Photon Unity Networkingで動的に同期オブジェクトを生成する方法](https://qiita.com/_karukaru_/items/f90fe4f269a72d2b46d0)
これを応用すればゲストクライアントにコメントを落とすことが可能になります。

# ゆかりさんじゃないけどVR交流会　2018/06/14
6/14にニコ生で放送しました。
タイムシフトはこちら：http://live.nicovideo.jp/gate/lv313824917
<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm33518558/script?w=640&h=360"></script><noscript><a href="http://www.nicovideo.jp/watch/sm33518558">【VR配信】ゆかりさんじゃないけどVR交流会　2018/06/14【あーかいぶ】</a></noscript>

## 問題
Trello に移行しました。
https://trello.com/b/qE36194K

# ゆかりさんじゃないけどVR交流会　2018/06/19
6/19にニコ生で放送しました。
タイムシフトはこちら：http://live.nicovideo.jp/gate/lv313928462
<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm33518712/script?w=640&h=360"></script><noscript><a href="http://www.nicovideo.jp/watch/sm33518712">【VR配信】ゆかりさんじゃないけどVR交流会　2018/06/19【あーかいぶ】</a></noscript>

## 問題
Trello に移行しました。
https://trello.com/b/qE36194K

# ゆかりさんじゃないけどVR交流会　2018/07/07
7/7にニコ生で放送しました。
タイムシフトはこちら：http://live2.nicovideo.jp/watch/lv314269793
<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm33518895/script?w=640&h=360"></script><noscript><a href="http://www.nicovideo.jp/watch/sm33518895">【VR配信】ゆかりさんじゃないけどVR交流会　2018/07/07【あーかいぶ】</a></noscript>

## 問題
Trello に移行しました。
https://trello.com/b/qE36194K