开场白
这篇文章介绍一下正背面剔除和深度测试相关知识,demo中渲染一个甜甜圈,效果如图:
1.画个甜甜圈
1.1 甜甜圈的API
GLTools提供了API方法画甜甜圈
void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor)
- 参数1:容器辅助类,使用一个三角形批次类。
- 参数2:外边缘半径
- 参数3:内边缘半径
- 参数4、5:主半径和从半径的细分单元数量
SetupRC中的调用:
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
参数4、5:主从半径细分单元数量比例通常保持2:1
1.2 着色器设置
1.2.1 平面着色器
使用我们比较熟悉的平面着色器GLT_SHADER_FLAT:
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
来看看效果:
使用这个着色器看起来没有颜色的层次感,看来这个平面着色器不适合。
1.2.2 默认光源着色器
颜色没有层次感,也就看起来不立体。其实缺少的是光源和阴影。所以我们用默认光源着色器GLT_SHADER_DEFAULT_LIGHT看看效果:
RenderScene中代码调用:
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(),vRed);
注意:默认光源着色器需要四个参数
运行后看看效果:
完美!!!
但是,当你动的时候,你会看到:
甜甜圈好像变质了,这是什么原因?这个是我们接下来要了解的正背面剔除
2.解决甜甜圈变质-正背面剔除
2.1 变质原因:
在绘制3D场景中的物体时,我们能看到的正面是我们想看到的地方,反面是我们视角不可见的地方,但是当我们移动物体的时候,本该不可见的地方可见了,此时我们就得根据一定规则来确认和调整可视部分。
2.2 油画算法
比较简单的解决方法就是对于看不到的部分就丢弃掉,不进行渲染。这种情况叫做“隐藏面消除”。油画算法可以实现这种方式,油画算法是先绘制较远的物体,再绘制较近的物体。如图:
先绘制红色框框,再绘制黄色的圆,最后绘制带圆角的灰色框框。这样可以做到“隐藏面消除”的效果。
但是油画算法也有它的弊端,如图:
油画算法无法实现三个三角形互相叠加的效果,因为不知道哪个图形是较远的,哪个图形是较近的。简单的说,油画算法可以处理2D图形叠加情况,但无法处理3D图形。
2.3 正背面剔除
对于一个3D图形,我们该如何处理叠加情况呢?
我们从观察一个长方体来思考:
从任意方向看,最多能看到长方体的三个面,那么其他三个看不到面的数据,可以丢弃不用。这样还可以提高OpenGL的渲染效率。算下来最低可以提升50%的性能。
有些朋友会问,那当我移动物体的时候呢?
当物体每次移动时,OpenGL都会去进行每个像素的重新渲染,也就是调用RenderScene回调函数。
是中这种方式就可以解决3D图形叠加的情况。这种模式我们叫做正背面剔除法
如何确认哪些面观察者能看到?其实任何平面都有2个面,正面和北面,而我们只能在固定时间内看到一个面,要不正面,要不背面。
OpenGL是如何来确定哪个是正面的呢?
OpenGl默认规定,顶点顺序是逆时针连接的为正面,相反顺时针的情况为背面。但是这个取决于观察者的位置,所以观察者位置也是条件之一。
如上图:
当观察者在右侧的时候,看到的右边三角形为正面,而左侧的三角形为背面。当观察者在左侧的时候,则左侧为正面,右侧为背面。
简单总结:
正面和背面取决于顶点连接顺序的方向以及观察者的位置共同决定的。随着观察者位置的变化,正面和背面也是交替变化的。
2.4 代码实现
说了这么多理论,代码要如何写呢?
其实代码很简单,只要在OpenGl的状态机中开启相应的功能就可以:
//开启表面剔除
glEnable(GL_CULL_FACE);
//关闭表面剔除
glDisable(GL_CULL_FACE);
也可以用户自己选择剔除面
void glCullFace(GLenum mode);
//mode参数为: GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK
还可以修改顶点绕序。这个不建议修改,如果修改了,不被同事打4,也得被打残!
void glFrontFace(GLenum mode);
//mode参数为: GL_CW,GL_CCW,默认值:GL_CCW
工程中代码实现:
void RenderScene(void) {
...
if (iCull) {
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
} else {
glDisable(GL_CULL_FACE);
}
...
}
看看运行效果:
问题解决了,但是好像甜甜圈被要了一口!!!
3. 甜甜圈被咬一口解决方案-深度测试
先了解一些概念:
- 深度:就是3D世界中z轴方向中值。
- 深度缓冲区:一块内存区,专门存储像素点(绘制在屏幕上的)的深度值。深度值越大,说明离观察者越远。
- 使用深度缓冲区存在的意义:当先绘制离观察者比较近的物体,再绘制比较远的物体。会出现远物体覆盖近物体的情况。如果有深度信息(存放在深度缓冲区中的信息),那么就可以不用在意绘制物体的先后顺序。其实只要有深度缓冲区,OpenGL就会把像素的深度记录在缓冲区中,除非调用glDepthMask(GL_FALSE)方法,明确说明禁止写入。
通过上面的概念,我们来了解一下什么是深度测试:
深度缓冲区(DepthBuffer)和颜⾊缓存区(ColorBuffer)是对应的.颜⾊缓存区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息. 在决定是否绘制⼀个物体表⾯时, ⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较. 如果⼤于深度缓冲区中的值,则丢弃这部分.否则利⽤这个像素对应的深度值和颜⾊值.分别更新深度缓冲区和颜⾊缓存区. 这个过程称为”深度测试”
3.1开启深度测试:
glEnable(GL_DEPTH_TEST);
在绘制场景前,清除颜⾊缓存区,深度缓冲glClearColor(0.0f,0.0f,0.0f,1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
清除深度缓冲区默认值为1.0,表示最⼤的深度值,深度值的范围为(0,1)之间.
3.2指定深度测试判断模式
void glDepthFunc(GLEnum mode);
参数是枚举类型,取值有:
3.3打开/阻断 深度缓存区写⼊
void glDepthMask(GLBool value);
//value : GL_TURE 开启深度缓冲区写⼊; GL_FALSE 关闭深度缓冲区写⼊
3.4 代码实现
工程中代码实现:
void RenderScene(void) {
...
if (iDepth) {
glEnable(GL_DEPTH_TEST);
} else {
glDisable(GL_DEPTH_TEST);
}
...
}
这样展示的效果就完美了
4. demo地址
demo地址
demo中是使用右击菜单选择的方式进行深度测试和正反面剔除效果展示的,想要看指定效果就选择指定的选线即可。