Cocos Shader入门基础七:一文读懂深度图

开篇

经过一段时间的持续输出,社区中越来越多的人踏上了3D图形渲染学习之旅,麒麟子非常开心,说明输出的内容对大家都产生了实际的帮助。

特别是上一篇 《用实时反射Shader增强画面颜值》 的文章发表后,有开发者竟然等不及了,找上门来索取。

这样的要求还算合理,毕竟实时3D渲染最终的目的,是创造出更高效、炫酷的画面效果。

因此,在后面的内容输出中,麒麟子的案例会尽可能兼顾这个广大人民群众都能感知到的需求。

上个星期,一个朋友问我如下所示的水面深度效果是如何实现的。

还有一个朋友问我,为什么别人游戏里的特效都非常柔和(参考本文开头的动图),而自己做的就有很明显的接缝,如下所示:

这些事,通常依靠深度图才能够解决。

如果说高级Shader效果中,什么东西最重要的话,那肯定要数深度图了。

所以今天向大家完整介绍一下深度相关知识,希望大家看完本文后,对深度图相关问题再无死角。

深度缓冲区(Depth Buffer)

深度缓冲区解决的问题

假如有两个 3D 模型 AB,且 AB 有一定的交叉。

图形管线在渲染模型的时候是以单个几何体为单位的,会先渲染完一个,再渲染另一个。

这种情况下,不管是先渲染 A 还是先渲染 B,都需要解决像素遮挡问题,如果没有深度缓冲区的协助,可能出现下图中,左边这样的情况:

什么是深度缓冲区

各类计算机图形学书籍、文章上都对深度缓冲区作出了解释,麒麟子综合各家之言,给一个通俗点的解释:

深度缓冲区为非透明物体提供了像素级的前后关系判断能力。

深度缓冲区用于记录帧缓冲区(帧缓冲区用于存放颜色信息)中每个像素的深度值。

深度缓冲区的宽高通常与帧缓冲区大小相等,每一个像素位置的值是 0.0 ~ 1.0,若输出为图片,则如下所示:

离摄像机越近的值越小,摄像机近裁面处的值为 0.0

离摄像机越远的值越大,摄像机远裁面处的值为 1.0

所以,上图中离摄像机越近的像素越黑,离摄像机越远的像素越白。

深度写入

并不是所有的像素都会写入深度的!

3D 模型在渲染时,可以通过渲染状态决定是否开启深度写入,材质面板的开关如下所示:

当一个像素被渲染时,除了将颜色信息写入帧缓冲区外,同时还会将它的深度值写入深度缓冲区。

深度测试

写入的深度拿来干什么呢?

当一个像素被渲染时,在颜色被写入帧缓冲区之前,会进行深度测试,此时就会用到深度缓冲区。

我们可以通过开启深度测试,来丢弃较后绘制但被 “遮挡” 的像素,从而保证渲染结果的正确性。

如上图所示,深度测试有两个控制因子,一个是开关,另一个是比较函数

默认的比较方式为 LESS ,表示小于则通过测试(除非有特别的需要,否则应该保持这个值)。

通过深度测试的像素才会改写深度缓冲区和帧缓冲区的值,否则会被丢弃,不再执行后面的像素处理流程。

深度测试的过程如下图所示:

有了深度缓冲区以后,绘制非透明物体时的顺序就不那么重要了,任意顺序都能获得正确的渲染效果。

麒麟小贴士:

非透明物体的深度写入开启,深度测试开启,绘制顺序可以与远近无关。

半透明物体的深度写入关闭,深度测试开启,需要按从远及近的顺序进行绘制。

深度图

什么是深度图

深度图是一种其像素内容可以反映深度缓冲区内容的纹理,通常是一张渲染纹理(Render Texture)。

对于一些特殊的场合,需要使用深度图才能实现更好的效果。比如:

  • 半透明物体与非透明物体接缝柔和处理
  • 景深等后期效果
  • 水体深浅效果
  • 动态阴影

下面是一张完整的深度图效果:

深度图的存储格式

常见的深度图存储方式有 3 种,接下来我们从兼容性、数值精度、存取效率三个维度来比较的它们的优缺点。

方式1:使用 RGBA8 格式纹理中的 R 通道

由于每个像素的深度值在 0.0~1.0 之间,我们很容易想到,直接将深度信息存储到某个颜色通道中即可。

兼容性: 优,RGBA8 可以兼容所有设备。

数值精度: 差,RGBA8 中每个通道可以表示的浮点数精度为 1.0/255.0,约等于 0.0039精度不够将会导致深度图在用于深度判定时出现较大误差。

存取效率: 优,直接存取,无额外运算。

2、R32F 浮点纹理

R32F 浮点纹理是单通道纹理,且这个通道可以存储32位浮点数。

兼容性: 一般,原生平台普及度已经非常高,但在 Web 平台,WebGL 2.0 才正式支持浮点纹理。

数值精度: 优,可以达到 32 位浮点数精度。

存取效率: 优,直接存取,无额外运算。

3、RGBA8 格式纹理编码
此方案是将深度信息编码到 RGBA8 格式纹理的四个通道中,使用时再用与编码对等的解码方式进行解码。

兼容性: 优,RGBA8 可以兼容所有设备。

数值精度: 优,可以达到 32 位浮点数精度。

存取效率: 一般,需要额外的编码、解码运算。

Cocos Creator 引擎内的深度信息编码、解码函数,如下图所示:

**麒麟小贴士:**只需要添加 #include< packing > ,就可以在自己写的 Cocos Shader 中调用。

深度图的获取

深度图的获取是非常简单的,只需要写一个简单的 Cocos Shader 就可以。

1、Vertex Shader

新建一个 Cocos Effect,写上下图这样的 unlit-vs,将投影过后的位置信息通过 v_screenPos 传给 fs

2、Fragment Shader

fs 中,执行透视除法,将 v_screenPos 转换到 NDC空间, NDC 空间中的 z 值,就是深度值。

麒麟子在这里写了两种深度显示方法,depth_8bits 用于 R 通道存储, depth_32bits 用于RGBA 四通道编码存储。

3、模型渲染

我们新建两个材质,分别使用 8bits32bits 两种方法渲染模型。

depth_8bits 的内容如下所示:

depth_32bits 的内容如下所示(不必感到诧异,它只是没有解码而已,解码之后的显示效果与上图一致):

麒麟小贴士:

负责渲染深度信息的摄像机的 clearFlags 属性需要设置为 SOLID_COLORclearColor 属性需要设置为纯白色(255,255,255,255)

4、深度图

新建一个渲染纹理(Render Texture),并将负责渲染深度材质的摄像机输出到此渲染纹理。

5、注意事项

本示例中,我们在 v_screenPos.z/v_screenPos.w 的基础上还进行了 *0.5 + 0.5 操作。

这是由于在 Cocos Creator 引擎中,v_screenPos.z/v_screenPos.w 的值域为 [-1.0,1.0],我们需要将它映射到 [0.0,1.0]。

对于这个值域的范围,在进阶阅读:线性深度和非线性深度会讲到。

我们可以通过下面的简单测试来验证其范围:

如上图所示,只需要加上,若小于零就显示为红色的判断,就能得到如下图所示的深度图。

图中有红色,表示这个值是会小于零的,说明 depth 的值域为 [-1.0,1.0],需要修正到 [0.0,1.0]。

深度图使用案例

接下来,我们以如何实现柔和特效为例,讲解深度图的使用。

第一步:

新建一个场景,拖入我们的平台模型。如下图所示:

第二步:

复制此模型作深度渲染使用,并将所有子节点的材质,改为 material-depth-32bits。如下图所示:

第三步:

新增一个 LAYER,取名为 DEPTH,并将上一步复制的模型 LAYER 切换为 DEPTH(包括其子节点),如下图所示:

第四步:

新建一个摄像机,作为 Main Camera 的子节点。

需要注意以下几点:

1、确保此摄像机的 position、rotation、scale 均为初始化状态。

2、确保此摄像机的参数,如 fov、near、far 等与 Main Camera 一致。

3、确保此摄像机的渲染层级(Visibility)仅保留 DEPTH

4、确保此摄像机的 clearFlagsSOLID_COLORclearColor纯白色(255,255,255,255)

详细情况,如下图所示:

第五步:

新建一个 Render Texture,起名为 RT_Depth,并赋值给上一步创建的摄像机。

第六步:

拖入本系列教程上一节课中使用的 “虚空幻镜入口” 特效,如下图所示:

第七步:

复制一个 builtin-unlit.effect,并加入深度判断。

核心内容为,求得当前特效像素的深度与深度图中的差值,并做一个线性过渡。如下图所示:

完整 Shader 请看 DEMO 源文件 assets/tutorial_7/effects/effect-unlit.effect

第八步:

将第六步特效使用的材质的 effect 切换为 effect-unlit.effect,并将 RT_Depth 赋值给材质的 DepthMap 参数。

最终得到的效果如下所示:

从上面的动图中,我们可以很清晰的看到,不管是近处还是远处,半透明特效与非透明物体的接缝变得柔和了。

进阶阅读:线性深度与非线性深度

如果不太想面对数学的朋友,可以直接拖到末尾点赞、评论、转发了,避免中途流失。

初中几何角度解释非线性


上图为透视投影视锥体,所有视锥体内的物体会投影到**近裁面(Front/Near Clipping Plane)**成像。

由初中学过的三角形比例定理我们可以推导出,越远的物体在近裁面成像时缩小越多,从而出现了近大远小的效果。

代数角度解释非线性深度

如上图所示,是非常经典的 OPENGL 透视投影矩阵(不同引擎由于选择的坐系不同,正方向不同,会略有差异)。

在与 Cocos Creator 引擎的 Mat4.perspective 对比后,确认 Cocos Creator 也是用的上图的透视投影矩阵。

设观察空间中有一个点 Pview(x,y,z,1.0) ,那它投影后的各值为:

  • Pproj.x = x*cot(fovy/2)/aspect
  • Pproj.y = y*cot(fovy/2)
  • Pproj.z = z*-(f+n)/(f-n)-(2*f*n/(f-n))
  • Pproj.w = -z

为方便阅读

Pview.z 简写为 z

Pproj.z 简写为 z

Pproj.z/Pproj.w 简写为 z’'

由此可得:

此时的 z’’ (Pproj.z/Pproj.w) 是一个处于[-1.0~1.0]区间的值,将其映射到[0.0~1.0]区间,就能得到我们的非线性深度。

最终公式整理后可得:

depth = z’’ * 0.5 + 0.5

为了更直观的体现这个非线性关系,麒麟子花了较长时间做了下面的图:

需要关注几个点:

1、上面的投影公式中,正方向为 -Z,所以图中 Pview.z 值为负。

2、Pview.z-10 时,depth 就已经大于 0.9 了,这就说明靠近摄像机跟前的这 10个单位,占用了 90% 的深度缓冲区精度。

3、整个表很长, -1.0~-1000.0,无法完整截图。

线性深度

线性深度一般用于需要精确表达两个物体深度差异反算像素在观察坐标系中的位置或者基于物理公式的运算等情况。

线性深度有两种表示方法:

  • LinearEyeDepth = -Pview.z
  • Linear01Depth = (-Pview.z-n)/(f-n) 或者 -Pview.z/f

麒麟小贴士: 再次提醒,由于系统中是以 -Z(0.0,0.0,-1.0) 为正方向,所以求深度的时候,z 值需要取反。

线性深度的使用场合,一般是指当我们拥有深度图时,此时并没有 Pview.z 这个数据。

所以上面的公式只能作为理解什么是线性深度用,在实际生产环境中派不上用场。

下面我们来研究,如何在拥有深度图的情况下,反算出线性深度。

LinearEyeDepth 推导过程

由我们刚刚讲到的非线性深度公式,可以反推出以下结论:

depthz’’'

则可以得到 z’’ = z’’’ * 2.0 - 1.0

代入上面的投影公式可以反向求解出观察坐标系下的 Pview.z

由于坐标系以 -Z 为正方向,这样求出来的 Pview.z 是负数,取反即可得到我们视野空间的线性深度。

最后我们得到的 LinearEyeDepth 计算公式如下:

很容易做一个验算:

  • 当 z’’’ 为 0 时,LinearEyeDepth 为 n。
  • 当 z’’’ 为 1 时,LinearEyeDepth 为 f。

如果担心端点是特例难以信服,可以在 Excel 中,双向验证公式,如下图所示:

Linear01Depth 推导过程

线性深度除了用 -Pview.z 表示外,还可以将其归一化到 [0,1] 区间。

实际上,[0,1]的线性深度使用率远高于 LinearEyeDepth,因为它可以不用关注 nearfar 的数值变化。

Linear01Depth 有两个公式可以使用:

  • 公式 A: (-Pview.z-n)/(f-n)
  • 公式 B: -Pview.z/f

两个公式的区别在于区间的选择。

公式 A 仅考虑 nf 之间的值,当 Pview.z 值为 -n 时,Linear01Depth0

公式 B 则将观察点也纳入了考虑,当 Linear01Depth0 时,Pview.z 与观察点重合。

两个公式都是可以的,但由于 公式 B 的运算更为简单,且能够处理 Pview.zn 离摄像机更近的情况,所以大多数系统中采用了公式 B

采用 公式 B,只需将 LinearEyeDepth 除以 f,即可得到 Linear01Depth 最终计算公式,如下图所示:

核心代码

代码反而是最简单的了,如下图所示:

敲黑板!

学习过程中可能遇上的疑惑!!!

如果一切都像上面这样的话,本文就可以开心的结束。

但麒麟子在核实深度图相关内容的过程中,在网上找到了一个出场率非常高的内容,如下所示:

如果盲目照着这段代码来的处理的话,只会掉入一个深坑。这段代码有一个大前提是:

该引擎的运行时中,OPENGL 环境下拿到的深度信息处于 [-1.0,1.0] 的非线性区间。D3D 环境下拿到的深度信息处于 [0.0,1.0]区间。

因此,采用下面哪个组合,是要根据深度信息的范围而定,而不是平台和图形 API。

  • zc0 = 1.0 - f/n , zc1 = f/n

  • zc0 = (1.0 - f/n)*2.0 , zc1 = (1.0 + f/n) * 2.0

线性深度与非线性深度曲线

由于 LinearEyeDepth 即为 -Pview.z,不方便展示,所以下图仅展示了 Linear01Depth(红线):

值得注意的是,Linear01Depth 的精度是受 f 决定的。f 的值会影响同一视角下同一像素的深度值,如下图所示:

关于写作

有人问过麒麟子,说市面上已经有非常多的计算机图形学、3D渲染相关教程了,为什么我还要重写。

我统一答复一下吧:

市面上的文章、教程,不管是讲某引擎的,还是讲某API的。

要么是贴代码,要么是抄公式,去吻合计算机图形学理论,生怕与理论相悖。

最终导致了国内3D图形相关人才与项目需求的断层。

而麒麟子输出的内容,是结合实际项目需求中广泛采用的方案来讲解,从原理、实现、到应用。

麒麟子提供的不是理论,是理论知识在行业中的实际应用。

相信这样的方式更能提升大家的学习效率,做到学以致用。

知道你们喜欢看视频,特意做了一个科普视频给大家:

image

DEMO免费获取

方式一:

https://store.cocos.com/app/detail/3521

**方式二:**Cocos Dashboard -> 商城 搜索 kylins

13赞

起飞~ :100:

:+1::+1::+1::+1::+1:

謝謝 :+1::+1::+1:

什么时候出卡通水的教程啊

玉兔之前已经出了一个卡通水了,你是想要文章中的那个卡通水吗?

1赞

是啊,文章中那个水非常不错,一直想要但没做出来,能出个教程吗

是啊, :+1::+1::+1:

请问一下,粒子贴图材质,怎么设置深度图呀?
我这边粒子材质只能选默认的,但是跟模型交错还是会有接缝。
求教大佬。

本文只是演示如何利用深度图实现柔和特效。
如果要在引擎中使用的话,需要改造渲染流程,自定义Shader。
3.6正式版提供的新版本管线可以更好的处理这个事情,敬请期待!

哈哈哈哈,,,貌似在x,y2个坐标系上是线性关系(都和Z相关),而z坐标系非线性的(和x,y都无关), 之前我一直没懂z到底是怎么压缩进-1 ~ +1的,,,,,

请问一下,粒子贴图材质,怎么设置深度图呀?
我这边粒子材质只能选默认的,但是跟模型交错还是会有接缝。
求教大佬。好的,感谢大佬。