リリース済みSteamVRゲームをQuestに移植した話

#おことわり
このQiitaの内容は2019/06/18に行われた[Standalone VR Meetup #02](https://standalone-vr.connpass.com/event/132070/)のごんびぃー登壇の焼き直しになります。
駆け足すぎてメモれなかったという声がちらほらありましたので、[スライドは公開しました](https://drive.google.com/file/d/11zWVBOvcQyH2GXIGLzAi8zJ9Kz9T3ojI/view)が
Qiita化もしてみればとの声も出てきたので記事化します。

#はじめに
Steamでリリース済みとは書いてありますが、**SteamVRSDK(OpenVR)で構築されたゲーム全般**を指します。
リリースしてある完成品状態でもすぐに差し替えられるよ、というアピールです。
OculusQuestに移植したい人向けの記事になります。
引っかかったところ、ハマったところが何点かありましたので、備忘録がてらまとめます。
スライドを横に出しながら読み進めるのをおすすめします。

#結論
**SteamVRからQuestの移植は**（移植元の作りに注意すれば）**一瞬でできます。**
Oculusストアに提出する際はSteamVRSDKがバイナリに同梱されていると弾かれますが、
自分でデプロイして遊ぶor展示する分には問題ありません。開発中のものでも差し替えることが可能です。
\#defineで実行中デバイスを確認して、動的にCameraRigを差し替えることも恐らく可能です。

以下詰まった所を書いていきます。

#[CameraRig]の差し替え
SteamVRSDKのPrefab [CameraRig] からOculusIntergrationのPrefab OVRCameraRig への差し替えをしていきます。

##両Rigの対照表
| SteamVR [CameraRig]       |      Oculus OVRCameraRig|
|:-----------------|------------------:|
| Camera(eye)             |              CenterEyeAnchor |
| Controller(Left)         |         LeftHandAnchor  | 
| Controller(Right)             |              RightHandAnchor| 

OVRCameraRigはSteamVRに比べ項目が増えていますが、基本的に使うのは上記3つになります。
上から頭部、左コントローラ、右コントローラです。
それぞれの子として設置されているGameObjectを対応するTransformに移動させてあげれば基本的に動きますが、
後述する通りコントローラの角度でハマりポイントがあるので注意してください。
またCamera(eye)などは**階層の深さが変わる**ので、
親オブジェクトの取得を`transform.parent`でハードコーディングしている人は、こちらも注意が必要です。

##OVRManagerのアプリ対象設定
OVRCameraRigについているコンポーネントのOVRManagerから
実行対象とするデバイスを変更しましょう。
初期設定では**GearVR or Go**となっており、そのままではQuestで動きません。
ビルドとデプロイは通りますが、アプリ起動後にすぐ落ちます。
ここでプチハマりしました。

![InkedSnapCrab_2019-06-18_15-00-53_No-0000_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/4ff1daed-9a88-5650-6616-6314cace1ea5.jpeg)

##OVRManagerのトラッキング原点設定
SteamVRとOculusではトラッキングの初期設定が違います。
SteamVRは原点が床にありますが、Oculusの初期設定はRig座標が目線になります。
歴史の時間です。昔のCV1はTouchがなく、センサーも1台でした。
その当時から頭部をトラッキングして立ってプレイという設計ではなかったため、
Rig原点が目線になっていると考えられます。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">デフォの頭の起点がeye/floorで違う原因はGoよりもっと前で、初期ロンチ時点でのRiftがセンサー1台+ゲームパッド前提で、ルームスケール対応してなかったという歴史的経緯からですな…懐かしい。 <a href="https://twitter.com/hashtag/%E3%81%99%E3%81%9F%E3%81%BF%E3%81%A8?src=hash&amp;ref_src=twsrc%5Etfw">#すたみと</a></p>&mdash; Kenji Iguchi (@needle) <a href="https://twitter.com/needle/status/1140942824336060418?ref_src=twsrc%5Etfw">June 18, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

`Tracking/TrackingOriginType`から
対象を`Floor Level`に変更してください。
これでQuestで設定した床の高さにRig原点が来て、頭部がトラッキングされるようになります。

このドロップダウンには`Stage`という項目もありますが、これは詳細不明です。
公式のOVRManagerのドキュメントにも記述がなく、使い方も不明です。
参照：https://developer.oculus.com/documentation/unity/latest/concepts/unity-utilities-overview/#ovrmanager

![InkedSnapCrab_2019-06-18_15-00-53_No-0001_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/ee97c2a7-0bd2-2929-68ac-3b654cec474d.jpeg)

##コントローラ角度の違い
Rig差し替えの項目で記しましたが、SteamVRでのコントローラとOculusのコントローラは
原点の角度が違います。
**ViveコンやWinMRコンは回転(0,0,0)のとき、剣がキレイに収まる角度です。**
対して
**Touchは回転(0,0,0)のとき、銃（ハンドガン）がキレイに収まる角度です。**
参考画像は[スライドの10ページ](https://drive.google.com/file/d/11zWVBOvcQyH2GXIGLzAi8zJ9Kz9T3ojI/view)を見てください

SteamVR [CameraRig]でコントローラ下のオブジェクトが **Euler(0,0,0)だった場合**
Oculus OVRCameraRigではAnchor下に設置後 **Euler(60,0,0)に設定する**とちょうどよく握れます。
棒状のアイテムなら60度でちょうどよかったですが、銃や手オブジェクトなどは不明です。
とにかく**SteamVRとOculusではコントローラの回転が違う**とだけ意識して、自力で修正してください。
ごんびぃーは**画面に分度器を当てました。**

##インプット周り
登壇でもいいましたが、
**知見なしです、ごめんなさい！**
移植元のゲームの作りがよくて、プラットフォーム依存のボタン入力を全て排除して構築されていました。
UIの操作は右手コントローラから伸びたレーザーポインタを当ててホールドするとインタラクトされるシステムで、
コントローラの座標さえトラッキングできていればTrackerでもコントローラでも遊べるデザインになっています。

インプット周りはなかじくんの記事が参考になるので、こちらをご覧ください。
https://qiita.com/nkjzm/items/96cd9cddc645c45dd5e5

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">Oculus Questのキー入力に関する記事を書きました。<br>一瞬混乱したんですけど、使い方を理解したらかなり便利そうです。<br>よかったら参考にしてください〜！<br><br>Oculus Questコントローラーが持つ複数のキーマッピングについて <a href="https://t.co/KzbFbFdhET">https://t.co/KzbFbFdhET</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a> <a href="https://t.co/PS8JQRUc75">pic.twitter.com/PS8JQRUc75</a></p>&mdash; Nakaji Kohki / リリカちゃん (@nkjzm) <a href="https://twitter.com/nkjzm/status/1140556837210583040?ref_src=twsrc%5Etfw">June 17, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#OVRHapticsClip is 神
次はコントローラの振動に関してです。
振動は移植後の方がプレゼンスが向上しています。悲しいことに。

##SteamVR - TriggerHapticPulse
SteamVRでのコントローラ振動関数は
`SteamVR_Controller.Device.TriggerHapticPulse(ushort);`です。
Deviceは右か左を指定します。
**この関数が叩かれた瞬間だけ ushort の周波数でコントローラを振動させます。**
この関数は事実上のRawDataいじりで、非常にわかりにくい設計になっています。
さらに長時間振動させる場合は自分でコルーチンやUpdateで振動させる回数をかかなければなりません。
めんどくさい上に周波数とフレームレートのよってはうまく増幅せず震えているかわからない程度に減衰します。

##Oculus - OVRHapticsClip

**追記: 現在はDeprecatedでOVRInput Hapticsが推奨されています([ドキュメント](https://developer.oculus.com/documentation/unity/latest/concepts/unity-haptics/))**
情報提供:なかじくん
ありがとう！

Steamに対してOculusでの振動関数は非常にシンプルで使いやすくなっています。
`OVRHaptics.OVRHapticsChannel.Mix(OVRHapticsClip clip);`などです。
効果音などのAudioClipからコントローラ振動のプロファイルを作り、
それを与えることで振動させます。
音源からプロファイルが作られるので、非常にデザインしやすいです。

OVRHapticsChannelで右か左を指定します。
その後にある振動用メソッドはいくつか種類があります。
とりあえずOculusの振動関数 is 神。
参照：[Unity＋Oculus Touch開発メモ](https://framesynthesis.jp/tech/unity/touch/#touch%E3%82%92%E6%8C%AF%E5%8B%95%E3%81%95%E3%81%9B%E3%82%8B%E3%81%AB%E3%81%AF)

```Haptic.cs
public class Haptic : MonoBehaviour
{
    public AudioClip audioClip;//振動させたい音源
    private OVRHapticsClip hapticsClip;//振動用のプロファイル保持

    void Start()
    {
        hapticsClip = new OVRHapticsClip(audioClip);//初期化で音源からプロファイルを作成する
    }

    //振動させるメソッド
    void Vibrate()
    {
        OVRHaptics.RightChannel.Mix(hapticsClip);//右手コントローラを振動させる、既に振動していた場合重ねて再生
        OVRHaptics.LeftChannel.Mix(hapticsClip);//左手、同上
    }
}
```

| 関数      |効果 |
|:-----------------|------------------|
| Mix             | 再生中の振動に重ねて（追加して）振動する |
| Preempt           |再生中の振動を停止して（止めてから新たに）振動する |
| Clear             | 再生を停止する |
| Queue               |再生中クリップが終わったら再生を開始する |

#公式提供PPS is 重い
ここからは見た目の話になります。

##PostProcessingStack v1
移植前のPC向けビルドではUnity公式提供の`PostProcessingStack v1`を使っていました。
v2もありますが、移植の話には関わってこないので割愛します。

公式PPSはPC向けで、グラボ等そこそこいい環境で動くことが保証されている環境で動くものなので、
Androidからしてみると負荷が高めです。
QuestはいくらVRとはいえ、中身はAndroidなので性能限界があります。
PCで使っていたものをそのまま投入すると重すぎてとても遊べるものではありません。
そこでモバイルでも動くPPSアセットを投入します。

##Sleek Render: Mobile Post Processing Stack
モバイル向けで評判がいい（人伝）PPSアセットを見つけました。（上司が）
https://assetstore.unity.com/packages/vfx/shaders/fullscreen-camera-effects/sleek-render-mobile-post-processing-stack-103628
基本価格19$で、モバイル対応を公言している軽量PPSアセットになります。
アセット説明欄で**Please note that any form of VR is not supported yet.**と記されていますが、
少なくとも**OculusQuestでは普通に動作しました。**
ただし今回はMultiPassStereoで構築したので、SinglePassでやろうとしている人は注意してください。
一応SinglePassでも動作したような気がしますが、元のモデルに適応していたシェーダーが動かなかったので
Sleek Renderの調査は行っていません。
今回の知見はMultiPassStereo環境下での話になります。

###適用方法
使い方はPPSv1に極めて似ています。
適用させたいCameraコンポーネントと同じGameObjectに
`SleekRenderPostProcess`コンポーネントを貼ります。
貼った直後はプリセットのプロファイルが画面に適用されます。微妙に汚いのが特徴です。
PPSv1と同じく、D&Dでプロファイルを設置するか、右端の◎からプロファイルを選択してください。
![SnapCrab_2019-06-18_16-26-35_No-0000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/5e4b029f-7bc4-a47f-3d04-4281b5919e5d.png)

###プロファイル設定
今回のプロジェクトではBloomしか使用しませんでした。
そのため他の画面効果は検証していません。
~~**普通に便利なアセットだったので、近々SleekRender解体新書を書く予定です。**~~
**書きました→[Sleek Render 解体新書](https://qiita.com/GONBEEE_project/items/7b1320a1576ea930891c)**
今回はPPSv1からSleekのBloomの移植だけ記します。
![SnapCrab_2019-06-18_16-11-49_No-0000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/fc0644f5-4835-a548-eaa1-d58d93a328a6.png)
BloomIntensityとBloomThresholdはPPSv1のものとほぼ同じですが、強さは違うので同じ数値を入れても同じ結果にはなりません。
自分で調整してください。
PPSになかった項目を説明します。

・Bloom Tint
これは発光度合いを検知した後、何色のBloomを上乗せするかの選択です。
基本的に白安定です。
夕焼けなどの場面ではTintを夕焼けカラーにするとキレイに発色すると思われますが、
汎用性はないので割愛します。
**PPSv1からの移植でBloomを使いたい場合はTintは白にしておくことをおすすめします。**

・Bloom passes
Bloomの発光数です。3 or 5 を選ぶことができます。
5のほうが負荷は高くなるようですが、キレイに発光します。
あまり負荷の増加は感じません。描画にかかる時間が2ms増えてるらしいです。
よっぽど性能がカツカツでなければ5でいいと思います。

・Luma calculation
これは恐らく”何を以て発光状態と検知するか”の話だと思います。
公式ドキュメントを見つけられなかったので、プリセット設定のUniformにしてあります。
解体新書を書くころには調査しておきます。

#VideoPlayerの相性問題
ここが一番ハマりました。既存機能を削るのはつらいです。
今回移植したゲームはVR音ゲーで、楽曲の選択シーンでサムネイルに動画を流していました。
Quest移植する際、初めはそのまま移植しましたが、Questアプリが落ちるほど重かったので、機能丸ごと削除しました。

##起きたこと・原因
Unityの基本機能の`VideoPlayer`を使用して動画を再生していました。
Questにそのまま、VideoClipもスクリプト側も何も手を加えず移植したところ、
**Questアプリがフリーズして落ちるほど激重**になりました。
![SnapCrab_2019-06-18_16-36-23_No-0000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/d2bf8717-21b9-ea8d-0e13-84cc3f0fdbdb.png)
調査していった所、どうも
**VideoClipをメモリにロードしてくる所でフリーズしてる**ような動き方をしました。
例えばコルーチンでClipロードを2秒遅らせると、フリーズのタイミングも2秒遅れます。
これを踏まえて対策をいくつか考えていきます。

##対策
結論から言ってしまうと、
**今回の移植では動画機能を削除しました。**
この移植はあくまでも趣味の範囲だったので、全戦力を上げて行っていた訳ではありません。
そのため、手軽に調査できそうな所に留まっています。
もしかしたらアセット投入などで解決する可能性も考えられるので、
それも合わせて記していきます。

###その1 - 動画を使わない
**悲しいことに結局これに落ち着きます。**
Unity公式提供の機能だけで実装するとどの手段を取ってもフリーズ落ちしたので、
今回の移植では動画機能を削除しました。
そのくらい公式提供VideoPlayerは重いです。

###その2 - VideoPlayer.Prepare();
https://docs.unity3d.com/ja/current/ScriptReference/Video.VideoPlayer.Prepare.html
https://docs.unity3d.com/ja/current/ScriptReference/Video.VideoPlayer-prepareCompleted.html
VideoPlayerの機能で、設定されているVideoClipを再生できるように事前にロードするメソッドと、
ロードが終わって再生可能になった段階で発火されるイベントがあります。
これは機能的に非同期ロードであることは自明ですが、
**移植で試したところ、このメソッドを叩いた瞬間にフリーズしました。**
想定では
1.Prepare()を叩く
2.クリップのロードが非同期で始まる    **←Androidだとここが伸びると思ってた**
3.クリップのロードが終わるとprepareCompletedが発火される。
4.PC版と変わらず動画が再生される。
という流れでした。
しかし実際にはPrepare()を叩いた段階でフリーズしてアプリが落ちたので、
VideoPlayerとAndroidの相性が悪いのか、Clipが重すぎてPrepareすらできなかったのかの
2つの可能性が考えられます。

また、このようなコメントもありました。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">VideoPlayerはGoもQuestもPrepare直後にやると死んだからそこも含めて再生まで何回か数フレーム待ちを挟んでた気がする（自分で実装してないからうろ覚え） <a href="https://twitter.com/hashtag/%E3%81%99%E3%81%9F%E3%81%BF%E3%81%A8?src=hash&amp;ref_src=twsrc%5Etfw">#すたみと</a></p>&mdash; オークマネコ@夏コミ月南メ-38a (@ookumaneko_XD) <a href="https://twitter.com/ookumaneko_XD/status/1140943473106812928?ref_src=twsrc%5Etfw">2019年6月18日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これとは少し状況が違い、そもそもPrepareを叩いた段階で落ちているので、
再生で数フレーム待つこととはあまり関係がないかもしれません。
ただ別環境でも似たような問題に引っかかっていることが判明したので、
VideoPlayerとAndroidの相性が悪い可能性が高いです。

###その3 - 動画自体の容量削り
Clipが重すぎてPrepareできなかった可能性に絞って対策してみました。
元の動画データ側で解像度や長さを変更して軽量化する方針と、
Unityでのインポート設定で圧縮する方針があります。
結論から言うと**多少ロードが短くなってアプリ落ちも確率は下がりましたが、劇的改善とは言えませんでした。**

元データの軽量化で、1Clipで1MBほどまで削り、
加えてUnityインポート設定でほぼ最低の画質まで圧縮しています。
（元が1024*1024の変則解像度でした）
![SnapCrab_2019-06-18_16-57-24_No-0000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/9e484b80-a9c8-49ba-5a97-124e5d2ff283.png)
アプリ落ちの確率が下がるというのは
”アプリは落ちるがライブラリから同じアプリを起動するとロード完了状態から開始する”という現象が起こりやすくなったことを指します。
Questでホームに戻ると実行中アプリを停止するか再開するかの窓が出ていますが、あれは消滅しています。
しかし同じアプリを起動すると上記窓で再開を押したときと同じ挙動をします。
遊べるようにはなっていますが、安定動作とは程遠いので失敗と判断しました。

###その4 - マルチスレッドレンダリング停止
参照：[[Unity][Android]VideoPlayerでカクつく問題への対応](https://qiita.com/furui/items/153e5705a7b322e07079)
Unity2018では不明ですが、マルチスレッドレンダリングが有効になっているとVideoPlayerがカクつくという情報がありました。
詳細は上記Qiitaを読んでください。

マルチスレッドレンダリングを無効にして試してみましたが、結局ロードは改善しませんでした。
恐らく上記記事とは詰まっている場所が違い、動画の再生自体ではなくその前段階なので
変化がなかったのかなと思います。
いずれにせよマルチスレッドレンダリングとVideoPlayerの相性が悪いという知見は得られました。ありがとうございます。


###未検証 - 他の動画再生機構を使う
いくつか対策をしてみましたが、結局どれも安定動作まではいかなかったので、
VideoPlayerとAndroidの相性が悪いと結論づけても問題ないかと思います。
そうなると今度は他の動画再生機構を試す方針に切り替えるべきですが、
今回の移植ではそこまでコストをかけられなかったので、**ここから先は未検証となります。**

登壇中に[歩ける全天球動画](https://w360v.forgevision.com/)でお馴染みのフォージビジョンしちEさんから
AVProはAndroidで動くというコメントがありました。
https://assetstore.unity.com/packages/tools/video/avpro-video-56355

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">AV Pro VideoはQuest(Android)でも動くよ！　<a href="https://twitter.com/hashtag/%E3%81%99%E3%81%9F%E3%81%BF%E3%81%A8?src=hash&amp;ref_src=twsrc%5Etfw">#すたみと</a> 　<a href="https://twitter.com/hashtag/standaloneVR?src=hash&amp;ref_src=twsrc%5Etfw">#standaloneVR</a></p>&mdash; しちE (@shichi_14) <a href="https://twitter.com/shichi_14/status/1140943212334288897?ref_src=twsrc%5Etfw">2019年6月18日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

他にも”動画に迷ったらAVPro”といった旨のコメントがちらほらありました。
恐らくAndroid（以外でも）で動画を再生する場合AVProを使うユーザーが多いようです。
今後Questで動画を扱う場合AVProを使ってみようと思いますが、
フルパッケージで450\$、Androidのみのバージョンでも150\$となかなかのお値段です。
有名なアセットなので、所属企業や団体などが所持している可能性もあります。
まずは仕事名義で一旦調査してから、個人での購入に踏み切るのが理想的な流れかと考えています。

###非対応 - MovieTexture
動画再生方式の1つなので紹介しておきますが、
**Androidでは非対応です。**

https://docs.unity3d.com/ja/2018.1/Manual/class-MovieTexture.html
古くからUnityに存在する動画再生方式にMovieTexureというものがあります。
これは既に**(Deprecated)**となっており、将来的に使えなくなる可能性が高いです。
インポートにはQuickTimeのインストールが必要で、Windowsでは一手間多くかかります。
この方式はVideoPlayerとは違い動画を再生するというより、その名の通り”動くテクスチャ”です。
PlaneやCubeなどのメッシュやMaterialに貼り付けることが可能で、VideoPlayerを必要としない動画再生方式です。
しかしVideoPlayerに比べ、Android非対応・事前非同期ロードがない・MaterialからMovieTextureを取得しないとアクセスできないなど、
不便な点がいくつか目立ちます。

もしこれがAndroidに対応していてDeprecatedが付いていなければ、今回の移植で特効薬になり得たかもしれません。

###論外 - Handheld.PlayFullScreen
動画再生方式つながりでこちらも紹介しておきますが、項目名通り論外なので読み飛ばして問題ありません。

https://docs.unity3d.com/ja/2018.1/ScriptReference/Handheld.PlayFullScreenMovie.html
MovieTextureのドキュメントで”Androidはこっちを使え”とリンクを飛ばされている機能です。
名前の通りフルスクリーンで再生します。
VRゲームで空間上に動画を再生するのでフルスクリーンであってはなりません。
これは完全に興味だけですが、Questでこの機能を叩いた場合何が起こるのか・どんな表示になるのか少し気になります。
VideoPlayerが出てきてしまったので、この機能の知見もほとんど残されていないようです。


#まとめ
要所要所のハマりポイントに注意すればSteamVRからQuest移植は比較的簡単にできます。
少なくとも平面PCゲームから平面Androidゲームへの移植よりは簡単だと思います。
今回はかなり動画移植にハマっていましたが、動画を一切使わないゲームであれば更に簡単に移植できるでしょう。
コントローラの角度さえ注意すればすぐに遊べるようになります。
自分が制作を担当したPCVRゲームなどがあれば試してみてはいかがでしょうか。

##あとがき
~~SleekRenderの項目でちらっと記しましたが、”SleekRender解体新書”を書こうかと予定しております。
予定だけなのでやるとは確定していません。~~
**書きました→[Sleek Render 解体新書](https://qiita.com/GONBEEE_project/items/7b1320a1576ea930891c)**

モバイルで動くPPSアセットで、PCでも使えるようなのでこれはこれで面白かったです。

また何か追記してほしい情報や質問があれば、
Qiitaのコメント欄か [@GONBEEE_project](https://twitter.com/GONBEEE_project) までリプDMでご連絡ください。

最後までお付き合いいただきありがとうございました。