[Unity] Box Projectionで屋内シーンの反射による映り込みの品質を向上する

これは [【unityプロ技】 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/unityforpro)の24日目の記事です。

---

Unity標準の ReflectionProbe のオプションにある **Box Projection** を設定することで、屋内シーンの反射の破綻を軽減して、映り込みの品質を向上する方法を紹介します。

百聞は一見に如かずということで、 **Box ProjectionをON/OFF** した動画を用意しました。

<img width="100%" alt="※画像が読み込まれるまでお待ちください Box Projection" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/9b1dbd6b-164c-ca33-e434-e3b00c389b20.gif">

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Box Projectionで屋内シーンの反射による映り込みの品質を向上する<br>という記事を書きました！<br>▶️<a href="https://t.co/gz0CpeizHb">https://t.co/gz0CpeizHb</a><br><br>カメラの位置を考慮して、Cubemapの映り込みの位置を正しく補正する手法です。<br>設定が簡単なわりに、劇的に見た目のリッチ感UPできるのでオススメです！<a href="https://twitter.com/hashtag/Unity3D?src=hash&amp;ref_src=twsrc%5Etfw">#Unity3D</a> <a href="https://twitter.com/hashtag/shader?src=hash&amp;ref_src=twsrc%5Etfw">#shader</a> <a href="https://twitter.com/hashtag/HLSL?src=hash&amp;ref_src=twsrc%5Etfw">#HLSL</a> <a href="https://t.co/o75TDgPBL2">pic.twitter.com/o75TDgPBL2</a></p>&mdash; がむ (@gam0022) <a href="https://twitter.com/gam0022/status/1209256255488815104?ref_src=twsrc%5Etfw">December 23, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- **Box Projection: OFF** では、カメラの位置が無視されて、家具などの映り込みの位置がずれてしまっています。
- **Box Projection: ON** では、カメラの位置が正しく考慮されて、家具などの映り込みの位置が正しくなっています！

# 環境

- 2018.4.13f1 (LTS)
- Build-in Rendering Pipeline および LWRP（Light Weight Render Pipeline）

# Box Projectionとは、どんな技術なのか？

公式マニュアルにもBox Projectionの解説はあるのですが、ちょっと初心者には理解しずらかったので、簡単に解説をします。

- [より高度なリフレクションプローブの機能 - Unity マニュアル](https://docs.unity3d.com/ja/current/Manual/AdvancedRefProbe.html)

端的に言うと、Cubemap（ReflectionProbe）のテクスチャをサンプリングするための反射ベクトルの向きを、カメラの位置と部屋の広さを定義するBoundingboxを考慮して補正する手法です。

普通のCubemapとの違いは、反射ベクトルの計算の違いだけなので、GPUの処理負荷もそれほど高くはありません。

反射はしていますが、レイトレをするわけではないので、RTXも不要です！

## Parallax-Corrected Cubemapの解説

---

*※Box Projectionの理論に興味がない人は、この節はスキップして[本題](#本題-box-projectionの設定)から読んでください。
Unity標準のStandardShaderでBox Projectionは実装済みなので、理論は理解しなくても使うことはできます。*

---

UnityではBox Projectionという名称ですが、**Parallax-Corrected Cubemap** とも呼ばれる手法です。

Parallax-Corrected Cubemapの具体的な理論と実装については、このスライドに非常に詳しく解説してあります。

- [Parallax Corrected CubeMaps from Siggraph 2012](https://seblagarde.files.wordpress.com/2012/08/parallax_corrected_cubemap-siggraph2012.pdf)

この図は上記のスライドから引用しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/1d67c9b6-7e31-cd21-ac89-caf170ad4237.png)

この図を用いて簡単にParallax-Corrected Cubemapの解説を行います。基本的な考え方はそれほど難しくはありません。

まず各記号の説明をします。

- $\vec{V}$: カメラのビューベクトル
- $C$: ReflectionProbeの位置
- $\vec{N}$: 法線
- $\vec{R}$: 反射ベクトル（補正前）
- $\vec{R'}$: 反射ベクトル（補正後）
- $P$: $\vec{R}$と壁の交差点

$\vec{R}$ 方向をサンプリングすれば、 **普通のCubemap** ですが、
$\vec{R'}$ 方向をサンプリングすると視差が考慮された **Parallax-Corrected Cubemap** になります。

そして、図が示すように $\vec{R}$ を補正した $\vec{R'}$ は

```math
\vec{R'} = P - C
```

によって計算できます。

$C$ は既知なので、 $\vec{R}$と壁の交差点である $P$ を計算すれば、$\vec{R'}$ が求まります。

ベクトルとAABBの交差判定のアルゴリズムを用いれば、$P$ は計算ができます。

HLSLによる Parallax-Corrected Cubemap の反射ベクトル $\vec{R'}$ を求める実装例です（MITライセンス）。

```glsl
// Parallax-Corrected Cubemap
// https://seblagarde.files.wordpress.com/2012/08/parallax_corrected_cubemap-siggraph2012.pdf
float3 calcParallaxCorrectedCubemapReflect(float3 worldPos, float3 worldReflect, float3 cubemapPos, float3 boxMin, float3 boxMax)
{
    // AABB と 反射ベクトル の交差点 worldIntersectPos を計算します
    float3 firstPlaneIntersect  = (boxMax - worldPos) / worldReflect;
    float3 secondPlaneIntersect = (boxMin - worldPos) / worldReflect;
    float3 furthestPlane = max(firstPlaneIntersect, secondPlaneIntersect);
    float dist = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z);
    float3 worldIntersectPos = worldPos + worldReflect * dist;

    // Cubemapから交差点に向かうベクトルが ParallaxCorrected された反射ベクトルになります
    return worldIntersectPos - cubemapPos;
}
```

壁の近似はAABBに限定しなくても交差判定ができれば何でも良いのですが、計算コストと品質のバランスを考慮してAABBが一般的に用いられるようです。

## Box Projectionに対応した自作シェーダーの紹介

当初は、Parallax-Corrected Cubemapに対応したUnityシェーダーを実装するという記事を執筆する予定だったのですが、Unity標準のStandardShaderで実装済みであることを記事用のシェーダーの実装が完成してから知りました :sob:

供養のために、Box ProjectionとLightmapに対応した自作シェーダーのリンクを紹介します。

- [ParallaxCorrectedCubemap.shader](https://github.com/gam0022/unity-shader-experiments/blob/master/Assets/Experiments/2019-12-20_ParallaxCorrectedCubemap/ParallaxCorrectedCubemap.shader)

# [本題] Box Projectionの設定

それでは、ここからが本題です！

Box Projectionの設定をして、最初の動画のようなシーンを作る方法をチュートリアル形式で紹介していきます！

## 0: 屋内のアセットを用意する

まずは屋内シーンのアセットを用意します。

今回はUnityアセットストアから [Pack Gesta Furniture #1](https://assetstore.unity.com/packages/3d/props/furniture/pack-gesta-furniture-1-28237) をダウンロードして使わせていただきました :bow: 

## 1-1: ReflectionProbeを配置する

まずは ReflectionProbe を配置します。

普通は部屋の中心あたりに適当に配置すればOKです。

今回は映り込みの品質を向上するために、ちょっとカメラの位置に寄せました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/5c73fea8-78fe-88ef-428b-ad9558bac756.png)

## 1-2: ReflectionProbe の BoxSize と BoxOffset を設定する

部屋の広さにぴったり合うように、BoxSize と BoxOffset を設定します。

Wireframeモードにしたほうが、作業がしやすいかもしれません。

また、BoxSize と BoxOffset は ReflectionProbe の影響範囲の指定も兼ねているので、床のMeshに少しオーバーラップするようにサイズを調整する必要があります。

オーバーラップしないと、ReflectionProbe の影響範囲外となってしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/78cba0ba-4cd2-9c37-9f1f-ac61f3313b99.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/f5e6a93a-473e-354f-4a0f-bbf2336ba57c.png)

## 1-3: Box Projection等の設定とBake

ReflectionProbeの設定を変更します。

- **Box Projection**: ON
- **Type**: Baked
- その他はお好みでOK

設定が完了したら、 [Bake] ボタンを押して、Cubemapのテクスチャを生成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/0771f7fb-5f0f-5d94-c614-c6fdc89c1615.png)

## 2: 床のマテリアルの設定

床のマテリアルを反射が見えやすく調整します。

- **StandardShader** を設定（ReflectionProbeに対応していれば何でも良い）
- **Metalic**: お好みですが、フローリングの床感を出すなら 0.2 前後あたりがいい感じです
- **Smoothness**: 1.0 に近づくほど反射がハッキリ見えます
- **Normal Map**: なしにするか、影響度を 0.01 など小さな値にするほど反射がハッキリ見えます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/87be57f3-de52-8f50-2d61-d8477d753a25.png)

以上でいい感じに反射するようになると思います。

まるで年末の大掃除でピカピカに磨き上げられた綺麗なフローリング床みたいですね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/b4eae487-ebe1-aa1c-dba2-e79f64ea8b25.png)

# トラブルシューティング

記事の執筆中に遭遇したトラブルをまとめます。

## ReflectionProbe に映らないオブジェクトがある

- 対象の GameObject は static になっていますか？
- ReflectionProbe の CullingMask は Everything になっていますか？

## 再生したら反射が消えた

staticバッチングが原因の可能性があります。床のMeshだけ static を OFF にしましょう。

## Box Projection が有効にならない

`Project Settings > Graphics > Reflection Probes Box Projection` が ON になっていることを確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/10cf1077-b22b-f6d8-1a56-70ec818d37d6.png)

## URPでBox Projection が有効にならない

URP Assetsに設定項目が移動しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17718/a9cb178b-795f-9f7d-dbcb-6d412440c1ad.png)


https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.1/manual/lighting/reflection-probes.html?_fsi=dTrIyUYC

# まとめ

Unity標準ReflectionProbeのBox Projectionと標準シェーダーだけで、屋内シーンの反射による映り込みを大きく改善できました。

映り込みが綺麗にできると、光沢感やリッチ感を表現できますし、設定もそこまで手間がかからないので、コストパフォーマンスが高いと思います！

みなさんも是非 Box Projection を使いこなしてみてください！