问题
用GLTools中的自带函数gltMakeTorus绘制一个红色的甜甜圈,然后用点光源着色器进行渲染渲染。当让甜甜圈进行旋转后就会出现有黑色块状的问题,如下图:
出现这种问题的原因是,甜甜圈是一个立体图形,有正面和背面。我们用点光源着色器,正常情况下,正面被渲染成红色,背面为黑色。在甜甜圈旋转的过程中,正面依次变成了背面,而背面依次变成了正面,正背面交织在一起,就出现了有红色有黑色的情况。
解决方案1:油画算法
在绘制物体时,先绘制离观察者较远的部分,再绘制较近的部分,这种绘制方式叫做油画算法。通过油画算法可以有效解决正背面交织显示的问题。
但是油画算法存在弊端:
- 耗费性能:由于是由远至近的绘制,导致所有景物都必须绘制一遍,而远处物体被近处物体所遮挡住的部分,其实是不需要绘制的。比如上图中山体被数目遮挡的部分,要先绘制成山体,再绘制成树木,就是一种性能浪费。
- 无法绘制:多个物体在z轴上相互交错时,无法确定先后绘制顺序,如下图
解决方案2:正背面剔除
一个3D物体,我们从任何一个方向去观察,都是只能看到它的一部分表面。对于看不到的表面,其实是没有必要进行绘制的。这种只绘制看得到的部分而剔除掉看不到的部分的绘制方式叫做正背面剔除。
在OpenGL中,所绘制的图形是通过一个个的三角形组合成的。OpenGL通过分析顶点数据的顺序得知哪些面是正面、哪些面是背面。默认规则:
- 正面:按照逆时针顶点连接顺序的三角形面。
- 背面:按照顺时针顶点连接书序的三角形面。
上图中,左侧三角形顶点顺序为1->2->3,右侧三角形顶点顺序为1->2->3。
- 当观察者在右侧时:坐侧三角形为顺时针方向,判定为背面;右侧三角形为逆时针方向,判定为正面。
- 当观察者在左侧时:左侧三角形为逆时针方向,判定为正面;右侧三角形为顺时针方向,判定为背面。
- 总结:正面和背面是由三角形的顶点顺序和观察者方向共同决定的,随着观察者的角度变化,正背面也会跟着改变,这也与现实中的情况相吻合。
//开启正背面剔除
glEnable(GL_CULL_FACE);
//关闭正背面剔除
glDisable(GL_CULL_FACE);
/*
选择剔除哪个面(默认背面剔除)
GL_FRONT:正面
GL_BACK:背面
GL_FRONT_AND_BACK:正面和背面
*/
glCullFace(GL_BACK);
/**
指定哪个顶点顺序为正面,默认GL_CCW
GL_CW:顺时针为正面
GL_CCW:逆时针为正面
*/
glFrontFace(GL_CCW);
正/背面的判断方式尽量以默认方式为准,不要自己去修改,防止混乱。
解决方案3:深度测试
开启正背面剔除后,有效解决了红、黑两色交错显示的问题,但却仍然有问题。
当圈圈沿着Y轴旋转到左右两边的圈圈重合时,重合处会出现一个缺口。如图中显示,甜甜圈的左边向屏幕里面移动,右边向屏幕外面移动,当左右两边重合时,左边圈圈的内测和右边圈圈的外侧,都面向观察者,所以都被判定为正面而需要显示。绘制重叠就造成了图中的缺口景象。要解决这个问题,需要用到新的技术,深度测试。
- 深度:深度其实就是该像素点在3D世界中距离观察者的距离。对于屏幕上的每个像素点爱说,深度缓存区都会记录场景中的物体与视点在这个像素上的距离信息。需要注意,观察者可能在z轴的正方向和负方向,所以不能简单地认为z值越大深度就越大。
- 深度缓冲区:就是⼀块内存区域,专⻔存储着每个像素点的深度值。
- 为什么需要深度缓冲区:在不使⽤深度测试的时候,如果我们先绘制⼀个距离⽐较近的物体,再绘制距离较远的物体,则距离远的位图因为后绘制,会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。实际上,只要存在深度缓冲区,OpenGL都会把像素的深度值写⼊到缓冲区中。除⾮调用
glDepthMask(GL_FALSE)
来禁⽌写⼊。 - 深度测试:深度缓冲区(DepthBuffer)和颜⾊缓冲区(ColorBuffer)是对应的。颜⾊缓冲区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息。在决定是否绘制⼀个物体表⾯时,⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较。如果⼤于深度缓冲区中的值,则丢弃这部分。否则利⽤这个像素对应的深度值和颜⾊值,分别更新深度缓冲区和颜⾊缓存区。这个过程称为深度测试。
- 深度值计算:深度值⼀般由16位、24位或32位值表示,通常是24位。位数越⾼的话,深度的精确度越好。深度值的范围在[0,1]之间,值越⼩表示越靠近观察者,值越⼤表示远离观察者。
深度测试中,默认是:新深度值 < 老深度值,则进行替换。但也有其他规则供开发者调用glDepthFunc来修改:
参数 | 说明 |
---|---|
GL_ALWAYS | 总是通过测试 |
GL_NEVER | 总是不通过测试 |
GL_EQUAL | 在当前深度值 = 存储的深度值时通过 |
GL_NOTEQUAL | 在当前深度值 != 存储的深度值时通过 |
GL_LESS | 在当前深度值 < 存储的深度值时通过 |
GL_LEQUAL | 在当前深度值 <= 存储的深度值时通过 |
GL_GREATER | 在当前深度值 > 存储的深度值时通过 |
GL_GEQUAL | 在当前深度值 >= 存储的深度值时通过 |
//开启深度测试
glEnable(GL_DEPTH_TEST);
//关闭深度测试
glDisable(GL_DEPTH_TEST);
//开启/关闭深度缓冲区写入。默认开启,当关闭时,开启深度测试会无效,因为深度值数据无法写入缓冲区。
glDepthMask(GL_TRUE);
//修改深度测试规则,默认是GL_LESS
glDepthFunc(GL_LESS);
多边形偏移
ZFighting闪烁
开启深度测试后,OpenGL就不会再去绘制模型被遮挡的部分。但是由于深度缓冲区精度有限,对于深度相差⾮常⼩的情况,OpenGL就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测,显示出来的现象时交错闪烁。尤其在一些早期的机子中,发生的概率比较大。
ZFighting闪烁解决方案-多边形偏移
ZFighting闪烁的根本原因是深度值之间的差距太小,所以OpenGL采用了多边形偏移的方案解决此问题。也就是说,在深度测试前,将物体的深度值做一些细微的调整,避免因差距过小产生ZFighting现象。这个调整不需要开发者手动调整,通过调用函数glEnable(GL_POLYGON_OFFSET_LINE)开启,OpenGL就会自动进行调整。
第⼀步:启⽤多边形偏移。
/*
启⽤多边形偏移
由于渲染物体时光栅化模式有三种,所以多边形偏移的方式也有三种。开启时,参数需要跟填充方式一致:
GL_POLYGON_OFFSET_POINT对应光栅化模式:GL_POINT
GL_POLYGON_OFFSET_LINE对应光栅化模式:GL_LINE
GL_POLYGON_OFFSET_FILL对应光栅化模式:GL_FILL
*/
glEnable(GL_POLYGON_OFFSET_FILL)
第⼆步:指定偏移量。
glPolygonOffset (GLfloat factor, GLfloat units)
每个深度缓冲区的深度值都会增加如下所示的偏移量:Offset = (m * factor) + (r * units)
- m: 多边形的深度的斜率的最⼤值,理解为⼀个多边形越是与近裁剪⾯平⾏,m就越接近于0。
- r: 能产⽣于窗⼝坐标系的深度值中可分辨的差异最⼩值。r是由具体OpenGL平台指定的⼀个常量。
- ⼀个⼤于0的Offset会把模型推到离你(摄像机)更远的位置,相应的⼀个⼩于0的Offset会把模型拉近。⼀般⽽⾔,只需要将-1.0和-1这样的简单数字赋值给glPolygonOffset基本可以满⾜需求。
应⽤到⽚段上总偏移计算⽅程式:DepthOffset=(DZ*factor)+(r*units)
- DZ: 深度值(Z值)。
- r: 使得深度缓冲区产⽣变化的最⼩值。
- 负值,将使得z值距离我们更近,⽽正值,将使得z值距离我们更远。
第三步:关闭PolygonOffset。
glDisable(GL_POLYGON_OFFSET_FILL)
ZFighting闪烁问题预防
- 不要将两个物体靠的太近,避免渲染时三⻆形叠在⼀起。这种⽅式要求对场景中物体插⼊⼀个少量的偏移,那么就可能避免ZFighting现象。例如上⾯的⽴⽅体和平⾯问题中,将平⾯下移0.001f就可以解决这个问题。当然⼿动去插⼊这个⼩的偏移是要付出代价的。
- 尽可能将近裁剪⾯设置得离观察者远⼀些。上⾯我们看到,在近裁剪平⾯附近,深度的精确度是很⾼的,因此尽可能让近裁剪⾯远⼀些的话,会使整个裁剪范围内的精确度变⾼⼀些。但是这种⽅式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪⾯参数。
- 使⽤更⾼位数的深度缓冲区,通常使⽤的深度缓冲区是24位的,现在有⼀些硬件使⽤使⽤32位的缓冲区,使精确度得到提⾼。
裁剪
裁剪是在OpenGL中提⾼渲染的⼀种⽅式,只刷新屏幕上发⽣变化的部分,OpenGL允许将要进⾏渲染的窗⼝只去指定⼀个裁剪框。
基本原理:⽤于渲染时限制绘制区域,通过此技术可以在屏幕(帧缓冲)指定⼀个矩形区域。启⽤剪裁测试之后,不在此矩形区域内的⽚元被丢弃,只有在此矩形区域内的⽚元才有可能进⼊帧缓冲。因此实际达到的效果就是在屏幕上开辟了⼀个⼩窗⼝,可以在其中进⾏指定内容的绘制。
//开启裁剪测试
glEnable(GL_SCISSOR_TEST);
//关闭裁剪测试
glDisable(GL_SCISSOR_TEST);
/**
指定裁剪窗⼝
x,y:指定裁剪框左下⻆位置
width,height:指定裁剪区域尺寸
*/
glScissor(x, y, width, height);
理解窗⼝、视⼝、裁剪区域:
- 窗⼝:就是显示界⾯的容器。
- 视⼝:就是窗⼝中⽤来显示图形的⼀块矩形区域,它可以和窗⼝等⼤,也可以⽐窗⼝⼤或者⼩。只有绘制在视⼝区域中的图形才能被显示,如果图形有⼀部分超出了视⼝区域,那么那⼀部分是看不到的。通过glViewport()函数设置。
- 裁剪区域(平⾏投影):就是视⼝矩形区域的最⼩最⼤x坐标(left、right)和最⼩最⼤y坐标(bottom、top),⽽不是窗⼝的最⼩最⼤x坐标和y坐标。通过glOrtho()函数设置,这个函数还需指定最近最远z坐标,形成⼀个⽴体的裁剪区域。
颜色混合
在OpenGL渲染时会把颜⾊值存在颜⾊缓存区中,每个⽚段的深度值也是放在深度缓冲区。当深度缓冲区被关闭时,新的颜⾊将简单的覆盖原来颜⾊缓存区存在的颜⾊值,当深度缓冲区再次打开时,新的颜⾊⽚段只是当它们⽐原来的值更接近邻近的裁剪平⾯才会替换原来的颜⾊⽚段。
//开启颜色混合
glEnable(GL_BlEND);
//关闭颜色混合
glDisable(GL_BLEND);
颜色混合方程式
当混合功能被启动时,⽬标颜⾊和源颜⾊的组合⽅式是混合⽅程式控制的。在默认情况下,混合⽅程式如下所示:
Cf = (Cs * S) + (Cd * D)
- Cf:最终计算出来的颜⾊。
- Cs:源颜⾊ -> 作为当前渲染命令结果进⼊颜⾊缓存区的颜⾊值。
- Cd:⽬标颜⾊ -> 已经存储在颜⾊缓存区的颜⾊值。
- S:源混合因⼦。
- D:⽬标混合因⼦。
设置混合因⼦,需要⽤到函数glBlendFunc (GLenum sfactor, GLenum dfactor);
- sfactor: 源混合因⼦。
- dfactor: ⽬标混合因⼦。
表中R、G、B、A分别代表红、绿、蓝、alpha;表中下标S、D分别代表源、⽬标;表中C代表常量颜⾊(默认⿊⾊)。
示例
如果颜⾊缓存区已经有一个目标颜色(Cd)红⾊(1.0f, 0.0f, 0.0f, 1.0f),这时候进来一个源颜色(Cs)alpha为0.6的蓝色(0.0f, 0.0f, 1.0f, 0.6f)。设置混合因子:glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
则
- Cd = (1.0f, 0.0f, 0.0f, 1.0f)
- Cs = (0.0f, 0.0f, 1.0f, 0.6f)
- S = 源颜色alpha值 = 0.6f
- D= 1 - 源颜色alpha值 = 1- 0.6f = 0.4f
- ⽅程式:Cf = (Cs * S) + (Cd * D) = (Blue * 0.6f) + (Red * 0.4f)
总结
最终颜⾊是以原先的红⾊(⽬标颜⾊)与后来的蓝⾊(源颜⾊)进⾏组合。源颜⾊的alpha值越⾼,添加的蓝⾊颜⾊成分越⾼,⽬标颜⾊所保留的成分就会越少。
混合函数经常⽤于实现,在⼀些不透明的物体上面绘制⼀个透明物体的效果。比如两个图层叠加,上层的图层透明,则混合。上面的图层不透明,则不混合,直接覆盖下面的图层颜色。
修改颜色混合方程式
上面的颜色混合方程式是OpenGL中的默认混合方程式,实际上一共存在5种方程式。可以通过调用宏定义glBlendEquation(GLenum mode)
来选择使用哪种混合方程式。
GLenum | 方程式 |
---|---|
GL_FUNC_ADD | Cf = (Cs * S) + (Cd * D) |
GL_FUNS_SUBTRACT | Cf = (Cs * S) - (Cd * D) |
GL_FUNC_REVERSE_SUBTRACT | Cf = (Cd * D) - (Cs * S) |
GL_MIN | Cf = min(CS, Cd) |
GL_MAX | Cf = max(Cs, Cd) |