UNITY_MATRIX_P と unity_CameraProjection は違う

# はじめに
透視投影と並行投影を滑らかにつなぐため、外部から取得した投影行列を用いて投影変換をするシェーダを作ろうとしたら予想外のところでだいぶハマったので備忘として記しておきます。

# vertex Shader における座標変換
Unity で Unlit Shader を新規作成すると、vertex Shader を記述する部分は以下のような処理になっています。

```hlsl:unlit.shader
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}
```

この `UnityObjectToClipPos` でモデル空間からプロジェクション空間への変換を行っており、これはモデル→ワールド変換、ワールド→ビュー変換、ビュー→プロジェクション変換に分割できます。またそれぞれの変換は頂点座標値に行列を掛ける演算に相当します。

参考：
https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html
https://light11.hatenadiary.com/entry/2019/01/27/160541
https://virtualcast.jp/blog/2020/06/the-reason-unity-is-overwriting-your-shader-code/

## 実験
試してみましょう。以下のような Unlit Shaderを用意します。

```hlsl:test0.shader

Shader "Unlit/test0"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
		[MaterialToggle] _defaultMatrix("useDefaultMatrix", Float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
				float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
				float3 viewDir : TEXCOORD1;
				float3 lightDir : TEXCOORD2;
				float3 normal : TEXCOORD3;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
			float _defaultMatrix;

            v2f vert (appdata v)
            {
                v2f o;
				if (_defaultMatrix) {
					o.vertex = UnityObjectToClipPos(v.vertex);
				}
				else {
					o.vertex = mul(UNITY_MATRIX_M, v.vertex);
					o.vertex = mul(UNITY_MATRIX_V, o.vertex);
					o.vertex = mul(UNITY_MATRIX_P, o.vertex);
				}
                
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				o.lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
				o.normal = UnityObjectToWorldNormal(v.normal);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
				col *= fixed4(1, 0, 0, 1);
				col *= dot(i.normal, i.lightDir);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}
```

デフォルトの Unlit Shader に適当なシェーディングを足したものに、_defaultMatrixによる分岐を作っています。これを切り替えてみても、描画される結果は変わりません。

> `_defaultMatrix == true`
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321302/bb58c9e3-f2b3-7359-d30d-47914c24a52d.png)

>`_defaultMatrix == false`
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321302/b454ae7e-328e-a161-4bda-940e2a8f150e.png)



# unity_CameraProjection 
シェーダ内で利用できるデフォルト変数の中に、`unity_CameraProjection` というのがあります。[公式のドキュメント](https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html) には、

>Camera’s projection matrix.

と説明があります。一方、先ほど登場した `UNITY_MATRIX_P`は、次のように説明されています。

> Current projection matrix.

はじめにこれを同一のものと考えてしまったことから沼にハマってしまったのですが、両者は実は異なるものです。実験してみましょう。先ほどのシェーダを一部書き換えます。

```hlsl:test0.shader
if (_defaultMatrix) {
    o.vertex = UnityObjectToClipPos(v.vertex);
}
else {
    o.vertex = mul(UNITY_MATRIX_M, v.vertex);
    o.vertex = mul(UNITY_MATRIX_V, o.vertex);
    //o.vertex = mul(UNITY_MATRIX_P, o.vertex);
    o.vertex = mul(unity_CameraProjection , o.vertex);
}
```
>`_defaultMatrix == true`
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321302/8dc670b0-2a43-9505-15d0-2e68aeb45009.png)

>`_defaultMatrix == false`
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321302/f0ff45e4-c1d7-9f97-2237-b61704648d11.png)

なんだか上下が逆転していたり、陰影がおかしかったりします。

# 原因

[こちらのフォーラム](https://forum.unity.com/threads/whats-difference-between-unity_matrix_p-and-unity_cameraprojection.1079549/)に回答がありました。要するに、`unity_CameraProjection` はOpenGL を想定した変換行列である一方、`UNITY_MATRIX_P` は実際にレンダリングに用いられている変換行列であり、想定しているグラフィックAPIに齟齬があるとこういったことが起きてしまうそうです。

さて、そうなると、`unity_CameraProjection` を無理やり用いるために、この齟齬を解消してやらねばなりません。[こちらの記事](https://logicalbeat.jp/blog/929/)によれば、プラットフォーム依存の差異は垂直方向の向きと奥行きのクリッピング範囲に現れるようです。公式のドキュメントにも言及があります。(https://docs.unity3d.com/Manual/SL-PlatformDifferences.html)

## 投影行列の中身
[こちらの記事](https://qiita.com/ryutorion/items/0824a8d6f27564e850c9) を参考に、投影行列を補正します。

```hlsl:test0.shader
if (_defaultMatrix) {
    o.vertex = UnityObjectToClipPos(v.vertex);
}
else {

    o.vertex = mul(UNITY_MATRIX_M, v.vertex);
    o.vertex = mul(UNITY_MATRIX_V, o.vertex);

    float4x4 tmp = unity_CameraProjection;
    tmp[1][1] *= _ProjectionParams.x;
    float near = _ProjectionParams.y;
    float far = _ProjectionParams.z;
    tmp[2][2] = (near) / (far - near);
    tmp[2][3] = (1 * far*near) / (far - near);

    //o.vertex = mul(UNITY_MATRIX_P, o.vertex);
    //o.vertex = mul(unity_CameraProjection, o.vertex);
    o.vertex = mul(tmp, o.vertex);
}
```

[公式ドキュメント](https://docs.unity3d.com/Manual/SL-PlatformDifferences.html)と[こちらの記事](https://qiita.com/ryutorion/items/0824a8d6f27564e850c9)を参考に、`_ProjectionParams` というビルトイン変数の情報を元に投影行列を補正します。
`tmp[1][1] *= _ProjectionParams.x` で垂直方向の向きを補正、`               tmp[2][2] = (near) / (far - near); tmp[2][3] = (1 * far*near) / (far - near);` で、奥行き方向を 0 ~ 1 とした場合の行列の要素の値を再計算しています。

## 結果
>`_defaultMatrix == true`
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321302/4b656c74-e3e2-3d48-5c94-5870107290da.png)
>`_defaultMatrix == false`
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321302/30ab34de-f284-d577-c5f0-ef0417d7afff.png)

`UnityObjectToClipPos` と同じ結果を再現できました。