OpenGL正背面剔除与深度测试
在我们绘制3D场景的时候,我们需要决定哪些部分是对观察者可见,哪些部分是不可见的。这时候,我们往往对于不可见的部分都是丢弃,不渲染的,这样可以节省性能问题。这种情况叫做“隐藏面消除(Hidden surface elimination).”
这种情况在我们从一个角度来看是没有任何问题的,例如下图:
15943013638312.png
但是当我们切换角度去观察这个物体,就会出现像下面图形一样的情况。
15943012886525.png出现这种情况的原因就是系统认为你观察不到的地方他就不去渲染,从而达到节省性能的情况,但是作为一个3D的图形,我们经常会从不同的角度去看物体的情况。那么我们如何解决这种情况呢?
解决方案一:油画算法
1、先绘制场景中离观察者较远的物体,再绘制较近的物体。
例如下面的三个图形,第三个为最终图片;先绘制最里面的红色正方形,再绘制黄色的圆形,最后绘制灰色的方块,像图层一样,一层一层渲染,跟之前写的iOS离屏渲染类似。
15943015889784.png但是,油画算法的好处很明显,但是缺点也很明显,例如下图的三个三角形。在使用油画算法绘制图形时,我们并不清楚哪个三角形是在最里面,哪个三角形是最外面的;因此又出现了问题。
15943017643483.png那么我们如何解决像这种情况的的问题呢?
下面介绍一种方法,正背面剔除(Face Culling)。
正背面剔除出现的背景:
尝试从任意一个角度去观察一个3D图形,你最多可以观察到几个面?
答案是最多是3个面。
那么我们看不到的面,OpenGL在渲染时也就将它丢弃。
那么我们在绘制图形时,就要告诉OpenGL图形的那个面是正面,那个面是背面,从而让他绘制出正面的那个样式。
那么问题又来了,我们如何告诉OpenGL你绘制的图形哪个面是正面,那个面是背面?
答案是通过分析顶点数据的顺序
分析顶点顺序
15943024293232.png正面:按照逆时针顶点连接顺序的三角形面
背面:按照顺时针顶点连接顺序的三角形
分析立方体中的正背面
15943025134827.png
分析:
左侧的三角形顶点顺序为:1->2>3;右侧三角形顶点顺序为:1->2->3.
当观察者在右侧时,则右侧的三角形方向为逆时针方向为正面,而左侧的三角形为顺时针面。
当观察者在左侧时,则相反。
总结:
正面和背面是有三角形的顶点定义顺序和观察者方向共同决定的,随着观察者的角度方向的改变,正背面也会跟着改变。
因此,在上面的甜甜圈中,我们可以开启正背面剔除来解决出现黑块的现象。
开启表⾯面剔除(默认背⾯面剔除)
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
例例如,剔除正⾯面实现(1)
glCullFace(GL_BACK);
glFrontFace(GL_CW);
例例如,剔除正⾯面实现(2)
glCullFace(GL_FRONT);
你以为使用正背面剔除问题就解决了吗?
当我们开启正背面剔除之后,在切换观察者视角时,又出现了如下图所示的情况
15943029345082.png那这是什么原因呢?
下面介绍一下深度的一些概念:
什么是深度?
深度就是该像素点在3D世界中距离摄像机的距离,Z值
什么是深度缓冲区
深度缓冲区就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值,深度值(z值)越大,则离摄像机越远。
那为什么要深度缓冲区?
在不使⽤用深度测试的时候,如果我们先绘制一个距离⽐较近的物理理,再绘制距离较远的物理,则距离 远的位图因为后绘制,会把距离近的物体覆盖掉. 有了深度缓冲区后,绘制物体的顺序就不那么重要了. 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写入到缓冲区中. 除⾮调用 glDepthMask(GL_FALSE).来禁止写入。
解决方法,深度测试
深度缓冲区(DepthBuffer)和颜色缓存区(ColorBuffer)是对应的.颜⾊缓存区存储像素的颜⾊信 息,⽽深度缓冲区存储像素的深度信息. 在决定是否绘制⼀个物体表⾯面时, 首先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏比较. 如果大于深度缓冲区中的值,则丢弃这部分.否则利用这个像素对应的深度值和颜色值.分别更新深度缓冲区和颜色缓存区. 这个过程称为”深度测试”
深度值计算:
深度值⼀一般由16位,24位或者32位值表示,通常是24位。位数越高的话,深度的精确度越好。深度值的范围在[0,1]之间,值越小表示越靠近观察者,值越大表示远离观察者。 深度缓冲主要是通过计算深度值来比较⼤小,在深度缓冲区中包含深度值介于0.0和1.0之间, 从观察者看到其内容与场景中的所有对象的 z 值进⾏了⽐较。这些视图空间中的 z 值可以在投影平头截体的近平⾯和远平面之间的任何值。我们因此需要一些方法来转换这些视图空间z值到 [0,1] 的范围内,下⾯面的 (线性) ⽅方程把 z 值转换为 0.0 和 1.0 之间的值 :
15943031388995.png
因此,我们需要利用深度测试去解决我们出现的另一种情况。
开启深度测试
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)之间. 值越⼩小表示越靠近观察者,值越⼤大表示 越远离观察者
指定深度测试判断
void glDepthFunc(GLEnum mode);
15943032588116.png
void glDepthMask(GLBool value);
value : GL_TURE 开启深度缓冲区写⼊入; GL_FALSE 关闭深度缓冲区写⼊入
当我们开启正背面剔除,深度测试之后,所存在的问题也就解决了。
下面附上完整的代码:
//演示了OpenGL背面剔除,深度测试,和多边形模式
#include "GLTools.h"
#include "GLMatrixStack.h"
#include "GLFrame.h"
#include "GLFrustum.h"
#include "GLGeometryTransform.h"
#include <math.h>
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>
#endif
//设置角色帧,作为相机
GLFrame viewFrame;
//使用GLFrustum类来设置透视投影
GLFrustum viewFrustum;
GLTriangleBatch torusBatch;
GLMatrixStack modelViewMatix;
GLMatrixStack projectionMatrix;
GLGeometryTransform transformPipeline;
GLShaderManager shadeManager;
//标记
int iCull = 0;
int iDepth = 0;
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);
}
if(iDepth)
glEnable(GL_DEPTH_TEST);
else
glDisable(GL_DEPTH_TEST);
//把摄像机矩阵压入模型矩阵中
modelViewMatix.PushMatrix(viewFrame);
//绘制颜色
GLfloat vRed[] = {1.0f,0.0f,0.0f,1.0f};
shadeManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT,transformPipeline.GetModelViewMatrix(),transformPipeline.GetProjectionMatrix(),vRed);
//绘制
torusBatch.Draw();
//出栈
modelViewMatix.PopMatrix();
//交换缓冲区
glutSwapBuffers();
}
void SetupRC(){
//设置背景颜色
glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
//设置着色管理器
shadeManager.InitializeStockShaders();
//将相机向后移动7个单元
viewFrame.MoveForward(7.0);
//4.创建一个甜甜圈
//void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
//点的大小
glPointSize(4.0f);
}
void ChangeSize(int w,int h){
//1、防止h变为0
if(h == 0)
h = 1;
//2、设置视口窗口尺寸
glViewport(0,0,w,h);
//3、函数的参数是从一个顶点方向去看的视场角度
viewFrustum.SetPerspective(35.0f,float(w)/float(h),1.0f,100.0f);
//4、把透视矩阵加载到透视矩阵对阵中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
//5、初始化渲染管线
transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
}
void SpecialKeys(int key,int x,int y){
if(key == GLUT_KEY_UP)
viewFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_DOWN)
viewFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_LEFT)
viewFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
if(key == GLUT_KEY_RIGHT)
viewFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
glutPostRedisplay();
}
void ProcessMenu(int value){
switch (value) {
case 1:
iDepth =!iDepth;
break;
case 2:
iCull =!iCull;
break;
case 3:
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
break;
case 4:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
break;
case 5:
glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
break;
}
glutPostRedisplay();
}
int main(int argc,char* argv[]){
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
glutInitWindowSize(800, 600);
glutCreateWindow("Geometry Test Program");
glutReshapeFunc(ChangeSize);
glutSpecialFunc(SpecialKeys);
glutDisplayFunc(RenderScene);
//添加右键菜单
glutCreateMenu(ProcessMenu);
glutAddMenuEntry("深度测试", 1);
glutAddMenuEntry("正背面剔除", 2);
glutAddMenuEntry("Set Fill Mode", 3);
glutAddMenuEntry("Set Line Mode", 4);
glutAddMenuEntry("Set Point Mode", 5);
glutAttachMenu(GLUT_RIGHT_BUTTON);
GLenum err = glewInit();
if(GLEW_OK != err){
fprintf(stderr, "GLEW Error : %s\n",glewGetErrorString(err));
return 1;
}
SetupRC();
glutMainLoop();
return 0;
}