四、OpenGL的正背面剔除和深度测试

179 阅读6分钟

一、绘制一个甜甜圈

首先通过以前的知识绘制一个甜甜圈,然后通过解决出现的问题来阐述正背面剔除和深度测试。

  • 在setupRC函数中创建一个甜甜圈

    //设置肉眼到物体之间的距离
    viewFrame.MoveForward(7.0);
    
    //创建一个甜甜圈
    //参数1:GLTriangleBatch 容器帮助类
    //参数2:外边缘半径
    //参数3:内边缘半径
    //参数4、5:主半径和从半径的细分单元数量
    gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
    
    //设置点的大小,方便肉眼观察
    glPointSize(4.0f);
    
  • 在ChangeSize函数里设置投影矩阵

    //设置透视投影,初始化透视矩阵
    viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);
    
    //把透视矩阵加载到透视矩阵对阵中
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    //初始化渲染管线
    transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
    
  • 在RenderScene函数里利用默认光源着色器绘图

    //把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);
    
    //设置绘图颜色
    GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
    
    //使用默认光源着色器通过光源、阴影效果跟实现立体效果
    //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
    //参数2:模型视图矩阵
    //参数3:投影矩阵
    //参数4:颜色值
     shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
    
    //绘制
    torusBatch.Draw();
    
    //出栈 绘制完成恢复
    modelViewMatix.PopMatrix();
    
    //交换缓存区
    glutSwapBuffers(); 
    

通过这几步就可以呈现出一个漂亮的甜甜圈

但是我们通过键位函数移动的时候,发现甜甜圈变成了这样

这显然不是我们想要的效果。

二、隐藏面消除

为了解决上边的问题,我们需要对背面进行消除,即隐藏面消除

  • 油画算法

    油画算法有一条规则: 先绘制场景中距离观察者远的物体,再绘制近的物体。
    

如上图所示,我们可以先画红色,再画黄色,然后画灰色,这样就可以解决正背面的绘制顺序问题。但是并不是所有的图形都是有顺序的,例如下边这种图形叠加的情况,油画算法就没办法解决了。

  • 正背面剔除

    正背面剔除有一条规则:只画看的到的,看不到的不画。
    那么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);

 glEnable(GL_CULL_FACE);
 glCullFace(GL_BACK);

采用正背面剔除以后,问题迎刃而解,但是又出现了新的问题,甜甜圈有个窟窿

三、深度测试

甜甜圈之所以会出现一个窟窿,是因为OpenGL不知道深度,后面的像素绘制出来的时候覆盖了前面绘制的像素,然后我们就看到了后面的窟窿。那我们可以通过开启深度测试来修复这个问题,将需要绘制的新像素的Z值与深度缓冲区中对应位置的Z值进行比较,如果比深度缓冲区的Z值小,那就用新像素的颜色值更新帧缓存中对应像素的颜色值。

 glEnable(GL_DEPTH_TEST);
 /*
 * mode :GL_NEVER(用不处理)、GL_ALWAYS(处理所有)、GL_LESS(小于)、GL_LEQUAL(小于等于)、GL_EQUAL(等于)、GL_GEQUAL(大于等于)、GL_GREATER(大于)或GL_NOTEQUAL(不等于),其中默认值是GL_LESS。
 * 普遍使用GL_LEQUAL 来表达一般物体之间的遮挡关系。 
 */
 glDepthFunc(GLEnum mode);
 // 在绘制场景前,清除颜色缓存区,深度缓冲区
 // 清除深度缓冲区默认值为1.0,表示最⼤大的深度值,深度值的范围为(0,1)之间,
 // 值越⼩小表示越靠近观察者,值越⼤大表示 越远离观察者
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 glEnable(GL_DEPTH_TEST);
 glDepthFunc(GL_LEQUAL);

经过了深度测试以后,我们就得到了一个甜甜的甜甜圈。

问题解决了我们再来看一下深度和深度缓冲区

  • 深度

    深度是物体的像素点在3d世界中,距离摄像机的距离,z值越大距离越大。

  • 深度缓冲区(DEPTH_BUFFER)

    深度缓冲区是一块内存区域,用于缓存每个像素点的深度值。

  • 为什么需要深度(DEPTH_BUFFER)?

    在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,
    会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。而有了深度缓冲以后,绘制物体的顺序就不那么重要了,
    都能按照远近(Z值)正常显示。
    
  • 深度缓冲区原理

    深度缓冲区的原理就是把一个距离观察平面(近裁剪面)的深度值(或距离)与窗口中的每个像素关联起来。首先,使用
    glClear(GL_DEPTH_BUFFER_BIT),把所有像素的深度值设置为最大值(一般是远裁剪面)。然后,在场景中以任
    意次序绘制所有物体。硬件或者软件所执行的图形计算把每一个绘制表面转换为窗口上一些像素的集合,此时并不考
    虑是否被其他物体遮挡。其次,OpenGL会计算这些表面和观察平面的距离。如果启用了深度缓冲区,在绘制每个像素
    之前,OpenGL会把它的深度值和已经存储在这个像素的深度值进行比较。新像素深度值<原先像素深度值,则新像素
    值会取代原先的;反之,新像素值被遮挡,他颜色值和深度将被丢弃。
    深度缓冲区的作用就是区分颜色所在的层次,防止把被遮挡住的颜色显示出来。当一个像素第二次被绘制时,深度缓
    冲要么保留前面的深度值,要么使用第二个像素的深度值替换当前深度值。保留或抛弃哪个深度取决于你选择的深度
    函数。例如,如果当前深度函数是CompareFunction.LessEqual时,只有小于等于当前深度值的值才会被保留,
    而大于当前深度值的值会被抛弃。这叫做深度测试,每次绘制像素时都会进行深度测试。当对一个像素进行深度测试
    时,它的颜色会被写入渲染目标,而深度被写入深度缓冲。