1.渲染过程产生的问题
上一篇文章介绍了使用平面着色器
绘制金字塔,花环,花盘的实现,为了让渲染更接近真实世界,需要增加光照效果。
上图是使用默认光源着色器
渲染的甜甜圈,甜甜圈是由OpenGL
提供实现的,外界只需调用即可得到。
当静止不动时,甜甜圈效果很完美。可一旦转动,就穿帮了。
出现隐藏面的原因在于:
使用默认光源着色器
时,正面正常渲染,背面会被渲染成黑色。旋转发生时,背面的隐藏面无法被正面遮挡。
而使用平面着色器
绘制的金字塔,花环,花盘也进行了旋转等操作却没有出现此现象。这是因为平面着色器
使用同一颜色渲染所有图元,正面和背面都是同一种颜色,旋转之后正背面无视觉差。
2.油画算法
对于这个问题,一个可行的解决方法是:对这些三角形进行排序,然后首先渲染较远的三角形,在渲染较近的三角形,这种方式叫做油画算法
。
如图,先绘制远处的山。在绘制草地,最后绘制树,这样即可解决隐藏面的问题。
但这样的图形处理方式在计算机中是非常低效的。因为任何发生重叠的像素都要进行多次写操作。
更重要的是,当遇到某些场景时,油画算法无法很好的解决问题。
当显示这样的图形时,油画算法退出了群聊。
3.正背面剔除
三角形被区分为正面和背面的用处之一就是可以进行正背面剔除
。
试想,任何一个3d
物体,从任意方向看过去不可能看到它的全貌,也就一定会存在一些不可见的面。对于观察者而言,这些不可见的面不渲染也不影响观察物体;对于计算机而言,既然不可见为什么还去渲染,直接丢弃不就好了。
于是问题就变成了如何区分可见和不可见
,这个问题等价于如何区分正面和背面
。
至于如何区分正面和背面
:是通过三角形顶点的连接顺序,而连接顺序决定法向量的朝向。
-
正面
:顶点按照逆时针
顺序连接的三角形面,法向量朝向观察者 -
背面
:顶点按照顺时针
顺序连接的三角形面,法向量背对观察者
但是立体图形的正面和背面还和观察者的视角有关。
-
当观察者在右侧时,右边的三角形为逆时针则是正面,左侧的三角形为顺时针则是反面
-
当观察者在左侧时,左边的三角形为逆时针则是正面,右侧的三角形为顺时针则是反面
不难理解,观察者看到的为正面是合理的。
正面和背面
是由三角形的顶点连接顺序和观察者方向共同决定的。随着观察者角度方向的改变,正面背面也会跟着改变。
这里的背面是指被遮住的面,而不是一个面的前后两面。同一个面的前后两面在3D中同一位置的点是一模一样颜色的,面是没有厚度概念的。
由于状态机
的存在,正背面剔除
使用的方式及其简单:
- 开启正背面剔除(默认背面剔除)
glEnable(GL_CULL_FACE);
复制代码
- 关闭正背面剔除(默认背面剔除)
glDisable(GL_CULL_FACE);
复制代码
状态机
决定了一个功能使用完之后必须关闭,否则会影响到后续帧的渲染。
如图,使用正背面剔除
渲染的甜甜圈隐藏面问题已经解决,但当旋转到一定角度后,出现甜甜圈被啃一口的现象。
这是因为当前后的像素重叠时,OpenGL不确定是渲染前面还是后面。
要想解决这个问题,需要用到深度测试
。
4.深度测试
什么是深度?
深度
其实就是该像素点在3D世界中距离摄像机的距离,即Z值
什么是深度缓冲区?
深度缓存区
就是⼀块内存区域,专⻔存储着每个像素点深度值.深度值(Z值)越⼤,则离摄像机就越远
为什么需要深度缓冲区?
当场景中包含很多模型时,对于
不透明物体
,不考虑它们的渲染顺序也能得到正确的排序效果,这正是由于深度缓冲区
的存在
在实时渲染中,深度测试
相当于油画算法的颠倒,先处理离观察者近的对象,在处理远的对象,它可以决定哪些物体的哪些部分会被渲染在前面,而哪些部分会被遮挡,所以它也能用来处理可见性
的问题。
它的工作原理是:当渲染一个片元时,需要把它的深度值
和已存于深度缓冲区
该片元的深度值
作对比,如果它的值距离摄像机更远,说明这个片元不应该被渲染,直接丢弃,否则应该覆盖掉颜色缓冲区
对应的像素值
,并且在深度缓冲区
更新它的深度值
(深度写入
开启时)。
深度写入
:将刚通过测试的片元的深度值更新到深度缓存。深度写入的目的在于更新了一个深度阈值。之后进行深度测试时,不符合新阈值条件的片元都会被舍弃。
由于状态机
的存在,深度测试
使用的方式也及其简单:
- 开启深度测试
glEnable(GL_DEPTH_TEST);
复制代码
- 关闭深度测试
glDisable(GL_DEPTH_TEST);
复制代码
- 是否开启深度写入
glDepthMask(GLboolean flag);
复制代码
注意,如果没有申请
深度缓冲区
,深度测试
的命令将被忽略。
- 申请颜色缓冲区和深度缓冲区
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
复制代码
如图,使用深度测试
后的甜甜圈解决隐藏面
和缺口
问题了。
深度值
⼀般由16位,24位或者32位值表示,通常是24位。位数越⾼的话,深度的精确度越好。深度值
的范围在[0,1]之间,值越⼩表示越靠近观察者,值越⼤表示远离观察者。由于深度缓冲区精度的限制,在深度值相差非常小的情况下,会出现斑驳
现象,显示出来的效果交错闪烁。(想象下,渲染一架机翼上带有贴纸logo的飞机时,机翼表面和贴纸深度值非常接近)
导致这个现象的原因,叫做ZFighting
。解决ZFighting
的冲突,可以使用多边形偏移
。
5.多边形偏移
多边形偏移
在上一篇金字塔案例中描边的步骤有涉及。
多边形偏移
的思想是:
在执⾏深度测试
前将深度值做⼀些细微的改变,让深度值
之间产生间隔,就能将重叠的2个图形深度值
区分开来。
同样,由于状态机
的存在,多边形偏移
使用的方式也及其简单:
- 开启
多边形偏移
,参数对应多边形的填充方式
glEnable(GL_POLYGON_OFFSET_POINT);
glEnable(GL_POLYGON_OFFSET_LINE);
glEnable(GL_POLYGON_OFFSET_FILL);
复制代码
- 关闭多边形偏移
glDisable(...);
复制代码
- 开启多边形偏移后,还需要指定偏移量
glPolygonOffset(GLfloat factor, GLfloat units);
复制代码
Offset
大于0
会把模型推到离摄像机更远的位置,⼩于0
的Offset
会把模型拉近。
⼀般⽽⾔,只需要给glPolygonOffset
赋值为-1和1
基本可以满⾜需求。对于金字塔案例中的描边是设置为-1
。
ZFighting
问题的预防:
- 不要将两个物体靠的太近,避免渲染时三⻆形叠在⼀起。这种⽅式要求对场景中物体插⼊⼀个少量的偏移,那么就可能避免
ZFighting
现象。 - 尽可能将
近裁剪⾯
设置得离观察者远⼀些。在近裁剪平⾯附近,深度的精确度是很⾼的,因此尽可能让近裁剪⾯远⼀些的话,会使整个裁剪范围内的精确度变⾼⼀些。但是这种⽅式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪⾯参数。 - 使⽤更⾼位数的
深度缓冲区
,通常使⽤的深度缓冲区是24位的,现在有⼀些硬件使⽤使⽤32位的缓冲区,使精确度得到提⾼
6.混合
相关术语:
目标颜色:已经存储在颜⾊缓存区的颜⾊值
源颜⾊:作为当前渲染命令结果进⼊颜⾊缓存区的颜⾊值
如果一个片元通过了所有测试,并且开启了混合
,那么颜色缓冲区
已有的颜色(目标颜色
)和当前片元的颜色(源颜⾊
)会进行混合
,目标颜色
和源颜⾊
的组合方式是由混合方程式
控制的。
默认的混合方程式
如下:
Cf
:最终计算参数的颜⾊Cs
: 源颜⾊Cd
:⽬标颜⾊S
:源混合因⼦D
:⽬标混合因⼦
Cf = (Cs * S) + (Cd * D)
复制代码
其中,可以设置混合因⼦
,需要⽤到glBlendFun
命令:
S
:源混合因⼦D
:⽬标混合因⼦
glBlendFunc(GLenum S,GLenum D);
复制代码
通过常用的混合命令组合说明:
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)
复制代码
如果源颜色的alpha
值为0.6
,即S=0.6
,那么目标混合因子D=1-0.6
。
代入混合方程式就是:
Cf = (Cs * 0.6) + (Cd * 0.4);
复制代码
源颜⾊的alpha
值越⾼,添加的源颜⾊成分越⾼,⽬标颜⾊所保留的成分就会越少。
除了可以设置混合因子
,混合函数
本身也可以被设置:
glbBlendEquation(GLenum mode);
复制代码
使用以上组合可以任意更改混合函数
,默认的混合函数是使用加号
。
同样,由于状态机
的存在,混合
使用的方式也及其简单:
- 开启混合
glEnable(GL_BLEND);
复制代码
- 关闭混合
glDisable(GL_BLEND);
复制代码
需要注意的是,
混合
针对图层时一般不自行处理混合方程式,只需要开关;而在片元着色器
给定某个颜色而不是图层进行处理时,需要使用混合方程式自定义。
由于深度测试和深度写入
的存在,渲染不透明物体可以不讲究顺序,可是当开启混合
时,意味着场景中存在半透明物体,这时候渲染的顺序就变得很重要了。
试想,当半透明物在前时,如果开启了深度写入
,意味着该片元的深度值被更新,该片元深度值之下的物体不会被渲染,可半透明物体遮挡后面场景是错误的。
- 所以在渲染半透明物体时,是需要关闭
深度写入
的,但要开启深度测试
。
那为什么需要保留深度测试?
- 因为
深度测试
的目的是通过比对深度值判断是否需要舍弃该片元。如果该片元在不透明物体之后,是要被舍弃的,自然需要深度测试
来判断是舍弃还是留下。
基于这半透明和不透明物体的渲染特性,渲染引擎一般会对物体先进行排序,在渲染:
-
先渲染所有的不透明物体,渲染的时候开启
深度测试
和深度写入
-
半透明物体按照从后前的顺序渲染,渲染的时候开始
深度测试
,关闭深度写入
关于混合的案例,感受下深度测试
和深度写入
的打开关闭的区别
混合案例1 &
混合案例2
7.正背面剔除和深度测试的区别
关于正背面和深度,我曾经思考过一个问题,不知道看到这里的你是否也会思考同样的问题。
正背面剔除
和深度测试
似乎做的是同一件事,本质都是去除被遮挡住的像素。那为什么需要两步操作,似乎只做深度测试最终结果也是一样的,它们有什么区别?
正背面剔除是根据三角形面片的法向量,一旦可以确定这个法向量是不可见的,那就可以成片成片的消除场景中的三角形,而深度测试是一个一个像素的判断,效率是低下的。这也是二者必须共存的本质区别。
下面思考两种特殊情况:
只做深度测试时
:
可以想象,像素会一个一个被处理,也能得到同样的渲染结果,只是效率比较低。
只做正背面剔除时
:
甜甜圈案例说明了动态情况下,只做正背面剔除渲染会发生错误,那静态情况下是否就能得到正确的结果呢,答案也是否定的。
一个面除了分为完全可见和完全不可见外,还有部分可见的情况。部分可见的面用法向量判断会被标记为完全可见,导致渲染错误,这时就需要做更多的测试来保证渲染结果。而更多的测试也就是深度测试
了。