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 }

Demo

posted @ 2020-07-10 00:26  张张_z  阅读(1340)  评论(0编辑  收藏  举报