一、绘制一个甜甜圈
首先通过下面一个案例引入今天要讨论的问题
环境搭建在之前的博客 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默认规定按照逆时针顶点连接顺序的三⻆形⾯是正面,按照顺时针顶点连接顺序的三⻆形⾯是背面,当然也可以修改为顺时针没正面,但不建议这么修改(觉大多数人或项目都适用默认的规则,如果你偏要修改岂不是显得很另类)
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进⾏关联以及存储;
为什么需要深度缓冲区
深度缓冲区原理
深度缓冲区(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 | 在片段深度值大于等于缓冲区的深度时通过测试 |
总结
隐藏面消除的解决方案
- 正背面剔除:根据顶点数据顺序判断物体的正面与隐藏面,隐藏面直接丢弃,不绘制,只绘制可见部分
- 深度测试:开启深度测试就可以直接隐藏面消除问题,即使不使用真背面剔除。因为无论有多少图层,只显示可见图层,不可见的图层都丢弃不绘制。
附:Github完整Demo 05-OpenGL-正背面剔除与深度测试