CG飯処

腹が減ってはCGができぬ

【Unity】_CameraDepthTexture と _CameraDepthNormalsTexture の扱い方を改めて理解する

Unity で深度テクスチャを扱いたいときに使う _CameraDepthTexture_CameraDepthNormalsTexture のそれぞれで得られる深度値の内容を深堀りながら、それぞれのテクスチャの扱い方を改めて理解していこうと思います。自分の理解の整理も兼ねてまとめてみます。

今回のゴール

上記の画像は、それぞれのパターンで「テクスチャから得た深度値を表示」しただけの結果です。
これらの結果が異なるのがどんな理由なのか(各パターンの挙動の違い)を調べてみます。

環境

  • Unity 2021.2.9f1
  • Built-in Render Pipeline
  • グラフィックスAPI: Direct3D11

DepthTextureMode について

Unity で深度テクスチャから深度値を取得したいとき、以下のマニュアルにある通り、 DepthTextureMode を指定する必要があります。
docs.unity3d.com

これにはいくつか種類があり、

  • DepthTextureMode.Depth - 深度テクスチャ。
  • DepthTextureMode.DepthNormals - 深度とビュー空間の法線を 1 まとめにしたテクスチャ。
  • DepthTextureMode.MotionVectors - 現在のフレームの各スクリーンテクセルのピクセルスクリーン空間のモーション。

の3種類があります。


この中の Depth と DepthNormals を指定した場合に、シェーダ内で深度テクスチャを参照するために使うのが、それぞれ _CameraDepthTexture_CameraDepthNormalsTexture です。これらはグローバル変数としてシェーダ内で使うことができます。

深度バッファについて

深度バッファについても説明しておきます。
深度バッファは、座標変換後のオブジェクトのz値を格納するものです。
基本的に z 値には 0~1 の値が入ります。


この z 値はカメラのクリッピングプレーンの Near プレーンと Far プレーンを元に計算されますが、DirectX11, 12 や Metal の環境では Near プレーンで z=1 、 Far プレーンで z=0 となり、それ以外のプラットフォームでは逆になるという注意点があります。

DirectX 11, DirectX 12, Metal: Reversed direction

  • 深度 (Z) バッファーは、ニアクリップ面で 1.0、ファークリップ面の 0.0 まで減少します。

docs.unity3d.com

この注意点はこの後の説明でも出てきますので覚えておいてください。

今回使うシーン

今回は、以下のようなシーンを用意しました。

カメラのクリッピングプレーンの Near プレーンを 10 、Far プレーンを 20 に設定し、各 Cube をz=11,13,15,17,19 に配置しています。

_CameraDepthTexture

まず _CameraDepthTexture からです。

以下のコードで、深度テクスチャから読み取った深度値を、ポストエフェクトとしてそのまま画面に表示させてみます。

using UnityEngine;

[ExecuteInEditMode]
public class DepthTexture : MonoBehaviour {

    void Start() {
        // DepthTextureMode.Depth を指定
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
    }
}
Shader "DepthTest"
{
    HLSLINCLUDE
    #include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
    };

    TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture);

    half4 frag(v2f i) : SV_Target {
        float2 uv = i.uv;
        
        // 深度値を取得
        float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,sampler_CameraDepthTexture, uv);
        return depth;
    }

    ENDHLSL

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM

            #pragma vertex VertDefault
            #pragma fragment frag

            ENDHLSL
        }
    }
}

このような色になる理由は、2点あります。

  1. 深度バッファの z値 が、手前が1、奥が0になっていること
  2. プロジェクション変換による z 値の偏り (非線形)

まず 1 の方は前述の通りで、 DirectX11, 12の環境では z 値は手前の Near プレーンが1、奥の Far プレーンが0 になります。そのため、一番手前の Cube は白色で、段々灰色になっていき、背景(Farプレーン)は黒色が描画されています。


しかし、最奥にあるCubeの色と背景の色が急激に変わっています。カラーピッカーで見てみると、Cube の色は #474747 でした。
白から黒へのグラデーションであれば、最奥にあるCubeの色はもう少し黒(#000000) に近づいてもよさそうです。


そこで出てくるのが2の理由です。
プロジェクション変換後、z 値は以下の図のように手前に偏っています。(縦軸が座標変換後の z 値で、横軸が変換前の z 値(エディタ上の z 値))

引用: http://marupeke296.com/DXG_No70_perspective.html

そのため、Cube の色は最奥(ほぼFarプレーンと同じ位置)でも白に寄った色となってしまいます。

SAMPLE_DEPTH_TEXTURE について

コード内の SAMPLE_DEPTH_TEXTURE は、 include している PostProcessing のライブラリ内 で定義されており、テクスチャをサンプリングして R チャンネルだけとってくる処理になっています。

#define SAMPLE_DEPTH_TEXTURE(textureName, samplerName, coord2) SAMPLE_TEXTURE2D(textureName, samplerName, coord2).r


ちなみに、Rチャンネル以外になにが入っているかも確認してみましたが、(G,B,A) = (0, 0, 1) となっており、特に何かの値が入っているわけではありませんでした。
(おそらく深度バッファに使われるフォーマットが関係していそうですが、詳しく調べられていません)

_CameraDepthNormalsTexture

続いて、 _CameraDepthNormalsTexture の方も見てみます。


_CameraDepthNormalsTextureマニュアル内でも説明されている通り、法線と深度がひとつになったテクスチャからBとAチャンネルをデコードすることで深度値を得ることができます。(なのでこのテクスチャ自体は「深度テクスチャ」ではないです)

これは画面サイズ 32 ビット (8 ビット/チャネル) テクスチャを作ります。ビュー空間法線が R と G チャネルに、深度が B と A チャネルにエンコードされます。法線は、ステレオ投影を使用してエンコードされ、深度は 16 ビットの値で、2 つの 8 ビットチャネルにパックされます。


UnityCG.cginc include ファイル にはヘルパー関数 DecodeDepthNormal があり、エンコードしたピクセルの値から深度と法線をデコードします。0 から 1 の範囲で深度を返します。


以下のコードで、テクスチャから読み取った深度値を、ポストエフェクトとしてそのまま画面に表示させてみます。

using UnityEngine;

[ExecuteInEditMode]
public class DepthTexture : MonoBehaviour {

    void Start() {
        // DepthTextureMode.DepthNormals を指定
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }
}
Shader "DepthTest"
{
    HLSLINCLUDE
    #include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"

    inline float DecodeFloatRG( float2 enc )
    {
        float2 kDecodeDot = float2(1.0, 1/255.0);
        return dot( enc, kDecodeDot );
    }

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
    };

    TEXTURE2D_SAMPLER2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture);

    half4 frag(v2f i) : SV_Target {
        float2 uv = i.uv;
        
        // テクスチャをサンプリング
        float4 cdn =  SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture,sampler_CameraDepthNormalsTexture, uv);
        // z と w 値から深度値を取得
        float depth = DecodeFloatRG(cdn.zw);
        return depth;
    }

    ENDHLSL

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM

            #pragma vertex VertDefault
            #pragma fragment frag

            ENDHLSL
        }
    }
}


_CameraDepthTexture のときとは結果が違います。


このような色になる理由は、以下の通りです。

  1. _CameraDepthNormalsTexture から得られる深度値はカメラからの距離(0 から Far プレーンの範囲)で、且つ0~1に線形化されている

実際に試してみます。カメラの Near プレーンの値を 10 から 0.01 へ変更し、カメラの目の前に Cube を置いてみます。(わかりにくいですが、小さい Cube があります)



すると、以下のように濃い黒色で表示されます。


つまり、_CameraDepthTexture とは異なり、ここで得られている深度値はクリッピングプレーン基準ではなく、カメラからの距離を表していることがわかります。


また、以下のように z=5 の位置へ新しく Cube を置いてカラーピッカーで各 Cube の色を確かめてみても急激に色が変わる箇所はないことから、 線形になっていそうなこともわかります。

DecodeFloatRG について

コード内で、DecodeFloatRG という関数を使っています。
これはマニュアル内でも説明されている DecodeDepthNormal 関数の中で呼ばれている関数で、 UnityCG.cginc 内で定義されています。

inline float DecodeFloatRG( float2 enc )
{
    float2 kDecodeDot = float2(1.0, 1/255.0);
    return dot( enc, kDecodeDot );
}

inline void DecodeDepthNormal( float4 enc, out float depth, out float3 normal )
{
    depth = DecodeFloatRG (enc.zw);
    normal = DecodeViewNormalStereo (enc);
}

今回、ポストエフェクト用に HLSL での実装にしたため、 UnityCG.cginc を include できなかったので、実装をコピーして持ってきています。


この関数でやっていることは、B チャンネルと A チャンネルに格納された各 8 bit の深度情報をつなぎあわせて 16 bit にしています。
dot( enc, kDecodeDot ) の部分を展開してみると cdn.z * 1.0 + cdn.w * 1/255 となり、 w の方を 8 bit 分後ろに下げて足し合わせている様子がわかります。


ちなみに、 B チャンネルと A チャンネルをそれぞれ出力してみると以下のようになります。



Linear01Depth との関連

前述の「カメラからの距離(0 から Far プレーンの範囲)で、且つ0~1に線形化」というのは、Linear01Depth の挙動と同じに見えます。

実際に _CameraDepthTexture から取得した深度値を Linear01Depth に渡した後の値を表示してみると以下のようになります。


どちらも、カメラからの距離を0~1の線形な形に変換したものを返しているので、結果は同じになるようです。


つまり、 「Linear01Depth後の_CameraDepthTexture の深度値 = _CameraDepthNormalsTexture の深度値」という関係がありそうです。

まとめ

今回の調査で以下のことがわかりました。

対象 対象の範囲 値の範囲 線形/非線形
_CameraDepthTexture の深度値 深度バッファの値 Near プレーンから Far プレーン 1~0 非線形
_CameraDepthNormalsTexture の深度値 カメラからの距離 カメラ位置から Far プレーン 0~1 線形
Linear01Depth カメラからの距離 カメラ位置から Far プレーン 0~1 線形

また、以下の関係性がありそうでした。
「Linear01Depth 後の_CameraDepthTexture の深度値 = _CameraDepthNormalsTexture の深度値」


マニュアルに載っているようなことを改めて調べ直した感じになりましたが、何か理解の助けになれば幸いです。