Unity3D Shader系列之深度纹理

目录

  • 1 引言
  • 2 深度纹理
    • 2.1 深度纹理是什么
    • 2.2 NDC坐标中的Z值
      • 2.2.1 OpenGL与DirectX的差异
      • 2.2.2 OpenGL下的NDC
      • 2.2.3 DirectX下的NDC
      • 2.2.4 小结
      • 2.2.5 将Zndc转换为线性深度值
    • 2.3 深度纹理为什么存的是NDC坐标中的深度值
      • 2.3.1 透视校正插值
      • 2.3.2 深度纹理使用NDC的Z值的原因
    • 2.4 深度值的精度问题
      • 2.4.1 Z Fighting
      • 2.4.2 DirectX中的深度值精度问题
      • 2.4.3 OpenGL中的深度值精度问题
    • 2.5 Unity中生成深度纹理
      • 2.5.1 前向渲染中的深度纹理
      • 2.5.2 延迟渲染中的深度纹理
      • 2.4.3 两者对比
  • 3 示例程序
    • 3.1 代码解释
    • 3.2 场景扫描效果
  • 4 其他问题
  • 5 参考文章


1 引言

Unity3D Shader系列之深度纹理_第1张图片
这篇总结下场景扫描的效果。场景扫描背后的原理其实挺简单,先获取相机的深度图,再获取相机绘制的画面,然后从深度图中取出当前像素的深度值,如果深度值在我们扫描线的深度值的范围内,那么当前像素的颜色就为扫描线的颜色与原画面颜色的叠加。所以实现这个效果的重点在于,如何去获取相机的深度图。
其实网上有很多文章以及很多书籍都说到了如何在Unity中获取相机的深度图,但是自己还是想用这篇文章梳理下。只有自己能够完整表达出的知识才是真真正正掌握了的知识。

2 深度纹理

2.1 深度纹理是什么

我们知道GPU有一个深度缓冲区Z-Buffer,用来存储各个像素的深度值,这个深度值是从摄像机到对应该像素点的顶点之间的距离。既然是深度值,那它存的是哪个坐标系下的深度值呢,观察空间、裁剪空间还是NDC坐标系下的呢?答案是Z-Buffer存储的是NDC坐标系下的深度值,即该顶点对应的NDC坐标的Z值。至于为什么选择NDC坐标系下的Z值而不用其他两个空间的Z值,原因见下一节
在OpenGL下,NDC坐标的Z值的范围是[-1, 1];而在DirectX下,NDC坐标的Z值的范围是[0, 1]。我们的深度纹理其实就是在Z-Buffer的基础上稍微处理了一下得到的一张纹理。
这个稍微处理是什么呢?如下:
①在OpenGL平台下,由于NDC坐标的Z值的范围是[-1,1],而纹理每个像素只能存储的值的范围为0~1,所以需要对其进行编码,即深度纹理中该某像素的深度值=0.5 * ZNDC + 0.5
②在DirectX平台下,虽然NDC坐标的Z值的范围是[0,1],可以直接存储到深度纹理中,但是不是直接存的!DirectX平台下,深度纹理实际存储的是1 - ZNDC,即[1, 0],同时Unity的Shader中宏变量UNITY_REVERSED_Z 将会被定义。这个反转操作被称为Reverse-Z。至于为什么需要这样处理,这样处理带来的好处是什么,详见2.4.2节。
这里先看几个零散的知识点:
①深度纹理一般是16位或者32位的,当然也有用24位的,具体要看相机的设置和实际应用的平台。
②Unity3D中,如果将Camera的depthTextureMode设置为DepthTextureMode.DepthNormals,此时Unity3D会生成一张与屏幕大小一样、精度为32位(每通道各8位)的法线+深度纹理,其中,观察空间的法线信息会被编码到纹理的R与G通道,深度值会被编码到纹理的B和A通道(即此时的深度值的精度为16位)。
③另外,由于深度纹理是通过NDC坐标中的Z值计算来的,而NDC坐标是由裁剪空间的坐标进行齐次除法(xyz分别除以w)得到的,所以深度纹理中存储的深度值是非线性的。也就是说通过对深度纹理采样得到的深度值是非线性的,如果要使用线性的深度值,需要将采样深度纹理得到的深度值转换到观察空间下,这一点我们后面再说。
上面这些内容可参考以下官方文档,我这儿为了阅读的连贯性,直接截图了过来。

  • Camera’s Depth Texture
  • Using Depth Textures
    Unity3D Shader系列之深度纹理_第2张图片
    Unity3D Shader系列之深度纹理_第3张图片

2.2 NDC坐标中的Z值

通过上一节我们知道深度纹理中存储的深度值其实是由NDC坐标中的Z值转换而来的,这一节我们来看看NDC坐标是怎么计算来的。
我们知道要将模型渲染到屏幕上,需要对模型上的顶点进行一系列坐标变换,从模型空间到世界空间再到观察空间(MV变换),再到裁剪空间(P变换、裁剪变换或投影变换),然后经过齐次除法得到NDC坐标,最后再进行屏幕映射得到该顶点对应的像素坐标。由于深度值与相机有关,所以我们这里只需关注投影变换和齐次除法即可。

2.2.1 OpenGL与DirectX的差异

在具体看投影变换和齐次除法之前,我们需要先知道两个平台上的几个差异,主要有以下3点:
①OpenGL平台的NDC坐标的Z值范围是[-1,1],而DirectX平台的NDC坐标的Z值范围是[0,1]
②DirectX的观察空间是左手标系(即观察空间中,距离相机越远,值越大),而OpenGL的观察矩阵是右手坐标系(即观察空间中,距离相机越远,值越小)
③OpenGL中,点或向量在进行坐标变换时,是把点或向量写成列向量,即将点或向量写在矩阵的右边Mv;而DirectX中,点或向量在进行坐标变换时,是把点或向量写成行向量,即将点或向量写在矩阵的左边vM
我们下面给出的投影矩阵以及NDC坐标中的Z值都是基于上面这三个条件的。

2.2.2 OpenGL下的NDC

OpenGL中观察空间是右手坐标系,点或向量写成列向量,NDC坐标的Z值范围为[-1,1]。
①透视相机
先看看透视相机的裁剪矩阵。
(注:本小节灰色截图均来自冯乐乐的《Unity Shader入门精要》一书)
Unity3D Shader系列之深度纹理_第4张图片
裁剪空间中的坐标值如下。
Unity3D Shader系列之深度纹理_第5张图片
经过透视除法之后,其NDC坐标为如下。
Unity3D Shader系列之深度纹理_第6张图片
即NDC坐标系下的深度值为:
OpenGL透视相机的Zndc
将Zview=-Near和-Far带入可知,其值分别为-1,1。
即ZNDC=-1时,表示该顶点在透视相机的近裁剪平面;ZNDC=1时,表示该顶点在透视相机的远裁剪平面。
Unity3D Shader系列之深度纹理_第7张图片
②正交相机
正交相机的裁剪矩阵。
Unity3D Shader系列之深度纹理_第8张图片
裁剪空间中的坐标值如下。
Unity3D Shader系列之深度纹理_第9张图片
NDC坐标值。
Unity3D Shader系列之深度纹理_第10张图片
即NDC坐标系下的深度为
OpenGL透视相机NDC坐标下的深度值
同透视相机,ZNDC=-1时,表示该顶点在正交相机的近裁剪平面;ZNDC=1时,表示该顶点在正交相机的远裁剪平面。
Unity3D Shader系列之深度纹理_第11张图片

2.2.3 DirectX下的NDC

DirectX中观察空间是左手坐标系,点或向量写成行向量,NDC坐标的Z值范围为[0,1]。
Unity3D Shader系列之深度纹理_第12张图片
①透视相机
裁剪空间中的坐标值如下。
Unity3D Shader系列之深度纹理_第13张图片
NDC坐标系下的深度值为:
DirectX透视相机NDC坐标下的深度值
将Zview=Near和Far带入可知,其值分别为0,1。
即ZNDC=0时,表示该顶点在透视相机的近裁剪平面;ZNDC=1时,表示该顶点在透视相机的远裁剪平面。
②正交相机
裁剪空间中的坐标值如下。
Unity3D Shader系列之深度纹理_第14张图片
NDC坐标系下的深度值为:
DirectX正交相机NDC坐标下的深度值
将Zview=Near和Far带入可知,其值分别为0,1。
即ZNDC=0时,表示该顶点在透视相机的近裁剪平面;ZNDC=1时,表示该顶点在透视相机的远裁剪平面。

2.2.4 小结

Unity3D Shader系列之深度纹理_第15张图片

2.2.5 将Zndc转换为线性深度值

我们上面知道,深度纹理中存的是NDC坐标中的深度值,它是非线性的。但是我们一般需要使用线性的深度值,即在观察空间中的深度值Zview,所以这就存在一个将深度纹理中的深度值转换为Zview的变换过程。
我们这里以OpenGL下的透视相机为例,DirectX下的求法类似就不再赘述。
具体步骤如下:
①我们在2.1节中说过,在OpenGL平台下,深度纹理中该某像素的深度值=0.5 * ZNDC + 0.5
OpenGL深度纹理与Zndc值的关系
②在2.2.4中,我们可知ZNDC与Zview的关系如下
OpenGL中ZNDC与Zview的关系
③两式联立可得Zview与Ztexture的关系式
Zview与Ztexture的关系式
④由于OpenGL中观察空间是右手坐标系,而裁剪空间是左手坐标系,所以Zview还要乘以-1
最终的Zview
此时,Zview的取值范围为[Near,Far]。
⑤值范围为[0,1]的深度值
但有时我们想使用取值范围为[0, 1]的线性深度值,这时我们只需要将Zview除以Far即可。
OpenGL值范围为[0,1]的深度值
当然,在写代码时不用我们这样去计算,也不用区分OpenGL与DirectX,Unity已经将上述过程封装为了LinearEyeDepth与Linear01Depth。
LinearEypDpeth复杂把深度纹理的采样结果转换到观察空间下的深度值,即第④步得到的Zview;
Linear01Depth返回的是范围在[0,1]的线性深度值,即第⑤步得到的Z01
两者位于UnityCG.cginc,定义如下:

// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{
    return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

_ZBufferParams的值如下,位于UnityShaderVariables.cginc。
可参考:https://forum.unity.com/threads/_zbufferparams-values.39332/

// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
// or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1)
// x = -1+far/near
// y = 1
// z = x/far
// w = 1/far
float4 _ZBufferParams;

需要注意:
使用LinearEyeDepth得到的Zview永远都是正值!!!所以如果想真真正正得到观察空间的深度值的话,还需要乘以-1!
使用LinearEyeDepth得到的Zview永远都是正值!!!所以如果想真真正正得到观察空间的深度值的话,还需要乘以-1!
使用LinearEyeDepth得到的Zview永远都是正值!!!所以如果想真真正正得到观察空间的深度值的话,还需要乘以-1!

2.3 深度纹理为什么存的是NDC坐标中的深度值

我们看2.2节中透视相机的ZNDC,它其实是正比与1/Zview的。Zview是观察空间的深度值,它是线性的,而1/Zview不是线性的。而且我们在实际使用深度纹理时,一般会将其转换为Zview。那么问题来了,既然我们实际使用使用Zview,但为什么深度纹理中保存的值确实ZNDC呢?深度纹理为什么不直接保存我们的Zview值呢?
深度纹理保存NDC坐标的深度值主要原因是为了方便硬件进行透视校正插值

2.3.1 透视校正插值

什么是透视校正插值呢?具体细节可以参考这两篇文章《【基础】透视校正插值(Perspective-Correct Interpolation)》《图形学基础之透视校正插值》,我这儿就简单提一下。
我们知道,在GPU渲染流水线中的三角形遍历阶段中,会根据三角形三个顶点的属性,对该三角形所覆盖的像素进行插值,这个插值一般是用重心坐标法进行插值。
Unity3D Shader系列之深度纹理_第16张图片
假设三角形三个顶点的属性分别为I1、I2、I3,现在我们要插值得到三角形所覆盖的某一像素的属性值It
注意这里不是直接使用I1、I2、I3来插值,而是使用I1/ZNDC1、I2/ZNDC2、I3/ZNDC3来插值。插值完成后再除以I/ZNDC从而得到该像素的属性值。如下图。
透视校正插值
用I/ZNDC来插值就叫透视校正插值。 但是为什么一定要用I/ZNDC来插值呢?看上面那两篇文章。

2.3.2 深度纹理使用NDC的Z值的原因

现在我们回到本节最初提出的问题。如果深度纹理保存的是观察空间的深度值Zview,那么我们在三角形遍历进行透视校正插值时,不可避免的需要进行一次除法(因为需要将Zview变换为ZNDC),而三角形遍历完全是由硬件来实现的,这无疑会增加硬件实现的复杂度。所以深度纹理直接保存ZNDC还好一点,虽然我们在Shader中使用时需要将其转换为Zview。

2.4 深度值的精度问题

2.4.1 Z Fighting

我们先来看看由于深度值的精度问题导致的一个现象。
比如我们下面这个测试场景,Ground都是Plane,一个是绿色一个是灰色,两者的Y坐标完全相同,所以两者有重叠。
Unity3D Shader系列之深度纹理_第17张图片
然后我们变换观察角度,会发现重叠部分一会儿出现绿色一会出现灰色。就是因为两个面片隔得太近,由于深度缓冲区的精度问题,深度测试没办法确定哪一个面片在前哪一个面片在后。

这种由于面片隔得太近或者由于深度值精度问题,导致深度测试没法区分两者的先后顺序的现象,就被称为Z Fighting(Z就是深度值,Fighting就是打架、竞争)。
下面我们看看深度值精度问题是如何出现的。
注:以下两小节基本是对《Depth Precision Visualized》这篇文章的翻译。

2.4.2 DirectX中的深度值精度问题

从2.2节推导的公式我们可以知道,DirectX中,透视相机中,深度纹理存储的深度值ZNDC是正比与1/Zview的,其值范围为0~1。
注意:下面这几张图都是基于DirectX的
用一个通用公式来表示就是
d = ZNDC = a * 1/Zview + b,
其中a,b为与相机近裁剪平面与远裁剪平面有关的常数。
用曲线将这个公式画出来,就是这样。
Unity3D Shader系列之深度纹理_第18张图片
上面那张图中,横坐标是相机观察空间的深度值,即Zview;纵坐标是我们深度纹理中存储的ZNDC值,即ZNDC = 0时,表示近裁剪平面,ZNDC = 1时,表示远裁剪平面。图中,我们假设使用了一个4bit的变量来存储我们的ZNDC,或者说我们的深度纹理是4位的。4bit能表示多少个数字呢?24=16个数字。这16个数字将ZNDC等间隔划分,即纵坐标的刻度是等间隔的。但是我们会发现横坐标刻度在近裁剪平面附近明显要比远裁剪平面密集得多。也就是说虽然我们的ZNDC值是均匀分布的,但是转换后的观察空间的深度值却不是均匀分布的了,在近裁剪平面密集,在远裁剪平面稀疏。也就是说,我们通过采样深度纹理得到ZNDC值后,再转换为观察空间的深度值,其在近裁剪平面的精度远远大于远裁剪平面的精度。

其实正常来说,这样的深度分布也是有好处的,因为我们在近处的精度高一些,远处精度低点,感觉也比较符合正常思维。如果只是为了保证近处渲染的效果,那么直接用正常的ZBuffer就是最好的选择了。但是,主要就在于超大视距,类似超大地图这种,既需要保证远处的精度,又希望保证近处的精度,远处精度衰减太厉害,所以ZFighting现象就出现了。

如果我们将近裁剪平面拉得更近(即near值减小),会导致近裁剪平的深度值精度变得更高,而远裁剪平面附近的深度精度越来越低。两者的不平衡进一步拉大。
Unity3D Shader系列之深度纹理_第19张图片
当然,如果我们将近裁剪平面拉远,这种精度分布不均的情况将会有所缓解。
Unity3D Shader系列之深度纹理_第20张图片
那么我们怎么解决这个精度分布不均的问题呢?
我们想到用浮点数来存储深度值ZNDC。下图中的ZNDC是用指数位为3位,尾数也为3位的浮点数存储。虽然现在可以分隔为40段,但是上述近远裁剪平面分布不均的问题依然存在。
Unity3D Shader系列之深度纹理_第21张图片
那到底怎么解决这个问题呢?很简单,将深度纹理中的值翻转以下即可,即深度纹理中不直接存储ZNDC而是存储的是1-ZNDC,即1表示近裁剪平面,0表示远裁剪平面。
然后我们看看结果。可见使用这种方式有效解决了这个问题。这种技术被形象的称为Reverse Z(反转Z值)。
Unity3D Shader系列之深度纹理_第22张图片
在DirectX中,Reverse Z已经是标配的操作了。也就是说,1表示近裁剪平面,0表示远裁剪平面。我们一般用R通道来存储深度值,所以在DirectX中,深度纹理越红的地方表示距离相机越近。

2.4.3 OpenGL中的深度值精度问题

在DirectX中,我们可以使用Reverse Z来解决深度值的精度问题,OpenGL中是否也可以这样处理呢?
很不幸,OpenGL中Reverse Z是无效的,因为其ZNDC的值范围为[-1, 1],反转了也没意义。
其深度值分布如下。
Unity3D Shader系列之深度纹理_第23张图片

2.5 Unity中生成深度纹理

2.5.1 前向渲染中的深度纹理

前向渲染中,Unity使用了额外的一个Pass来渲染深度纹理,这个Pass同时也是渲染阴影的Pass,即名为ShadowCaster的Pass(Unity内置Shader中的Fallback都会包含此Pass)。
需要注意不论是前向渲染还是延迟渲染,Unity都只会将Shader的渲染队列为2500(即Background、Geometry、AlphaTest)以下的物体渲染到深度纹理中。
Unity3D Shader系列之深度纹理_第24张图片
有同学可能会问,正常渲染场景的时候我们的深度缓冲区不是已经有整个屏幕的深度值了么,为什么还要单独再让相机额外渲染一遍整个场景,导致Draw Call加倍?
这想法确实没错,理论上不透明物体渲染全部渲染完毕后我们就可以直接从深度缓冲区拿到整个屏幕的深度值了,但是Unity目前还不支持。之前有人在论坛问过类似的问题,Unity技术人员回答说,没这样实现(直接将深度缓冲区保存成深度纹理)主要有以下两方面原因:
①是对于非全屏渲染的情况,本来是想拿对应相机渲染的深度,但是Depth Buffer是全屏的
②是因为很多平台不支持直接拿Depth Buffer的数据
Unity3D Shader系列之深度纹理_第25张图片
论坛详情见这里。

但是有个特殊的地方,由于Unity生成阴影使用的是屏幕空间的阴影映射技术(Screen Space Shadow Map),在生成阴影时本身就会生成一张相机的深度纹理图加上一张基于光源空间的阴影映射纹理。所以如果灯光开启了阴影,我们去获取相机的深度纹理并不会导致相机再额外去渲染一遍场景(因为生成阴影时本身就会生成一张深度纹理),也并不会额外增加DrawCall,这也是渲染深度纹理用到的Pass与渲染阴影用到的纹理是同一个Pass的原因。

2.5.2 延迟渲染中的深度纹理

延迟渲染的深度纹理只需要从G-Buffer中拿就行了,不需要像前向渲染那样再额外渲染一遍场景,不会增加任何DrawCall。因为无论我们是否需要深度纹理,延迟渲染都会生成一张深度纹理到G-Buffer中。
Unity3D Shader系列之深度纹理_第26张图片
说个题外话,延迟渲染只渲染一遍场景,但输出却有多个渲染纹理,所以延迟渲染需要显卡支持MRT(Multiple Render Targets,多重渲染目标)。各个渲染纹理的内容如下。
Unity3D Shader系列之深度纹理_第27张图片

2.4.3 两者对比

自然而然,我们就会问用哪一种渲染路径获取深度纹理效率更高?
如果只是考虑深度纹理的获取,无疑延迟渲染效率是更高的。
但并不是或延迟渲染就一定比前向渲染好,延迟渲染也有不好的地方。比如延迟渲染就无法支持半透明物体,同时显卡需要支持MRT,而且不支持真正的抗锯齿。

3 示例程序

3.1 代码解释

深度纹理的原理说了这么多,但其实在Unity中使用起来非常简单。
具体步骤如下:
①创建一个脚本,并继承自Monobehaviour
②指定相机的深度纹理模式为DepthTextureMode.Depth,让此相机输出深度纹理
(如果指定为DepthNormals,将会输出一张深度+法线纹理,这时采样深度+法线纹理需要特殊处理,具体见后面)

m_Camera.depthTextureMode |= DepthTextureMode.Depth;

③新建一个Shader,并将脚本中的m_Material材质指定为此Shader
④实现OnRenderImage方法,内部使用 Graphics.Blit方法,将source原图像(这里是相机看到的画面)经过m_Material的Shader处理后拷贝到destination(这就是我们将看到的画面)

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (m_Material == null)
        {
            Graphics.Blit(source, destination);
            return;
        }

        Graphics.Blit(source, destination, m_Material);
    }

⑥Shader中我们直接使用_CameraDepthTexture即可访问到该相机输出的深度纹理(当然,我们需要在Shader中声明,这样Unity才会自动给我们赋值)

sampler2D _CameraDepthTexture;

⑦然后在片元着色器中,用uv对_CameraDepthTexture采样可得到"深度值",然后再使用Linear01Depth将其转换为归一化后的观察空间深度值

float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
depth = Linear01Depth(depth);

SAMPLE_DEPTH_TEXTURE的定义如下,其实就是使用uv对深度纹理采样,然后返回R通道值,不过该宏对PS2平台进行了差异化处理。

// Depth texture sampling helpers.
// On most platforms you can just sample them, but some (e.g. PSP2) need special handling.
//
// SAMPLE_DEPTH_TEXTURE(sampler,uv): returns scalar depth
// SAMPLE_DEPTH_TEXTURE_PROJ(sampler,uv): projected sample
// SAMPLE_DEPTH_TEXTURE_LOD(sampler,uv): sample with LOD level

#if defined(SHADER_API_PSP2) && !defined(SHADER_API_PSM)
#   define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D<float>(sampler, uv))
#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2DprojShadow(sampler, uv))
#   define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod<float>(sampler, uv))
#   define SAMPLE_RAW_DEPTH_TEXTURE(sampler, uv) SAMPLE_DEPTH_TEXTURE(sampler, uv)
#   define SAMPLE_RAW_DEPTH_TEXTURE_PROJ(sampler, uv) SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv)
#   define SAMPLE_RAW_DEPTH_TEXTURE_LOD(sampler, uv) SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv)
#else
    // Sample depth, just the red component.
#   define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)
#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)
#   define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(sampler, uv).r)
    // Sample depth, all components.
#   define SAMPLE_RAW_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv))
#   define SAMPLE_RAW_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv))
#   define SAMPLE_RAW_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(sampler, uv))
#endif

// Deprecated; use SAMPLE_DEPTH_TEXTURE & SAMPLE_DEPTH_TEXTURE_PROJ instead
#if defined(SHADER_API_PSP2)
#   define UNITY_SAMPLE_DEPTH(value) (value).r
#else
#   define UNITY_SAMPLE_DEPTH(value) (value).r
#endif

注意,如果指定相机的深度纹理模式为DepthTextureMode.DepthNormals,即让此相机输出一张深度+法线纹理。其中观察空间的法线信息会被编码到纹理的R与G通道,深度值会被编码到纹理的B和A通道(即此时的深度值的精度为16位)。

m_Camera.depthTextureMode |= DepthTextureMode.DepthNormals;

此时,我们在Shader中不能直接使用SAMPLE_DEPTH_TEXTURE对深度+法线纹理(此时,Shader中需要定义名为_CameraDepthNormalsTexture的sampler2D变量)进行采样,而要使用Unity封装的DecodeDepthNormal方法来获取深度值与法线值。其位于UnityCG.cginc中,定义如下。

inline void DecodeDepthNormal(float4 enc,out float depth, out float3 normal)
{
	depth = DecodeFloatRG(enc.zw);
	normal = DecodeViewNormalStereo(enc);
}

需要注意,使用DecodeDepthNormal得到的深度值是值为[0,1]的观察空间的线性深度值,不需要再调用Linear01Depth进行处理;得到的法线值也是观察空间下的法线。

3.2 场景扫描效果

Unity3D Shader系列之深度纹理_第28张图片
相信只要看懂了深度纹理的原理,实现这个效果就是小菜一叠啦。
C#脚本,挂载到相机上。

using UnityEngine;

[RequireComponent(typeof(Camera))]
public class SceneScanDemo : MonoBehaviour
{
    private Camera m_Camera;
    private Material m_Material;

    [SerializeField]
    private Color m_ScanColor = Color.red;
    [SerializeField]
    private float m_ScanWidth = 0.02f;
    [SerializeField]
    private float m_ScanSpeed = 0.03f;

    private void Awake()
    {
        m_Camera = gameObject.GetComponent<Camera>();
        m_Material = new Material(Shader.Find("LaoWang/DepthTexture"));
    }

    private void OnEnable()
    {
        m_Material.SetColor("_ScanColor", m_ScanColor);
        m_Material.SetFloat("_ScanWidth", m_ScanWidth);
        m_Material.SetFloat("_ScanSpeed", m_ScanSpeed);
        m_Camera.depthTextureMode |= DepthTextureMode.Depth;
        
    }

    private void OnDisable()
    {
        m_Camera.depthTextureMode &= ~DepthTextureMode.Depth;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (m_Material == null)
        {
            Graphics.Blit(source, destination);
            return;
        }

        Graphics.Blit(source, destination, m_Material);
    }
}

Shader

Shader "LaoWang/DepthTexture"
{
	Properties
	{
		_MainTex ("Main Tex", 2D) = "white"{}
		_ScanColor ("Scan Color", Color) = (1, 0, 0, 0.5)
		_ScanWidth ("Scan Width", Range(0.001, 0.5)) = 0.002
		_ScanSpeed ("Scan Speed", Range(0.001, 8)) = 0.03
	}

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

		ZTest Off
		Cull Off
		ZWrite Off
		Fog{ Mode Off }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _CameraDepthTexture;
			fixed4 _ScanColor;
			float _ScanSpeed;
			float _ScanWidth;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
				fixed4 color = tex2D(_MainTex, i.uv);
 
				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
				depth = Linear01Depth(depth);
				float pos = (_Time.y * _ScanSpeed * 1000 % 1000) * 0.001;

				// sign(x) 如果x<0,返回-1;如果x=0,返回0;如果x>0,返回1
				float scanFlag = saturate(sign(depth - pos)) * saturate(sign(pos + _ScanWidth - depth));
				color.rgb = color.rgb * (1 - _ScanColor.a * scanFlag) + _ScanColor.rgb * scanFlag;
				return color;
            }
            ENDCG
        }
    }
}

相机设置。
Unity3D Shader系列之深度纹理_第29张图片
当然,使用深度纹理可以实现很多酷炫的效果,我们后面会专门来实现几个。

4 其他问题

深度纹理怎么是全黑的?
那是因为近裁剪平面设得太小了,会导致深度值的精度问题,具体见2.4节。
Unity3D Shader系列之深度纹理_第30张图片


博主本文博客链接。


5 参考文章

  • 《Unity Shader入门精要》第13章
  • the-direct3d-transformation-pipeline
  • Depth Precision Visualized
  • 【Unity】深度图(Depth Texture)的简单介绍
  • Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)
  • 深度的应用
  • 关于_CameraDepthTexture的疑惑
  • 如何在shader中避免使用if else

你可能感兴趣的:(Unity3D,Unity3D,Shader,深度纹理,Reverse,Z,Depth,Texture)