OpenGL(5)——正背面剔除与深度测试

603 阅读9分钟

1.甜甜圈绘制

关于绘制图像的项目架构和主要方法在之前的文章中都介绍过了。这里只讲一下差异化的地方,重点介绍下面三个方法。甜甜圈的投影方式,选择使用透视投影。在这个例子中,我们不在改变物体,而是操作视角做相应的形变处理。

1.1 SetupRC

viewFrame视角帧,就是透视投影中人的视角位置。将视角后移,来显示物体,后移的位置越远,看到的物体也就越小(显示的图像越小)。gltMakeTorusOpenGL提供给我们的一个方法,可以构建一个圆环面。

void SetupRC()
{
    //1.设置背景颜色
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
    
    //2.初始化着色器管理器
    shaderManager.InitializeStockShaders();
    
    //3.将相机向后移动7个单元:肉眼到物体之间的距离
    viewFrame.MoveForward(7.0);
    
    //4.创建一个甜甜圈
    //void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
    //参数1:GLTriangleBatch 容器帮助类
    //参数2:外边缘半径
    //参数3:内边缘半径
    //参数4、5:主半径和从半径的细分单元数量
    gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
    
    //5.点的大小(方便点填充时,肉眼观察)
    glPointSize(4.0f);
}

1.2 ChangeSize

void ChangeSize(int w, int h)
{
    //1.防止h变为0
    if(h == 0)
        h = 1;
    
    //2.设置视口窗口尺寸
    glViewport(0, 0, w, h);
    
    //3.setPerspective函数的参数是一个从顶点方向看去的视场角度(用角度值表示)
    // 设置透视模式,初始化其透视矩阵
    viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);
    
    //4.把透视矩阵加载到透视矩阵对阵中
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    //5.初始化渲染管线
    transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
}

1.3 SpecialKeys

与之前的例子不同,SpecialKeys中不再改变物体的坐标,而是改变视角viewFrame坐标。

void SpecialKeys(int key, int x, int y)
{
    //1.判断方向
    if(key == GLUT_KEY_UP)
        //2.根据方向调整观察者位置
        viewFrame.RotateWorld(m3dDegToRad(-5.0), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_DOWN)
        viewFrame.RotateWorld(m3dDegToRad(5.0), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_LEFT)
        viewFrame.RotateWorld(m3dDegToRad(-5.0), 0.0f, 1.0f, 0.0f);
    
    if(key == GLUT_KEY_RIGHT)
        viewFrame.RotateWorld(m3dDegToRad(5.0), 0.0f, 1.0f, 0.0f);
    
    //3.重新刷新
    glutPostRedisplay();
}

1.4 RenderScene

void RenderScene()
{
    //1.清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //2.把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);
    
    //3.设置绘图颜色
    GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
    
    //4.使用默认光源着色器
    //通过光源、阴影效果跟提现立体效果
    //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
    //参数2:模型视图矩阵
    //参数3:投影矩阵
    //参数4:基本颜色值
    shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
    
    //5.绘制
    torusBatch.Draw();

    //6.出栈 绘制完成恢复
    modelViewMatix.PopMatrix();
    
    //7.交换缓存区
    glutSwapBuffers();
}

经过上面几个方法流程,最终运行项目会得到如前面所示的一样的效果图。但是当我们通过特殊键位,操作甜甜圈发生旋转时,就会出现下图所示的问题,我们发现甜甜圈上出现了黑色的阴影,这是什么原因呢?

2.隐藏面消除

在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者哪些部分对观察者是不可见的。对于不可见的部分,应该及早丢弃。例如在一个不透明的墙壁后,就不该渲染。这种情况叫做“隐藏面消除”(Hidden surface elimination)。
通过上面的定义,我们就能理解为什么会出现上面的情况了。在使用默认光源着色器时,正对向我们的部分,显示的红色,背对我们的是黑色阴影,通过旋转,我们看到了本不该被看到的部分,所以会出现这种现象。那么应该如何避免这种情况呢?

2.1 油画算法

先绘制场景中的离观察者较远的物体,再绘制较近的物体。这样一层层的绘制,就可以解决隐藏面消除的问题。

油画算法
但是油画算法并不是万能的,当遇到下图中这这种情况是,油画算法就无能为力了。

2.2 正背⾯剔除(Face Culling)

2.2.1 分析

当我们从任意方向和角度上看一个立方体时,最多只能看到它的三个面。那么我们是不是可以只绘制这三个面呢?如果我们能以某种方式,丢弃不需要绘制的部分数据,OpenGL在渲染的性能上即可提高超过50%。

2.2.2 正/背面

任何平面都有两个面,正面和背面,在同一个时刻我们只能看到一个面。OpenGL可以做到检查所有正面朝向观察者的面,并渲染它们,从而丢弃背面朝向的面,这样可以节约偏远着色器的性能。
但是我们又如何让OpenGL知道那个是正面,哪个是背面呢?答案就是分析顶点数据的顺序。 在前面那分析三角形环绕方式的时候,我们知道了按照逆时针顺序连接顶点的面试正面,按照顺时针顺序连接顶点的面试背面。

请看下图立方体中的正背面:

分析:

  • 左侧三角形的顶点顺序为1->2->3,右侧三角形的顶点顺序为1->2->3
  • 当观察者在右侧时,右侧三角形的顶点顺序为逆时针,也就是正面。左侧三角形的顶点顺序为顺时针,为反面。
  • 当观察者在做测试,则左侧三角形被判定为正面,右侧三角形呗判定为背面。

总结:

  • 正面和背面是由三角形的顶点顺序和观察者方向功能决定的。随着观察者方向的改变,正背面也会跟着改变。

2.2.3 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

2.2.4 剔除背面

通过上面的分析,我们是不是可以解决上面的隐藏面消除问题了呢。我们对RenderScene中的代码做如下修改,然后运行查看一下效果。

void RenderScene()
{
    //1.清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //开启/关闭正背面剔除功能
    if (iCull) {
        glEnable(GL_CULL_FACE);
        glFrontFace(GL_CCW);
        glCullFace(GL_BACK);
    }
    else
    {
        glDisable(GL_CULL_FACE);
    }
    
    //2.把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);
    .......
}

在我们刚开始旋转甜甜圈的时候,发现之前的黑色阴影已经没有了,那是不是问题已经得到解决了呢?

但是当我们一直旋转,就会发现又出现了如下情况:

当甜甜圈出现重叠的时候,会出现像是被啃掉了一块的情况,这是因为圆环出现重合时,会有两个正面,即前面的圆环部分和后面的圆环部分。这时OpenGL不知道该显示哪个正面,被啃掉的那一块,其实是被消除了。那么如何解决这个问题呢,请继续往下看深度测试

3 深度测试

3.1 了解深度

  • 什么是深度?
    深度其实就是在OpenGL坐标系中,像素点的Z坐标距离观察者的距离。由于观察者可以放在坐标系中的任意位置,所以不能简单的说Z值越小,物体越靠近观察者。
  • 什么是深度缓冲区?
    深度缓冲区就是一块显存区域,专门存储着每个像素点(绘制在屏幕上的)深度值。
  • 为什么需要深度缓冲区?
    在不使用深度测试的时候,如果我们先绘制一个距离比较近的物体,再绘制距离比较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉。这样显示的时候就会出现问题,所以要先绘制距离远的物体,再绘制距离近的物体。但是有了深度缓冲区,绘制的先后顺序就变得不那么重要了。
    深度缓存区的默认值为1.0, 表示最⼤的深度值,深度值的范围是[0,1]之间。

3.2 深度测试

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

glEnable(GL_DEPTH_TEST);

关闭深度测试:

glDisable(GL_DEPTH_TEST);

在开始绘制前,需要先清除深度缓冲区和颜色缓冲区。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

指定深度测试判断模式

void glDepthFunc(GLEnum mode);

打开/阻断 深度缓存区写⼊入

void glDepthMask(GLBool value);
value : GL_TURE 开启深度缓冲区写入; GL_FALSE 关闭深度缓冲区写入

3.3 修改调试

修改RenderScene中的代码,加入深度测试的代码,再次运行,旋转甜甜圈,发现不在出现缺一块的情况了。

// 召唤场景
void RenderScene(void)
{
    //清除窗口和深度缓冲区
    //可以给学员演示一下不清空颜色/深度缓冲区时.渲染会造成什么问题. 残留数据
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //开启/关闭正背面剔除功能
    if(iCull)
    {
        glEnable(GL_CULL_FACE);
        glFrontFace(GL_CCW);
        glCullFace(GL_BACK);
    }
    else
    {
        glDisable(GL_CULL_FACE);
    }
    
    //根据设置iDepth标记来判断是否开启深度测试
    if(iDepth)
        glEnable(GL_DEPTH_TEST);
    else 
    {
        glDisable(GL_DEPTH_TEST);
    }
   
}

3.4 ZFighting闪烁

3.4.1 问题描述

由于深度缓冲区的精度限制,对于深度相差非常小的情况下,OpenGL就可能会出现不能正确判断两者的深度值,会导致深度测试的结果不可预知。显示图像时,图像交错闪烁。如下图所示:

3.4.2 解决

启用多边形偏移(Polygon Offset)

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

Polygon Offset就是让深度值中间增加一些间隔,让OpenGL对重叠的两个图形能够加以区分。增加的偏移量的值是可以通过glPolygonOffset函数来指定的。一般⽽言,只需要将-1.0 和 -1 这样简单赋值给glPolygonOffset 的两个参数factorunits基本可以满⾜需求了。

void glPolygonOffset(Glfloat factor,Glfloat units);

应⽤用到⽚片段上总偏移计算⽅方程式:
Depth Offset = (DZ * factor) + (r * units);
DZ:深度值(Z值)
r:使得深度缓冲区产⽣生变化的最⼩小值
一个⼤于0的Offset会把模型推到离你(摄像机)更远的位置,相应的⼀个⼩于0的Offset 会把模型拉近

最后还要记得关闭Polygon Offset,因为相当于有一个全局状态机来控制,我们不能把所有的绘图过程都开启Polygon Offset

glDisable(GL_POLYGON_OFFSET_FILL)