在绘制3D场景的时候,我们需要决定哪一部分对于观察者来说是可见,哪些是不可见,这样才可以做到隐藏面消除(Hidden surface elimination),同时应为渲染的东西少了,还可以提高性能。
先看看下面的一张图情况:
出现上面的情形,应该如何解决呢?
有人可能会推荐使用油画算法~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 油画算法:先绘制场景中的离观察者较远的物体,再绘制较近的物体
先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分,即可解决隐藏面消除的问题
但是这种解决会衍生出弊端:就是如果三个三角形时叠加的情况,油画算法就无法按照物理具体观察者的距离远近进行排序,进而也就无法处理。因此我们不使用油画算法,考虑使用正面剔除(Face Culling)
正面剔除(Face Culling)
背景:
一个3D图形,从任何一个方向去观察,最多能看到3个面,既然这样,我们可以舍去看不到的面,丢弃这部分的数据,不需要渲染
解决:
- 任何平面都有两个面(正面/反面),而OpenGL可以做到检查所有正面朝向观察者的面,并渲染它们,从而丢弃背面朝向的面
区分
如何知道哪个面是正面,哪个面是背面呢?
其实OpenGL中是通过分析顶点数据的顺序进行判断的
正面:按照逆时针顶点链接顺序的三角形面
反面:按照顺时针顶点连接顺序的三角形面
例子分析
左侧三角形顶点顺序为:1->2->3;右侧三角形的顶点顺序为:1->2->3.
当观察者在右侧时,则右边的三角形方向为逆时针方向则为正面,而左侧三角形为顺时针的则为反面。
当观察者在左侧时,则左边的三角形为逆时针方向判断为正面,而右侧的三角形为顺时针则判定为背面
总结:正面和背面是有三角形的顶点定义顺序和观察者方向共同决定的,随着观察者的角度方向的改变,正面背面也会跟着改变。
解决上述例子的方法:
//开启、关闭正背面剔除功能
if (iCull) {
//开启正背面剔除
glEnable(GL_CULL_FACE);
//指定那个面为正面
glFrontFace(GL_CCW);
//剔除哪个面
glCullFace(GL_BACK);
}else{
glDisable(GL_CULL_FACE);
}
但是可以看到出现了新的问题,这个时候应该怎么处理呢?
在解决这个问题之前,我们先引入一个概念叫做深度。
什么是深度?
深度其实就是该像素点在3D世界中距离摄像机的距离Z值
什么是深度缓冲区
深度缓存区,就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值,深度值(Z值)越大,则离摄像机就越远。
为什么需要深度缓冲区
在使用深度测试的时候,如果我们先绘制一个距离比较近的物体,再绘制距离较远的物体,则距离远的位图会后绘制,会把距离近的物体覆盖掉,有了深度缓冲区后,绘制物体的顺序就不那么重要的,实际上,只要存在深度缓冲区,OpenGL都会把像素的深度值写入到缓冲区中,除非调用glDepthMask(GL_FALSE)来禁止写入。
在renderScene() 方法中加入深度测试代码
//根据设置iDepth标记来判断是否开启深度测试
if (iDepth) {
glEnable(GL_DEPTH_TEST);
}else{
glDisable(GL_DEPTH_TEST);
}
这样,就可以解决了上面出现的情形。有兴趣的可以继续往下看看深度的一些相关拓展知识点
深度拓展知识
Z-buffer 方法
深度测试
深度缓冲区(DepthBuffer)和颜色缓存区(ColorBuffer)是对应的,颜色缓存区存储像素的颜色信息,而深度缓冲区存储像素的深度信息,在决定是否绘制一个物体表面时,首先要将表面对应的像素的深度值与当前深度缓冲区中的值进行比较。如果大于深度缓冲区中的值,则丢弃这部分。否则利用这个像素对应的深度值和颜色值。分别更新深度缓冲区和颜色缓存区。这个过程称为"深度测试"
深度值计算
深度值一般由16位,24位或者32位值表示,位数越高的话,深度的精确度越好。深度值的方位在[0,1]之间,值越小表示越靠近观察者,值越大表示远离观察者。
-
深度缓冲主要是通过计算深度值来比较大小,在深度缓冲区中包含深度值介于0.0和1.0之间,从观察者看到其内容与场景中的所有对象的z值进行了比较。这些视图空间中的z值可以在投影平头截体的近平面和远平面之间的任何值,我们因此需要⼀一些⽅方法来转换这些视图空间 z 值 到 [0,1] 的范围内,下⾯面的 (线性) ⽅方程把 z 值转换为 0.0 和 1.0 之间的值 :
far 和near 是提供到投影矩阵设置可见视图截锥的远近值
-
⽅方程带内锥截体的深度值 z,并将其转换到 [0,1] 范围。在下⾯面的图给出 z 值和其相应的深度值的 关系:
在实践中是可以减少使用这样的线性深度缓冲区。正确的投影特性的非线性深度方程是和1/z成正比的,由于非线性函数是和1/z成正比的,例如1.0和2.0之间的z值,将变为1.0到0.5之间,这样在z非常小的时候给我我们很高的精度,方程式如下:
上面就是非线性深度缓存,线性关系图如下:
总结 屏幕空间的深度值是⾮非线性如他们在z很⼩小的时候有很⾼高的精度,较⼤大的 z 值有较低的精度。该⽚片段的深度值会迅速增加,所以⼏几乎所 有顶点的深度值接近 1.0。如果我们⼩小⼼心的靠近物体,你最终可能会看到的⾊色彩越来越暗,意味着它们的 z 值越来越⼩小,这清楚地表明 深度值的⾮非线性特性。近的物体相对远的物体对的深度值⽐比对象较⼤大的影响。只移动⼏几英⼨寸就能让暗⾊色完全变亮。
但是我们可以让深度值变换回线性。要实现这⼀一⽬目标我们需要让点应⽤用投影变换逆的逆变换,成为单独的深度值的过程。这意味着我 们必须⾸首先新变换范围 [0,1] 中的深度值为单位化的设备坐标(normalized device coordinates)范围内 [-1,1] (裁剪空间(clip space))。然后,我们想要反转⾮非线性⽅方程 :
- 指定深度测试判断模式
- 指定深度测试判断模式
void glDepthFunc(GLEnum mode);
函数 | 链接 |
---|---|
GL_ALWAYS | 总是通过测试 |
GL_NEVER | 总是不通过测试 |
GL_LESS | 在当前深度值<存储的深度值时通过 |
GL_EQUAL | 在当前深度值 = 存储的深度值时通过 |
GL_LEQUAL | 在当前深度值<= 存储的深度值时通过 |
GL_GREATER | 在当前深度值> 存储的深度值时通过 |
GL_NOTEQUAL | 在当前深度值不等于存储的深度值时通过 |
GL_GEQUAL | 在当前深度值>= 存储的深度值时通过 |
- 打开、阻断 深度缓存区写入
void glDepthMask(GLBool value);
value:GL_TRUE 开启深度缓冲区写入;GL_FALSE关闭深度缓冲区写入
有了正背面剔除,深度缓冲区和深度测试可以很好的避免了上面的问题,但是有时候还是会出现闪烁问题的情况,如下图:
因为开启深度测试后,OpenGL就不会再去绘制模型被遮挡的部分,这样实现就会显得更加真实,但是由于深度缓冲区的精度读对于深度相差非常小的情况下,例如在同一平面上精心2次绘制,OpenGL就可能出现不能正确判断两者的深度值,会知道深度测试的结果不可预测,显示出来的现象交错闪烁。
解决问题的方案
- 启用Polygon Offset方式解决
- 让深度值之间产生间隔,如果2个图形之间有间隔,是不是意味着就不会产生干涉,可以理解为在执行深度测试前将立方体的深度值做一些细微的增加。于是就能将重叠的2个图形深度之前有所区分。
/启⽤用Polygon Offset ⽅方式 glEnable(GL_POLYGON_OFFSET_FILL)
参数列列表: GL_POLYGON_OFFSET_POINT GL_POLYGON_OFFSET_LINE GL_POLYGON_OFFSET_FILL
对应光栅化模式: GL_POINT 对应光栅化模式: GL_LINE
对应光栅化模式: GL_FILL
- 指定偏移量
- 通过glPolygonOffset来指定,glPolygonOffset需要2个参数:factor,units
- 每个Fragment的深度值都会增加如下所示的偏移量:
Offset = (m * factor) + (r * units);
m:多边形的深度的斜率的最大值,理解一个多边形越是与近裁剪面平行,m就越接近于0
r:能产生于窗口坐标系的深度值中可分辨的差异最小值,r是由具体OpenGL平台指定的一个常量
- 一个大于0的Offset会把模型推到距离摄像机更远的位置,相应的一个小于0的Offset会把模型拉近
- 一般而言,只需要将-1.0和-1 这样简单复制给glPolygonOffset基本就可以满足需求
void glPolygonOffset(Glfloat factor,Glfloat units);
应⽤用到⽚片段上总偏移计算⽅方程式:
Depth Offset = (DZ * factor) + (r * units); DZ:深度值(Z值)
r:使得深度缓冲区产⽣生变化的最⼩小值
负值,将使得z值距离我们更更近,⽽而正值,将使得z值距离我们更更远, 对于上节课的案例例,我们设置factor和units设置为-1,-1
- 关闭Polygon Offset
glDisable(GL_POLYGON_OFFSET_FILL)
ZFighting 闪烁问题预防
- 不不要将两个物体靠的太近,避免渲染时三⻆角形叠在⼀一起。这种⽅方式要求对场景中物体插⼊入⼀一个少量量的 偏移,那么就可能避免ZFighting现象。例例如上⾯面的⽴立⽅方体和平⾯面问题中,将平⾯面下移0.001f就可以解决这个问题。当然⼿手动去插⼊入这个⼩小的偏移是要付出代价的。
- 尽可能将近裁剪⾯面设置得离观察者远⼀一些。上⾯面我们看到,在近裁剪平⾯面附近,深度的精确度是很⾼高 的,因此尽可能让近裁剪⾯面远⼀一些的话,会使整个裁剪范围内的精确度变⾼高⼀一些。但是这种⽅方式会使
• 离观察者较近的物体被裁减掉,因此需要调试好裁剪⾯面参数。 使⽤用更更⾼高位数的深度缓冲区,通常使⽤用的深度缓冲区是24位的,现在有⼀一些硬件使⽤用使⽤用32位的缓冲 区,使精确度得到提⾼高
裁剪
在OpenGL中提高渲染的一种方式。只刷新屏幕上发生变化的部分.OpenGL 允许将要进行渲染的窗口只去指定一个裁剪框
基本原理:用于渲染时限制绘制区域,通过此技术可以在屏幕(幁缓冲)指定一个矩形区域,启用裁剪测试之后,不在此矩形区域内的片元被丢弃,只有在此矩形区域内的片元才有可能进入幁缓冲。因此实际达到的效果就是在屏幕上开辟了一个小窗口,可以再其中进行指定的内容绘制
//1 开启裁剪测试
glEnable(GL_SCISSOR_TEST);
//2.关闭裁剪测试
glDisable(GL_SCISSOR_TEST);
//3.指定裁剪窗⼝口
void glScissor(Glint x,Glint y,GLSize width,GLSize height);
x,y:指定裁剪框左下⻆角位置; width , height:指定裁剪尺⼨寸
窗口,视口,裁剪区域
- 窗口:就是显示界面
- 视口:就是窗口中用来显示图形的一块矩形区域,它可以和窗口等大,也可以比窗口或者小,只有绘制在视口区域中的图形才能被现实,如果图形有一部分超出了视口区域,那么哪一部分是看不到的,通过glViewport()函数设置
-
裁剪区域(平行投影):就是视图矩形区域的最小最大x坐标(left,right)和最小最大y坐标(bottom,top),而不是窗口的最小最大X坐标和y坐标,⽽不是窗⼝口的最⼩小最⼤大x坐标和y坐标。通过glOrtho()函数设置,这个函数还需指定最近 最远z坐标,形成⼀一个⽴立体的裁剪区域。
混合
我们把OpenGL 渲染时会把颜⾊色值存在颜⾊色缓存区中,每个⽚片段的深度值也是放在深度缓冲区。当深度 缓冲区被关闭时,新的颜⾊色将简单的覆盖原来颜⾊色缓存区存在的颜⾊色值,当深度缓冲区再次打开时,新 的颜⾊色⽚片段只是当它们比原来的值更更接近邻近的裁剪平⾯面才会替换原来的颜⾊色⽚片段。
glEnable(GL_BIEND);
组合颜色
- 目标颜色:已经存储在颜色缓存区的颜色值
- 源颜色:作为当前渲染命令结果进入颜色缓存区的颜色值
当混合功能被启动时,源颜色和目标颜色的组合方式是混合方程式控制的。在默认情况下,混合方程式如下所示:
Cf = (Cs * S) + (Cd * D) - Cf:最终计算参数的颜色
- Cs:源颜色
- Cd:目标颜色
- S: 源混合因子
- D: 目标混合因子
设置混合因子
设置混合因子,需要用到glBlendFun函数
glBlendFunc(GLEnum S,GLenum D);
S:源混合因子
D:目标混合因子
表中R,G,B,A分别代表红、绿、蓝、alpha,其中下标S,D分表代表源,目标,C代表常量颜色(默认黑色)
该表组合方程式
默认混合方程式:
Cf = (Cs * S) + (Cd * D)
可以选择的混合方程式函数:
- glBlendFuncSeparate 函数
除了了能使⽤用glBlendFunc 来设置混合因⼦子,还可以有更更灵活的选择。
void glBlendFuncSeparate(GLenum strRGB,GLenum dstRGB ,GLenum strAlpha,GLenum dstAlpha);
strRGB: 源颜⾊色的混合因⼦子 dstRGB: ⽬目标颜⾊色的混合因⼦子 strAlpha: 源颜⾊色的Alpha因⼦子 dstAlpha: ⽬目标颜⾊色的Alpha因⼦子
glBlendFuncSeparate 注意
- glBlendFunc 指定 源和⽬目标 RGBA值的混合函数;但是glBlendFuncSeparate函数则允许为RGB 和 Alpha 成分单独指定混合函数。
- 在混合因⼦子表中GL_CONSTANT_COLOR,GL_ONE_MINUS_CONSTANT_COLOR,GL_CONSTANT_ALPHA,GL_ONE_MINUS_CONSTANT值允许混合⽅方程式中引⼊入一个常量量混合颜⾊色。
常量混合颜色
常量量混合颜⾊色,默认初始化为⿊黑⾊色(0.0f,0.0f,0.0f,0.0f),但是还是可以修改这个常量量混合颜 ⾊色。
void glBlendColor(GLclampf red ,GLclampf green ,GLclampf blue ,GLclampf alpha );