OpenGL-深度测试

通过本文你可以了解到的

  • 什么是深度?
  • 什么是深度缓缓冲区?
  • 如何使用深度测试?
  • 什么是可视化深度值?
  • 深度值相同如何解决冲突问题?

接下来,请带着这些问题看后面的内容

简述背景

首先可以大致看下我的下面两篇文章
视觉开发-OpenGL优化之油画法
视觉开发-OpenGL优化之背面剔除

这两篇文章都是OpenGL处理隐藏面消除所用到的方法。
那么解决隐藏面消除问题的算法有很多,具体可以参考Visible Surface Detection

结合OpenGL,我们使用的是Z-buffer方法,也叫深度缓冲区Depth-buffer。

深度缓冲区(Detph buffer)颜色缓冲区(color buffer)是对应的,颜色缓冲区存储的像素的颜色信息,而深度缓冲区存储像素的深度信息。

在决定是否绘制一个物体的表面时,首先将表面对应像素的深度值与当前深度缓冲区中的值进行比较,如果大于等于深度缓冲区中值,则丢弃这部分;否则利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓冲区。这一过程称之为深度测试(Depth Testing)。

在OpenGL中执行深度测试时,我们可以根据需要指定深度值的比较函数,后面会详细介绍具体使用。

了解深度

深度其实就是该像素点在3D世界中距离摄像机的距离,z值
怎么样,是不是很好理解?!那关于深度在这里我们就不做过多的介绍了😝

深度缓冲区

  • 先思考下,为什么要使用深度缓冲区?

这里重申一下,在不使用深度测试的时候,如果我们先绘制一个距离比较近的物理再绘制距离较远的物理,则距离远的位图因为后绘制,会把距离近的物体覆盖掉,有了深度缓冲区后,绘制物体的顺序就不那么重要的.实际上,只要存在深度缓冲区OpenGL都会把像素的深度值写入到缓冲区中除非调用glDepthMask(GL FALSE).来禁止写入。

  • 究竟什么是深度缓冲区?

其实深度缓冲区,就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值,深度值(Z值)越大,则离摄像机就越远。

深度缓冲区一般由窗口管理系统,例如GLFW来创建,深度值一般由16位,24位或者32位值表示,通常是24位。当然位数越高的话,深度的精确度越好

  • 如何使用?
    在OpenGL里面开启深度测试非常简单,如下所示
//  开启深度测试
 glEnable(GL_DEPTH_TEST);

只需要一行代码(注:深度测试默认是关闭的

另外还需要在绘制场景前,清除颜色缓冲区时,清除深度缓冲区:

glClearColor(0.18f, 0.04f, 0.14f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

清除深度缓冲区的默认值是1.0,表示最大的深度值,深度值的范围在[0,1]之间,值越小表示越靠近观察者,值越大表示远离观察者。

在进行深度测试时,当前深度值和深度缓冲区中的深度值,进行比较的函数,可以由用户通过glDepthFunc指定,这个函数包括一个参数

具体的参数如下表所示:

参数

例如我们可以使用GL_AWALYS参数,这与默认不开启深度测试效果是一样的:

glDepthFunc(GL_ALWAYS);

下面我们绘制两个立方体和一个平面,通过对比开启和关闭深度测试来理解深度测试。
当关闭深度测试时,我们得到的效果却是这样的:

关闭深度测试

这里先绘制立方体,然后绘制平面,如果关闭深度测试,OpenGL只根据绘制的先后顺序决定显示结果。那么后绘制的平面遮挡了一部分先绘制的本应该显示出来的立方体,这种效果是不符合实际的。

我们开启深度测试后绘制场景,得到正常的效果如下:

开启深度测试
  • 需要注意的地方
    • 使用深度测试,最常见的错误是没有使用glEnable(GL_DEPTH_TEST);
    • 开启深度测试,或者没有使用glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);清除深度缓冲区。
    • 与深度缓冲区相关的另一个函数是glDepthMask,它的参数是布尔类型,GL_FALSE将关闭缓冲区写入,默认是GL_TRUE,开启了深度缓冲区写入。

可视化深度值

在可视化深度值之前,首先我们要明白,这里的深度值,实际上是屏幕坐标系下的zwin坐标,屏幕坐标系下的(x,y)坐标分别表示屏幕坐标系下以左下角(0,0)为起始点的坐标。zwin我们如何获取呢? 可以通过着色器的输入变量gl_FragCoord.z来获取,这个gl_FragCoordz坐标表示的就是深度值。

我们在着色器中以这个深度值为颜色输出:

  // 原样输出
float asDepth()
{
  return gl_FragCoord.z;
}
void main()
{
    float depth = asDepth();
    color = vec4(vec3(depth), 1.0f);
}

输出后的效果如下图所示:

可以看到图中,只有离观察者较近的部分有些黑色,其余的都是白色。这是因为深度值zwinzeye是成非线性关系的,在离观察者近的地方,精确度较高,zwin值都保持在较小范围,成黑色。但是一旦超出一定距离,精确度变小,zwin值都挤在1.0附近,因此成白色。当我们向后移动,拉远场景与观察者的距离后,zwin值都落在1.0附近,整个场景都变成白色,如下图所示:

作为深度值的可视化,我们能不能使用线性的关系来表达zwin
zeye ?

这里我们做一个尝试,从zwin反向推导出z_{eye}$

在投影矩阵和视口变换矩阵一节,我们计算出了相机坐标系下坐标和规范化设备坐标系下坐标之间的关系如下:

在OpenGL中从规范化设备坐标系转换到屏幕坐标系使用函数主要是:

glViewport(GLint sx
 , GLint sy
 , GLsizei ws
 , GLsizei hs
); 
glDepthRangef(GLclampf ns
 , GLclampf fs
 );

继而可以得到规范化设备坐标系和屏幕设备坐标系之间的关系如下:


默认情况下glDepthRange函数的n=0,f=1,因此从(2)式可以得到:


从式子(1)我们可以得到:

上面的式子(5)如果用来作为深度值,由于结果是负数,会被截断到0.0,结果都是黑色,因此我们对分母进行反转,写为式子(6)作为深度值。

对式子(6)的深度值进行归一化,保持在[0,1]范围内,则在着色器中实现为:

// 线性输出结果
float near = 1.0f; 
float far  = 100.0f; 
float LinearizeDepth() 
{
    // 计算ndc坐标 这里默认glDepthRange(0,1)
    float Zndc = gl_FragCoord.z * 2.0 - 1.0; 
    // 这里分母进行了反转
    float Zeye = (2.0 * near * far) / (far + near - Zndc * (far - near)); 
    return (Zeye - near)/ ( far - near);
}
void main()
{
    float depth = LinearizeDepth();
    color = vec4(vec3(depth), 1.0f);
}

使用zwinzeye线性关系得到深度值,绘制的效果如下图所示:

很多网络教程都近似表达zwinzeye的非线性关系,用来可视化,我们可以从(4)(6)得到非线性关系:

在着色器中实现为:

   // 非线性输出
float nonLinearDepth()
{
    float Zndc = gl_FragCoord.z * 2.0 - 1.0; 
    float Zeye = (2.0 * near * far) / (far + near - Zndc * (far - near)); 
    return (1.0 / near - 1.0 / Zeye) / (1.0 / near - 1.0 / far);
}
void main()
{
    float depth = nonLinearDepth();
    color = vec4(vec3(depth), 1.0f);
}

这个非线性关系输出,和利用gl_FragCoord.z作为深度值输出效果是差不多的。

深度值相同如何解决冲突问题(ZFighting)?

实际使用时不使用zwinzeye的线性关系,因为在场景中,近处的物体,我们想让它看的清楚,自然 要求精度高;但是远处的物体,我们不需要很清晰的看到细节,因此精确度不必和近处的物体一样。使用公式(7)绘制的zwinzeye
关系图如下所示(来自:www.learnopengl.com Depth testing):

我们看到,zeye在[1.0,2.0]范围内时zwin保持在0.5的范围内,精确度高。而当zeye超过10.0后,zwin的值就在0.9以后了,也就是说zwin在[10.0,50.0]范围内的深度值将挤在[0.9,1.0]这么一个小的范围内,精确度很低。

实际上深度值是通过下式计算的(来自:depth buffer faq):


其中,S=2d−1
,d表示深度缓冲区的位数(例如16,24,32)。这个式子的右边括号部分是由(1)(4)得到,同时放大S倍数后得到最终的深度值(可以参看depth buffer faq)。

找到两个特殊点,zwin=1zwin=S−1
,得到:

取n = 0.01, f = 1000 and s = 65535,那么有:

注意OpenGL中相机坐标系的+Z轴指向观察者,因此上面的坐标是负数。从上面的值我们可以看到,当zeye在[-395,-1000]范围内时,深度值将全部挤在65534或者65535这两个值上,也就是说几乎60%的zeye只能分配1到2个深度值,可见当zeye
超过一定范围后,精度值是相当低的。(这个例子原本解释来自depth buffer faq)。

当深度值精确度很低时,容易引起ZFighting现象,表现为两个物体靠的很近时确定谁在前,谁在后时出现了歧义。例如上面绘制的平面和立方体,在y=-0.5的位置二者贴的很近,如果进入立方体内部观察,则出现了ZFighting现象,立方体的底面纹理和平面的纹理出现了交错现象,如下图所示:

交错1
交错2

OpenGL里解决ZFighting问题

  • 第一步:启用Polygon Offset方式解决
    解决方法:让深度值之间产生间隔,如果2个图形之间有间隔,是不是意味着就不会产生干涉.可以理解为在执行深度测试前将立方体的深度值做些细微的增加于是就能将重叠的2个图形深度值之前有所区分,
//启用Polygon Offset方式
// 参数列表:
// GL POLYGON OFFSET POINT对应光栅化模式: GL POINT
// GL POLYGON OFFSET LINE 对应光栅化模式: GL LINE
// GL POLYGON OFFSET FILL 对应光栅化模式: GL FILL
glEnable(GL POLYGON OFFSET 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基本可以满足需求.
/**
应用到片段上总偏移计算方程式
Depth Offset = (DZ * factor) + (r * units);

DZ:深度值(Z值)
r:使得深度缓冲区产生变化的最小值

负值,将使得z值距离我们更近,而正值,将使得z值离我们更远
*/
void glPolygonOffset(Glfloat factor, Glfloat units);

预防ZFighting的方法

  • 不要将两个物体靠的太近,避免渲染时三角形叠在一起。这种方式要求对场景中物体插入一个少量的偏移,那么就可能避免ZFighting现象。例如上面的立方体和平面问题中,将平面下移0.001f就可以解决这个问题。当然手动去插入这个小的偏移是要付出代价的。

  • 尽可能将近裁剪面设置得离观察者远一些。上面我们看到,在近裁剪平面附近,深度的精确度是很高的,因此尽可能让近裁剪面远一些的话,会使整个裁剪范围内的精确度变高一些。但是这种方式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪面参数。

  • 使用更高位数的深度缓冲区,通常使用的深度缓冲区是24位的,现在有一些硬件使用使用32位的缓冲区,使精确度得到提高。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容