ShaderTips

シェーダーTips

主にUnityシェーダーについての記事を書いています。

【Unity】【シェーダー】描画するオブジェクトの深度値と深度バッファの取得方法

Shader "Zpos"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 projPos : TEXCOORD1;
            };

            // デプステクスチャの宣言
            UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                //  深度バッファの値を取得するためにスクリーンスペースでの位置を求める
                o.projPos = ComputeScreenPos(o.vertex);

                // ビュー座標系での深度値を求める
                COMPUTE_EYEDEPTH(o.projPos.z);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 描画するピクセルの深度バッファの値
                float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,i.projPos));
                 // 描画するピクセルの深度値
                float partZ = i.projPos.z;

                return fixed4(1,1,1,1);
            }
            ENDCG
        }
    }
}

深度バッファの取得

ComputeScreenPos

定義は以下。

inline float4 ComputeScreenPos(float4 pos)
{
    float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

inline float4 ComputeNonStereoScreenPos(float4 pos)
{
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}
UNITY_SINGLE_PASS_STEREO

VRChatなどのUnityのVRアプリはシングルパスステレオレンダリングという方法でレンダリングされており、その際に有効となります。 そのため、無視してください。

ComputeNonStereoScreenPos

_ProjectionParams.x にはプラットフォーム毎の y の向きの扱いを吸収するために +1.0 または -1.0 が入っています。これを y 座標に掛けると y の向きを統一することができます。 ny-program.hatenablog.com

1行目と2行目でクリップ座標に対して、0.5を乗算し、w成分を加算しています。 これでスクリーン座標に変換できる原理を説明します。

正規デバイス座標系はX(幅)とY(高さ)が2で-1〜1の範囲をとります。

https://cdn-ak.f.st-hatena.com/images/fotolife/N/Ny_Program/20190915/20190915205556.gif

その70 完全ホワイトボックスなパースペクティブ射影変換行列引用

クリップ空間から正規デバイス座標系に変換するにはwで除算します。

ということはクリップ空間のXYは-w〜wの範囲をとることになります。

posに0.5を乗算することでo.xy は -0.5w ~ +0.5wの範囲をとり、wは0.5wになります。

そして、-0.5w ~ +0.5wに0.5wを足すことで、0 ~ wに変換しています。 これは後で w で除算して 0 ~ 1 となる UV 座標として使いたいためです。

求めたスクリーン座標から深度バッファにアクセスしてz値を算出します。

// 描画するピクセルの深度バッファの値
float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,i.projPos));

SAMPLE_DEPTH_TEXTURE_PROJ

#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)

tex2Dprojの結果からr成分を返しているだけです。 深度バッファはモノトーンなので、r成分だけ返せば充分ということですね。

tex2Dproj()は与えられた座標のxyをw除算をしてからtex2D()のuvとして使うという処理をします。 先述したとおり、ComputeScreenPosで求めた0~wの値をw除算して、uv値(0 ~ 1 )として、使用するためです。

inline float LinearEyeDepth(float z)
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

深度バッファには正規デバイス座標系のz値が格納(0〜1)されていますが、実際のz値に対して、線形ではありません。 near付近の正規デバイス座標系のz値の範囲は大きいですが、far付近の正規デバイス座標系のz値の範囲はとても小さいです。 そのため、これを線形にしています。

f:id:Ny_Program:20211020201916g:plain

その70 完全ホワイトボックスなパースペクティブ射影変換行列引用

この関数は純粋な深度バッファの値が欲しければ必要ありませんが、ソフトパーティクルや他のオブジェクトと重なっている部分を判定したいときなどに描画するオブジェクトの深度値と比較したい場合は必要になります。

描画するオブジェクトの深度値の取得

COMPUTE_EYEDEPTH

#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z

inline float3 UnityObjectToViewPos( in float3 pos )
{
    return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;
}

defineにより、引数に渡した値にUnityObjectToViewPosで求めた座標のz値を代入しています。

UnityObjectToViewPos

UnityObjectToViewPosの引数にはv.vertexが指定されています。 これは頂点シェーダーの引数であるローカル座標です。

ローカル座標に対して、unity_ObjectToWorld(ワールド行列)とUNITY_MATRIX_V(ビュー行列)を乗算しています。

つまり、COMPUTE_EYEDEPTHはローカル座標をビュー座標に変換し、そのz値を引数oに対して代入しています。

float partZ = i.projPos.z;

あとはフラグメントシェーダーでそのまま値を使えばOKです!

関連

ny-program.hatenablog.com

参考

tips.hecomi.com