OVR + VRM + Photonを用いたマルチプレイ対応アバターシステム

# はじめに
Unityでアバターを使用する方法は様々あると思いますが（Meta Avatars SDKなど），今回はVRMをもちいて開発をおこないます．
自分が研究ではOculusをもちいてVRApp開発をしていたので，Oculus IntegrationにPhoton(PUN2)を利用してマルチプレイ対応Appの基盤的なものを作成します．
Unity初めて1年程度なので，もっといい方法があったら情報提供おねがいします！！！

# 開発環境
- Windows10
- Oculus Link対応GPU搭載PC
- Unity version: 2021.3.26f1



# 目次
[1. 完成イメージ](#完成イメージ)
[2. VRMのimport](#vrmのimport)
[3. Final IKの導入](#finalikの導入)
[4. Oculus integrationの導入](#oculus-integrationの導入)
[5. photonの導入](#photonの導入)
[6. Avatarの設定](#avatarの設定)
[7. 実行結果](#実行結果)
[8. 自然な歩行を可能にする方法(2023/11/11追記)](#自然な歩行を可能にする方法20231111追記)
[9. おわりに](#おわりに)

以上の流れで内容を書いていきます．


# 完成イメージ
こんな感じでマルチプレイができます．
![vr_avatar_system.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/1d198e52-3c5d-6188-c84a-c553dd3bc5c7.gif)



# VRMのimport
VRMをUnityへimportするためのunitypackageがあります．
これに関しては，以下の記事を参考に行うとよいかと思います．
また，VRMをアバターとして用いる際には，FinalIK（有料アセット）を使用します．

VRMに関しては，VRM0系とVRM1系がありますが，それぞれ使いたいVRMの種類によって追加するunitypackageをimportしてください．
自分の所感ですが，最近のVRoid HubはVRM1系が多い気がします．（詳しい方情報お願いします！！！）

- 参考記事
    - [VRM ドキュメント](https://github.com/vrm-c/UniVRM/releases)
    
自作できる方はBlender?などを用いてVRMアバターを作成してもいいですが，気軽にVRMアバターを使用したいなら[VRoid Hub](https://hub.vroid.com/)をおすすめします．
今回は，以下のアバターを用いてマルチプレイ対応VRAppを作成します．
- [AvatarSample_A](https://hub.vroid.com/characters/2843975675147313744/models/5644550979324015604)

ダウンロードしたVRMファイルをUnityにドラッグ＆ドロップすれば正常にimportされるはずです．

↓こんな感じ↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/1f30cd81-872c-41b6-7c2d-8e6c3e89745b.png)


# FinalIKの導入
Final IKを購入後，Package ManagerのMy Assetsからimport



# Oculus integrationの導入
これに関してはネット上にたくさん情報があふれているかと思います．
今回はOculus Linkを使用して開発をしています．Linkに関する設定も他の記事を見てください．
OculusでLinkを立ち上げ，Unityでゲーム再生したときにVRのAppが立ち上がれば大丈夫です．（もちろんOVRCameraRig等をシーン内に配置したときに）

- 参考記事
    - [【Oculus Quest開発メモ】開発初期設定まとめ Oculus Integration【Unity】](https://raspberly.hateblo.jp/entry/OculusIntegrationSetup)
        - 今回はOculus Linkをもちいて開発を行うので，Android関連の設定は必要ありません
- シーン内に，OVRPlayerController，plane（Floor）を追加
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/85d3d749-ab31-f37e-aa0c-0bf4ab0a63cd.png)

- この状態でplayすればFloor内を自由に動けます
# photonの導入
以下の記事を見ながら基本的には設定をすれば大丈夫です．
- 参考記事
    - [PUN2（Photon Unity Networking 2）で始めるオンラインゲーム開発入門 🔰 初期設定とチュートリアル](https://zenn.dev/o8que/books/bdcb9af27bdd7d/viewer/c04ad5)
    - この記事はかなり参考にさせていただきました
    
以下に設定手順をざっくり書いていきます
1. [公式サイト](https://www.photonengine.com/ja-JP/PUN)から，アカウント登録，サインインする
1. アプリケーションを作成する
    1. ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/83641b8e-4124-7001-ab21-add291e14b72.png)
    1. ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/affb002e-7a85-19d5-1eef-36c98afd9670.png)
    1. ![photon_sample_app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/61d7a4a3-1fc1-5d28-04b8-33988ef29fce.png)
    1. 上画像のアプリケーションIDが必要となります（添付画像では消してあります）
    1. [Assetストア](https://assetstore.unity.com/packages/tools/network/pun-2-free-119922)からphotonをUnityプロジェクトにimport
    1. PUN Wizardが開いたら，アプリケーションIDを入れて，setup
    1. PUN2の設定ファイル（PhotonServerSettings）の設定

次に，PhotonManager.csを作成
```PhotonManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class PhotonManager : MonoBehaviourPunCallbacks
{
    [SerializeField] string avatarName = "AvatarSample_A";
    [SerializeField] Transform spawnpoint;


    private void Start()
    {
        PhotonNetwork.ConnectUsingSettings();
    }

    public override void OnConnectedToMaster()
    {
        PhotonNetwork.JoinOrCreateRoom("Room", new RoomOptions(), TypedLobby.Default);
    }

    public override void OnJoinedRoom()
    {
        Vector3 position = spawnpoint.position;
        PhotonNetwork.Instantiate(avatarName, position, Quaternion.identity);
    }
}
```
- scene内に，PhotonManager（空オブジェクト）を作成，PhtonManager.csをアタッチ
- SpawnPoint（空オブジェクト）を任意の場所に作成
- PhotonManagerコンポーネントにおいて，Inspector内からSpawnPointを設定
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/87291767-185a-db50-c5de-19544efb6423.png)

# Avatarの設定
- 一般的なVRMアバターの利用方法は，OVRPlayerControllerの子オブジェクトとし，VRIKによって，頭，両手を追従させることです
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/ab2c0fcb-c217-0781-13e9-e06159e191cd.png)

- しかし，このVRM込みのOVRPlayerControllerをPhotonサーバー接続時に生成してしまうと，OVRManager周りのエラーが出ます
- かつ，Avatar操作周りをマルチプレイと切り分けたほうが，OVRPlayerContorollerに変更を加えずにすみます
    - これにより，マルチプレイを気にせずに世にあふれているOVR周りの開発が容易になります
    - よって，以下でその切り分け方について説明します

## OVRとPhotonまわりをどうやって切り分けるか
- VRMアバターを，以下の構成に変更を変更します
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/45d3c0eb-819f-b80c-1f71-d404026c9882.png)
- prefabになっている`AvatarSample_A`がVRM，他はすべて空オブジェクト
- これにより，以下のようにネットワークオブジェクトとローカルオブジェクトを切り分けられます
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/d3a98693-6ff2-9eb1-210d-2db645e7b54f.png)
- Photonによって共有されるTransformは，OVRPlayerControllerの座標ではなく，新しく作成したAvatarSample_Aの各オブジェクトの座標となり，OVR系統はphotonとは無関係のオブジェクトにできます．（各オブジェクトの詳細な設定は後述）
- 次に，ネットワークオブジェクトの追従設定のためのTagを用意します
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/88a9ab32-3599-9528-a288-5c15ca502c44.png)

- それぞれシーン内の，OVRPlayerController，OVRCameraRig，TrackingSpace,LHandTargetAnchor，RHandTargetAnchorにタグ付けします
- 以下の`CreatePhotonAvatar.cs`を作成ます（VRMオブジェクトににアタッチします）


```CreatePhotonAvatar.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RootMotion.FinalIK;
using Photon.Pun;

public class CreatePhotonAvatar : MonoBehaviourPunCallbacks
{ 
    private GameObject masterPlayerObject;
    private GameObject[] rootTargets = new GameObject[7];

    [SerializeField] GameObject[] Targets = new GameObject[7];

    private bool isCreated = false;


    private void OnCreate()
    {
        masterPlayerObject = GameObject.FindGameObjectWithTag("MasterPlayer");
        rootTargets[0] = GameObject.FindGameObjectWithTag("CameraRig");
        rootTargets[1] = GameObject.FindGameObjectWithTag("TrackingSpace");
        rootTargets[2] = GameObject.FindGameObjectWithTag("MainCamera");
        rootTargets[3] = GameObject.FindGameObjectWithTag("LHandTargetAnchor");
        rootTargets[4] = GameObject.FindGameObjectWithTag("RHandTargetAnchor");
        for(int i = 0; i < 5; i++)
        {
            if(rootTargets[i] == null)
            {
                Debug.Log(i);
                Debug.LogError("Target is not found");
            }
        }

        isCreated = true; 
    }

    private void OnEnable()
    {
        if(photonView.IsMine)
        {
            OnCreate();
        }
    }

    void Update()
    {
        if(isCreated)
        {
            this.transform.position = masterPlayerObject.transform.position;
            this.transform.rotation = masterPlayerObject.transform.rotation;

            for(int i = 0; i < 5; i++)
            {
                Targets[i].transform.localPosition = rootTargets[i].transform.localPosition;
                Targets[i].transform.localRotation = rootTargets[i].transform.localRotation;
            }

        }
    }
}

```
### SampleAvatar_Aの各種設定
1. ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/dcc3d6c7-a2f2-736e-7ee3-314900b96e96.png)

1. ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/24962931-71fa-de9c-cafb-9820cc5a665e.png)
1. ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/1e8bc7d2-6a5c-cbd5-35f0-b3954290f8ae.png)
1. HeadTargetAmchor，RHandTargetAnchor，LHandTargetAnchorすべてに以下の設定
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/482a7da0-c38b-b848-8107-ea3b0998ebf6.png)
    - それぞれのTransformは以下
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/59081fac-98ca-564d-542b-82c83a199407.png)
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/77f5ddb3-9c9b-55cb-8589-9a74900ddf04.png)
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/68df836d-7935-7660-d153-670540adc3fe.png)
1. AvatarSample(VRMのほう)にVRIKをアタッチ
    - tramsform設定はこれ 
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/0136708f-43df-a75d-2f58-c5605969eb51.png)
1. VRIK内の設定
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/570d6ebe-7bfa-c58e-66b5-94371b5be461.png)
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/cc09243d-a0e3-2f62-3ba3-13ce32cb0835.png)
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/0d578160-b084-9dfa-5cbe-c5c5cf35c0c9.png)


<font color="Red">これで完成です！！！</font>

# 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/aae95066-f259-61e6-531e-b209c94f1143.png)

![vr_avatar_system.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348679/bad8af06-c1af-f6ce-3278-2d0b4bd7549c.gif)

歩き方がだいぶ不自然なので，以下の記事を参考にVRIKの設定をいじっていい感じにしましょう．
[Final IK の VRIK の Solver にある各値の説明](https://qiita.com/_karukaru_/items/b74bb5bdf08f5de32d0e)

# 自然な歩行を可能にする方法(2023/11/11追記)
新しくFinal IKを使った自然な歩行アニメーションを実現する方法についての記事を書きました．参考までに．
[FinalIKを使用したVRMアバターの自然な歩行を実現する方法](https://qiita.com/4sk/items/c91bf10962b8dd81563c)

# おわりに
今回はOVR, VRMとphotonをもちいてアバターシステムの作成方法を紹介しました．
初投稿で，拙い点が多いと思いますが，許してください！！！
様々な先人の知恵をお借りしながら作成しました．この記事内でも沢山リンクをはらせていただいております．ありがとうございます．
まだまだ開発途中で，よりよい方法があるかと思いますが．少しでも助けになればと思います．