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

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

σo

月並だが、

怖い事、悲しいこと、嫌なこと、苦しいこと、ストレス、を構成する出来事と記憶、記憶から感情を演繹するための回路、がいつか短絡してしまい、正しい、と我々が今思っている論理、認識、それと感情、を永遠に失ってしまうイベントというのが低確率ではあるが誰にでも実装されていて、なにかの拍子に、例えば事故、薬、老化、あるいはたった一度、いつもの悲しい夜のたった一瞬「論理はもういいや」という事を思ってしまった事が原因で、ほんの一度世界を全否定してしまうだけで、回路が閉じてしまい、もう二度と正しい感情を生み出せない、視界には光の輪が、耳元では天使の囀りが、家には知らない同居人がいて、あなたは何度1+1を試しても3になってしまう、そんな日がいつか来るかもしれない

というのを子供の頃から恐れている。