Unityで、スタンドアロン型VR機のゲームで高ポリゴンな背景モデルを実現する。＜１＞ ステレオビルボード

# [1-0] はじめに

いま最も売れているMeta Quest2のような、スタンドアロン型のVRゴーグル向けのアプリケーションを開発するには、多くの制約があります。
スタンドアロン型のVRゴーグルは、基本的にスマホ向けのCPU/GPUのため、PCのようなリッチな3Dモデルを使うのは速度やメモリサイズ面で厳しいためです。さらに、VRゴーグルは高解像度なディスプレイでステレオレンダリングとなるため、スマホ向けのゲームより要件は厳しくなります。

弊社が開発したVRテニスゲーム"[CYBER TENNIS](https://www.cybertennis.jp/)"は、当初Oculus Goという性能的には格安スマホ並の性能しかないVRゴーグル向けにUnityで開発を行い、その後Quest/Quest2とVRゴーグルの進化に合わせてアップデートを繰り返してきました。

現在では、Questシリーズのゲームの中では最高の画質だという評価をいただいております。そして、今回のアップデートでさらに大幅に画質を向上させたステージを製作しました。

ここでは、いくつかの章にわけてQuest2の限界に迫るリッチな映像を実現させた手法について解説したいと思います。

CYBER TENNISの紹介Movie（本記事の内容は、2022年6月時点ではアプリとして公開されていません)

https://youtu.be/Adn7-la9LwY

Meta Quest2で開発中の京都ステージキャプチャ動画

https://drive.google.com/file/d/1imW777YhXxd00Uz24aZYx0gphHVLu_9n/view?usp=sharing

---
# [1-1] 高ポリゴンな京都の風景をQuest用テニスゲームの背景として制作

+ 京都の風景（一部京都以外もありますが）を表現したテニスコートのスクリーンショットです。

<img width="45%" alt="SS_20220608043524.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/cf39afbd-46fe-c23f-3e4a-b8fde35797df.png">

<img width="45%" alt="SS_20220608091419.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/66014c6c-a332-3aa2-7895-b080bb4c6667.png">

+ 京都ステージデモ動画

https://drive.google.com/file/d/1NbsajMFlMA2V4k8FBm_LJZAPuBIRvBWq/view?usp=sharing

このシーンにあるオブジェクトのポリゴン数をまとめてみました。

|建造物|ポリゴン数(原型)|Blenderでリダクション後|コメント|
|:-:|:-:|:-:|:-:|
|金閣寺|3,431,093|34,964|屋根のポリゴンが膨大だったため、大幅なリダクションに成功|
|平等院   | 12,400,000 |約2,200,000|瓦や梁のディテールが細かく、サイズも大きいためこの程度が限界|
|三重の塔   |29,702|29,702|元データがリダクション済みで低ポリゴンのため無加工|
|大仏   |72,908   |30,146|リダクションしてもほとんど変わりません|
|鳥居   | 1,892  |1,892|低ポリゴンのためそのまま利用|
|**建造物合計**|**15,935,595**|**2,296,704**|平等院以外はそれなりに小さくなった|
|**地物**|**ポリゴン数**|-||
|地形|49,152||UnityのTerrainをMesh化|
|草花,橋,石など|56,154||Mobile向けアセット|
|樹木   | 695,600|-|Mobile向け軽量樹木 500本|
|**地物合計**|**865,503**|-|山を樹木で埋め尽くしたため樹木が大半|
|**全背景合計**|**16,807,937**|**3,157,289**|目標まで96%削減が必要|

これらの3Dモデルは、ほとんどが Unity Asset Storeもしくは [TurboSquid](https://www.turbosquid.com/) のような3Dデータ販売サイトから購入したものを、必要に応じてBlenderで加工したものを使用しています。

Blenderでのリダクションにより大幅にポリゴン数を削減できましたが、それでもレンダリングするモデルのポリゴン数は約317万ポリゴンになります。
さらに、背景が鏡面反射で池に映り込むため、実際にレンダリングするときは約1.5倍になり、約475万ポリゴンとなります。
VRなので、これを左右の目についてそれぞれレンダリングしますから、2Dゲームに換算すると950万ポリゴンの背景になります。

**Meta Quest2で、1フレームでレンダリングするポリゴン数は、推奨10万ポリゴン以下となっています。**

実際に、Meta Quest2ならば、キャラ含めて20万ポリゴン程度までならフレーム落ちなく遊べるので、背景は**20万ポリゴン以下を目標**とします。

視野角は100度程度ですから、カメラの場所にもよりますが中央付近なら半分程度はカリングできます。
これらのことから、Blenderで加工した状態のモデルを使用した場合、実際に画面に映るポリゴン数は片目当たり**220万ポリゴン程度**となります。

<img width="10%" alt="business_woman2_4_think.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/f37090c2-628e-1985-2cf0-d3db0899e333.png"> 

**全然ダメですね**

平等院がリダクションしても220万ポリゴンと圧倒的に多いです。平等院はディテールが細かいですし、美しい瓦屋根がポリゴン数を押し上げています。
瓦屋根をBumpMap化すれは、かなり削減できます。コートの正面にありますから、斜めの角度からは見ないので、BumpMapは有効です。

<img width="10%" alt="kaisya_shigoto_nemui_woman.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/232a08f4-788a-a0ce-b5c5-15c8312138e0.png">


Blenderで試してみましたが、それでも70万ポリゴンぐらいまでしか削減できませんでした。細かい梁が多く、これらをBumpMapやHeightMapに置き換えるのはかなり骨が折れます。腕の良い3Dモデル職人に依頼すれば、低ポリゴンで品質の良い平等院を作成してもらうことは可能ですが、諸般の事情によりそれはかないません。（予算がないだけ）

そこで、見る角度が限定されている事を利用して、観衆レンダリングの時と同じ手法である3Dビルボード化にトライしてみることにしました。

---
# [1-2] 高ポリゴンオブジェクトのステレオ3Dビルボード化

3Dビルボードとは、ビルボード処理（常にカメラの方向を向いているテクスチャ）を3D化し、カメラの方向によって表示するテクスチャを切り替え、疑似的に立体的なモデルとして見せる技術です。

https://qiita.com/hgodai/private/a6fc7bdcef0940f11d00

:::note info
3Dビルボードによる観衆レンダリング
<img width="45%" alt="Human3DBB1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/e55f323d-9d0d-410c-03f0-ae2f7856573e.png">
観衆（人物）を左右60度から見た画像を複数枚用意します。<br>
<img width="45%" alt="people.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/7ba5e14f-e6b7-9ef1-6b69-e16f297447dd.png">
カメラの角度から適切な画像をビルボード表示することで、立体的なモデルのように見せる技術です。
:::

360枚のテクスチャを用意しておけば、1度刻みでテクスチャが切り替わるため、少し離れたところから見ればそれが1枚のテクスチャではなく3Dのモデルとして見えるということになります。ただし、プレイヤーの位置が上下方向に移動した場合に対応するとテクスチャの枚数が膨大になるため、プレイヤーの目の高さがあまり変化しないことが条件となります。

平等院のモデルは、テニスコートの中央から50m離れた距離に配置しました。プレイ中に後ろを向くことはないので、プレイヤーの視線と平等院が最も角度がつく位置は、ネット際のコートの端になります。角度は約10.5度なので、左右20.1度の範囲から見た3Dビルボードを作成すれば、平等院をビルボード化できます。

<img width="50%" alt="平等院の図1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/a125af60-4baa-4f48-051e-84700b3526bb.png">

ところが、この3Dビルボードは草木や人間といった小さなものなら良いのですが、幅が47mもある建物となると、問題が生じます。
下の図のとおり、ビルボードとは通常は4角形のポリゴンが常にカメラのほうを向く処理のため、大きなポリゴンが斜めになると手前にあるはずのモデルを隠してしまう不具合が生じます。

<img width="50%" alt="平等院の図2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/24593c22-04c3-19aa-3959-cf047ea70b8f.png">

### 斜投影によるレンダリング

今回の平等院のケースでは、パースが±10.5度と小さいこと、平等院は50m以上離れていることから、ポリゴンをカメラの方向に向けるビルボード処理は行わず、ポリゴンは常にコートに対して正面に固定しておき、画角のついたテクスチャをレンダリングすることで解決できました。

具体的には、カメラのProjection MatrixをMatrix4x4.Frustumを使って、左右非対称な視野ピラミッドを形成し、斜めから見た平等院をレンダリングします。

<img width="50%" alt="平等院の図3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/bf404dc3-c8fe-65f3-f76d-bed25fd21f08.png">

これによってレンダリングした平等院は以下のようになります。
![byodoin_127@0.25x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/e612cb46-6576-71d2-4ad4-0b19eec4ef72.png)
ほんの少し、左側から見た形になっていることがわかります。


128枚すべての画像を連続して表示したアニメーションです。
![btodoin_anime_small.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/dcd120fb-0c7e-c012-9a3f-3f6beaa1a959.gif)

コードサンプル
```
    // コンポーネントにターゲットをテクスチャにしたCameraを登録しておく
    // カメラの位置(this.transform.position)を左右に移動すると、レンダリングされるテクスチャは斜めから見た絵になる
    class FrustumCapture : MonoBehabiour
    {
        public Camera RenderTextureCamera;  // Renderカメラ。位置を目の高さにしておく。
        public Vector3 TargetPosition;      // 対象物の位置(Y=0の高さから建っている前提)
        public float TargetWidth = 56.0f;   // 対象物の横幅
        public float TargetHeight = 14.5f;  // 対象物の高さ

        private void Awake()
        {
            this.RenderTextureCamera = this.GetComponent<Camera>();
        }

        private void OnPreRender()
        {
            this.RenderTextureCamera.ResetProjectionMatrix();
            var eye = this.RenderTextureCamera.transform.position;

            // 対象物までの距離（-z方向に建物がある前提)
            float near = eye.z - this.TargetPosition.z;
            float far = 1000.0f; // farは適当な値
            float left = this.TargetPosition.x - this.TargetWidth * 0.5f + eye.x; // 左辺を計算
            float right = left + this.TargetWidth; // 右辺は左辺＋幅
            float bottom = -eye.y; // 下辺は、目の高さだけ中心から下になる
            float top = (bottom + this.TargetHeight); // 上辺は下辺から建物の高さ

            // Frustumのleft/rightはnearプレーンでの位置になるため、nearプレーンを
            // 半分の位置にして、left/right/top/bottomも半分の値にする。
            Matrix4x4 mat = Matrix4x4.Frustum(left * 0.5f, right * 0.5f, bottom * 0.5f, top * 0.5f , near * 0.5f, far);
            this.RenderTextureCamera.projectionMatrix = mat;
        }
    }

```



### ステレオ化

CYBER TENNISはVRですから、視点がわずかに違う右目用と左目用の2枚の絵をレンダリングします。
VRゴーグルで右目と左目でレンダリングされるテクスチャが同じものだと、ステレオ効果がなくなり無限大の距離の構造物として見えてしまいます。さらに、遠近感の目の錯覚により、近くの物体を左右同一の絵でレンダリングするとサイズが巨大化してみえるという現象があります。（この現象の名前を知りたい）

人間の左右の目の距離はおおむね6cmぐらいです。テニスコードでは左右に移動できる距離は約18.3mありますから、すべての位置で左右の絵が異なるようにテクスチャを用意するには、18.3m ÷ 6cm = 305枚のテクスチャを用意する必要があります。

コートの両端における平等院との視野角度は、20.1度ですから、305等分すると0.0659度刻みに絵を用意することになります。
はたして、50m離れた場所にある物体をレンダリングして、0.0659度の違いがわかるでしょうか？　実際に試してみたところ、判別できるのはせいぜい0.1度ぐらいまでで、0.0659度の画角の違いは私の目では判別できませんでした。

したがって、多少ズルをして枚数を減らしてもごまかせると思い、理論値の半分以下の128枚で生成してみました。1枚あたりの画角の違いは、20.1 ÷ 128 = 0.157度になります。何とか識別できる角度です。さらに、左右の目で同じ画像が表示されないように、強制的に右目と左目の画像を角度の違う2枚をレンダリングするようにしました。


![平等院の図4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/80b50f44-ea88-1769-d25c-a9ac90397003.png)


その結果、不思議なことに普通に3Dに見えるではありませんか。理論上は左右の目の距離が14cmぐらいになっているので、平等院が実際より手前に見えるはずなのですが、先に述べたように画角の違いが判別できるギリギリの角度のため、手前に見えるには情報量が足りないらしく、本体の位置にあるように自然に見えました。これは、実際に見てみないとわかりませんので、ぜひ目で確かめる機会を設けていただければと思います。

# [1-3] 平等院の斜投影型3Dビルボード処理による結果

平等院の斜投影型3Dビルボード化により、220万ポリゴンのモデルが2ポリゴンになりました。その代償として、1024x256ピクセルのテクスチャを128枚用意したため約33MBのメモリを占有することになります。観衆のレンダリングの時に使用している3Dテクスチャは67MBありましたから、このサイズならCYBER TENNISでは問題ないでしょう。


|建造物|ポリゴン数(原型)|Blenderでリダクション後|対策後のポリゴン数|**対策**|
|:-:|:-:|:-:|:-:|:-:|
|金閣寺|3,431,093|34,964|←|
|平等院   | 12,400,000 |約2,200,000|2|128枚のテクスチャ化|
|三重の塔   |29,702|29,702|←|
|大仏   |72,908   |30,146|←|
|鳥居   | 1,892  |1,892|←|
|**建造物合計**|**15,935,595**|**2,296,704**|**<font color=red>96,706**|**95%削減**|
||
|**地物**|**ポリゴン数**|**概要**|**対策後のポリゴン数**|**対策**|
|地形|49,152|UnityのTerrainをMesh化|←|
|草花,橋,石など| 56,154|Mobile向けアセット|←|
|樹木   | 695,600|Mobile向け樹木 500本|←||
|**地物合計**|**800,906**|-|←|山を樹木で埋め尽くしたため樹木が大半|
|**全背景合計**|**16,736,501**|**3,157,289**|**<font color=red>897,612**|目標まであと80%削減が必要|


-----
# 次の記事

https://qiita.com/hgodai/items/332fe559ba47d13a5080

次回コンテンツ
##### Meshの最適化によるレンダリングコストの軽減
+ [2-0] はじめに
+ [2-1] Meshの結合
+ [2-2] Meshの分割
+ [2-3] 見えない面の削除
+ [2-4] メッシュ結合と裏面カリングの結果
+ [2-4] MeshCombinerの使い方
  + MeshCombinerのソースコードと説明