传统的画家算法存在的问题

画家算法指的是,画家往画布上绘画的时候,先绘制远方的景物,再绘制近处的,一层层叠加在一起后,形成完整的一幅画。



传统画家算法会有一个严重的问题,画布上的某一个点,可能会反复的多次涂上不同的颜色(同一个点,有几个景物叠加在此处)。假设有5个景物由远到近都在画布上某个点绘制,根据画家算法,先绘制物体1,再绘制物体2,最后绘制物体5,同一个点被涂了5次,但是只有最后这一次的颜色是有效的,其他的4次都是无效的。关键是每次着色,3D引擎都要计算颜色,光照,计算完着色上去后却没有用,这样消耗性能的无用功,3D游戏引擎必须要很好的优化它。

由近到远绘制,逆画家算法

分析出来了画家算法的问题,接下来是着手解决,游戏引擎采用的方式是反画家算法由近到远绘制。画家的画布,相当于是游戏的屏幕,画布上的每个点,相当于屏幕上一个个着色单元元(像素)。游戏引擎绘制物体的时候,先对物体进行排序, 离摄像机最近的物体排在渲染序列的前面,离摄像机越远的物体排在后面。遍历每个物体,针对每个物体的着色片元(物体要绘制到屏幕上的每个点),如果屏幕着色点已经被更靠近摄像机的物体片元给占据了,那这个片元不用去计算光照着色,可以直接丢弃,如果没有或更靠近摄像机,就绘制这个片元,并把片元距摄像机的深度信息保存起来,下一个片元来绘制的时候,就和这个深度信息做比较,如果离得更近,那么就替换原来得,如果远就丢弃,不着色。


我们结合上图把算法实现完整的分析一遍。我们把屏幕画布分成一个的着色点(像素),每个着色点我们为它分配一个颜色缓存,一个深度缓存。 颜色缓存用来存放屏幕着色点的颜色信息(RGBA),深度缓存用来存放屏幕着色点的深度信息(距离摄像机的z值)。游戏引擎,根据物体距离摄像机的远近,将物体的绘制顺序排序为:先绘制绿色物体A,再绘制蓝色物体B。绘制绿色物体A的时候,需要占据屏幕的着色点绿色的区域(如图),而这些区域,刚开始没有被任何物体的着色片元写入过,将物体A着色片元(物体A需要绘制到屏幕上的像素点)的颜色(绿色)写入屏幕着色点的颜色缓存,并更新屏幕着色点的深度信息为物体A着色片元的深度信息。接下来绘制蓝色的物体B, 它需要占用屏幕中的蓝色的屏幕区域着色点。如上图,外围一圈的屏幕着色点,没有任何物体着色过,所以直接将物体B的着色片元颜色(蓝色)写入颜色缓存,并更新对应屏幕着色点的深度信息为蓝色片元的深度。当着色蓝色物体被绿色物体挡住部分的时候,发现屏幕上的着色点的深度信息是之前绿色片元的深度,而当前蓝色片元的深度,要更远离摄像机,所以不用着色,直接丢弃。这样被绿色挡住的蓝色物体的片元就不用计算着色了。

新的算法,也会引入新的问题

由近至远的绘制算法避免了物体被遮挡部分的不必要的物体片元着色计算,但是也带来了一个新问题:Alpha透明度。同样如上图,如果绿色物体是一个半透明物体,希望透过绿色物体,能看到蓝色物体,那么上面的算法先绘制绿色的物体,绿色的半透写入颜色缓存(蓝色物体还没有进来), 而后面蓝色物体被绿色物体挡住的片元不会写入颜色缓存,直接丢弃就会出现下图的效果:



这可不是我们想要的结果,可是我们由近到远的绘制,必然会产生这样的结果。那怎么办呢?游戏引擎又设计了一个机制,把要绘制的物体分成一个一个的渲染队列,如下图,我们一起来看下,半透效果是怎么做到的。



游戏引擎会为每个游戏物体指定一个渲染队列,每个渲染队列会有一个数值大小,游戏引擎根据物体所在渲染队列先将要绘制的物体排序到渲染队列中。如下

渲染队列A(2000):物体A,物体B,物体C 。。。(由近到远)

渲染队列B(3000):物体1,物体2,物体3 。。。(由近到远)

游戏引擎先绘制数值小的渲染队列中的物体,再绘制数值大的渲染队列中的物体。上图的绿色物体A,在3000的渲染队列, 蓝色物体B,在2000的渲染队列,所以游戏引擎先渲染渲染队列2000的物体,那么蓝色物体B的每个片元颜色,就写入了颜色缓冲区,屏幕着色点深度信息也是物体B片元的深度信息。绘制完2000的渲染队列后,颜色缓存中就是蓝色了。接下来绘制渲染队列3000中的物体,绿色物体A比蓝色物体B距离摄像机要近,所以绿色物体的片元颜色就可以写入到屏幕着色点颜色缓存区,根据Alpha混合,叠加颜色缓存之前的颜色,计算新出新的颜色写入颜色缓存,这样就实现了半透效果,并更新屏幕着色点的深度值为绿色片元深度值。

ZTest, ZWrite, Blend

经过上面的讲解,相信大家对3D引擎的着色有了一个比较深刻的认识,接下来就是摆概念的时候了,我们一起来加深一下。

深度测试ZTest: 每个要渲染的物体的着色片元着色到屏幕的着色点时候,首先要到屏幕着色点的深度缓存里面判断一下,是否更靠近摄像机,更近的才着色,否则直接丢弃。这个过程我们叫做深度测试或ZTest。"更近"就是通过深度测试的法则,只有通过了,才会被写入颜色缓存。为了更好的灵活性,引擎可以给用户改变这个测试规则,默认是更近(LESS),用户也可以修改成更远(GREATE), 永远可以通过测试(ALLWAS)等。

深度值写入 ZWrite: 当我们物体上的着色片元通过测试可以着色以后,我们的Shader会计算出颜色值,把颜色值写入屏幕着色点,此时屏幕着色点的深度值应该就是这个物体着色片元的深度值。更新完颜色后,就要往深度缓存里面更新深度值,这样后续比当前深度更远的物体着色片元就不会绘制了。引擎也提供一个开关,可以让开发者控制,决定是否写入深度缓存, 默认ZWrite是开启的,你也可以设置为关闭,这样,物体的颜色写入颜色缓存后,不会刷新深度缓存。还是上面的例子,绿色物体与蓝色物体,我们把绿色物体的深度写入关闭,你会发现蓝色物体被绿色物体遮挡了也绘制到屏幕上了。


颜色混合Blend: 当经过深度测试,物体着色片元通过以后,就要计算着色,并写入颜色缓存里,这个时候就需要处理混合当前颜色与目标颜色,颜色混合,我们又叫Blend操作。默认的Blend是颜色直接覆盖。还有一些常用的颜色混合的方法,比如Alpha混合等。Blend指的是混合片元颜色与颜色缓存区颜色,并更新到颜色缓存里。 下面是游戏引擎里面支持的Blend操作,我总结了一下,常用的标红出来了,如下图:

终于讲完了渲染队列,ZTest, ZWrite, Blend,如果还不过瘾,可以点击下方来看看这个Blake老师讲解的这个视频教程