OpenGL 四 - 001、OpenGL 图形渲染 - 隐藏面消除-正背面剔除-深度测试
代码 or demo查看请滑动到最后。
一、问题场景:
在 3D 图形的渲染过程中,我们是需要来决定哪部分是要对观察者 可见/不可见 的,对于不可见的部分,我们就没有渲染的必要了,要及早丢弃掉他们。例子:一间草屋,我们站在门前的时候,草屋的背后我们是看不到的,那么就不要渲染它了。否则就会出现下图中的场景(我们绘制一个立体图形后 对其进行旋转操作查看时,背面我们本应是看不到的面也被看到了),叫做“隐藏面消除”(Hidden surface elimination)。 (图中的)
背面为什么黑色呢?光源着色器,想象一下我们在太阳光下,光打下来,朝阳和背阳的两个场景下,是不是一面亮一面暗呢。
二、解决方案
1:油画算法(过时且浪费性能-重复渲染)
先绘制画面中距离观察者远的物体,再绘制较*的物体。由远及*,如下图,远的会被*物遮住。
但是如果是下面这种场景,油画算法就无法解决了:
这种每个面都要看到部分的场景怎么办呢?
2、正背面剔除(Face Culling)
背景:
想象一个 3D 图形,从任何一个⽅向去观察,我们最多可以看到⼏个⾯? 最多3面。从⼀个⽴方体的任意位置和⽅向上看,最多不可能看到多于3个面。那么,为何还要多余的去绘制那些根本看不到的3个面呢? 如果能以某种⽅式去丢弃这部分数据, OpenGL 在渲染的性能就可以提高超过 50% 呢。
问题分析:
如何知道某个⾯在观察者的视口中不会出现? 任何*⾯都有2个面:正⾯/背面。这意味着我们在一个时刻只能看到一⾯。
OpenGL 可以做到检查所有正面朝向观察者的面 并渲染它们,⽽丢弃背⾯朝向的面,这样可以节约片元着⾊器的性能。
OpenGL 如何知道我们绘制的图形哪是正面呢?可通过分析顶点数据的顺序。
分析顶点数据:
1)正背面区分:OpenGL 中 按逆时针进行顶点相连的三角形面为 正面,顺时针相连的三角形面为 背面 -- 规则如此
2)立方体的正背面
眼睛在右侧时,右边顶点按逆时针顺序,为正面,左侧为顺时针背面;眼睛在左侧时,则左侧为正面。
归结:
正面和背面是由三角形的顶点定义顺序鹅观察者方向共同决定的,随着观察者观察角度的改变,正背面也会跟着改变。
解决操作:
a、开启表面剔除(默认背面剔除)
void glEnable(GL_CULL_FACE);
b、关闭表⾯剔除(默认背面剔除)
void glDisable(GL_CULL_FACE);
c、用户选择剔除哪个面(正面/背面)
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);
我们对背面进行剔除后,图形好像正常了,但是,旋转到一定角度,发现:
这又是怎么回事儿呢?
下图中,当我们旋转到这个角度继续旋转时,此时A、B面都是正面,但继续旋转至A、B面重合,此时OpenGL无法区分谁才是正面应该也无法确定要显示A or B,因而出现了上图的场景。
我们应该怎么在解决隐藏面消除的同时,告诉OpenGL我们要显示谁呢?
3、Z-buffer 方法(深度缓冲区Depth-buffer)
1)深度
a、深度:像素点在3D世界中,距离观察者(眼睛)的距离 -- Z值(这个值是指物体的Z值)
b、深度缓冲区:存储在显存值的一块区域,它主要就存储每个像素点的深度值,Z绝对值越大,说明距离观察者越远 -- 想象我们的现实世界。
Z值的绝对值:因为我们可能在Z轴的负轴。负值(负数)越小对应的绝对值(正数)越大。
c、深度缓冲区用来做什么呢?
想象一个场景:当我们绘制一幅草屋图时,不按远*顺序而是随意绘制,若先画了*处场景(门前的栏杆)再画远处场景(大门),那么此时在我们的画中大门就在栏杆的前面了,这是不符合逻辑的。想象下在现实生活中,如果眼前远景和*景无法分辨产生了混乱,我们眼前的世界就光怪陆离了。
有了深度,我们就可以知道每个点距离我们眼睛的远*,进而也就知道当2处重叠时我们应该看见的是哪一部分了 --> 对比到OpenGL上,有了深度缓冲区,知道了深度值,绘制时也就不担心绘制顺序和渲染谁了。 OpenGL中,只要存在深度缓存区(缓存==缓冲 名字而已),OpenGL都会把像素的深度值写入进去。除非手动禁止写入:glDepthMask(GL_FALSE)。(深度缓冲区中针对某个像素点 只存一个对应值,重叠时我们需要知道应该存哪个值)
2)深度测试
a、什么是深度测试?DepthBuffer(深度缓冲区)和ColorBuffer(颜色缓冲区)是一一对应的,我们知道颜色缓冲区是存储像素的颜色信息,深度缓冲区是存储像素的深度信息。在决定是否要绘制某个物体表面时,首先要将表面对应的像素深度(A)与深度缓冲区中的深度值(B)进行对比,A>B则丢弃掉A对应的表面不绘制;A<B,则拿出这个像素对应的 深度值和颜色值,对应的分别去更新深度缓冲区和颜色缓冲区。这个过程称为“深度测试”。
b、深度测试的使用
开启:glEnable(GL_DEPTH_TEST) -- 深度测试的值默认1,范围[0,1]
绘制开始前,首先清空缓冲区(颜色和深度都要清):glClear()GL_COLEO_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
深度测试的模式 mode:
关闭:glDisable(GL_DEPTH_TEST)
开启深度测试后:我的圈圈终于正常了,它是不是不会再出问题了呢?
开启深度测试也是存在风险的
4、ZFlighting 闪烁问题 - (深度测试的潜在风险)
1)产生场景:受限于设备的精度,当我们的绘制精度 大于设备所支持精度就会出现。(根本原因:精度。目前的设备上较少会出现)
原因:开启深度测试后,OpenGL 就不会绘制被遮挡的部分了。此时绘制的图形已符合我们的现实场景了,但是,由于深度缓存区精度的限制,当出现两个像素的深度值相差很小(例子:0.0000005/0.0000007)时 --> OpenGL可能无法正确判断2个值大小 --> 深度测试的结果不确定性 --> 画面交错闪烁(AB的重叠绘制 可能出现随机绘制 - 一会儿A一会儿B)。如下图白框中的画面
2)解决办法
导致问题的原因既然是因为2个深度值相差太小了,那我们就想办法把2个值偏差的大点 --> 深度测试前,将两个深度值 手动做下细微的修改 使之可以区分 --> 手动复杂又不精确 --> OpenGL提供了办法:多边形偏移 -- 使产生 ZFlighting 的图层产生一个的偏移,即深度值间产生一个间隔。
a、启用多边形偏移:
glEnable(GL_POLYGON_OFFSET_FILL)
GL_POLYGON_OFFSET_FILL:对应光栅化的 GL_FILL
GL_POLYGON_OFFSET_LINE:对应光栅化的 GL_LINE
GL_POLYGON_OFFSET_POINT:对应光栅化的 GL_POINT
b、指定偏移量:
glPolygonOffset(Glfloat factor,Glfloat units)
深度:Depth offset = DZ * factor + r * units // 负值,使得模型距离观察者更*;正值,将使得模型距离观察者更远
DZ:多边形的深度斜率 最大值
r:使深度缓冲区产生变化的最小值,即可分辨的最小差异值
这里我们只需传 factor 和 units 两个值给 glPolygonOffset 即可:一般传入-1, -1
c、关闭
glDisable(GL_POLYGON_OFFSET_FILL)
3)预防
a、不要将2个物体放太*,避免渲染时重叠。这种方式要求对场景中物体插入一个少量的偏移,自然这个操作是要付出一定代价的。
b、将裁剪面设计的距离观察者远些,上⾯我们看到,在*裁剪*⾯附*,深度的精确度是很高的,因此尽可能让*裁剪⾯远一些的话,会使整个裁剪范围内的精确度变高一些。但是这种⽅式会使离观察者较*的物体被裁减掉,因此需要调试好裁剪⾯参数。
c、使用高位数的深度缓存区。通常使用的深度缓存区是24位的,现在有一些使用32位的缓冲区的硬件设备,使精度提高。
三、主要代码
压栈:记录临时状态 保存一下下 - 状态的回滚
出栈:恢复初始状态 -- 不需要了 记录的数据就可以扔掉,避免对后面的渲染产生异常影响 ---> 有点类似 Git 的 stash 命令
1 GLBatch triangleBatch; 2 3 GLShaderManager shaderManager; 4 5 // 设置角色帧,作为相机 6 GLFrame viewFrame;// 观察者位置 7 GLFrustum viewFrustum;// 透视投影 - GLFrustum类 8 GLTriangleBatch torusBatch; 9 // 两个矩阵 10 GLMatrixStack modelViewMatix;// 模型矩阵 11 GLMatrixStack projectionMatrix;// 透视矩阵 12 // 13 GLGeometryTransform transformPipeline;// 几何转换 14 15 16 // 定义字段判断 正背面剔除/深度测试 开启与否 17 int iCull = 0; 18 int iDepth = 0; 19 20 21 // 右键菜单栏选项 开启关闭深度测试 22 void ProcessMenu(int value) { 23 24 switch(value) 25 { 26 case 1: 27 iCull = !iCull; 28 break; 29 30 case 2: 31 iDepth = !iDepth; 32 break; 45 } 46 47 glutPostRedisplay(); 48 } 49 50 // 键盘上下左右 事件 51 // 观察者移动 物体不动 52 void SpecialKeys(int key, int x, int y) { 53 54 if(key == GLUT_KEY_UP) 55 viewFrame.RotateWorld(m3dDegToRad(-5.0), 1.0f, 0.0f, 0.0f); 56 57 if(key == GLUT_KEY_DOWN) 58 viewFrame.RotateWorld(m3dDegToRad(5.0), 1.0f, 0.0f, 0.0f); 59 60 if(key == GLUT_KEY_LEFT) 61 viewFrame.RotateWorld(m3dDegToRad(-5.0), 0.0f, 1.0f, 0.0f); 62 63 if(key == GLUT_KEY_RIGHT) 64 viewFrame.RotateWorld(m3dDegToRad(5.0), 0.0f, 1.0f, 0.0f); 65 66 //重新刷新window 67 glutPostRedisplay(); 68 } 69 70 // 初始化 设置 71 void SetupRC() { 72 73 // ================ 简单绘制一个立体的圈圈 ================= 74 // 设置背景颜色 75 glClearColor(0.0f, 0.3f, 0.3f, 1.0f ); 76 77 //初始化着色器管理器 78 shaderManager.InitializeStockShaders(); 79 80 //将相机向后移动7个单元:肉眼到物体之间的距离 81 viewFrame.MoveForward(7.0); 82 83 //创建一个圈 84 //void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor); 85 //参数1:GLTriangleBatch 容器帮助类 86 //参数2:外边缘半径 87 //参数3:内边缘半径 88 //参数4、5:主半径和从半径的细分单元数量 89 gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26); 90 //点的大小 91 glPointSize(4.0f); 117 } 118 119 120 // 视口 窗口大小改变时接受新的宽度和高度,其中0,0代表窗口中视口的左下角坐标,w,h代表像素 121 void ChangeSize(int w,int h) { 122 // 防止h变为0 123 if(h == 0) 124 h = 1; 125 126 // 设置视口窗口尺寸 127 glViewport(0, 0, w, h); 128 129 // setPerspective 函数的参数是一个从顶点方向看去的视场角度(用角度值表示) 130 // 设置透视模式,初始化其透视矩阵 131 viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f); 132 133 //4.把 透视矩阵 加载到 透视矩阵对阵中 134 projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix()); 135 136 //5.初始化渲染管线 137 transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix); 138 } 139 140 // 渲染 141 void RenderScene(void) { 142 143 //清除窗口和深度缓冲区 144 // 145 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 146 147 148 // iCull 判断是否开启背面剔除 GL_CULL_FACE 149 if(iCull) 150 { 151 glEnable(GL_CULL_FACE); 152 glFrontFace(GL_CCW); 153 glCullFace(GL_BACK); 154 } 155 else 156 glDisable(GL_CULL_FACE); 157 158 // iDepth 判断是否开启深度测试 GL_DEPTH_TEST 159 if(iDepth) 160 glEnable(GL_DEPTH_TEST); 161 else 162 glDisable(GL_DEPTH_TEST); 163 164 //把摄像机矩阵压入模型矩阵中 入栈 165 modelViewMatix.PushMatrix(viewFrame); 166 167 GLfloat vRed[] = { 0.0f, 0.5f, 0.5f, 1.0f };// 画笔颜色 168 169 //使用默认光源着色器 170 //通过光源、阴影效果跟提现立体效果 171 //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器 172 //参数2:模型视图矩阵 173 //参数3:投影矩阵 174 //参数4:基本颜色值 175 shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed); 176 177 //绘制 178 torusBatch.Draw(); 179 180 //出栈 181 modelViewMatix.PopMatrix(); 182 glutSwapBuffers(); 205 } 206 207 int main(int argc,char* argv[]) { 208 209 //设置当前工作目录,针对MAC OS X 210 211 gltSetWorkingDirectory(argv[0]); 212 213 //初始化GLUT库 214 215 glutInit(&argc, argv); 216 /* 初始化双缓冲窗口,其中标志GLUT_DOUBLE、GLUT_RGBA、GLUT_DEPTH、GLUT_STENCIL分别指 217 双缓冲窗口、RGBA颜色模式、深度测试、模板缓冲区 218 */ 219 glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL); 220 221 //GLUT窗口大小,标题窗口 222 glutInitWindowSize(800,600); 223 glutCreateWindow("Triangle"); 224 225 //注册回调函数 226 glutReshapeFunc(ChangeSize); 227 glutDisplayFunc(RenderScene); 228 glutSpecialFunc(SpecialKeys);// 注册键盘特殊键位(上下左右键) 处理点击事件 229 230 // Create the Menu 231 glutCreateMenu(ProcessMenu);// 右键菜单栏选项 开启关闭深度测试 232 glutAddMenuEntry("Toggle cull backface",1);// 正背面剔除 233 glutAddMenuEntry("Toggle depth test",2);// 深度测试的开启与否 234 235 glutAttachMenu(GLUT_RIGHT_BUTTON); 236 239 //驱动程序的初始化中没有出现任何问题。 240 GLenum err = glewInit(); 241 if(GLEW_OK != err) { 242 243 fprintf(stderr,"glew error:%s\n",glewGetErrorString(err)); 244 return 1; 245 } 246 247 //调用SetupRC 248 SetupRC(); 249 250 glutMainLoop(); 251 252 return 0; 253 }