OpenGL渲染技巧-正背面剔除及深度测试

464 阅读8分钟

一、绘制一个甜甜圈

首先通过下面一个案例引入今天要讨论的问题

环境搭建在之前的博客 OpenGL 环境搭建 里已经做过介绍,下面就描述一下绘制甜甜圈的核心代码:

SetupRC函数

使用gltMakeTorus()函数传入批次类对象、内外边缘半径、以及主从半径细分单元数量,该方法会创建顶点数据并复制到批次类对象中。案列中使用的方式是“物体不动,观察者动”,即改变观察者到物体的距离

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);
}

RenderScene()函数

主要是用于绘制甜甜圈,使用默认的光源着色器

在绘制前,要先清空缓存区,这里的缓冲区就包括颜色缓冲区和深度缓冲区,因为如果缓存区不清空,之前的数据会有残留,会对目前图形的绘制造成影响。

void RenderScene()
{
   
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //2.把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);

    //3.设置绘图颜色
    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);
    
    //5.绘制
    torusBatch.Draw();

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

运行绘制出的甜甜圈如下图:

二、正背面剔除

完成甜甜圈绘制后,我们通过特殊键位控制甜甜圈旋转的时候,出现一个问题如下图:


在这里引入我们今天讨论的问题,我们为什么要使用正背面剔除。首先分析一下上图现象产生的原因:

因为一个物体在光照下是有两面的:阳面(光照覆盖的面)和阴面(背光的面),在OpenGL中同理,甜甜圈的背光面其实是看不到的部分,OpenGL将它绘制成了黑色,而在甜甜圈在旋转过程中,OpenGL不知道该显示哪些界面,导致本来是观察者不应该看到且该丢弃部分,不仅看到了,而且没有将隐藏部分丢弃。

隐藏面的消除方案:

之前博客中我们提到过“画家算法”,即由远及近以此绘制不同的图层,近的图层会把远的图层覆盖,这样用户就不会看到被覆盖的图层,如下图:


但“画家算法”并不适用所有场景,有这么一个问题,油画算法只能解决排序好的三角形渲染,且重叠部分渲染性能较差,而且也不能区分未排序的叠加三角形。如下图:

这样交叉叠加的三角形,“画家算法”也无法知道应该先绘制哪个图层。

方案二:正背面剔除(Face Culling)

正/背⾯剔除原理 : 我们不去绘制那些根本看不到的⾯,以某种⽅式去丢弃这部分数据,我们可以给平⾯定义正⾯和背⾯,OpenGL可以做到检查所有正⾯朝向观察者的⾯,并渲染它们,从⽽丢弃背⾯朝向的⾯。OpenGL渲染的性能即可提⾼超过50%。

正背面区分:

OpenGL默认规定按照逆时针顶点连接顺序的三⻆形⾯是正面,按照顺时针顶点连接顺序的三⻆形⾯是背面,当然也可以修改为顺时针没正面,但不建议这么修改(觉大多数人或项目都适用默认的规则,如果你偏要修改岂不是显得很另类)

⽤户指定绕序那个为正⾯

void glFrontFace(GLenum mode);// mode参数为: GL_CW,GL_CCW,默认值:GL_CCW 

选择剔除那个⾯(正⾯/背⾯) 

void glCullFace(GLenum mode);
// mode参数为: GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK

正背面剔除常用的三个方法

  • 开启正背面剔除
//开启表面剔除 (默认背面剔除)
void glEnable(GL_CULL_FACE);
  • 关闭正背面剔除
//关闭表面剔除(默认背面剔除)
void glDisable(GL_CULL_FACE);
  • 设置需要剔除的面
void glCullFace(GLenum mode);


甜甜圈实现正背面剔除

main()函数里增加回调函数

//添加右击菜单栏
    // Create the Menu
    glutCreateMenu(ProcessMenu);
    glutAddMenuEntry("Toggle depth test",1);
    glutAddMenuEntry("Toggle cull backface",2);
    glutAddMenuEntry("Set Fill Mode", 3);
    glutAddMenuEntry("Set Line Mode", 4);
    glutAddMenuEntry("Set Point Mode", 5);
    glutAttachMenu(GLUT_RIGHT_BUTTON);


在RenderScene函数中新增正背面剔除的开启/关闭操作

//开启/关闭正背面剔除功能
    if (iCull) {
        glEnable(GL_CULL_FACE);
        glFrontFace(GL_CCW);
        glCullFace(GL_BACK);
    }else
    {
        glDisable(GL_CULL_FACE);
    }

运行可以发现甜甜圈正背面绘制异常的问题解决了,如下图:

但是继续旋转,又出现了另外一个问题:甜甜圈“被吃了一口”,如下图:

产生原因:

在甜甜圈旋转过程中,当前后两部分重叠时正常我们应该需要显示前面部分,但OpenGL并不能清除的区分两个图层谁在前谁在后,导致甜甜圈产生了缺口。我们就需要使用深度测试解决改问题了。

三、深度测试

深度

深度是指OpenGL坐标系中,物体的像素点Z轴距观察者的距离

深度与图形中像素点的Z坐标有如下关系:

  • 如果观察者在Z轴的正方向,Z值越大则越靠近观察者
  • 如果观察者在Z轴的负方向,Z值越小则越靠近观察者
当观察者可以放在坐标系的任意位置, 所以. 不能简单的说Z 数值越⼤或越⼩. 观察者就越靠近物体; 

深度缓冲区(DepthBuffer)

深度缓存区,就是⼀块内存区域,专⻔存储着每个像素点(绘制在屏幕上的)深度值。

  • 深度缓冲区存储在显存里。
  • 深度缓存区原理, 就是把距离观察者平⾯(近裁剪⾯)的深度值 与 窗⼝中每个像素点1对1进⾏关联以及存储; 

为什么需要深度缓冲区

在不使⽤深度测试的时候,如果我们先绘制⼀个距离⽐较近的物理,再绘制距离较远的物理,则距离远的位图因为后绘制,会把距离近的物体覆盖掉. 有了深度缓冲区后,绘制物体的顺序就不那么重要了。 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写⼊到缓冲区中. 除⾮调⽤
glDepthMask(GL_FALSE).来禁⽌写⼊。

深度缓冲区原理

深度缓冲区(DepthBuffffer)和颜⾊缓存区(ColorBuffffer)是对应的.颜⾊缓存区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息.

  • 在深度缓存区中,每个像素点只会记录一个深度值
  • 深度缓冲区的范围是[0, 1]之间,默认值是1.0,表示深度值的最大值

深度测试

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

同过对深度测试的知识了解,我们应用到上面甜甜圈案例中,不难理解,甜甜圈缺口问题就是由于物体像素点的深度导致的,在不开启深度测试的情况下,就有可能出现像素点被覆盖的问题,类似场景就需要将OpenGL深度测试打开了。

打开深度测试代码:

//开启深度测试
    if (iDepth) {
        glEnable(GL_DEPTH_TEST);
    }
    else{
        glDisable(GL_DEPTH_TEST);
    }

运行项目,发现缺口问题已经解决了,如图:

深度测试相关API:

开启/关闭深度测试:

glEnable(GL_DEPTH_TEST);
glDisable(GL_DEPTH_TEST);

清除颜色缓冲区和深度缓冲区,深度缓冲区默认值为1

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

深度缓冲区写入开关

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

深度测试函数:定义深度测试通过的规则,默认是GL_LESS

glDepthFunc(GL_LESS);

我们可以通过 glDepthFunc(GLenum func) 来修改 深度测试的测试规则. 那么测试的规则主要在于该函数。


参数说明
GL_ALWAYS永远通过测试
GL_NEVER永远不通过测试
GL_LESS在片段深度值小于缓冲区的深度时通过测试
GL_EQUAL在片段深度值等于缓冲区的深度时通过测试
GL_LEQUAL在片段深度值小于等于缓冲区的深度时通过测试
GL_GREATER在片段深度值大于缓冲区的深度时通过测试
GL_NOTEQUAL在片段深度值不等于缓冲区的深度时通过测试
GL_GEQUAL在片段深度值大于等于缓冲区的深度时通过测试

总结

隐藏面消除的解决方案

  1. 正背面剔除:根据顶点数据顺序判断物体的正面与隐藏面,隐藏面直接丢弃,不绘制,只绘制可见部分
  2. 深度测试:开启深度测试就可以直接隐藏面消除问题,即使不使用真背面剔除。因为无论有多少图层,只显示可见图层,不可见的图层都丢弃不绘制。


附:Github完整Demo  05-OpenGL-正背面剔除与深度测试