OpenGL-甜甜圈引发的问题(正背面剔除/深度测试)

301 阅读8分钟

甜甜圈

根据之前学习的OpenGL相关知识,我们可以实现这样一个效果,具体代码可见Demo,如下图所示:

同时我们还实现了通过上、下、左、右方向键可以让甜甜圈进行旋转的功能,但是在旋转的时候出现了问题:

出现问题的原因

那么造成这种情况的原因是什么呢?

默认情况下,我们所渲染的每个点、线或者三角形都会在屏幕上进行光栅化,并且按照在组合图元时制定的顺序进行排列,如果我们绘制一个由很多个三角形组成的实体对象,那么第一个绘制的三角形可能会被后面绘制的三角形覆盖。在绘制3D场景的时候,我们需要决定哪些部分是对观察者可⻅的,或者哪些部分是对观察者不可⻅的.对于不可⻅的部分,应该及早丢弃.例如在⼀个不透明的墙壁后,就不应该渲染.这种情况叫做"隐藏⾯消除"(Hidden surface elimination).

在我们的甜甜圈案例中也是一样,甜甜圈是由很多个三角形组成的,其中一些三角形在甜甜圈的背面,而另一些在甜甜圈的正面。在本案例中,我们是不应该看到背面的,但是由于背面默认也会显示出来,因此出现了上图中的奇怪的效果,其本质就是在光照的情况下,背面的黑色三角形覆盖了正面的红色三角形。

解决问题的方案

1. 油画算法

所谓的油画算法就是在绘制过程中,先绘制场景中离观察者较远的物体,再绘制较近的物体。
例如下面的图例:

上图中,各个视图距离观察者由远及近依次为:红色->黄色->灰色。在绘制的时候,首先绘制红色视图,其次绘制黄色视图,最后绘制灰色视图,就是说离观察者较近,会被观察者直接看到的内容一定是在最上层,这种情况可以解决我们的甜甜圈问题。

但是油画算法也不是万能的,而且它在计算机图形处理中也是非常低效的:

  • 没有明显的层次区分时

上图中红黄蓝三个立体的三角形交织在一起,没有办法区分出来谁先谁后。这种情况下油画算法将无法处理。

  • 我们必须对任何发生几何图形重叠地方的每个像素进行多次写操作,而在存储器中进行写操作会使速度变慢
  • 对独立的三角形进行排序的开销会过高

出于上面的种种考虑,油画算法并不是解决隐藏面消除问题的最优解

2. 正面和背面剔除(Face Culling)

OpenGL中的环绕

首先来讲一下OpenGL中的环绕OpenGL中绘制三角形是按照一定的顺序将三角形的三个顶点进行连接,那么这时候会出现两种情况:顺时针绘制逆时针绘制

这种顺序与方向结合来指定顶点的方式称为环绕。在默认情况下,OpenGL认为具有逆时针环绕方向的多边形是正面的。

正背面剔除

接下来说道正面和背面剔除。

背面剔除就是在渲染的图元装配阶段将背面的三角形抛弃,并且没有执行任何不恰当的光栅化操作,这种处理,可以极大地提高性能,毕竟少许安然了很多观察者不该看到也不会看到的三角形。

什么时候会用到正面剔除呢?例如在渲染透明对象时,我们经常会对一个对象进行两次渲染,第一次开启透明并剔除正面,第二次则消除北冥。这样就在渲染正面之前渲染了背面,这也是渲染透明物体的需要。

开启正背面剔除的代码也很简单:

• 开启表⾯剔除(默认背⾯剔除) 
void glEnable(GL_CULL_FACE); 
• 关闭表⾯剔除(默认背⾯剔除) 
void glDisable(GL_CULL_FACE); 

• ⽤户选择剔除那个⾯(正⾯/背⾯) 
void glCullFace(GLenum mode); 
mode参数为: GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK 

• ⽤户指定绕序那个为正⾯,不常用
void glFrontFace(GLenum mode); 
mode参数为: GL_CW,GL_CCW,默认值:GL_CCW 

• 例如,剔除正⾯实现(1) 
glCullFace(GL_BACK); 
glFrontFace(GL_CW); 
• 例如,剔除正⾯实现(2) 
glCullFace(GL_FRONT)

开启了背面剔除之后,我们的甜甜圈案例终于不会出现黑色重叠情况啦:

黑色重叠情况确实消失了,但是,我们发现了另外一个问题:

在甜甜圈垂直于屏幕的时候好像哪里有点不对劲。在甜甜圈垂直屏幕的情况下,其实甜甜圈有两个正面,只不过它们产生了叠加。我们虽然剔除了所有的背面,但是对于正面叠加的情况我们还没有处理,很显然,开启背面剔除对于我们当前的问题还是不能完全解决。

3. 深度测试

深度测试是另外一种高效消除隐藏表面的技术。

深度

屏幕中每一个像素都对应了一个z值,表示它与观察者的距离,深度值的范围在[0,1]之间,值越⼩表示越靠近观察者,值越⼤表示远离观察者

深度缓冲区

深度缓冲区是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值。在不使用深度测试的时候,对于同一像素来说,如果遇到了多次绘制即有重叠的情况,后绘制的像素会覆盖先绘制的内容,但是有了深度缓冲区后,绘制的顺序就没那么重要了。

深度测试

深度缓冲区(DepthBuffer)和颜⾊缓存区(ColorBuffer)是对应的.颜⾊缓存区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息.在决定是否绘制⼀个物体表⾯时,⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较.如果⼤于深度缓冲区中的值,则丢弃这部分.否则利⽤这个像素对应的深度值和颜⾊值.分别更新深度缓冲区和颜⾊缓存区. 这个过程称为深度测试。

深度值计算

  • 深度值一般由16位24位或者32位表示,通常是24位。位数越高,精度值约准确。
  • 深度值计算公式如下图所示:

开启深度测试

  • 在我们使用GLUT设置OpenGL窗口时,可以请求一个深度缓冲区和颜色缓冲区。
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);

其中GLUT_DEPTHGLUT_RGBA分别代表深度缓冲区和颜色缓冲区。

  • 开启和关闭深度测试,只需要调用
glEnable(GL_DEPTH_TEST) //开启
glDisable(GL_DEPTH_TEST) //关闭
  • 在绘制场景前,清楚颜色缓冲区和深度缓冲区
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  • 打开/阻断深度缓冲区写入
void glDepthMask(GLBool value)
  • 指定深度测试判断式
void glDepthFunc(GLEnum mode);

开启深度测试之后,再来看我们的甜甜圈:

补充内容

多边形模式

多边形(含三角形)不一定是实心的。在默认情况下,多边形是作为实心图绘制的,但是我们可以通过将多边形指定为显示轮廓或者只有点(顶点)来改变这种行为。

void glPolygonMode (GLenum face, GLenum mode)

通过glPolygonMode函数可以选择多边形模式,face参数可用值为:

  • GL_FRONT
  • GL_BACK
  • GL_FRONT_AND_BACK

mode参数为:

  • GL_FILL(默认值)
  • GL_LINE
  • GL_POINT 将甜甜圈的多边形模式改为GL_FRONT_AND_BACK+GL_POINT之后效果如下:

ZFighting闪烁问题

在开启深度测试之后,OpenGL不会再去绘制模型被遮挡的部分,但是由于深度缓冲区精度的限制,在深度相差非常小的情况下,OpenGL有可能出现不能正确判断两者的深度值,导致深度测试的结果不可预测。显示出来的现象是交错闪烁的画面。

如下图,当我们需要绘制如下三角锥时,三角形的颜色都为绿色,它们拼接起来的默认效果是没有黑线的,如下图所示:

因此需要我们加上边框线,此时边框线和三角形的边几乎是处于同一深度的。如果我们正常添加会出现如下的问题:

可以看到,在三角锥沿x轴旋转时,边框的黑线会出现闪烁,这就是ZFighting

ZFighting问题解决

1. 启用Polygon Offset(多边形偏移)

多边形偏移本质上是让深度值之间产生间隔,如果2个图形之间有间隔,就意味着不会产生干涉。开启多边形偏移的方法如下:

glEnable(GL_POLYGON_OFFSET_FILL)
参数列表: 
GL_POLYGON_OFFSET_POINT 对应光栅化模式: GL_POINT 
GL_POLYGON_OFFSET_LINE 对应光栅化模式: GL_LINE 
GL_POLYGON_OFFSET_FILL 对应光栅化模式: GL_FILL

2. 指定偏移量

void glPolygonOffset(Glfloat factor,Glfloat units);
  • 通过glPolygonOffset 来指定glPolygonOffset 需要2个参数: factor , units
  • 每个Fragment 的深度值都会增加如下所示的偏移量:
Offset = ( m * factor ) + ( r * units); 
m : 多边形的深度的斜率的最⼤值,理解⼀个多边形越是与近裁剪⾯平⾏,m 就越接近于0.    

r : 能产⽣于窗⼝坐标系的深度值中可分辨的差异最⼩值.r 是由具体是由具体OpenGL 平台指定的⼀个常量. 
  • ⼀个⼤于0的Offset 会把模型推到离你(摄像机)更远的位置,相应的⼀个⼩于0的Offset
  • 会把模型拉近 ⼀般⽽⾔,只需要将-1.0 和 -1这样简单赋值给glPolygonOffset 基本可以满⾜需求.

开启了多边形偏移之后,最终效果如下图:

3. 关闭多边形偏移

glDisable(GL_POLYGON_OFFSET_FILL)