Unity Shader-渲染队列,ZTest,ZWrite,Early-Z
作者:互联网
对于刚学习Shader的开发人员来说,对于渲染队列中ZTest和ZWrite可能一点都不清楚,为了帮助大家开发,本篇文章就给初学Shader的朋友准备了渲染队列学习,ZTest,ZWrite的基本使用以及分析一下Unity为了Early-Z所做的一些优化。
简介 在渲染阶段,引擎所做的工作是把所有场景中的对象按照一定的策略(顺序)进行渲染。最早的是画家算法,顾名思义,就是像画家画画一样,先画后面的物体,如果前面还有物体,那么就用前面的物体把物体覆盖掉,不过这种方式由于排序是针对物体来排序的,而物体之间也可能有重叠,所以效果并不好。所以目前更加常用的方式是z-buffer算法,类似颜色缓冲区缓冲颜色,z-buffer中存储的是当前的深度信息,对于每个像素存储一个深度值,这样,我们屏幕上显示的每个像素点都会进行深度排序,就可以保证绘制的遮挡关系是正确的。而控制z-buffer就是通过ZTest,和ZWrite来进行。但是有时候需要更加精准的控制不同类型的对象的渲染顺序,所以就有了渲染队列。今天就来学习一下渲染队列,ZTest,ZWrite的基本使用以及分析一下Unity为了Early-Z所做的一些优化。 Unity中的几种渲染队列 首先看一下Unity中的几种内置的渲染队列,按照渲染顺序,从先到后进行排序,队列数越小的,越先渲染,队列数越大的,越后渲染。 Background(1000) 最早被渲染的物体的队列。 Geometry (2000) 不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染,也是Unity Shader中默认的渲染队列。 AlphaTest (2450) 有透明通道,需要进行Alpha Test的物体的队列,比在Geomerty中更有效。 Transparent(3000) 半透物体的渲染队列。一般是不写深度的物体,Alpha Blend等的在该队列渲染。 Overlay (4000) 最后被渲染的物体的队列,一般是覆盖效果,比如镜头光晕,屏幕贴片之类的。 Unity中设置渲染队列也很简单,我们不需要手动创建,也不需要写任何脚本,只需要在shader中增加一个Tag就可以了,当然,如果不加,那么就是默认的渲染队列Geometry。比如我们需要我们的物体在Transparent这个渲染队列中进行渲染的话,就可以这样写:Tags { "Queue" = "Transparent"}我们可以直接在shader的Inspector面板上看到shader的渲染队列: 另外,我们在写shader的时候还经常有个Tag叫RenderType,不过这个没有Render Queue那么常用,这里顺便记录一下: Opaque:用于大多数着色器(法线着色器、自发光着色器、反射着色器以及地形的着色器)。 Transparent:用于半透明着色器(透明着色器、粒子着色器、字体着色器、地形额外通道的着色器)。 TransparentCutout:蒙皮透明着色器(Transparent Cutout,两个通道的植被着色器)。 Background:天空盒着色器。 Overlay:GUITexture,镜头光晕,屏幕闪光等效果使用的着色器。 TreeOpaque:地形引擎中的树皮。 TreeTransparentCutout:地形引擎中的树叶。 TreeBillboard:地形引擎中的广告牌树。 Grass:地形引擎中的草。 GrassBillboard:地形引擎何中的广告牌草。 相同渲染队列中不透明物体的渲染顺序 拿出Unity,创建三个立方体,都使用默认的bump diffuse shader(渲染队列相同),分别给三个不同的材质(相同材质的小顶点数的物体引擎会动态合批),用Unity5带的Frame Debug工具查看一下Draw Call。(Unity5真是好用得多了,如果用4的话,还得用NSight之类的抓帧) 可以看出,Unity中对于不透明的物体,是采用了从前到后的渲染顺序进行渲染的,这样,不透明物体在进行完vertex阶段,进行Z Test,然后就可以得到该物体最终是否在屏幕上可见了,如果前面渲染完的物体已经写好了深度,深度测试失败,那么后面渲染的物体就直接不会再去进行fragment阶段。(不过这里需要把三个物体之间的距离稍微拉开一些,本人在测试时发现,如果距离特别近,就会出现渲染次序比较乱的情况,因为我们不知道Unity内部具体排序时是按照什么标准来判定的哪个物体离摄像机更近,这里我也就不妄加猜测了) 相同渲染队列中半透明物体的渲染顺序 透明物体的渲染一直是图形学方面比较蛋疼的地方,对于透明物体的渲染,就不能像渲染不透明物体那样多快好省了,因为透明物体不会写深度,也就是说透明物体之间的穿插关系是没有办法判断的,所以半透明的物体在渲染的时候一般都是采用从后向前的方法进行渲染,由于透明物体多了,透明物体不写深度,那么透明物体之间就没有所谓的可以通过深度测试来剔除的优化,每个透明物体都会走像素阶段的渲染,会造成大量的Over Draw。这也就是粒子特效特别耗费性能的原因。 我们实验一下Unity中渲染半透明物体的顺序,还是上面的三个立方体,我们把材质的shader统一换成粒子最常用的Particle/Additive类型的shader,再用Frame Debug工具查看一下渲染的顺序: 半透明的物体渲染的顺序是从后到前,不过由于半透相关的内容比较复杂,就先不在这篇文章中说了,打算另起一篇。 自定义渲染队列 Unity支持我们自定义渲染队列,比如我们需要保证某种类型的对象需要在其他类型的对象渲染之后再渲染,就可以通过自定义渲染队列进行渲染。而且超级方便,我们只需要在写shader的时候修改一下渲染队列中的Tag即可。比如我们希望我们的物体要在所有默认的不透明物体渲染完之后渲染,那么我们就可以使用Tag{“Queue” = “Geometry+1”}就可以让使用了这个shader的物体在这个队列中进行渲染。 还是上面的三个立方体,这次我们分别给三个不同的shader,并且渲染队列不同,通过上面的实验我们知道,默认情况下,不透明物体都是在Geometry这个队列中进行渲染的,那么不透明的三个物体就会按照cube1,cube2,cube3进行渲染。这次我们希望将渲染的顺序反过来,那么我们就可以让cube1的渲染队列最大,cube3的渲染队列最小。贴出其中一个的shader:
Shader "Custom/RenderQueue1" { SubShader { Tags { "RenderType"="Opaque" "Queue" = "Geometry+1"} Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata_base v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; } fixed4 frag(v2f i) : SV_Target { return fixed4(0,0,1,1); } ENDCG } } //FallBack "Diffuse" }其他的两个shader类似,只是渲染队列和输出颜色不同。 通过渲染队列,我们就可以自由地控制使用该shader的物体在什么时机渲染。比如某个不透明物体的像素阶段操作较费,我们就可以控制它的渲染队列,让其渲染更靠后,这样可以通过其他不透明物体写入的深度剔除该物体所占的一些像素。 PS:这里貌似发现了个问题,我们在修改shader的时候一般不需要什么其他操作就可以直接看到修改后的变化,但是本人改完渲染队列后,有时候会出现从shader的文件上能看到渲染队列的变化,但是从渲染结果以及Frame Debug工具中并没有看到渲染结果的变化,重启Unity也没有起到作用,直到我把shader重新赋给材质之后,变化才起了效果……(猜测是个bug,因为看到网上还有和我一样的倒霉蛋被这个坑了,本人的版本是5.3.2,害我差点怀疑昨天是不是喝了,刚实验完的结果就完全不对了……) ZTest(深度测试)和ZWrite(深度写入) 上一个例子中,虽然渲染的顺序反了过来,但是物体之间的遮挡关系仍然是正确的,这就是z-buffer的功劳,不论我们的渲染顺序怎样,遮挡关系仍然能够保持正确。而我们对z-buffer的调用就是通过ZTest和ZWrite来实现的。 首先看一下ZTest,ZTest即深度测试,所谓测试,就是针对当前对象在屏幕上(更准确的说是frame buffer)对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则否则不会写入颜色缓冲。ZTest提供的状态较多。ZTest Less(深度小于当前缓存则通过, ZTest Greater(深度大于当前缓存则通过),ZTest LEqual(深度小于等于当前缓存则通过),ZTest GEqual(深度大于等于当前缓存则通过),ZTest Equal(深度等于当前缓存则通过),ZTest NotEqual(深度不等于当前缓存则通过),ZTest Always(不论如何都通过)。注意,ZTest Off等同于ZTest Always,关闭深度测试等于完全通过。 下面再看一下ZWrite,ZWrite比较简单,只有两种状态,ZWrite On(开启深度写入)和ZWrite Off(关闭深度写入)。当我们开启深度写入的时候,物体被渲染时针对物体在屏幕(更准确地说是frame buffer)上每个像素的深度都写入到深度缓冲区;反之,如果是ZWrite Off,那么物体的深度就不会写入深度缓冲区。但是,物体是否会写入深度,除了ZWrite这个状态之外,更重要的是需要深度测试通过,也就是ZTest通过,如果ZTest都没通过,那么也就不会写入深度了。就好比默认的渲染状态是ZWrite On和ZTest LEqual,如果当前深度测试失败,说明这个像素对应的位置,已经有一个更靠前的东西占坑了,即使写入了,也没有原来的更靠前,那么也就没有必要再去写入深度了。所以上面的ZTest分为通过和不通过两种情况,ZWrite分为开启和关闭两种情况的话,一共就是四种情况:
- 深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区;
- 深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区;
- 深度测试失败,深度写入开启:不写深度缓冲区,不写颜色缓冲区;
- 深度测试失败,深度写入关闭:不写深度缓冲区,不写颜色缓冲区;
从本人刚刚开始接触渲染,就开始听说移动平台Alpha Test比较费,当时比较纳闷,直接discard了为什么会费呢,应该更省才对啊?这个问题困扰了我好久,今天来刨根问底一下。还是跟我们上面讲到的Early-Z优化。正常情况下,比如我们渲染一个面片,不管是否是开启深度写入或者深度测试,这个面片的光栅化之后对应的像素的深度值都可以在Early-Z(Z-Cull)的阶段判断出来了;而如果开启了Alpha Test(Discard)的时候,discard这个操作是在fragment阶段进行的,也就是说这个面片光栅化之后对应的像素是否可见,是在fragment阶段之后才知道的,最终需要靠Z-Check进行判断这个像素点最终的颜色。其实想象一下也能够知道,如果我们开了Alpha Test并且还用Early-Z的话,一块本来应该被剃掉的地方,就仍然写进了深度缓存,这样就会造成其他部分被一个完全没东西的地方遮挡,最终的渲染效果肯定就不对了。所以,如果我们开启了Alpha Test,就不会进行Early-Z,Z Test推迟到fragment之后进行,那么这个物体对应的shader就会完全执行vertex shader和fragment shader,造成over draw。有一种方式是使用Alpha Blend代替Alpha Test,虽然也很费,但是至少Alpha Blend虽然不写深度,但是深度测试是可以提前进行的,因为不会在fragment阶段再决定是否可见,因为都是可见的,只是透明度比较低罢了。不过这样只是权宜之计,Alpha Blend并不能完全代替Alpha Test。
关于Alpha Test对于Power VR架构的GPU性能的影响,简单引用一下官方的链接以及一篇讨论帖:
最后再附上两篇参考文章
http://blog.csdn.net/candycat1992/article/details/41599167 http://blog.csdn.net/arundev/article/details/7895839 原文链接:http://blog.csdn.net/puppet_master https://blog.csdn.net/puppet_master/article/details/73478905
标签:渲染,队列,物体,ZTest,写入,Shader,Early,深度 来源: https://www.cnblogs.com/heweiwei/p/11174322.html