【Unity】【シェーダ】カメラから見た深度を描画する

カメラから見た時の深度を描画する方法です。

_CameraDepthTextureをシェーダから取得する

マニュアルによると、_CameraDepthTextureをシェーダ内で定義すればデプステクスチャが得られるようです。

docs.unity3d.com

シェーダは次のようにします。

Shader "ShowDepthTexture"
{
    SubShader
    {
        Cull Off
        ZTest Always
        ZWrite Off

        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            
            sampler2D _CameraDepthTexture;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
            }
            ENDCG
        }
    }
}

これをポストエフェクトとして使うので、スクリプトを下記のように書きます。

ただしここで、カメラがデプステクスチャを生成するためにCamera.depthTextureModeをDepthTextureMode.Depthとしておく必要があります。

using UnityEngine;

public class DepthTexture : MonoBehaviour {

    [SerializeField]
    private Shader _shader;
    private Material _material;

    void Start () {
        // たとえばライトのShadow TypeがNo Shadowsのときなどに
        // これが設定されていないとデプステクスチャが生成されない
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;

        _material = new Material(_shader);
    }

    private void OnRenderImage(RenderTexture source, RenderTexture dest)
    {
        Graphics.Blit(source, dest, _material);
    }
}

これを設定しなくても_CameraDepthTextureが取れる場合があるのですが、 下記のフォーラムによると、PCやコンソールのみのようなので、きちんと設定しておく必要があります。

https://forum.unity.com/threads/no-depthtexture-on-forward-rendering-when-shadows-disabled.519230/

Unity uses the depth texture for directional light shadow receiving on desktop and consoles, which is why it exists when there is a light source.

ディレクショナルシャドウが作る影にデプステクスチャを使用しているとのことです。

_CameraDepthTextureの値を実際に使用する値に変換する

さてこうして得られた_CameraDepthTextureの深度値は1~0の範囲で示されます。
直感的に考えると0.5の部分がカメラのNear ClipとFar Clipの中間地点のように思えますが、
カメラのZ座標と深度値は非線形になっているので0.5はまったくもって中間地点にはなりません。

hiyoko1986.web.fc2.com

これでは実際に使うときに困るので、深度値を0~1の線形な値に変換します。
これはLinear01Depth関数を使えばできます。

fixed4 frag(v2f i) : SV_Target
{
    half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
    depth = Linear01Depth(depth);
    return depth;
}

これでdepthには0~1の値が入ってきます。

また、深度値をカメラからのワールド空間における距離として取得したい場合もあります。
このような場合にはLinearEyeDepthを使います。

fixed4 frag(v2f i) : SV_Target
{
    half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
    depth = LinearEyeDepth(depth);
    return depth;
}

これでdepthにはカメラから見た距離の値が入ってきます。

結果

それでは深度値をLinear01Depthで変換したものを描画してみます。
まずこのようなシーンを作ります。

f:id:halya_11:20191022113450p:plain

カメラのNearが0、Farが1として表されるので、これらの値を適当に調整します。
調整出来たら再生してポストエフェクトを掛けます。

f:id:halya_11:20191022113558p:plain

深度が描画されることが確認できました。

余談: CommandBufferを使った実装

任意のタイミングのデプステクスチャを得たい場合はCommandBufferを使えばよさそうです。

light11.hatenadiary.com

using UnityEngine;
using UnityEngine.Rendering;

[RequireComponent(typeof(Camera))]
public class DepthTexture : MonoBehaviour
{

    [SerializeField]
    private Shader _shader;

    private void Awake()
    {
        Initialize();
    }

    private void Initialize()
    {

        var camera = GetComponent<Camera>();

        // たとえばライトのShadow TypeがNo Shadowsのときなどに
        // これが設定されていないとデプステクスチャが生成されない
        camera.depthTextureMode |= DepthTextureMode.Depth;

        if (camera.allowMSAA)
        {
            // MSAAがONになっていると正常に動作しない
            return;
        }
        var material = new Material(_shader);
        var commandBuffer = new CommandBuffer();
        int tempTextureIdentifier = Shader.PropertyToID("_PostEffectTempTexture");
        commandBuffer.GetTemporaryRT(tempTextureIdentifier, -1, -1);
        commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, tempTextureIdentifier);
        commandBuffer.Blit(tempTextureIdentifier, BuiltinRenderTextureType.CurrentActive, material);
        commandBuffer.ReleaseTemporaryRT(tempTextureIdentifier);
        camera.AddCommandBuffer(CameraEvent.AfterForwardOpaque, commandBuffer);
    }
}