Depth only pass
unlit shader中包含了一个Depth Only Pass,这个pass的代码在Packages\com.unity.render-pipelines.universal\Shaders\DepthOnlyPass.hlsl
中。这是一个公共pass,几乎所有的URP shader都会包含这个pass。本篇说一说这个pass的作用以及实现细节。
作用
Depth only pass的作用是生成一张场景的深度图,一般是在渲染不透明物体之前,对所有包含该pass的材质对应的物体执行这个pass,当所有物体执行完毕后,就得到了深度图。这个pass执行的前提是URP判断需要深度图,比如URP assets中配置了:
或者camera开启了后处理。使用深度图,游戏可以从屏幕空间深度重建出世界空间深度,从而完成很多有意思的效果,比如水面渲染的岸边泡沫,粒子面片进入地面时柔和的过渡。另外很多屏幕空间的后处理效果都会用到深度图。
但是如果只是为了得到深度图,其实也不必执行depth only pass,只要在不透明物体渲染之后,从当前的depth buffer copy出深度就可以。所以这个pass也不是必然执行的。主要看copy depth的条件是否满足,如果满足则执行copy depth,而不是depth only pass。具体的逻辑,可以参考ForwardRenderer.cs
代码,注意是URP的c#代码,不是shader:
// Depth prepass is generated in the following cases:
// - If game or offscreen camera requires it we check if we can copy the depth from the rendering opaques pass and use that instead.
// - Scene or preview cameras always require a depth texture. We do a depth pre-pass to simplify it and it shouldn't matter much for editor.
// - Render passes require it
bool requiresDepthPrepass = requiresDepthTexture && !CanCopyDepth(ref renderingData.cameraData);
requiresDepthPrepass |= isSceneViewCamera;
requiresDepthPrepass |= isPreviewCamera;
requiresDepthPrepass |= renderPassInputs.requiresDepthPrepass;
requiresDepthPrepass |= renderPassInputs.requiresNormalsTexture;
重点是CanCopyDepth
方法:
bool CanCopyDepth(ref CameraData cameraData)
{
bool msaaEnabledForCamera = cameraData.cameraTargetDescriptor.msaaSamples > 1;
bool supportsTextureCopy = SystemInfo.copyTextureSupport != CopyTextureSupport.None;
bool supportsDepthTarget = RenderingUtils.SupportsRenderTextureFormat(RenderTextureFormat.Depth);
bool supportsDepthCopy = !msaaEnabledForCamera && (supportsDepthTarget || supportsTextureCopy);
// TODO: We don't have support to highp Texture2DMS currently and this breaks depth precision.
// currently disabling it until shader changes kick in.
//bool msaaDepthResolve = msaaEnabledForCamera && SystemInfo.supportsMultisampledTextures != 0;
bool msaaDepthResolve = false;
return supportsDepthCopy || msaaDepthResolve;
}
从该函数可以看到,如果Camera开启了MSAA,则就不能使用Copy Depth了,为啥呢?注释有说,URP暂时不能在不损失精度的前提下从MSAA RT resolve出depth。在PC上,这几乎是唯一的限制,如果把MSAA关闭,在FrameDebugger中就会发现Depth Only Pass消失了,而在DrawOpaqueObjects
之后多了一个CopyDepth的pass。
Z-Pre Pass和Early-Z测试
一般来说,这种在渲染场景之前,先把场景的深度渲染出来的过程叫做Z-Pre Pass(或者Depth Pre Pass等),其用途除了生成深度图之外,最重要的作用是给硬件Early-Z的执行提供一个优化好的Depth Buffer。Early-Z在Fragment Shader之前执行,如果片段不能通过Early-Z测试,则不会执行Fragment Shader,这可以大大降低Overdraw。但Early-Z可以起作用的条件在于我们绘制的顺序是从近到远(对于不透明物体),而正常渲染时并不能保证这个顺序。通过Z-Pre Pass,渲染一遍场景之后,depth buffer中保存的是离camera最近的这些片段的z值,之后再正常渲染场景,此时进行Early-Z测试就会发现只有和depth buffer中z值相等的片段才需要绘制,即只要进行一个Equal测试就可以排除掉所有潜在的overdraw片段,让Early-Z的作用发挥到最大。另外Early-Z本身也是有条件的,对于一个draw call,如果FS中执行了clip操作,或者修改了深度值,那么就不能进行Early-Z测试,GPU就会使用正常的Late-Z测试。如果我们要绘制大量使用Alpha Test材质的物体,比如一大片草地,这些草本身overdraw就很严重,还不能启用Early-Z,对性能影响很大。但如果使用Z-Pre Pass,就可以在此时对于草的draw call使用alpha test来更新depth buffer,这样那些透明片段就会被丢弃掉,留下的片段的深度值会被写入到depth buffer上(当前前提上通过深度测试),经过Z-Pre Pass之后,再普通pass中绘制草时就可以不使用Alpha Test了,这样Early-Z可以使用了,仍然是简单的进行深度的Equal测试就可以将所有应该被画出来的片段画出来。
看到这儿,相信你心中已经有了想法,既然很多效果需要深度图,而做一个Z-Pre Pass又可以优化Ealry-Z,一举两得嘛,还需要CopyDepth干嘛。想法是很好,可惜的是Unity的Depth only pass并不能作为一个优化Early-Z的Z-Pre Pass,因为Depth only pass的输出是一个depth Render texture,而不是depth buffer,这样后面普通的pass绘制时,执行深度测试没法用这个RT去比较。深度测试使用的当前Render Target的depth attachment,且必须是同一个Render Target。后面的普通Pass的Render Target是_cameraColorTexture,它有自己的depth attachment。总之,可惜了,白白执行了那么多draw call,只得到了深度图,而不能优化Early-Z。
这个事情的原因可参考Unity论坛的官方解释:https://forum.unity.com/threads/need-clarification-on-urps-use-of-the-depth-prepass.1004577/
总之,未来是可以解决的,暂时还不行。
Shader代码分析
由于只是输出depth texture,其VS代码也很简单,就是计算clip space坐标即可。但是URP的这个代码稍微复杂一些:
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, _BaseMap);
output.positionCS = TransformObjectToHClip(input.position.xyz);
return output;
}
half4 DepthOnlyFragment(Varyings input) : SV_TARGET
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
Alpha(SampleAlbedoAlpha(input.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)).a, _BaseColor, _Cutoff);
return 0;
}
VS中会计算uv坐标,为啥呢?在FS中可以看到,会采样贴图中的alpha。这儿有两个函数:
half Alpha(half albedoAlpha, half4 color, half cutoff)
{
#if !defined(_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A) && !defined(_GLOSSINESS_FROM_BASE_ALPHA)
half alpha = albedoAlpha * color.a;
#else
half alpha = color.a;
#endif
#if defined(_ALPHATEST_ON)
clip(alpha - cutoff);
#endif
return alpha;
}
half4 SampleAlbedoAlpha(float2 uv, TEXTURE2D_PARAM(albedoAlphaMap, sampler_albedoAlphaMap))
{
return SAMPLE_TEXTURE2D(albedoAlphaMap, sampler_albedoAlphaMap, uv);
}
即,URP的depth only pass会执行Alpha Test,且Alpha值除了来源于颜色本身,也来源于贴图的Alpha,当然需要开启相应的关键字。URP的做法当然是对的,因为对于Alpha Test材质的物体,其深度必然也受Alpha Test影响。其实渲染阴影贴图也一样,对于Alpha Test材质都需要处理。
本篇小结
URP的这个Depth Only Pass Shader本身比较简单,没啥可说,但是这个Depth Only Pass确实要说道说道,必须要了解到当前版本下面这个Depth Only Pass不能起到Z-Pre Pass的作用去优化Early-Z,因此如果可能直接使用Copy Depth可以节省大量的draw call,当然前提是不能使用MSAA,在低端移动设备上是一个可以考虑的优化选项,关闭MSAA,然后使用一个基于后处理的AA代替,即节省了MSAA的开销,又节省了Depth Only Pass的draw call,一石二鸟!