【VRChat】VRC Mirror 内で Fog が効くようにする

# 概要
Unity の Built-in Shader は VRC Mirror 内で Fog が効きません．
そこで，Standard Shader を改造して対応させます．なんちゃって対応ですが，少なくとも VRChat 内では自然に見えるので OK です．
調べてはいませんが，Terrain 関係の Shader にも同様の方法が使えるはずです．

![スクリーンショット 2024-11-17 120424.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/464f230d-859f-d9bc-83bc-0d0bddaa72ba.png)

# Fog が効かない原因
Mirror 内は深度の尺度が通常と異なり，深度の計算結果を Fog で利用できません．

自分も正確には理解できていませんが，Mirror 内では
- オブジェクトを反転させるため，ビュー行列の Z 軸を反転
- 鏡面でクリップするため，プロジェクション行列の Near Clip Plane を鏡面に設定

しているそうで[^1][^2][^3][^4]，実際に表示してみると確かに VP 行列が通常とは異なる値になっていました．（ P 行列が異常だったのでここで理解を諦めました... ）

![スクリーンショット 2024-11-17 115931.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/065ef4cd-fb2f-13fb-9e27-a7295f54ff60.png)

# 対策
対策は単純で，自分で深度を計算して，自分で Fog をかけます．

Standard Shader では，`UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC` で Fog 用の深度をフラグメントシェーダーに渡し，`UNITY_EXTRACT_FOG_FROM_EYE_VEC` で取り出して，`UNITY_APPLY_FOG` で Fog を適用しています．

```hlsl: UnityStandardCore.cginc
...

VertexOutputForwardBase vertForwardBase (VertexInput v)
{
    ...

    UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,o.pos);
    return o;
}

half4 fragForwardBaseInternal (VertexOutputForwardBase i)
{
    ...
    
    UNITY_EXTRACT_FOG_FROM_EYE_VEC(i);
    UNITY_APPLY_FOG(_unity_fogCoord, c.rgb);
    return OutputForward (c, s.alpha);
}

...
```

`UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC` には `o.pos`（クリップ空間座標）が渡され，`o.pos.z` が `o.eyeVec.w` に格納されます（非モバイル環境）．フラグメントシェーダーではこれを [0, Far Clip Plane] のスケールに変換し，Fog の強度を計算して色を付けています．

しかし，`o.pos.z` は Mirror の外と中とで尺度が違うので，Mirror で使うためにはこれを自分で計算する必要があります．ここでは，ビュー空間の Z 座標をオレオレ定義深度として採用します．

```diff_hlsl: UnityStandardCore.cginc
...

VertexOutputForwardBase vertForwardBase (VertexInput v)
{
    ...

-   UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,o.pos);
+   UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,UnityObjectToViewPos(v.vertex));
    return o;
}
...
```

頂点シェーダーで独自の Fog 深度を定義したので，フラグメントシェーダーでの処理も変更します．元の Standard Shader で使用されている `UNITY_APPLY_FOG` はプラットフォームによるプロジェクション行列の差を吸収するように作られていますが，今回は関係ないので入ってきた値をそのまま使います．

```diff_hlsl: UnityStandardCore.cginc
...

half4 fragForwardBaseInternal (VertexOutputForwardBase i)
{
    ...
    
    UNITY_EXTRACT_FOG_FROM_EYE_VEC(i);
-   UNITY_APPLY_FOG(_unity_fogCoord, c.rgb);
+   #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
+       UNITY_CALC_FOG_FACTOR_RAW(abs(_unity_fogCoord));
+       UNITY_FOG_LERP_COLOR(c.rgb, unity_FogColor, unityFogFactor);
+   #endif

    return OutputForward (c, s.alpha);
}

...
```

本当は，ビュー空間の Z 座標の範囲は [-near, -far] （環境依存）なので本来使いたい値とは少し違うのですが，細かいことは気にするなと `UnityCG.cginc` の 1026 行目に書いてあるので，気にしないことにします．

# 実装
まず，Unity の Built-in Shader をダウンロードします．

https://unity.com/ja/releases/editor/whats-new/2022.3.22#installs

次の 3 つのファイルを好きなディレクトリにコピーします．3 つのファイルは同じ階層におきます．
- `CGIncludes/UnityStandardCore.cginc`
- `CGIncludes/UnityStandardCoreForward.cginc`
- `DefaultResourcesExtra/Standard.shader`

競合するのでコピーしたファイルは名前を変えます．
- `UnityStandardCore-Fog.cginc`
- `UnityStandardCoreForward-Fog.cginc`
- `Standard-Fog.shader`

改造します．

```diff_hlsl: UnityStandardCore-Fog.cginc
...

VertexOutputForwardBase vertForwardBase (VertexInput v)
{
    ...

-   UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,o.pos);
+   UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,UnityObjectToViewPos(v.vertex));
    return o;
}

half4 fragForwardBaseInternal (VertexOutputForwardBase i)
{
    ...
    
    UNITY_EXTRACT_FOG_FROM_EYE_VEC(i);
-   UNITY_APPLY_FOG(_unity_fogCoord, c.rgb);
+   #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
+       UNITY_CALC_FOG_FACTOR_RAW(abs(_unity_fogCoord));
+       UNITY_FOG_LERP_COLOR(c.rgb, unity_FogColor, unityFogFactor);
+   #endif

    return OutputForward (c, s.alpha);
}

...

VertexOutputForwardAdd vertForwardAdd (VertexInput v)
{
    ...

-   UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,o.pos);
+   UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,UnityObjectToViewPos(v.vertex));
    return o;
}

half4 fragForwardAddInternal (VertexOutputForwardAdd i)
{
    ...
    
    UNITY_EXTRACT_FOG_FROM_EYE_VEC(i);
-   UNITY_APPLY_FOG_COLOR(_unity_fogCoord, c.rgb, half4(0,0,0,0)); // fog towards black in additive pass
+   #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
+       UNITY_CALC_FOG_FACTOR_RAW(abs(_unity_fogCoord));
+       UNITY_FOG_LERP_COLOR(c.rgb, half4(0,0,0,0), unityFogFactor); // fog towards black in additive pass
+   #endif

    return OutputForward (c, s.alpha);
}

...
```

```diff_hlsl: UnityStandardCoreForward-Fog.cginc
...

-   #include "UnityStandardCore.cginc"
+   #include "UnityStandardCore-Fog.cginc"

...
```

```diff_hlsl: Standard-Fog.shader
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

- Shader "Standard"
+ Shader "Standard-Fog"
{

    ...

    SubShader
    {
        Tags { "RenderType"="Opaque" "PerformanceChecks"="False" }
        LOD 300


        // ------------------------------------------------------------------
        //  Base forward pass (directional light, emission, lightmaps, ...)
        Pass
        {
            Name "FORWARD"

            ...
            
-           #include "UnityStandardCoreForward.cginc"
+           #include "UnityStandardCoreForward-Fog.cginc"

            ...

        }
        // ------------------------------------------------------------------
        //  Additive forward pass (one light per pass)
        Pass
        {
            Name "FORWARD_DELTA"

            ...

-           #include "UnityStandardCoreForward.cginc"
+           #include "UnityStandardCoreForward-Fog.cginc"

            ...

        }

    ...

    }

...

}
```

モバイルで使いたい場合は `CGIncludes/UnityStandardCoreForwardSimple.cginc` に関しても同様の処理をしてください．

# 雑記
ワールドで使いたい全てのシェーダーに同じ処理が必要なのでかなり面倒にはなりますが，頑張りましょう．せっかく Fog できれいに誤魔化したのに Mirror で丸見えになったら台無しですからね．

本記事の方法は，なるべく楽して解決するためにモバイルでの最適化や DirectX と OpenGL の差を無視しているので，しっかり対応するためにはもっと丁寧に改造する必要があります．

Unity Built-in Shader は MIT License なので，一応リンクを貼っておきます．

https://opensource.org/license/mit

[^1]: [【Unity】鏡に映るは鏡の世界なり (Unityで鏡の実装)](https://tsgcpp.hateblo.jp/entry/2020/05/31/215118)
[^2]: [Changing the Projection Matrix to achieve a mirrored camera.](https://discussions.unity.com/t/changing-the-projection-matrix-to-achieve-a-mirrored-camera/948884/3)
[^3]: [【Unity】Camera.projectionMatrixの罠まとめ](https://note.com/fuqunaga/n/n08d81b185514)
[^4]: [【Unite 2017 Tokyo】スマートフォンでどこまでできる？3Dゲームをぐりぐり動かすテクニック講座](https://www.slideshare.net/Unite2017Tokyo/unite-2017-tokyo3d-75800229)