对于刚学习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分为开启和关闭两种状况的话,一共就是四种状况:
- 深度测试经过,深度写入开启:写入深度缓冲区,写入颜色缓冲区;
- 深度测试经过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区;
- 深度测试失败,深度写入开启:不写深度缓冲区,不写颜色缓冲区;
- 深度测试失败,深度写入关闭:不写深度缓冲区,不写颜色缓冲区;
Unity中默认的状态(写shader时什么都不写的状态)是ZTest LEqual和ZWrite On,也就是说默认是开启深度写入,而且深度小于等于当前缓存中的深度就经过深度测试,深度缓存中原始为无限大,也就是说离摄像机越近的物体会更新深度缓存而且遮挡住后面的物体。以下图所示,前面的正方体会遮挡住后面的物体:
写几个简单的小例子来看一下ZTest,ZWrite以及Render Queue这几个状态对渲染结果的控制。
让绿色的对象不被前面的立方体遮挡,一种方式是关闭前面的蓝色立方体深度写入:
经过上面的实验结果,咱们知道,按照从前到后的渲染顺序,首先渲染蓝色物体,蓝色物体深度测试经过,颜色写入缓存,可是关闭了深度写入,蓝色部分的深度缓存值仍然是默认的Max,后面渲染的绿色立方体,进行深度测试仍然会成功,写入颜色缓存,而且写入了深度,所以蓝色立方体没有起到遮挡的做用。
另外一种方式是让绿色强制经过深度测试:
这个例子中其余立方体的shader使用默认的渲染方式,绿色的将ZTest设置为Always,也就是说无论怎样,深度测试都经过,将绿色立方体的颜色写入缓存,若是没有其余覆盖了,那么最终的输出就是绿色的了。
那么若是红色的也开了ZTest Always会怎么样?
在红色立方体也用了ZTest Always后,红色遮挡了绿色的部分显示为了红色。若是咱们换一下渲染队列,让绿色在红色以前渲染,结果就又不同了:
更换了渲染队列,让绿色的渲染队列+1,在默认队列Geometry以后渲染,最终重叠部分又变回了绿色。可见,当ZTest都经过时,上一个写入颜色缓存的会覆盖上一个,也就是说最终输出的是最后一个渲染的对象颜色。
再看一下Greater相关的部分有什么做用,此次咱们其余的都使用默认的渲染状态,绿色的立方体shader中ZTest设置为Greater:
这个效果就比较好玩了,虽然咱们发如今比较深度时,前面被蓝色立方体遮挡的部分,绿色的最终覆盖了蓝色,是想要的结果,不过其余部分哪里去了呢?简单分析一下,渲染顺序是从前到后,也就是说蓝色最早渲染,默认深度为Max,蓝色立方体的深度知足LEqual条件,就写入了深度缓存,而后绿色开始渲染,重叠的部分的深度缓存是蓝色立方体写入的,而绿色的深度值知足大于蓝色深度的条件,因此深度测试经过,重叠部分颜色更新为绿色;而与红色立方体重合的部分,红色立方体最后渲染,与前面的部分进行深度测试,小于前面的部分,深度测试失败,重叠部分不会更新为红色,因此重叠部分最终为绿色。而绿色立方体没有与其余部分重合的地方为何消失了呢?实际上是由于绿色立方体渲染时,除了蓝色立方体渲染的地方是有深度信息的,其余部分的深度信息都为Max,蓝色部分用Greater进行判断,确定会失败,也就不会有颜色更新。
有一个好玩的效果其实就能够考ZTest Greater来实现,就是游戏里面常常出现的,当玩家被其余场景对象遮挡时,遮挡的部分会呈现出X-光的效果;实际上是在渲染玩家时,增长了一个Pass,默认的Pass正常渲染,而增长的一个Pass就使用Greater进行深度测试,这样,当玩家被其余部分遮挡时,遮挡的部分才会显示出来,用一个描边的效果渲染,其余部分仍然使用原来的Pass便可。
Early-Z技术
传统的渲染管线中,ZTest实际上是在Blending阶段,这时候进行深度测试,全部对象的像素着色器都会计算一遍,没有什么性能提高,仅仅是为了得出正确的遮挡结果,会形成大量的无用计算,由于每一个像素点上确定重叠了不少计算。所以现代GPU中运用了Early-Z的技术,在Vertex阶段和Fragment阶段之间(光栅化以后,fragment以前)进行一次深度测试,若是深度测试失败,就没必要进行fragment阶段的计算了,所以在性能上会有很大的提高。可是最终的ZTest仍然须要进行,以保证最终的遮挡关系结果正确。前面的一次主要是Z-Cull为了裁剪以达到优化的目的,后一次主要是Z-Check,为了检查,以下图:
Early-Z的实现,主要是经过一个Z-pre-pass实现,简单来讲,对于全部不透明的物体(透明的没有用,自己不会写深度),首先用一个超级简单的shader进行渲染,这个shader不写颜色缓冲区,只写深度缓冲区,第二个pass关闭深度写入,开启深度测试,用正常的shader进行渲染。其实这种技术,咱们也能够借鉴,在渲染透明物体时,由于关闭了深度写入,有时候会有其余不透明的部分遮挡住透明的部分,而咱们其实不但愿他们被遮挡,仅仅但愿被遮挡的物体半透,这时咱们就能够用两个pass来渲染,第一个pass使用Color Mask屏蔽颜色写入,仅写入深度,第二个pass正常渲染半透,关闭深度写入。
关于Early-Z技术能够参考ATI的论文Applications of Explicit Early-Z Culling以及PPT,还有一篇Intel的文章。
Unity渲染顺序总结
若是咱们先绘制后面的物体,再绘制前面的物体,就会形成over draw;而经过Early-Z技术,咱们就能够先绘制较近的物体,再绘制较远的物体(仅限不透明物体),这样,经过先渲染前面的物体,让前面的物体先占坑,就可让后面的物体深度测试失败,进而减小重复的fragment计算,达到优化的目的。Unity中默认应该就是按照最近距离的面进行绘制的,咱们能够看一下Unity官方的文档中显示的:
从文档给出的流程来看,这个Depth-Test发生在Vertex阶段和Fragment阶段之间,也就是上面所说的Early-Z优化。
简单总结一下Unity中的渲染顺序:先渲染不透明物体,顺序是从前到后;再渲染透明物体,顺序是从后到前。
Alpha Test(Discard)在移动平台消耗较大的缘由
从本人刚刚开始接触渲染,就开始据说移动平台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/puppet_master https://blog.csdn.net/puppet_master/article/details/73478905