LoginSignup
10

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-26

この記事は前回の記事の続きです。また自分用メモのように使用しているため、記事内容のブラッシュアップをしていません。

モーフコントローラでエラー(アウトオブレンジ)

以前書いた 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
盛大に事故った放送だったので動画には残しません。 残しました。

問題

いつもこの項目でかなりの行数を持ってかれてしまっていたので、今後は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

ゲストが入室したときに無限ループしてる

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

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 を導入することにしました。

大体この動画通りにセットアップすれば問題ないです。
自分の画面で自分がリップシンクできないですが、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でのリップシンクですが、動作内容的に音量から口パクを生成している感じがするので自作することもできそうです。
逆に言うと正確なリップシンクはできません。それっぽく動かしてくれるだけです。
モーフの登録は以下のようにすれば大体それっぽくなります。



スクリーンショット (187).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

問題

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

VRゲストが別ユーザから動いてない

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

VRゲストの頭が表示されない

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

落物とボタンが接触しても反応してしまう

Physics の設定からレイヤーの物理判定を選択することで解決しました。
スクリーンショット (190).png

ゲスト側で落物が移動して見える

検証したところ、どうも Rigidbody がついているオブジェクト同士が接触すると不安定になるようです。
根本的な解決方法はないと思うので、Photon の同期方法と補間方法を変更することで誤魔化そうと思います。
スクリーンショット (191).png

ダンスステージに変更するとモデルが暗くて見えない

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

カメラを持つとブレる

VirtualCast のカメラの挙動を見てたら線形補間っぽかったので、これならできそうだと思ってココを参考に書いてみました。

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

ゲストクライアントにコメントを落とす

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

ゆかりさんじゃないけどVR交流会 2018/06/14

6/14にニコ生で放送しました。
タイムシフトはこちら:http://live.nicovideo.jp/gate/lv313824917

問題

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

ゆかりさんじゃないけどVR交流会 2018/06/19

6/19にニコ生で放送しました。
タイムシフトはこちら:http://live.nicovideo.jp/gate/lv313928462

問題

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

ゆかりさんじゃないけどVR交流会 2018/07/07

7/7にニコ生で放送しました。
タイムシフトはこちら:http://live2.nicovideo.jp/watch/lv314269793

問題

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10