Unityで透明なオブジェクトにDepth of Fieldが効かない時の対処法

PostProcessingStack、便利ですよね~。 僕はUnity新しいプロジェクト始めるとき、大抵最初にMain Cameraにセットして、Bloom、Depth of Field (DoF)、色収差ビネット、ACESトーンマップを有効にしてます。

ただ、最近の案件で、透過画像のスプライトにDoFが効かないという現象に出くわして苦労しました。 なんとか対処したので、対処法を書いておきます。

半透明部分の表示がおかしいなど、完全な解決策ではないので、詳しい方はぜひ教えてください~

問題のシーン

今回問題になるのは、このような半透明部分を含んだ透過png画像です。
表面の光沢も表現したいので、Standardマテリアルで Renderting ModeをFadeにしてみます。
この状態でシーンに配置してみましょう。

f:id:amagitakayosi:20190509172909p:plain

うーん、ピントが合わない……

シーン上には、キューブと半透明のスプライトを等間隔に並べています。
左のキューブにはフォーカスが当たっていますが、右側のスプライトは全てボヤケてしまっています。

f:id:amagitakayosi:20190509142123p:plain

半透明な部分がない画像の場合、StandardマテリアルでRendering ModeをCutoutにしてしまえばピントが合うのですが、今回のような画像だと半透明の部分が消えてしまいます。

f:id:amagitakayosi:20190509143535p:plain

Transparentなオブジェクトはデプスを書き込まない

unity transparent dof でググってみると、PostProsessingStackのissueが見つかりました。

github.com

Transparentなオブジェクトはデプスバッファに書き込まないため、デプスを利用するDoFでは無視されてしまう。 なので、明示的にデプスを書いてあげれば良いようです。

また、このissueでは、デプスを書き込むシェーダーのサンプルが紹介されています。

Dummy transparent shader that writes to depth (per request) · GitHub

シェーダーを読んで見ると、どうやら Tags { "LightMode"="ShadowCaster" } なPassを追加し、フラグメントシェーダー内で SHADOW_CASTER_FRAGMENT(i) を呼んでやれば、デプスバッファに書き込めるみたいです。

ShadowCasterのシェーダーは名前の通り影の計算に用いられますが、デプスバッファにも反映されます。

さっそく問題のスプライトでもやってみましょう。 シェーダーを Create > Shader > Standard Surface Shader から作成し、ShadowCasterのSubShaderを追加して、スプライトのマテリアルに指定します。

SubShader
{
    Pass
    {
        Name "ShadowCaster"
        Tags { "LightMode"="ShadowCaster" }

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

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

        struct v2f {
            V2F_SHADOW_CASTER;
            float2 uv : TEXCOORD1;
        };

        sampler2D _MainTex;
        float4 _MainTex_ST;
        float _Cutout;

        v2f vert(appdata v)
        {
            v2f o;
            o.uv = TRANSFORM_TEX(v.uv, _MainTex);
            TRANSFER_SHADOW_CASTER(o)
            return o;
        }

        float4 frag( v2f i ) : COLOR
        {
            fixed4 texcol = tex2D( _MainTex, i.uv );
            if (texcol.a < _Cutout)
            {
                discard;
                }
            SHADOW_CASTER_FRAGMENT(i)
        }
        ENDCG
    }
}

実行!

f:id:amagitakayosi:20190509172400p:plain

あれー

不透明な部分だけデプスバッファに書きこむ

どうやら、半透明な部分もデプスが書き込まれ、黒として描画されてしまったようです。

仕方ないので、半透明な部分にピントを合わせるのを諦め、ShadowCasterのPassではdiscardするようにします。
幸い、今回の画像で半透明な部分はドロップシャドウだけなので、ボヤケててもあまり気にならないはず……

float4 frag( v2f i ) : COLOR
{
    fixed4 texcol = tex2D( _MainTex, i.uv );
    if (texcol.a < _Cutout)
    {
        discard;
    }
    SHADOW_CASTER_FRAGMENT(i)
}

終結

f:id:amagitakayosi:20190509144038p:plain

やったー

よく見ると半透明部分の描画がおかしいけど、許容範囲ということで……

f:id:amagitakayosi:20190509174757p:plain

まとめ

必要な手順をまとめると以下のとおりです:

  • カメラをDeferredにする
  • シェーダーに ShadowCaster のパスを追加
  • 画像のアルファ値によって、透明部分をdiscardする

今回作ったSurfaceシェーダーはこちらに置いておきます。

https://gist.github.com/fand/31bcfa5cd9008a270efb628503200871

もっと良いやり方知ってる人教えてください、頼む!!!!!!