半透明EffectにDepthOfFieldを適用してみた
はじめに
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
というテクスチャに書き込むことにしました。
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);
}
}
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のRendererFeatures
にDrawTransparentDepthRendererFeature
を設定します。
そして、カスタムDOF(機能のほとんどはデフォルトのDOFをコピーしたもの)にて、_CameraDepthTexture
と_CameraTransparentDepthTexture
を利用してボケ度合いを計算するようにしました。
// この行を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情報を書き込むようにしました。
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を利用してボケ度合いを計算するようにしました。
// この行を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を追加する方法などの知識を深めることができたので、一度触ってみるのは良い経験でした。
Discussion