この投稿はグレンジ Advent Calendar 2021の2日目の記事です。
概要
こんにちは!
株式会社グレンジでクライアントエンジニアをしているGamu(@AblerBiri)です。
みなさん、SpriteRenderer等で描画しているキャラにDepth of Fieldが上手く効かなくて困ったという経験はありませんか?
例えば、上の動画のようにキャラの足元だけDoFが効いて、ほとんどがボヤっとしてしまうとか。
今回はUnityのURP(Universal RenderPipeline)で解決する方法を紹介します。
環境
OS : MacOS Big Sur(11.4)
Unity : 2020.3.12f1
URP : 10.5.0
GL : Metal
スプライトにDoFが効かない原因
結論からすると、スプライトの描画で使用しているシェーダが深度値をデプスバッファに書き込まないからです。
DoFはデプスバッファに書き込まれている深度値を用いてブラーを掛ける範囲を決めています。
なので、深度値を書き込んでいなければ正しくブラーが掛からないのは当然です。
例えば、SpriteRendererにデフォルトで付くマテリアルのシェーダ(Sprites/Default)やURP用のライティングを考慮するスプライト用のシェーダ(Universal Render Pipeline/2D/Sprite-Lit-Default)を見ると、深度値を書き込む設定(ZWrite)がOFFになっています。
では、それらのシェーダのZWriteをONにすれば解決!...とはいきませんでした。
スプライトを使ってキャラを描画する場合の問題とURPのForwardRendererを使う場合の問題が存在しています。
問題点1 : Zファイティング
スプライトを使ってキャラを描画する時に深度値も書き込むようにすると、次の画像のようになることがあります。
この現象はZファイティングと呼ばれます。
Zファイティングとは、カメラからの距離がほぼ同じ2つの平面がある時、どちらが手前に描画されるか定まらない現象です。
Zファイティング自体は3D描画で一般的に発生するものですが、スプライトを使ってパーツごとにキャラを描画している場合は特に発生しやすいです。
スプライトの描画でZファイティングを引き起こさないためにはZTestをAlways(深度値を考慮せず常に描画)等にするか、深度値を書き込まない等の解決方法があります。
今回はZTestをAlwaysにせず、かつ、キャラのスプライトの深度値を上手く書き込む方法として、キャラの色を描画した後に別のパスでキャラの深度値を書き込む方法を紹介します。
URPはRendererFeatureというものを使って実装してみます。
スプライトのシェーダに専用パスを定義する
URPのスプライト用シェーダ(Universal Render Pipeline/2D/Sprite-Lit-Default)をコピペし、新しいシェーダを作成します。
そして作成したシェーダのPassというブロックが並んでいる場所の一番最後に、次のコードを置きます。
Pass
{
Name "TransparentDepthOnly"
Tags { "LightMode" = "TransparentDepthOnly" "Queue"="Geometry" "RenderType"="Opaque"}
ZWrite On
ColorMask 0
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
#pragma vertex DepthOnlyVertex
#pragma fragment DepthOnlyFragment
// -------------------------------------
// Material Keywords
#pragma shader_feature_local_fragment _ALPHATEST_ON
#pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
//--------------------------------------
// GPU Instancing
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
struct Attributes
{
float4 position : POSITION;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
float4 _MainTex_ST;
Varyings DepthOnlyVertex(Attributes input)
{
Varyings output = (Varyings)0;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.uv = TRANSFORM_TEX(input.texcoord, _MainTex);
output.positionCS = TransformObjectToHClip(input.position.xyz);
return output;
}
half4 DepthOnlyFragment(Varyings input) : SV_TARGET
{
clip(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a - 0.5);
return 0;
}
ENDHLSL
}
これがスプライトの深度値だけを書き込む専用パスになります。
URPにスプライトの深度値だけを書き込む機能を追加する
URPのRendererFeatureを使って、先ほどの専用パスを使ってスプライトの深度値をデプスバッファに書き込む機能を実装します。
以下、RendererFeatureとRenderPassのコードです。
using UnityEngine.Rendering.Universal;
public 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);
}
}
}
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class DrawTransparentDepthRenderPass : ScriptableRenderPass
{
private readonly ShaderTagId shaderTagId = new ShaderTagId("TransparentDepthOnly");
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var cmd = CommandBufferPool.Get("Draw Transparent Depth");
cmd.SetRenderTarget(renderingData.cameraData.renderer.cameraDepthTarget);
context.ExecuteCommandBuffer(cmd);
var drawSettings = CreateDrawingSettings(shaderTagId, ref renderingData, SortingCriteria.CommonOpaque);
var filterSettings = new FilteringSettings(RenderQueueRange.transparent, renderingData.cameraData.camera.cullingMask);
context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings);
CommandBufferPool.Release(cmd);
}
}
RenderPassは拡張する描画機能の実装本体です。
RendererFeatureはRenderPassに渡すパラメータを設定したりRenderPassのインスタンスを生成したりする役割があります。
そして、このRendererFeatureを有効にするため、ForwardRenderer.assetというScriptableObjectを表示し、AddRendererFeatureからDrawTransparentDepthRendererFeatureを選択します。
RenderFeature追加前
これでZファイティングを起こさずにキャラにもDoFが効くように...なりません。
ここでもう1つの問題が立ちはだかります。
問題点2 : ポストエフェクト用のデプスバッファが別に存在している
先ほどまでの実装でZファイティングを起こさずにスプライトの深度値は確かに書き込まれます。
しかし、実はURPのForwardRendererのポストエフェクトで使われているデプスバッファとは別のものに書き込まれています。
FrameDebuggerやDebug.Logで確認してみると、
- renderingData.cameraData.renderer.cameraDepthTargetで参照できるデプスバッファは
_CameraDepthAttachment
- 実際にポストエフェクトで使われているデプスバッファは
_CameraDepthTexture
ということが確認できます。
_CameraDepthTextureは直接参照できない仕組みになっているので、GetTemporaryRTを使って無理やり参照してみます。
DrawTransparentDepthRenderPassクラスのコードを以下に置き換えて下さい。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class DrawTransparentDepthRenderPass : ScriptableRenderPass
{
private readonly ShaderTagId shaderTagId = new ShaderTagId("TransparentDepthOnly");
private readonly int depthId = Shader.PropertyToID("_CameraDepthTexture");
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var cmd = CommandBufferPool.Get("Draw Transparent Depth");
var descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.colorFormat = RenderTextureFormat.Depth;
descriptor.depthBufferBits = 32;
descriptor.msaaSamples = 1;
cmd.GetTemporaryRT(depthId, descriptor, FilterMode.Point);
cmd.SetRenderTarget(depthId, depthId);
context.ExecuteCommandBuffer(cmd);
var drawSettings = CreateDrawingSettings(shaderTagId, ref renderingData, SortingCriteria.CommonOpaque);
var filterSettings = new FilteringSettings(RenderQueueRange.transparent, renderingData.cameraData.camera.cullingMask);
context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings);
cmd.Clear();
cmd.ReleaseTemporaryRT(depthId);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
}
最後にReleaseTemporaryRTするのでダメかと思いきや、この方法で上手く書き込めました。
これで晴れてスプライトを使ったキャラにもDoFを効かせることが出来るようになりました!
最後に
Zファイティングを発生させずにスプライト(Transparent)にもDoFを効かせる方法を紹介しました。
3D空間にスプライトのキャラを配置するというのは珍しいシチュエーションだと思いますが、オクトパストラベラーのような表現のゲームを作ろうとした時は使えるかもしれません。
グレンジのアドベントカレンダーには引き続き記事を投稿していきます!
明日は、flankidsさんの記事が投稿される予定です!
参考
https://blog.amagi.dev/entry/2019/05/09/174926
https://forum.unity.com/threads/solved-urp-depth-of-field-transparency-render-queue-problem.859936/
https://qiita.com/t-matsunaga/items/09343ae7c683269374c4