[OpenGL]隐藏面消除解决方案

2020/8/1 posted in  音视频 总阅读量

隐藏面消除
观察上面的这个甜甜圈,我们会发现它在旋转的时候会出现阴影,这是因为我们在渲染的时候把对观察者不可见的面也渲染了,对于不可见的部分,应该尽早丢弃,比如在一个不透明墙壁后面的物体就不应该渲染。这种情况我们叫做隐藏面消除

油画算法


油画算法是指我们在绘制场景时,先绘制场景中离观察者较远的物体,再绘制较近的物体。就像上图,先绘制红色部分,再绘制黄色部分,最后绘制灰色部分,即可解决隐藏面消除的问题。
油画算法虽然可以解决隐藏面消除的问题,但是也有弊端,如下图所示这种情况,三个三角形是叠加的,油画算法将无法处理。

正背面剔除

  1. 正背面剔除原理
    隐藏面消除的原因是我们渲染了我们看不见的面导致,那我们要如何确定那个面是我们看不见的呢?任何平面都有两个面,正面和方面,同一时刻,我们只能看到两个面的其中一个,OpenGL在渲染之前检查哪个面是朝向观察者的面,并渲染它们,同时丢弃背面朝向的面,这样既解决了隐藏面消除的问题,还节约片元着色的的性能。

  2. 如何确定正面和背面
    OpenGL中是如何确定正面和背面的呢?我们一般是通过顶点数据的顺序来确定的。

    • 正面:按照顶点逆时针顶点连接顺序的三角形面
    • 背面:按照顶点顺时针顶点连接顺序的三角形面
  3. 立方体中的正背面

    看上图眼睛位置,如果我们是从右边观察这个立方体,右边这个面是逆时针的,左边的三角形是顺时针的,如果观察者移动到左边,我们会发现,左边这个面变成逆时针了,右边则变成顺时针。所以正⾯和背⾯是由三⻆形的顶点定义顺序和观察者⽅向共同决定的,随观察者的角度方向变化,正背面也会跟着改变。

  4. OpenGL中开启正背面剔除的方法

    • 开启表面剔除

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

      void glDisable(GL_CULL_FACE); 
      
    • ⽤户选择剔除那个⾯(正⾯/背⾯)

      // mode参数为: GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK
      void glCullFace(GLenum mode); 
      
    • ⽤户指定绕序那个为正⾯

      // mode参数为: GL_CW(顺时针),GL_CCW(逆时针),默认值:GL_CCW 
      void glFrontFace(GLenum mode); 
      
  5. 开启正背面剔除的问题

    开启正背面剔除后,我们发现旋转过程中阴影确实没有,但是每次旋转到圆环垂直向外的时候,都会缺一块,圆环的外圈正面和里圈正面重叠的时候,判断会有问题。

深度测试

  1. 我们在初始化OpenGL时一般都会做清空深度缓存区颜色缓存区的操作。我们第一次准备显示屏幕中心的像素时,准备数据阶段会把深度值写入深度缓冲区,颜色数据写入到颜色缓冲区中,最后渲染到屏幕上。
  2. 第二次准备要渲染一个颜色为黄色,深度值为0.8的片段,这时我们会调用深度测试函数进行深度测试的判断,假设我们目前判断成功的条件是片段小于深度缓冲区的值通过,我们一测试,0.8 > 0.6,深度测试不通过,则丢弃此片段,不渲染当前颜色。
  3. 第三次准备一个蓝色的片段,深度为0.3,这次测试成功了,就会去更新深度缓存区颜色缓冲区的值,然后渲染到屏幕上。

深度值计算

线性计算

  • 深度值⼀般由16位,24位或者32位值表示,通常是24位。位数越⾼的话,深度的精确度越
    好。深度值的范围在[0,1]之间,值越⼩表示越靠近观察者,值越⼤表示远离观察者。
  • 深度缓冲主要是通过计算深度值来⽐较⼤⼩,在深度缓冲区中包含深度值介于0.0和1.0之间,
    从观察者看到其内容与场景中的所有对象的 z 值进⾏了⽐较。这些视图空间中的 z 值可以在投
    影平头截体的近平⾯和远平⾯之间的任何值。我们因此需要⼀些⽅法来转换这些视图空间 z 值 到 [0,1] 的范围内,下⾯的 (线性) ⽅程把 z 值转换为 0.0 和 1.0 之间的值 :


这里far和near是我们用来提供到投影矩阵设置可见视图截锥的远近值 (见坐标系)。方程带内锥截体的深度值 z,并将其转换到 [0,1] 范围。在下面的图给出 z 值和其相应的深度值的关系:

非线性计算

然而,在实践中是几乎从来不使用这样的线性深度缓冲区。正确的投影特性的非线性深度方程是和1/z成正比的 。这样基本上做的是在z很近是的高精度和 z 很远的时候的低精度。用几秒钟想一想: 我们真的需要让1000单位远的物体和只有1单位远的物体的深度值有相同的精度吗?线性方程没有考虑这一点。

由于非线性函数是和 1/z 成正比,例如1.0 和 2.0 之间的 z 值,将变为 1.0 到 0.5之间, 这样在z非常小的时候给了我们很高的精度。50.0 和 100.0 之间的 Z 值将只占 2%的浮点数的精度,这正是我们想要的。这类方程,也需要近和远距离考虑,下面给出:

如果你不知道这个方程到底怎么回事也不必担心。要记住的重要一点是在深度缓冲区的值不是线性的屏幕空间 (它们在视图空间投影矩阵应用之前是线性)。值为 0.5 在深度缓冲区并不意味着该对象的 z 值是投影平头截体的中间;顶点的 z 值是实际上相当接近近平面!你可以看到 z 值和产生深度缓冲区的值在下列图中的非线性关系:

使用深度缓冲区

如果我们需要使用深度测试,调用下面的方法开启

glEnable(GL_DEPTH_TEST);

在绘制场景前,清除颜⾊缓存区,深度缓冲区

glClearColor(0.0f,0.0f,0.0f,1.0f); 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 

深度测试函数

OpenGL 允许我们修改它深度测试使用的比较运算符(comparison operators),我们可以通过调用glDepthFunc来设置比较运算符 (或叫做深度函数(depth function)):

glDepthFunc(GL_LESS);

此函数接受下表中列出的几个比较运算符:

运算符 描述
GL_ALWAYS 永远通过测试
GL_NEVER 永远不通过测试
GL_LESS 在片段深度值小于缓冲区的深度时通过测试
GL_EQUAL 在片段深度值等于缓冲区的深度时通过测试
GL_LEQUAL 在片段深度值小于等于缓冲区的深度时通过测试
GL_GREATER 在片段深度值大于缓冲区的深度时通过测试
GL_NOTEQUAL 在片段深度值不等于缓冲区的深度时通过测试
GL_GEQUAL 在片段深度值大于等于缓冲区的深度时通过测试

默认情况下使用GL_LESS

ZFighting闪烁问题

  • 因为开启深度测试后,OpenGL 就不会再去绘制模型被遮挡的部分. 这样实现的显示更加真实.但是由于深度缓冲区精度的限制对于深度相差⾮常⼩的情况下.(例如在同⼀平⾯上进⾏2次绘制),OpenGL 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测.显示出来的现象是交错闪烁的前⾯2个画⾯,交错出现。

  • 解决办法是开启多边形偏移(Polygon Offset),让深度值之间产⽣间隔。如果2个图形之间有间隔,是不是意味着就不会产⽣⼲涉。可以理解为在执⾏深度测试前将⽴⽅体的深度值做⼀些细微的增加,于是就能将重叠的2个图形深度值之间有所区分。

  1. 启⽤Polygon Offset

    参数列表: 
    /*
    GL_POLYGON_OFFSET_POINT 对应光栅化模式: GL_POINT
    GL_POLYGON_OFFSET_LINE 对应光栅化模式: GL_LINE
    GL_POLYGON_OFFSET_FILL 对应光栅化模式: GL_FILL
    */
    glEnable(GL_POLYGON_OFFSET_FILL)
  2. 指定偏移量

    void glPolygonOffset(Glfloat factor,Glfloat units);
    

    通过glPolygonOffset函数来指定偏移量,底层偏移量计算公式如下:

    Offset = ( m * factor ) + ( r * units); 
    
    • m : 多边形的深度的斜率的最⼤值,理解⼀个多边形越是与近裁剪⾯平⾏,m 就越接近于0.
    • r : 能产⽣于窗⼝坐标系的深度值中可分辨的差异最⼩值.r 是由具体是由具体OpenGL 平台指定的⼀个常量.
    • factor和units是我们需要传入的参数,⼀般⽽⾔,只需要将-1.0 和 -1.0这样简单赋值给glPolygonOffset 基本可以满⾜需求.
    • ⼀个⼤于0的Offset会把模型推到离你(摄像机)更远的位置,相应的⼀个⼩于0的Offset 会把模型拉近。
  3. 关闭多边形偏移

    glDisable(GL_POLYGON_OFFSET_FILL)