LoginSignup
4
2

More than 1 year has passed since last update.

UNITY_MATRIX_P と unity_CameraProjection は違う

Last updated at Posted at 2021-08-16

はじめに

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

vertex Shader における座標変換

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

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を用意します。

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

_defaultMatrix == false
image.png

unity_CameraProjection

シェーダ内で利用できるデフォルト変数の中に、unity_CameraProjection というのがあります。公式のドキュメント には、

Camera’s projection matrix.

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

Current projection matrix.

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

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

_defaultMatrix == false
image.png

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

原因

こちらのフォーラムに回答がありました。要するに、unity_CameraProjection はOpenGL を想定した変換行列である一方、UNITY_MATRIX_P は実際にレンダリングに用いられている変換行列であり、想定しているグラフィックAPIに齟齬があるとこういったことが起きてしまうそうです。

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

投影行列の中身

こちらの記事 を参考に、投影行列を補正します。

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);
}

公式ドキュメントこちらの記事を参考に、_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
_defaultMatrix == false
image.png

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2