🌐

半透明EffectにDepthOfFieldを適用してみた

2024/07/03に公開

はじめに

UnityのデフォルトのPostEffectのDepthOfField(以下、DOF)では、ShaderGraphやVFXGraphで作成した半透明エフェクトにはぼかしの効果がかかりませんでした。
この記事では、そのエフェクトにもぼかし効果が反映されるように対応した内容を紹介します。

動作環境

Unity 2022.3.23f1
Universal RP 14.0.10

結論から

デフォルトDOFで思った通りボケなかった半透明エフェクトもカメラからの距離に応じてボケるように対応しました。

デフォルトDOF カスタムDOF

DOFの仕組みについて

RenderQueueがAlphaTestまでの描画対象の深度情報は、_CameraDepthTextureに書き込まれており、DOFの処理ではこの_CameraDepthTextureを利用してボケ度合いを計算しています。
半透明なEffectは、Transparentで描画されているので、_CameraDepthTextureに書き込まれないため、ボケることがありません。

_CameraDepthTextureなど、描画に利用する中間テクスチャは、Window > Analysis > FrameDebuggerを利用して確認することができます。(※ 画像が暗く見づらいことがあったのでスクショして明るくすると見やすくなります。)

対応内容

1. Transparent用のDepth情報をTextureに書き込む

最初は、_CameraDepthTextureに追加しようと考えましたが、_CameraDepthTextureはDOFだけで使われているわけではないため、他の機能に影響が出る可能性があります。そこで、TransparentのDepthだけを_CameraTransparentDepthTextureというテクスチャに書き込むことにしました。

DrawTransparentDepthRenderPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

/// <summary>
/// TransparentのDepthを描画するRenderPass.
/// </summary>
public sealed class DrawTransparentDepthRenderPass : ScriptableRenderPass
{
    private readonly ShaderTagId _shaderTagId = new("DepthOnly");
    private const string DepthRenderTag = "DepthRender";
    private const string TextureName = "_CameraTransparentDepthTexture";

    private RTHandle _renderTexture;

    /// <summary>
    /// Passを実行する.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="renderingData"></param>
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // Tagが指定してあるカメラ以外は描画しない.
        if (!renderingData.cameraData.camera.CompareTag(DepthRenderTag))
        {
            return;
        }

        // CommandBufferを取得する.
        var cmd = CommandBufferPool.Get(nameof(DrawTransparentDepthRenderPass));
        var descriptor = renderingData.cameraData.cameraTargetDescriptor;
        descriptor.colorFormat = RenderTextureFormat.Depth;
        descriptor.depthBufferBits = 32;
        descriptor.msaaSamples = 1;

        RenderingUtils.ReAllocateIfNeeded(ref _renderTexture, descriptor, name: TextureName);

        // RenderTargetを_CameraTransparentDepthTextureに設定してクリアする.
        cmd.SetRenderTarget(_renderTexture);
        cmd.ClearRenderTarget(true, true, Color.white);
        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        // Transparent後のDepthを描画する.
        var drawSettings = CreateDrawingSettings(_shaderTagId, ref renderingData, SortingCriteria.CommonTransparent);
        var filterSettings = new FilteringSettings(RenderQueueRange.transparent, renderingData.cameraData.camera.cullingMask);
        context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings);

        Shader.SetGlobalTexture(Shader.PropertyToID(TextureName), _renderTexture);

        // RenderTargetを戻す.
        cmd.SetRenderTarget(BuiltinRenderTextureType.CameraTarget);
        context.ExecuteCommandBuffer(cmd);

        CommandBufferPool.Release(cmd);
    }

    public void Dispose()
    {
        RTHandles.Release(_renderTexture);
    }
}
DrawTransparentDepthRendererFeature.cs
using UnityEngine.Rendering.Universal;

/// <summary>
/// TransparentのDepthを描画するRendererFeature.
/// </summary>
public sealed class DrawTransparentDepthRendererFeature : ScriptableRendererFeature
{
    private DrawTransparentDepthRenderPass _pass;

    public override void Create()
    {
        _pass ??= new DrawTransparentDepthRenderPass();
        _pass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (_pass != null)
        {
            renderer.EnqueuePass(_pass);
        }
    }

    protected override void Dispose(bool disposing)
    {
        _pass?.Dispose();
        _pass = default;
    }
}

上記のファイルを使用しているRendererのRendererFeaturesDrawTransparentDepthRendererFeatureを設定します。

そして、カスタムDOF(機能のほとんどはデフォルトのDOFをコピーしたもの)にて、_CameraDepthTexture_CameraTransparentDepthTextureを利用してボケ度合いを計算するようにしました。

CustomBokehDepthOfField.shader(変更箇所抜粋)
// この行をTEXTURE2D_Xの箇所に追加
TEXTURE2D(_CameraTransparentDepthTexture);

half FragCoC(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
    // float depth = LOAD_TEXTURE2D_X(_CameraDepthTexture, _SourceSize.xy * uv).x;
    // 上記行を以下に変更
    // ここから
    float opaqueDepth = LOAD_TEXTURE2D_X(_CameraDepthTexture, _SourceSize.xy * uv).x;
    float transparentDepth = LOAD_TEXTURE2D_X(_CameraTransparentDepthTexture, _SourceSize.xy * uv).x;
    float depth = lerp(opaqueDepth, transparentDepth, step(opaqueDepth, transparentDepth));
    // ここまで
    float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams);

    half coc = (1.0 - FocusDist / linearEyeDepth) * MaxCoC;
    half nearCoC = clamp(coc, -1.0, 0.0);
    half farCoC = saturate(coc);

    return saturate((farCoC + nearCoC + 1.0) * 0.5);
}

エフェクトの透明な部分もはっきり見えるようになってしまった。

2. TransparentのAlpha情報もTextureに書き込む

透明な部分も上手くぼかす方法をチームのエンジニアと相談して、Alpha情報も書き込んでおいて透過具合によって、_CameraDepthTextureを使うか_CameraTransparentDepthTextureを使うかするようにすれば良いのではという結論にいたりました。この方針で調整しました。
_CameraTransparentDepthTextureとは別に、_CameraTransparentTextureにAlpha情報を書き込むようにしました。

DrawTransparentOnlyRenderPass.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

/// <summary>
/// TransparentのDepthを描画するRenderPass.
/// </summary>
public sealed class DrawTransparentOnlyRenderPass : ScriptableRenderPass
{
    private readonly List<ShaderTagId> _shaderTagIds = new()
    {
        new ShaderTagId("UniversalForward"), // 通常のオブジェクトはこちらの描画パスを使用する.
        new ShaderTagId("SRPDefaultUnlit") // ShaderGraph 等の LightMode に指定が無い場合はこちらを使用する.
    };

    private const string DepthRenderTag = "DepthRender";
    private const string TextureName = "_CameraTransparentTexture";

    private RTHandle _renderTexture;

    /// <summary>
    /// Passを実行する.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="renderingData"></param>
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // Tagが指定してあるカメラ以外は描画しない.
        if (!renderingData.cameraData.camera.CompareTag(DepthRenderTag))
        {
            return;
        }

        // CommandBufferを取得する.
        var cmd = CommandBufferPool.Get(nameof(DrawTransparentOnlyRenderPass));
        var descriptor = renderingData.cameraData.cameraTargetDescriptor;
        descriptor.colorFormat = RenderTextureFormat.ARGB32;
        descriptor.depthBufferBits = 0;
        descriptor.msaaSamples = 1;

        RenderingUtils.ReAllocateIfNeeded(ref _renderTexture, descriptor, name: TextureName);

        // RenderTargetを_CameraTransparentTextureに設定してクリアする.
        cmd.SetRenderTarget(_renderTexture);
        cmd.ClearRenderTarget(true, true, Color.clear);
        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        // Transparentを描画する.
        var drawSettings = CreateDrawingSettings(_shaderTagIds, ref renderingData, SortingCriteria.CommonTransparent);
        var filterSettings = new FilteringSettings(RenderQueueRange.transparent, renderingData.cameraData.camera.cullingMask);
        context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings);

        Shader.SetGlobalTexture(Shader.PropertyToID(TextureName), _renderTexture);

        // RenderTargetを戻す.
        cmd.SetRenderTarget(BuiltinRenderTextureType.CameraTarget);
        context.ExecuteCommandBuffer(cmd);

        CommandBufferPool.Release(cmd);
    }

    public void Dispose()
    {
        RTHandles.Release(_renderTexture);
    }
}

Featureはほとんど一緒なので省略します。

カスタムDOFで、それぞれのTextureを利用してボケ度合いを計算するようにしました。

CustomBokehDepthOfField.shader(変更箇所抜粋)
// この行をTEXTURE2D_Xの箇所に追加
TEXTURE2D_X_FLOAT(_CameraTransparentTexture);

half FragCoC(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
    float opaqueDepth = LOAD_TEXTURE2D_X(_CameraDepthTexture, _SourceSize.xy * uv).x;
    float transparentDepth = LOAD_TEXTURE2D_X(_CameraTransparentDepthTexture, _SourceSize.xy * uv).x;
    // float depth = lerp(opaqueDepth, transparentDepth, step(opaqueDepth, transparentDepth));
    // 上記行を以下に変更
    // ここから
    float alpha = LOAD_TEXTURE2D_X(_CameraTransparentTexture, _SourceSize.xy * uv).a;
    float depth = lerp(opaqueDepth, lerp(opaqueDepth, transparentDepth, alpha), step(opaqueDepth, transparentDepth));
    // ここまで
    float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams);

    half coc = (1.0 - FocusDist / linearEyeDepth) * MaxCoC;
    half nearCoC = clamp(coc, -1.0, 0.0);
    half farCoC = saturate(coc);

    return saturate((farCoC + nearCoC + 1.0) * 0.5);
}

できました!

FrameDebuggerで確認するとそれぞれのTextureの情報は以下のようになっていました。

_CameraDepthTexture _CameraTransparentDepthTexture _CameraTransparentTexture

まとめ

半透明EffectにDOFを掛ける方法を調査しました。
一定の表現に達したかと思いますが、実際の運用にあたってはEffectを制作されるクリエイターとルールなどを含めて調整が必要になるため、このまま使えるかはもう少し検討が必要そうでした。
本対応の調査を通して、URPのレンダリングの仕組みや独自Passを追加する方法などの知識を深めることができたので、一度触ってみるのは良い経験でした。

Happy Elements

Discussion