一、硬件平台 EarlyZ
1.1 TBR 及 TBDR 架构资料参考
TBR 和 TBDR 架构网上的文章实在太多了,并且不同的硬件,内部具体的实现原理都不太一样(例如 Early-DT 的实现技术对于 PowerVR 是 Hidden Surface Removal (HSR),对于 Adreno 是 Low Resolution Z (LRZ)……)更何况细节,虽说这些都不算机密,但是也不是说你想拿到就拿到,除非是像这种相当专业的专利文,因此网上极大多数文章也都只能说是对各种渠道信息的总结和搬运,不能保证100%正确性,甚至还有些内容只是单纯推测,尽管如此,它们依旧是有不错的参考价值的
这里不会再对一些比较常识性的东西进行简述和总结,只看单一篇文章也必有疏漏,因此直接贴链接供完全不了解这块的人做个参考好了,当然这种分析硬件架构的文章是没法原创的,说到底都是拾人牙慧,好听点的话叫翻译和总结:
- 三大主流移动厂商官方 TBDR 文档理解与翻译
- 老生新谈 TBDR
- TBDR 的 HSR 流程细节和使用 AlphaBlend 的效率提升程度?
- 针对移动端 TBDR 架构 GPU 特性的渲染优化
- 移动设备 GPU 架构知识汇总 - 知乎
1.2 但是还是要提一下 HSR 与 Early-DT,以及通过专利文章,分析当 HSR 遇到 Alpha-Test 时,具体的硬件流程细节究竟是什么样的
一个常识是:如果片段中做了 clip,也就是 Alpha-Test 的物体,是无法进行提前深度测试(Early-DT)的,而以 HSR 为例,其作为减少 overdraw 的一种类 Early-DT 手段,遇到 Alpha-Test 的物体,必然也会失效。但是问题就在这里,所谓“失效”,流程上会有哪些改变?会带来多大的性能损失?补救方案又是什么?这里网络上还是有不少争议点的,很多篇关于 TBDR 架构的文章,也多多少少会提到这一块
先放张图,后面可以用作参考,其实这里主要讨论的就是这一块 Defer
1.2.1 当 HSR 遇到 Alpha-Test
网上一个主流的说法是:HSR 对于不透明物体(Opaque)并不会关心(或者并不知道)当前的每一个 DrawCall 是否 Alpha-Test,等到 shading 时发现有片段被 clip 了,这时才后知后觉发现原先 HSR 的时候可能会出错,从而对于当前 clip 所在片段的 Tile 直接从头来过,暴力将前面所有的图元(fragment)都跑 ps
这听上去很恐怖:只要当前 Tile 有 clip 操作发生,整个 HSR 就会退化成同等于完全没有生效: overdraw 的问题当然也不会得到任何改善,也因为这个,Alpha-Test 在很多人心里性能会还不如 Alpha-Blend
但是这个科学么,其实是不太科学的,做驱动的不会这么暴力,但如果上面的说法没错,那么就应该是丢失了细节?
后面查阅了更多的资料,甚至还参考了 PowerVR 专利文,大致能得出一个更合理的推测,或者说为上面补充细节
首先提两个关键信息:
- 这个容易被忽略:尽管 HSR 时肯定不知道具体哪些 fragment 会被 clip 以无法确认到底哪个 fragment 是最终可见的,但 HSR 其实是知道每次 DrawCall 有没有开启 Alpha-Test 的
- 对于拥有 HSR 专利的 PowerVR,它推荐的渲染顺序是 Opaque → AlphaTest → AlphaBlend,也就是完全不透明物体全部提交绘制后,再提交所有开了 Alpha-Test 的物体,确实 Unity 也是这么做的
根据专利论文,中关于 FIG.25 的流程描述,可以得知:
- 在 HSR 的过程中,如果完全没有遇到开启了 Alpha-Test 的 DrawCall,那么它就可以顺利的将被挡住的 fragment 信息( 、) 全部丢弃,而不被挡住的 fragment()会使用 TagBuffer 标记延迟,等到后面统一执行片段着色计算,这对应着上图仅有 、、 的情况,这非常完美的解决了 overdraw 的问题
- 但是当 HSR 时遇到第一个开启 Alpha-Test 的 DrawCall(对应上图中的 )时,关键点来了:由于此时无法判断最后到底会显示 还是 ,所以此时硬件就会将前面 TagBuffer 标记的所有 fragment()直接进行 shading(可以理解为直接开始走 pipeline 的下一个流程),同时更新当前 HSR 的信息,并在确保 shading 完之前,HSR 会被 block
- 但是 HSR 还没有说因此废掉,如果在此之后收到 的 DrawCall,由于 Alpha-Test 的原因一样无法判断最后会显示 还是 ,硬件就会把 丢到 pipeline 的下一步直接进行 shading,到此时硬件就确定了每个 的每个 fragment 是否会被 clip,从而深度被确定(这里为什么不丢 到下一步先计算,而是丢 先计算,主要是由于策略最优:因为 你不 shading 深度信息是确定不了的,也无法写入,所以不如丢 直接把深度算出来,说不准它把 挡住了, 就可以直接确认丢弃),Alpha-Test 物体的深度确定了,在此之后的 HSR 当然是又会再次开启的(生效的)
好了,对于最后一个 ,由于他会挡住 ,并且已经是最后一个 DrawCall 了,确认在最上面就直接走下一步进行 shading,当此 Tile 下的所有 geometry 的都处理完,合法的片元信息都被送到 pipeline 的后面
总结一下,对于 ABCDEF 六个 DrawCall,如果都没有开启 Alpha-Test,那么最后会被送到 pipeline 的下一步进行 shading 的只会是最前面的 (完美情况),而对于实际情况:开启了 Alpha-Test,那么最后被送到 pipeline 的下一步进行 shading 的就会有
因此可以得出结论:
- 开启 Alpha-Test 确实会带来性能问题:明显会打断 HSR,遇到开启了 Alpha-Test 的 DrawCall 时不得不 block HSR 流程:将当前 flush 的 fragment 全部直接 shading,但同理:其也没有很夸张到所有 fragment 都会被 shading,至少在遇到第一个 Alpha-Test 前的所有不透明物体都享受了其优化
- 接上,这也很好印证为什么 PowerVR 推荐的渲染顺序是 Opaque → AlphaTest → AlphaBlend,至少你不要穿插着绘制 Alpha-Test 的物体
- 具体 Alpha-Test 和 Alpha-Blend 的性能比较,要看你怎么处理 Alpha-Test 的物体,和平台有关系,和当前的场景复杂程度也有关系
二、软件 DepthPrePass
2.1 引申:处理 Alpha-Test 物体的更多策略
除了将 Alpha-Test 物体放在最后面渲染外,还有一种方案就是对其现做一个软件的 Early-DT,也就是 Depth PrePass
一个经典的例子就是树叶的渲染,出于性能考虑,单片树叶的顶点数不能太多,因此树叶的形状需要用 clip 像素的方式扣出来,而绘制一棵树的时候还会有个比较吃性能的地方:就是树叶在大多数视角方向上,都有大量的 fragment 叠加(用人话说就是一条射线可以穿过无数片树叶),因此在不做 Early-Z 的情况下,一棵树必然会出现大量的 overdraw
但上面也分析了 HSR 和 Alpha-Test,看上去硬件并没有办法帮我们解决这么多 overdraw 的问题,实际打出的手机包拉近看树叶,确实也出现了掉帧的情况,因此为了减少计算量,Depth PrePass 就再所难免了
这样做还有一个好处是:对于 Alpha-Test 物体,可以直接在 Depth PrePass 中做 clip 操作,这样在后面绘制的时候,是可以完全当成不透明物体的,不但可以减少大量的片段着色计算,也同等于把 Alpha-Test 的物体也提到了最前面去画,对于上面的流程就是:
- 对树叶做 EarlyZ,这一步只写入深度,并且同时做 clip
- 再正常绘制树叶,考虑光照和环境,此时它就是不透明物体,无需标记 Alpha-Test 和 clip
- 绘制其它不透明物体
搞定!如果树遮挡了大量地形,后面地形 overdraw 的问题也不会出现
2.2 部分代码参考
DepthPrePassRenderFeature.cs:
public class MOpaqueEarlyZRenderFeature : ScriptableRendererFeature
{
RenderObjectsPass _preDepthPass;
RenderObjectsPass _RenderersPass;
public override void Create()
{
string profilerPreDepthTag = "Opaque EarlyZ PreDepth";
string[] preDepthShaderTagIds = {"OpaqueEarlyZPreDepth" };
RenderObjects.CustomCameraSettings cameraSettings = new RenderObjects.CustomCameraSettings();
_preDepthPass = new RenderObjectsPass(profilerPreDepthTag, RenderPassEvent.BeforeRenderingOpaques, preDepthShaderTagIds,
RenderQueueType.Opaque,-1, cameraSettings);
string profilerRenderersPassTag = "Opaque EarlyZ";
string[] renderersPassTagIds = { "OpaqueEarlyZ" };
_RenderersPass = new RenderObjectsPass(profilerRenderersPassTag, RenderPassEvent.BeforeRenderingOpaques, renderersPassTagIds,
RenderQueueType.Opaque, -1, cameraSettings);
}
// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(_preDepthPass);
renderer.EnqueuePass(_RenderersPass);
}
}
DepthPrePassShader:
Pass
{
//仅写入深度的 Pass,在这里做好 clip
Tags{ "LightMode" = "OpaqueEarlyZPreDepth" }
Blend One Zero, One Zero
ZTest LEqual
ZWrite On
ColorMask 0
HLSLPROGRAM
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 3.5
#pragma multi_compile_instancing
#pragma vertex DepthOnlyVertex
#pragma fragment DepthOnlyFragment
#define _ALPHATEST_ON
#define SC_OBJECT
#define USE_DISSOLVE_DISTANCE
#include "../Include/SceneDepthOnlyPass.hlsl"
ENDHLSL
}
Pass
{
//实际绘制物体的 Pass
}
half4 DepthOnlyFragment(Varyings input) : SV_TARGET
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
#if USE_ALPHATEST
half a = 1.0;
#ifdef _ALPHATEST_ON
a = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a * _Color.a;
a = a - _Cutoff;
#endif
#if USE_DITHER_ALPHATEST_CLIP
half dist = distance(input.worldPos, _WorldSpaceCameraPos);
a = min(a,DitherAlpha(input.scrPos, saturate(dist * 0.2)));
#endif
clip(a);
#endif
#if _Test_OUTPUT
return half4(1.0, 0.0, 0.0, 1.0);
#else
return 0;
#endif
}