EarlyZ 与 DepthPrePass

一、硬件平台 EarlyZ

1.1 TBR 及 TBDR 架构资料参考

TBR 和 TBDR 架构网上的文章实在太多了,并且不同的硬件,内部具体的实现原理都不太一样(例如 Early-DT 的实现技术对于 PowerVR 是 Hidden Surface Removal (HSR),对于 Adreno 是  Low Resolution Z (LRZ)……)更何况细节,虽说这些都不算机密,但是也不是说你想拿到就拿到,除非是像这种相当专业的专利文,因此网上极大多数文章也都只能说是对各种渠道信息的总结和搬运,不能保证100%正确性,甚至还有些内容只是单纯推测,尽管如此,它们依旧是有不错的参考价值的

这里不会再对一些比较常识性的东西进行简述和总结,只看单一篇文章也必有疏漏,因此直接贴链接供完全不了解这块的人做个参考好了,当然这种分析硬件架构的文章是没法原创的,说到底都是拾人牙慧,好听点的话叫翻译和总结:

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 专利文,大致能得出一个更合理的推测,或者说为上面补充细节

首先提两个关键信息:

  1. 这个容易被忽略:尽管 HSR 时肯定不知道具体哪些 fragment 会被 clip 以无法确认到底哪个 fragment 是最终可见的,但 HSR 其实是知道每次 DrawCall 有没有开启 Alpha-Test 的
  2. 对于拥有 HSR 专利的 PowerVR,它推荐的渲染顺序是 Opaque → AlphaTest → AlphaBlend,也就是完全不透明物体全部提交绘制后,再提交所有开了 Alpha-Test 的物体,确实 Unity 也是这么做的

根据专利论文,中关于 FIG.25 的流程描述,可以得知:

  1. 在 HSR 的过程中,如果完全没有遇到开启了 Alpha-Test 的 DrawCall,那么它就可以顺利的将被挡住的 fragment 信息( Z_AZ_C 全部丢弃,而不被挡住的 fragment(Z_B)会使用 TagBuffer 标记延迟,等到后面统一执行片段着色计算,这对应着上图仅有 Z_AZ_BZ_C 的情况,这非常完美的解决了 overdraw 的问题
  2. 但是当 HSR 时遇到第一个开启 Alpha-Test 的 DrawCall(对应上图中的 Z_D)时,关键点来了:由于此时无法判断最后到底会显示 Z_D 还是 Z_B,所以此时硬件就会将前面 TagBuffer 标记的所有 fragment(Z_B)直接进行 shading(可以理解为直接开始走 pipeline 的下一个流程),同时更新当前 HSR 的信息,并在确保 shading 完之前,HSR 会被 block
  3. 但是 HSR 还没有说因此废掉,如果在此之后收到 Z_E 的 DrawCall,由于 Alpha-Test 的原因一样无法判断最后会显示 Z_E 还是 Z_D,硬件就会把 Z_D丢到 pipeline 的下一步直接进行 shading,到此时硬件就确定了每个 Z_D 的每个 fragment 是否会被 clip,从而深度被确定(这里为什么不丢 Z_E 到下一步先计算,而是丢 Z_D 先计算,主要是由于策略最优:因为 Z_D 你不 shading 深度信息是确定不了的,也无法写入,所以不如丢 Z_D 直接把深度算出来,说不准它把 Z_E 挡住了, Z_E 就可以直接确认丢弃)Alpha-Test 物体的深度确定了,在此之后的 HSR 当然是又会再次开启的(生效的)

好了,对于最后一个 Z_F,由于他会挡住 Z_D并且已经是最后一个 DrawCall 了,确认在最上面就直接走下一步进行 shading,当此 Tile 下的所有 geometry 的都处理完,合法的片元信息都被送到 pipeline 的后面

总结一下,对于 ABCDEF 六个 DrawCall,如果都没有开启 Alpha-Test,那么最后会被送到 pipeline 的下一步进行 shading 的只会是最前面的 Z_F(完美情况),而对于实际情况:Z_D开启了 Alpha-Test,那么最后被送到 pipeline 的下一步进行 shading 的就会有 Z_F Z_D Z_B  

因此可以得出结论:

  1. 开启 Alpha-Test 确实会带来性能问题:明显会打断 HSR,遇到开启了 Alpha-Test 的 DrawCall 时不得不 block HSR 流程:将当前 flush 的 fragment 全部直接 shading,但同理:其也没有很夸张到所有 fragment 都会被 shading,至少在遇到第一个 Alpha-Test 前的所有不透明物体都享受了其优化
  2. 接上,这也很好印证为什么 PowerVR 推荐的渲染顺序是 Opaque → AlphaTest → AlphaBlend,至少你不要穿插着绘制 Alpha-Test 的物体
  3. 具体 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 的物体也提到了最前面去画,对于上面的流程就是:

  1. 对树叶做 EarlyZ,这一步只写入深度,并且同时做 clip
  2. 再正常绘制树叶,考虑光照和环境,此时它就是不透明物体,无需标记 Alpha-Test 和 clip
  3. 绘制其它不透明物体

搞定!如果树遮挡了大量地形,后面地形 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
}

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值