在上一篇OpenGL绘制甜甜圈、正反面剔除、深度测试问题中,文末还遗留一个问题未解决,在解决之前,先说说这种现象产生的原因
甜甜圈缺口产生原因
从图中可以看出,在甜甜圈旋转过程中,当前后两部分重叠时,对于我们而言,需要显示的是前面部分,后面部分是隐藏面,但是OpenGL中并不能清楚的区分,两个图层谁显示在前,谁显示在后,由此导致甜甜圈产生了缺口。
在解决这个问题前,我们先了解几个概念:
深度
深度其实就是该像素点在3D世界中距离摄像机的距离,Z值
深度与图形坐标中像素点的Z坐标有如下关系:
- 如果观察者在Z轴的正方向,Z值越大距离观察者越近
- 如果观察者在Z轴的负方向,Z值越小距离观察者越近
如下图所示:
深度缓冲区(Depth Buffer)
深度缓存区,就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值.深度值(Z值)越大, 则离摄像机就越远。
那么我们为什么要用深度缓冲区呢?
在不使⽤深度测试的时候,如果我们先绘制⼀个距离比较近的物体,再绘制距离较远的物体,则距离 远的位图因为后绘制,会把距离近的物体覆盖掉. 有了深度缓冲区后,绘制物体的顺序就不那么重要的. 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写入到缓冲区中. 除非调⽤glDepthMask(GL_FALSE).来禁止写入.
深度缓存区原理
将深度值与屏幕上的每个像素点进行一一对应,然后将深度值存储到深度缓冲区。
在深度缓存区中,每个像素点只会记录一个深度值
深度缓冲区的范围是[0, 1]之间,默认值是1.0,表示深度值的最大值
在之前的demo中,RenderScene函数绘制前,都会先清空缓存区,这里的缓冲区就包括深度缓冲区,因为如果缓存区不清空,之前的数据会有残留,会对目前图形的绘制造成影响。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
深度测试
深度缓冲区(DepthBuffer)和颜色缓存区(ColorBuffer)是对应的.颜色缓存区存储像素的颜⾊色信息,而深度缓冲区存储像素的深度信息. 在决定是否绘制一个物体表面时, ⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏比较. 如果大于深度缓冲区中的值,则丢弃这部分.否则利用这个像素对应的深度值和颜色值.分别更新深度缓冲区和颜⾊缓存区. 这个过程称为”深度测试”。
在上节我们绘制甜甜圈的过程中,我们在main函数中添加了有机菜单栏回调函数,我们可以通过点击“Toggle depth test”按钮来更改程序中设置的用于是否开启深度测试的变量iDepth
,同时我们在绘制的函数RenderScene中添加深度测试的代码:
if(iDepth){
glEnable(GL_DEPTH_TEST);
}else{
glDisable(GL_DEPTH_TEST);
}
当我们开启深度测试后,我们的甜甜圈的缺口就已经没了。
多边形偏移
首先我们先来了解下Z-Fighting(Z冲突,闪烁)的问题。
开启深度测试后,OpenGL 就不会再去绘制模型被遮挡的部分. 这样实现的显示更加真实.但是由于深度缓冲区精度的限制对于深度相差非常⼩小的情况下.(例如在同一平面上进⾏2次 制),OpenGL 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测.显示出来的现象交错闪烁.即前面2个画⾯交错出现。如下图:
其问题产生的主要原因是由于图形靠的太近,导致无法区分出图层先后次序,针对该问题,OpenGL提供了一种多边形偏移(Polygon Offset)方案。
解决⽅方法: 让深度值之间产⽣生间隔.如果2个图形之间有间隔,是不是意味着就不会产⽣生干涉.可以理解为在执行深度测试前将⽴方体的深度值做一些细微的增加.于是就能将叠的2个图形深度值之前有所区分.
第一步:
在绘制前,开启多边形偏移:
glEnable(GL_POLYGON_OFFSET_FILL)
参数列表:
GL_POLYGON_OFFSET_POINT 对应光栅化模式: GL_POINT
GL_POLYGON_OFFSET_LINE 对应光栅化模式: GL_LINE
GL_POLYGON_OFFSET_FILL 对应光栅化模式: 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 基本可以满⾜足需求.
glPolygonOffset (GLfloat factor, GLfloat units);
第三步:
在绘制完成后,关闭多边形偏移
glDisable(GL_POLYGON_OFFSET_FILL)
在我们绘制金字塔、六边形、和三角形圆环添加黑边的时候为了防止Z冲突我们当时使用了多变形偏移来进行解决的。
//画黑色边框
glPolygonOffset(-1.0f, -1.0f);// 偏移深度,在同一位置要绘制填充和边线,会产生z冲突,所以要偏移
glEnable(GL_POLYGON_OFFSET_LINE);
那么我们在以后开发中如何避免Z冲突呢?
不要将两个物体靠的太近,避免渲染时三角形叠在一起。这种方式要求对场景中物体插⼊入⼀个少量的偏移,那么就可能避免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坐标。通过glOrtho()函数设置,这个函数还需指定最近最远z坐标,形成⼀个立体的裁剪区域。
混合
我们把OpenGL 渲染时会把颜色值存在颜色缓存区中,每个片段的深度值也是放在深度缓冲区。当深度缓冲区被关闭时,新的颜色将简单的覆盖原来颜色缓存区存在的颜⾊值,当深度缓冲区再次打开时,新的颜色⽚段只是当它们⽐原来的值更接近邻近的裁剪平面才会替换原来的颜色片段。
但是当开启深度测试后,两个重叠的图层中,如果有一个图层是半透明的,另一个是非半透明,此时就不能通过深度值比较,来进行颜色值的覆盖,而是需要将两个颜色进行混合,然后存入颜色缓冲区。
使用
针对不同需求,颜色混合的使用方式有两种
- 开关方式
用于单纯的将两个图层重叠时进行颜色混合,这种混合并不能解决颜色的混合。在固定着色器和可编程着色器都可以使用这种方式
//开启,
glEnable(GL_BlEND);
//关闭
glDisable(GL_BlEND);
- 开关方式 + 混合方程式
用于处理类似滤镜效果的场景,简单描述就是将需要处理的图片颜色和图片上覆盖的半透明颜色进行混合即两股颜色混合,此时如果只是单纯的开关方式,已经不能满足我们的需求,需要借助混合方程式,来实现两股颜色的混合。一般是在可编程着色器中片元着色器中使用。
//开启,
glEnable(GL_BlEND);
//设置混合因子--默认值是 GL_SRC_ALPHA 和 GL_ONE_MINUS_SRC_ALPHA
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
//关闭
glDisable(GL_BlEND);
在glBlendFunc
方法中,是通过混合方程式来得到颜色的组合,默认情况下混合方程式如下所示
//Cf -- 最终组合的颜色值
//Cd:源颜色 -- 当前渲染命令传入的颜色值
//CS:目标颜色 -- 颜色缓冲区中已经存在的颜色值
//S:源混合因子
//D:目标混合因子
Cf = (Cs * S) + (Cd * D)
混合因子的设置如下图所示:
表中R、G、B、A 分别代表 红、绿、蓝、alpha。 表中下标S、D,分别代表源、⽬目标
表中C 代表常量量颜⾊色(默认黑色)
下⾯通过⼀个常⻅的混合函数组合来说明问题:
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
如果颜色缓存区已经有⼀种颜色红⾊(1.0f,0.0f,0.0f,0.0f),这个⽬标颜色Cd,如果在这上用⼀种alpha为0.6的蓝⾊色(0.0f,0.0f,1.0f,0.6f)
Cd (⽬标颜色) = (1.0f,0.0f,0.0f,0.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值越高,添加的新颜色成分就越高,旧颜色值就保留的越少
总结
在颜色缓冲区中,每个像素点只能存储一种颜色
颜色混合主要用于实现在不透明物体前绘制透明物体的效果
只有上面图层是透明时,才需要开启颜色混合,如果不是,则没有必要开启颜色混合