神様は有休消化中です。

Unity関連の技術ネタを書いてます。

【Unity】無駄なドローコールなしで深度バッファを取得する方法

UnityでDepth Bufferを使用する方法として公式に紹介されているのは、Camera.depthTextureModeを使用する方法です。
しかし、Camera.depthTextureModeを使用すると、UpdateDepthTextureなるレンダリングパスが増えてしまいます。

f:id:appleorbit:20171103050838p:plain

これは公式ページにも記載されている通り、Unityの仕様のようです。
https://docs.unity3d.com/jp/540/Manual/SL-CameraDepthTexture.html

デプステクスチャは、シャドウキャスターのレンダリングに使用するのと同じシェーダー内パスを使用してレンダリングされます(“ShadowCaster” pass type)。


Unity内部で、Shader Replaceを使ってDepth Bufferを書くためだけにドローコールを発行しているためにこの挙動となっています。
理由は不明ですが、おそらくOpenGL2.0世代のモバイル端末はNative Depth Textureをサポートしていない端末があったので、それらに対応するためなのかなと。
ハイエンド系ならMRTで自前で深度バッファ書くだろ的な。

Unityの仕様はどうあれ、Depth Bufferのためにドローコールを発行するとなると描画負荷に無視できないインパクトが出てきます。
そこで、パスを増やさずにDepth Bufferを取得する方法を模索しました。

Depth Bufferを取得する

結論から言うと、Camera.SetTargetBuffersを使用します。

private Camera m_Camera;
private RenderTexture m_ColorBuffer;
private RenderTexture m_DepthBuffer;

private void Start()
{
  m_Camera = GetComponent<Camera> ();

  // |私は深度バッファを取得するためにドローコールを発行しました><|
  //m_Camera.depthTextureMode = DepthTextureMode.Depth;

  // カラーバッファを生成
  m_ColorBuffer = new RenderTexture (Screen.width, Screen.height, 0);
  m_ColorBuffer.Create ();

  // 深度バッファを生成
  m_DepthBuffer = new RenderTexture (Screen.width, Screen.height, 24, RenderTextureFormat.Depth);
  m_DepthBuffer.Create ();

  m_Camera.SetTargetBuffers (m_ColorBuffer.colorBuffer, m_DepthBuffer.depthBuffer);
}


このままだとShaderから使用できないので、CommandBufferを使って任意のタイミングでセットします。
加えて、カメラの描画先がカラーバッファになってしまっているので、Back buffer(画面)に書き戻します。

private void AddCommand()
{
  // 深度バッファをセットするコマンド
  {
    CommandBuffer command = new CommandBuffer ();
    command.name = "Set depth texture";

    command.SetGlobalTexture ("_DepthTexture", m_DepthBuffer);

    m_Camera.AddCommandBuffer (CameraEvent.BeforeImageEffects, command);
  }

  // カラーバッファをバックバッファ(画面)に描きこむコマンド
  {
    CommandBuffer command = new CommandBuffer ();
    command.name = "blit to Back buffer";

    // (注)
    // カメラの書き込み先がRenderTextureなのに、CameraEvent.AfterEverythingのタイミングで
    // CameraTargetがback bufferを示すのは正しいのだろうか・・・
    // 確認バージョン:Unity5.6.1f1
    command.SetRenderTarget (BuiltinRenderTextureType.CameraTarget);
    command.Blit (m_ColorBuffer, BuiltinRenderTextureType.CurrentActive);

    m_Camera.AddCommandBuffer (CameraEvent.AfterEverything, command);
  }
}

これで、_DepthTextureにShaderからアクセスすることが可能になりました。
こんな感じのShaderで、画面に深度バッファが表示できます。

Shader "Test/BlitDepth"
{
  Properties
  {
    _MainTex ("Texture", 2D) = "white" {}
  }
  SubShader
  {
    Cull Off ZWrite Off ZTest Always

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

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

      struct v2f
      {
        float2 uv : TEXCOORD0;
        float2 uv_depth : TEXCOORD1;
        float4 vertex : SV_POSITION;
      };

      v2f vert (appdata v)
      {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = v.uv;
        o.uv_depth = v.uv;
        return o;
      }
      
      sampler2D _MainTex;
      sampler2D _DepthTexture;

      fixed4 frag (v2f i) : SV_Target
      {
        half rawDepth = SAMPLE_DEPTH_TEXTURE(_DepthTexture, i.uv_depth);
        half depth = Linear01Depth(rawDepth);
        return fixed4(rawDepth, rawDepth, rawDepth, 1);
      }
      ENDCG
    }
  }
}

UpdateDepthTextureパスなしでDepth bufferを取得することができました。
カラーバッファを画面に書き戻すコストは増えていますが、ドローコールを重複して発行するコストに比べれば安いとおもいます。
f:id:appleorbit:20171103055104p:plain