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

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

ゆかりさんとVR撮影会

2/13にバーチャルアイドルちゃんねる(VIC) さんで放送しました。
タイムシフトはこちら:http://live.nicovideo.jp/watch/lv311017182

【VR配信】ゆかりさんとVR撮影会【あーかいぶ】
ハッシュタグは#ゆかりさんとXXになりました。
〇ってTwitterのハッシュタグに設定できないんですね。

浮かび上がった問題点

実装・解決したものは打消し線を入れてます。

既存の問題

  1. キャプチャ映像と音がズレている
  2. カメラを持つとブレる
  3. 生放送の管理ができない
  4. デスクトップの解像度が高すぎて文字が読めない
  5. VRIKの再セットアップしたい
  6. 特定のコメントを受信したら色々なモデルを出現させたい
    例えば「ゾウ」と打ったらUnity内にゾウが出現するとか
  7. ボタンを置いていろいろしたい
    カメラの切り替えとかオブジェクトの出現とか再配置とかゲーム画面をデスクトップ画面と置き換えるとか
  8. HTC VIVE と Nintendo Switch のコントローラを同時に持つことが難しい
    腕にマジックテープなどでVIVEのコントローラを固定すれば何とかできるかも?
  9. ユニティちゃん Candy Rock Star ライブステージ!にあるスピーカー実装したい
  10. ゲーム画面を見ている時にゆかりさんが後ろを向いている
    鏡の設置や配信設定を変えることで対応可能?
  11. やっぱりWebCamTexture重い
  12. コメントを落とす範囲が広すぎて取りに行けない
  13. テレポート時に姿勢がおかしくなってる
  14. PCゲーム配信したい
  15. 配信用のオブジェクト配置にするのが面倒
  16. 車のモデル置いてレースクイーンごっことかしたい

新たな問題

  1. やっぱりクライアントが重い(特にCPUパワー)
  2. クッションを動かすとめっちゃ重くなるらしい
  3. 解像度低い状態でもログインできるようにする
  4. ゆかりさんが表示されない場合がある
  5. 操作方法をFPSゲームのようにしてほしい
  6. カメラの移動速度が遅い
  7. 表情変化と指の変化を同時にやるのに苦労する
  8. 表情が切り替わったことがわからない
  9. 最大455メッセージ/秒って多すぎない?
  10. ユーザーネームを入力しないでログインすると固まる
  11. UnityNicoliveClientへ移行したい
  12. 番組開始ボタンと終了ボタンを用意したい

マルチプレイ時の通信情報

ユーザーネームを入力しないでログインすると固まる

何も入力しないとnullが入ると思っていたけど、実際は空文字(プログラム的に書くと "" )が入っていた。
そのためnullチェックでは足りないことが判明したため、判定を以下のように修正。

Login.cs
/*** 前略 ***/

            //ユーザ名の設定
            if (userName != null && userName != "")
            {
                //ユーザ名を通知
                PhotonNetwork.playerName = userName;
            }
            else
            {
                //プレイヤーの数を取得
                int num = PhotonNetwork.otherPlayers.Length;
                //ユーザ名を生成
                userName = "Guest" + num;
                //ユーザ名を通知
                PhotonNetwork.playerName = userName;
            }

/*** 後略 ***/

最大455メッセージ/秒って多すぎない?

流石にゆかりさんのIK全てにPhotonView付けて同期設定するのは無茶だったと思う。
VRIKで指定しているIKだけ同期するようにスクリプトを以下のように修正

YukariPhoton.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class YukariPhoton : MonoBehaviour
{
    // Use this for initialization
    void Start()
    {
        //ゆかりさんのモデルからVRIKのIK情報を取得
        GameObject yukariObject = GameObject.Find("結月ゆかり_純_ver1.0");
        var vrik = yukariObject.GetComponent<RootMotion.FinalIK.VRIK>();

        //同期するIKのリスト
        List<GameObject> photonList = new List<GameObject>();
        photonList.Add(vrik.references.root.gameObject);
        photonList.Add(vrik.references.pelvis.gameObject);
        photonList.Add(vrik.references.spine.gameObject);
        photonList.Add(vrik.references.chest.gameObject);
        photonList.Add(vrik.references.neck.gameObject);
        photonList.Add(vrik.references.head.gameObject);
        photonList.Add(vrik.references.leftShoulder.gameObject);
        photonList.Add(vrik.references.leftUpperArm.gameObject);
        photonList.Add(vrik.references.leftForearm.gameObject);
        photonList.Add(vrik.references.leftHand.gameObject);
        photonList.Add(vrik.references.rightShoulder.gameObject);
        photonList.Add(vrik.references.rightUpperArm.gameObject);
        photonList.Add(vrik.references.rightForearm.gameObject);
        photonList.Add(vrik.references.rightHand.gameObject);
        photonList.Add(vrik.references.leftThigh.gameObject);
        photonList.Add(vrik.references.leftCalf.gameObject);
        photonList.Add(vrik.references.leftFoot.gameObject);
        photonList.Add(vrik.references.leftToes.gameObject);
        photonList.Add(vrik.references.rightThigh.gameObject);
        photonList.Add(vrik.references.rightCalf.gameObject);
        photonList.Add(vrik.references.rightFoot.gameObject);
        photonList.Add(vrik.references.rightToes.gameObject);

        foreach (GameObject go in photonList)
        {
            var photonView = go.GetComponent<PhotonView>();
            var photonTransformView = go.GetComponent<PhotonTransformView>();

            //位置を同期
            photonTransformView.m_PositionModel.SynchronizeEnabled = true;
            //回転を同期
            photonTransformView.m_RotationModel.SynchronizeEnabled = true;
            //Transformを同期するように設定
            photonView.ObservedComponents[0] = photonTransformView;
            //同期タイミングの設定
            photonView.synchronization = ViewSynchronization.UnreliableOnChange;
        }
    }
}

これだけだともみあげとかうさみみとかフワフワしないけど、ゲスト側でSpringBoneの演算処理をやらせることで解決。
メッセージ数減らしたいからゲスト側に頑張ってもらう方針にしました。

クッションを動かすとめっちゃ重くなるらしい

色々と調査したところ、どうもRigidbodyのUse Gravityがオンになってると重いことが判明しました。
マスターとクライアントで2重に重力処理を行っているから重いのかなと思ったのですが、マスターが居なくても重いので原因は謎です。
まあマスター側で重力演算やって、位置と回転の情報だけ同期してゲスト側に送るように処理してあげましょう。

解像度低い状態でもログインできるようにする

ログインボタンの位置を中心基準ではなくて画面の下辺基準にすることで対応。

カメラの移動速度が遅い

左シフトキーを押してるときに移動速度と回転速度が4倍になるようにしました。

GuestController.cs
/*** 前略 ***/

        //左シフトキーを押したときのイベント
        if (Input.GetKey(KeyCode.LeftShift))
        {
            coef = 4;
        }
        else
        {
            //押してないときのイベント
            coef = 1;
        }

        //スペースを押したときのイベント
        if (Input.GetKey(KeyCode.E))
        {
            Vector3 vector = go.transform.up * coef * (float)0.025;
            go.transform.position += vector;
        }

/*** 中略 ***/

        //↑を押したときのイベント
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.Rotate(-1 * coef, 0, 0);
        }

/*** 後略 ***/

UnityNicoliveClient

ゲスト側でやりますアンコちゃん!に接続する必要はないけど、処理順番的に必ずエラーが吐かれるという気持ち悪い状態だったので、Unityから直接ニコ生APIを叩く方法に変更します。

CommentEvent.cs
using NicoliveClient;
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Text;
using UniRx;
using UnityEngine;

public class CommentEvent : MonoBehaviour
{
    //アカウント情報が書いてあるテキストファイルのパス
    public string filePath;

    //デバッグ用に番組IDを指定できるように
    public string streamID;

    //オブジェクトを生成してくれるやつ
    CreateObject createComponent;

    //コメントクライアント
    NicoliveCommentClient commentClient;

    // Use this for initialization
    void Start()
    {
        var account = readFile();

        //アカウント情報がnullでないなら
        if (account != null)
        {
            //コンポーネントを取得
            GameObject createObject = GameObject.Find("CreateObject");
            createComponent = createObject.GetComponent<CreateObject>();

            //コルーチン開始
            StartCoroutine(LoginCoroutine(account[0], account[1]));
        }
    }

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

    }

    //ログインするやつ
    IEnumerator LoginCoroutine(string mail, string pass)
    {
        //ログイン実行
        var u = NiconicoUserClient.LoginAsync(mail, pass).ToYieldInstruction();
        yield return u; //ログイン処理を待機

        //ログインに成功するとユーザ情報が返ってくる
        NiconicoUser user = u.Result;

        //クライアントにユーザ情報を渡して初期化
        var client = new NicoliveApiClient(user);

        //UserAgentを設定する
        //日本語を入力するとバグるから英文字だけにしようね!
        client.SetCustomUserAgent("Yukarisan-XX");

        //番組IDが指定されていなければ放送中のIDを取得
        if (streamID == null || streamID == "")
        {
            Debug.Log("get StreamID");

            //番組ID取得
            var lv = client.GetCurrentNicoliveProgramIdAsync().ToYieldInstruction();
            yield return lv; //番組IDが取得できるまで待機

            //streamIDに記録
            streamID = lv.Result;
        }

        //それでもnullならコルーチンの破棄
        if (streamID == null || streamID == "")
        {
            Debug.Log("StreamID is NULL");
            yield break;
        }

        //操作したい番組ID登録
        client.SetNicoliveProgramId(streamID);

        //コメントを取得するコルーチンをStart
        StartCoroutine(CommentCoroutine(user, client));
    }

    //コメント取得するやつ
    IEnumerator CommentCoroutine(NiconicoUser user, NicoliveApiClient apiClient)
    {
        //番組情報取得
        var pi = apiClient.GetProgramInfoAsync().ToYieldInstruction();
        yield return pi;

        // 番組の部屋一覧
        // 自分が放送する番組の場合は全部屋取得できる
        // 他人の放送の場合は「座席を取得済み」の場合のみ、その座席のある部屋の情報が1つ取得できる
        var rooms = pi.Result.Rooms;

        if (rooms == null)
        {
            Debug.Log("Rooms is NULL");
            yield break;
        }

        //先頭の部屋に接続するコメントクライアントを作成
        commentClient = new NicoliveCommentClient(rooms.First(), user.UserId);

        //コメント購読設定
        commentClient.OnMessageAsObservable.Subscribe((comment) =>
        {
            //Debug.Log(comment.Content);

            //コメントの3Dオブジェクトを生成
            createComponent.CacheComment(comment);
        });

        //クライアント接続
        commentClient.Connect(resFrom: 0);

        /*
        上記プログラムは視聴者数が少ないならこのままでもいいけど、視聴者数が多いなら全部屋のコメントクライアントに接続するように変更しないとダメ
        サンプルの CommentPanel.cs が参考になる
        */

        Debug.Log("コメント取得開始");

        yield break;
    }

    //終了処理
    void OnApplicationQuit()
    {
        Debug.Log("おかたづけ");

        if (commentClient != null)
        {
            //おかたづけ
            commentClient.Disconnect();
            commentClient.Dispose();
        }

    }

    //アカウント情報が書いてあるテキストファイルの読込
    string[] readFile()
    {
        string[] splitDatas = null;

        //ファイル読み込みにトライ
        try
        {
            using (var fileStream = new FileStream(filePath, FileMode.Open))
            {
                using (var streamReader = new StreamReader(fileStream, Encoding.GetEncoding("UTF-8")))
                {
                    //一行ずつ読み込む
                    string line = streamReader.ReadLine();

                    //','区切りで分割したものを配列に追加
                    splitDatas = line.Split(',');
                }
            }
        }
        catch (Exception ex)
        {
            Debug.Log(ex);
        }

        return splitDatas;
    }
}

正直言ってコルーチンの使い方わかってないので、コルーチンの中でコルーチンを呼び出してたりする汚いコードです。しかもReadmeのコピペ。
おまけにエラー処理とかセキュリティの事とか全然考えてないので、実装する場合そこら辺をちゃんと考えてください。
コルーチンについてはココを参考にしました。

問題

  1. 番組情報を取得できない
    最初ユーザエージェントを以下のように設定してました。
CommentEvent.cs
        //UserAgentを設定する
        client.SetCustomUserAgent("ゆかりさんとXX");

これだと番組情報を取得する GetProgramInfoAsync を呼び出した時に、情報が返ってこなくて固まります。
日本語は対応してなく、英文字のみ対応のようなので以下のように修正。

CommentEvent.cs
        //UserAgentを設定する
        //日本語を入力するとバグるから英文字だけにしようね!
        client.SetCustomUserAgent("Yukarisan-XX");
  1. 開始されていない番組のコメントを取得しようとすると固まる
    同期的に処理すべき場所を非同期的に動かしてました。 具体例を挙げると、現在放送中の番組IDを取得するGetCurrentNicoliveProgramIdAsync を呼び出した時に
CommentEvent.cs
        //現在放送中の番組ID取得
        client.GetCurrentNicoliveProgramIdAsync().Subscribe(

        //正常に番組IDが返ってきたとき
        (lv) =>
        {
            Debug.Log("放送中ID取得:" + lv);
            streamID = lv;
        },

        //エラったとき
        (ex) =>
        {
            Debug.LogError(ex);
        });

        //それでもnullならコルーチンの破棄
        if (streamID == null || streamID == "")
        {
            Debug.Log("StreamID is NULL");
            yield break;
        }

という風に書いていた。これだと非同期的に番組IDを取得するため、その後の番組IDチェックが先に判定されてしまってコルーチンがbreakされてました。
同期的に番組IDを取得すれば解決します。

CommentEvent.cs
        //番組ID取得
        var lv = client.GetCurrentNicoliveProgramIdAsync().ToYieldInstruction();
        yield return lv; //番組IDが取得できるまで待機

        //streamIDに記録
        streamID = lv.Result;

非同期処理って難しいですね。スキルのなさを露呈させてしまいました。

表情変化と指の変化を同時にやるのに苦労する

グリップボタンを押しながら指の形を変化させたら、反対側の指を変化させるように機能を追加しました。

ControllerEvents.cs
                //手モーションモード
                case 1:
                    //グリップボタンを押していないなら
                    if (!device.GetPress(SteamVR_Controller.ButtonMask.Grip))
                    {
                        //通常モード
                        changeHand(position);
                    }
                    else
                    {
                        //反対の手を操作するモード
                        isRight = !isRight;
                        changeHand(position);
                        isRight = !isRight;
                    }

                    break;

表情が切り替わったことがわからない

表情変化モードの時にグリップボタンを押したら、自分だけ見える鏡を設置することで解決。
鏡の作り方はココを、最初からfalseなGameObjectを取得する方法はココを参考にしました。

ControllerEvents.cs
/*** 前略 ***/

    GameObject FaceMirror;

/*** 中略 ***/

    // Use this for initialization
    void Start()
    {

/*** 中略 ***/

        //最初からfalseのGameObjectは親から辿っていかないと取得できない
        //https://spphire9.wordpress.com/2013/11/17/unity%E3%81%A7%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%81%A7%E3%81%AA%E3%81%84gameobject%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B/
        FaceMirror = GameObject.Find("13.joint_Head").transform.Find("FaceMirror").gameObject;
    }

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

/*** 中略 ***/

        if (device.GetPressUp(SteamVR_Controller.ButtonMask.Grip))
        {
            Debug.Log("グリップボタンをクリックして離した");
            switch (menuState)
            {
                case 2:
                    FaceMirror.SetActive(!FaceMirror.activeSelf);
                    break;

                case 3:
                    GuestController.SendEnd();
                    break;

                default:
                    break;
            }
        }

/*** 後略 ***/

できたもの

VRIKの再カスタマイズ

執筆中

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.