本文将介绍THREE.js渲染顺序。这里的渲染顺序是指前后位置的物体如何渲染,以及物体之间的遮挡关系如何渲染。主要内容包括:不透明物体的默认渲染顺序是什么;透明物体的默认渲染顺序是什么?当不透明对象和透明对象一起渲染时,默认渲染顺序是什么?如何更改对象的默认渲染顺序。不透明物体的默认渲染顺序是什么首先我们通过一个简单的例子来介绍一下我们想要达到的效果:一个基本的场景,场景中放置了两个4X4的正方形,正方形平行于XOY平面,红色正方形放置在(0,0,0)处,绿色方块放置在(2,0,-2)处。相机使用THREE.PerspectiveCamera。这种摄像头会有近看远看的效果。相机放置在位置(0,0,6)。此时,相机看向Z轴的负方向。相机和两个平面的空间位置如下图1和图2所示:图1图2通过相机和两个平面的空间位置关系,我们知道红色平面显示在绿色平面前面飞机;红色平面会挡住绿色平面部分;最终的渲染效果如下图3所示:图3至此,你可以自己想一想,如果让自己实现,你会如何实现呢?我当时自己也想过。首先,我们知道两个物体和相机之间的距离。我们可以把物体按照距离相机的远近排序,然后按照从远到近的顺序渲染物体。即先渲染绿色平面,再渲染红色平面。上面这个简单的例子没有问题,但是是否适用于其他复杂的场景呢?例如,两个平面与相机的距离是否相同?如下图4所示:图4根据我们刚才的假设,对于上面的场景,我们渲染的要么是绿色完全在上面,要么是红色完全在上面,如图5或者下图6:图5和图6但是实际的渲染结果应该如下图7所示:图7所以上面的渲染方案按照从远到近的顺序只能满足部分使用场景。那么,THREE.js是如何实现的呢?THREE.js主要是一个使用WebGL构建3D场景的开源库,是对WebGL提供的能力的简单易用的封装。所以在讨论THREE.js中对象的渲染顺序之前,我们必须先看看WebGL是如何在不同的位置渲染对象的。而WebGL是基于OpenGL的,那么问题就变成了OpenGL如何渲染不同位置的物体。OpenGL使用深度测试来确保使用正确的遮挡关系渲染对象。深度测试使用深度缓冲区。与颜色缓冲区(colorbuffer)类似,只是每个像素的颜色值存储在颜色缓冲区中,而深度缓冲区存储的是颜色缓冲区中当前像素的颜色值的深度。深度缓冲区和颜色缓冲区具有相同的宽度和高度。然后,在渲染过程中,对于每个像素,我们存储的数据包括:当前像素的颜色值(通过颜色缓冲区获得);空间中当前像素对应的对象片段的深度(通过深度缓冲区获得)。下面我们通过上面图4的例子来说明OpenGL是如何通过深度测试实现物体正确的遮挡关系的。假设首先在绿色平面上绘制对象,则对象在绘制时是逐像素绘制的。这时我们知道的信息包括像素的颜色值和深度信息。对于绿色平面投射到的每个像素,我们在颜色缓冲区中写入该像素位置的颜色值,并在深度缓冲区中写入该像素位置的深度信息。然后,我们开始绘制红色对象。在绘制红色物体的每个像素时,我们知道该像素的颜色值和深度信息D2。然后我们根据像素的坐标,在深度缓冲区中获取绘制像素对应的深度值D1,然后比较D1和D2的值。有以下三种情况:如果D2小于D1,说明物体当前像素点在前面,即该像素点应该取物体当前像素点的颜色值。此时,将颜色缓冲区中当前像素的颜色值更新为红色,将深度缓冲区中当前像素的深度更新为D2;如果D2大于D1,则表示物体当前像素在后,即不显示该像素。所以当前像素的colorbuffer和depthbuffer的值不需要改变;如果D2等于D1,则行为与D2小于D1一致。综上所述,我们有一个判断像素是否被渲染的函数。该函数的输入是当前等待渲染的像素的深度值;深度缓冲区中当前像素的深度值。输出是一个布尔值,指示当前像素是否使用新颜色值进行渲染。THREE.js中这个函数的默认值为LessEqualDepth,也就是上面比较D2和D1的三种情况。该函数的所有取值请参考THREE.js官网的DepthMode。所以,还是上面的例子:对于红色物体左边的每个像素,深度值D2都小于深度缓冲区中的深度值D1,所以颜色缓冲区被更新为红色。对于红色物体右边的每个像素,深度值D2都大于深度缓冲区中的深度值D1,所以保持原来的颜色。最后的结果是左半边是红色,右半边是绿色。我们分析了先绘制绿色平面,再绘制红色平面的情况。大家可以尝试分析一下先画红色平面再自己渲染绿色平面的情况。最后的结果是渲染结果的遮挡关系基本上和绘制的顺序没有关系。透明物体的默认渲染顺序是什么?不透明对象的渲染顺序如上所述。那么,如果场景中的物体都是透明物体,它们是如何渲染的呢?还是以前面的例子为例,此时我们将两个平面都设置为半透明。如下图8所示:如果图8中仍然使用之前的逻辑,每个像素点的颜色要么不变,要么使用新物体的颜色,加上深度测试的逻辑后,渲染出来的效果如下图9所示:图9在现实生活中,透过一个透明物体,我们应该能够看到透明物体后面的物体。显然,图9并没有达到这样的效果。那么,问题是什么?深度测试时,将颜色值写入颜色缓冲区时,要么写入当前对象的颜色值,要么丢弃当前对象的颜色值。对于透明物体,最终显示的颜色值不是单个物体的颜色,而是多个可见物体颜色的混合。那么在前面的步骤中,当我们判断当前物体在前面时,我们就可以从简单的直接使用颜色值改为根据当前物体的颜色值和颜色缓冲区,然后使用混合颜色值更新颜色缓冲区。THREE.js提供了多种混合方式,默认为NormalBlending。NormalBlending的计算公式如下:color(RGB)=(sourceColor*sourceAlpha)+(destinationColor*(1-sourceAlpha))color(A)=(sourceAlpha*1)+(destinationAlpha*(1-sourceAlpha))添加up混合逻辑,最终效果如下图10所示,也符合我们的心理预期:图10在渲染不透明物体时,我们发现最终效果与物体的绘制顺序无关。那么,透明物体呢?我们来做个实验:先渲染红色平面,再渲染绿色平面。对于渲染来说,绘制的顺序会影响渲染结果的遮挡关系。那么是什么原因呢?下面分析一下先渲染红色平面,再渲染绿色平面的情况。首先,绘制红色平面后,颜色缓冲区和深度缓冲区存储与红色平面相关的数据。此时对于每个被红色平面遮挡的绿色像素点,先进行深度测试,深度测试失败,则直接丢弃该像素点。所以这里的问题是,当深度测试成功后,我们可以选择是否混合,以及混合的功能;但是当深度测试失败时,这个像素点直接被丢弃,而不是提供给你一个函数让你自定义这个像素点的颜色值。综上所述,透明物体最终的渲染结果与物体的绘制顺序有关。当透明物体按照从远到近的顺序绘制时,结果会更大程度地符合我们的预期;当透明物体按从近到远的顺序绘制时,结果基本不会达到我们的预期,除非你是故意的。上面之所以说比一定程度大,是有一些特殊情况的。从上面的结论我们也可以知道,渲染结果与绘制顺序有关。我们可以在绘制对象之前对对象进行排序。但是需要注意的是,我们排序使用的是一个代表物体整体位置的坐标信息,而不是按照物体的每个像素进行排序。所以对于两个相交的物体,无论绘制顺序如何,最终的渲染结果都会不正确。如下图13和14所示:图13和图14就是上面说的情况,我还没找到解决办法。当不透明物体和透明物体一起渲染时,默认的渲染顺序是什么?如果我们的场景中同时存在不透明物体和透明物体,那么,在前面的基础上想象一下,我们应该如何实现呢?首先,对于不透明的物体,绘制顺序是没有要求的;对于透明物体,需要按照从远到近的顺序绘制;综上,是不是可以把所有的物体按照从远到近的顺序排序,然后按照这个顺序绘制呢?自己想了想,觉得没有问题,但是发现THREE.js并不是按照这个逻辑来实现的。先说一下THREE.js默认的渲染顺序:首先,将场景中的物体按照是否透明分成两个数组;对于不透明物体数组,按照从近到远的顺序排列;对于透明物体数组,按照从远到近的顺序排列;绘制不透明物体所在的数组;绘制透明物体所在的数组。我想了想,THREE.js之所以这么实现,应该是从性能方面考虑的。首先,对于不透明的物体,绘制顺序虽然对渲染结果没有影响,但是对渲染性能还是有影响的。比如有两个平行平面AB,A平面比B平面更近,此时:先画A,再画B:对A平面的所有像素进行深度测试,如果测试成功,则重写颜色和深度缓冲区;对平面B的所有像素进行深度测试,对未被平面A遮挡的部分重写颜色和深度缓冲区;对于被阻塞的部分,如果深度测试失败,直接返回;先画B,再画A:平面B的所有像素进行深度测试,如果测试成功,则重写颜色和深度缓冲区;A平面的所有像素进行深度测试,测试成功,重写颜色和深度缓冲区。通过上面的对比可以发现,当不透明物体按照从近到远的顺序绘制时,可以省去重写后面被遮挡部分的颜色和深度缓冲区的操作,从而在一定程度上提高了性能。其次,当我们按照从近到远的顺序绘制不透明物体,开始绘制透明物体时,不透明物体后面的透明物体深度测试失败,所以不会进行后面的颜色和深度缓冲更新操作,所以也可以在一定程度上提高渲染透明物体的性能。所以,如果不区分的话,把不透明的物体和透明的物体画在一起的时候,那么所有的物体都要按照从远到近的顺序来画。那么,在大多数情况下,深度测试会成功,即颜色和深度缓冲区的更新操作会比较多,这会在一定程度上影响性能。如何改变物体的默认渲染效果上面说的大部分都是默认绘制顺序的效果,但是如果想改变默认渲染效果,有什么办法吗?答案是肯定的。控制深度测试前面我们提到了深度测试,可以控制深度测试的三个步骤:是否进行深度测试;深度测试函数的行为;是否更新深度缓冲区。这三个步骤由Material的以下三个属性控制:depthTest:是否进行深度测试;depthFunc:深度测试函数的行为;depthWrite:是否更新深度缓冲区。另外,当需要开启深度测试时,需要在初始化WebGLRenderer时开启depth参数,这会创建一个深度缓冲。该参数默认值为true,即一般情况下不需要关注该属性。当然,如果你的需求明确不需要深入测试,对性能要求比较高,可以手动关闭这个值,以降低存储成本。控制绘制顺序我们前面提到THREE.js分别按照从近到远和从远到近的顺序绘制不透明物体和透明物体。那么这个排序是THREE.js实现的吗?还是需要我们自己控制绘制顺序?THREE.js默认开启排序,通过WebGLRenderer的sortObjects属性实现。如果不启用自动排序,则绘制顺序为添加对象的顺序(注意透明对象和不透明对象仍然是分开渲染的)。不透明物体和透明物体的默认排序顺序,请参考源码的painterSortStable和reversePainterSortStable方法。那么,我们如何干预上述排序过程呢?主要有两种方式:上面两种方式,我们可以注意到有一个renderOrder属性,这就是我们需要的,具体说明可以查看renderOrder文档;通过setOpaqueSort和setTransparentSort完全自定义排序逻辑。自定义混合(blend)函数之前在讲透明物体的渲染时,提到过透明物体默认的混合函数是NormalBlending。此混合函数的行为也是可选的。具体支持的行为请参考Material.blending。总结本文主要介绍THREE.js中不透明物体和透明物体的渲染顺序,主要涉及THREE.js的以下内容:MaterialdepthWrite(defaultistrue)depthTest(defaultistrue)depthFunc(defaultisLessEqualDepth)blending和blending相关的一系列属性Object3DrenderOrder(defaultis0)WebGLRendererdepthsortObjects(defaultistrue)setOpaqueSortsetTransparentSort以上观点基于目前对THREE.js的研究成果,可能存在认知错误。如果是这样,欢迎发表评论。
