US3S11L2——深度和法线纹理背后的原理

知识回顾 —— 裁剪空间变换

具体内容详见:US2S1L13——观察空间变换、US2S1L14——齐次裁剪空间和NDC空间 等裁剪空间相关内容

  • 观察空间:也被称为摄像机空间,遵循右手坐标系原则,观察空间中原点为摄像机位置,x、y、z轴正方向分别对应摄像机的右、上、后方

  • 裁剪空间:也被称为齐次裁剪空间,是一个非常特殊的空间,它的坐标范围为 (-1,-1,-1) 到 (1,1,1) 的一个正方体范围

    我们需要将摄像机视锥体变换到其中,其中相对复杂的是 摄像机 的 透视投影正交投影齐次裁剪空间变换矩阵推导

    image

    image

    其中,正交投影的齐次裁剪空间坐标的转换矩阵为:

    (1Aspect×Size00001Size00002FarNearNear+FarFarNear0001)\begin{pmatrix} \frac{1}{Aspect \times Size} & 0 & 0 & 0 \\ 0 & \frac{1}{Size} & 0 & 0 \\ 0 & 0 & -\frac{2}{Far - Near} & -\frac{Near+Far}{Far - Near} \\ 0 & 0 & 0 & 1 \\ \end{pmatrix}

    透视投影的齐次裁剪空间坐标的转换矩阵为:

    (1Aspecttan(FOV2)00001tan(FOV2)0000Far+NearFarNear2FarNearFarNear0010)\begin{pmatrix} \frac{1}{Aspect \cdot \tan(\frac{FOV}{2})} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan(\frac{FOV}{2})} & 0 & 0 \\ 0 & 0 & -\frac{Far + Near}{Far - Near} & -\frac{2 Far\cdot Near}{Far - Near} \\ 0 & 0 & -1 & 0 \\ \end{pmatrix}

    利用观察空间中的点根据使用的摄像机类型决定和哪一个变换矩阵进行矩阵乘法后,便可以得到齐次裁剪空间下的坐标信息

深度纹理中存储的是什么信息

Unity 中的深度纹理中存储的信息,也就是 Shader 中使用 _CameraDepthTexture​ 或 _CameraDepthNormalsTexture​ 采样的信息,
是进行裁剪空间变换后的 z 分量再转换到 0~1 之后的结果

image

因为齐次裁剪空间坐标范围为 -1~1,而纹理中存储的信息范围是 0~1,因此 Unity 会将其利用以下公式进行转换:

深度纹理值=0.5×z+0.5深度纹理值 = 0.5 \times z + 0.5

也就是说,我们通过深度纹理直接采样得到的深度纹理值,是进行裁剪空间变换后的 z 分量再转换到 0~1 之后的结果

法线纹理中存储的是什么消息

Unity 中的法线纹理中存储的信息,也就是 Shader 中使用 _CameraDepthNormalsTexture​ 采样得到的 float4​ 中的部分信息
它是观察空间下的法线再转换到 0~1 之后的结果,

因为观察空间下的单位向量的分量取值范围是 -1~1,而纹理中存储的信息范围是 0~1,因此 Unity 会将其利用以下公式进行转换:

法线纹理值=(观察空间下法线+1)×0.5法线纹理值 = (观察空间下法线 + 1) \times 0.5

也就是说我们通过法线纹理直接采样得到的法线纹理值,是观察空间下的法线再转换到 0~1 之后的结果

Unity 是如何得到深度和法线纹理的

在 Unity 中深度和法线纹理一般通过两种途径获取:

  1. 从 G-buffer 几何缓冲区中获取
  2. 由一个专门的 Pass​ 渲染而来

具体 Unity 是通过哪种方式获取,取决于使用的渲染路径和设备的硬件限制。

  • 当使用延迟渲染路径时,深度和法线纹理可以直接访问到

    因为延迟渲染路径会把信息存储到 G-buffer 几何缓冲区中(深度和法线等信息都存储在其中)。

  • 而当无法直接获取到深度和法线纹理时(比如硬件不支持延迟渲染路径或使用的是前向渲染路径时)

    Unity 会通过一个单独的 Pass 来进行渲染,获取深度和法线信息。

需要注意的是,当使用单独的 Pass​ 渲染获得深度和法线纹理时,两者是有区别的,对于深度纹理来说:
Unity 内部会使用着色器替换技术(根据渲染类型替换内部 Shader) 选择渲染类型 "RenderType" = "Opaque"​(不透明物体),
然后判断它们的渲染队列 Queue​ 是否小于等于 2500( Background​ - 1000、Geometry​ - 2000、AlphaTest​ - 2450)

  • 如果满足这个条件,就会使用物体投射阴影时的 Pass(LightMode​ 为 ShadowCaster​ 的 Pass​)来得到深度纹理
  • 如果没有这个 Pass​,那么该物体不会出现在深度纹理中!

因此这里的重点是,如果我们希望物体能够正确的出现在深度纹理中:

  1. 必须在 Shader​ 中正确的设置 "RenderType"​ 标签
  2. 必须有投射阴影用的 Pass​(LightMode​ 为 ShadowCaster​ 的 Pass​)

对于法线纹理来说:

Unity 底层会使用一个单独的 Pass把整个场景再次渲染一次, 从而得到深度和法线信息,
这里为什么是深度和法线信息呢:因为当需要得到法线纹理时,Unity 中是和深度一起获取的,
这两个纹理在上节课的使用知识点中有讲解 (_CameraDepthNormalsTexture​)
这个 Pass​ 包含在 Unity 内置的 Shader 中,我们可以在官方下载源文件解压后进行查看

image

深度和法线纹理使用时调用的函数原理

通过上节课的学习,我们知道,想要在 Shader 当中使用深度和法线纹理需要做以下的事情:

  1. 设置 Camera​ 的深度纹理模式

    1
    2
    3
    4
    5
    6
    private void Start()
    {
    Camera.main.depthTextureMode = DepthTextureMode.Depth; // 获取一张深度纹理
    Camera.main.depthTextureMode = DepthTextureMode.DepthNormals; // 获取一张深度+法线纹理
    Camera.main.depthTextureMode = DepthTextureMode.Depth | DepthTextureMode.DepthNormals; // 获取深度纹理 和 深度+法线纹理
    }
  2. 在 Shader 中声明深度纹理或法线纹理的变量

    1
    2
    sampler2D _CameraDepthTexture;            // 深度纹理
    sampler2D _CameraDepthNormalsTexture; // 深度+法线纹理
  3. 通过内置函数采样、转换得到深度值和法线信息

    1. 获取深度纹理的深度值

      1
      2
      3
      4
      5
      6
      fixed4 frag(v2f i) : SV_Target
      {
      float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // 使用基本深度纹理采样,得到的结果是非线性的
      float viewDepth = LinearEyeDepth(depth); // 将非线性的深度值转换到观察空间下
      float linearDepth = Linear01Depth(depth); // 将非线性的深度值转换为[0,1]区间内的线性深度值
      }
    2. 获取深度+法线纹理的法线消息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      fixed4 frag(v2f i) : SV_Target
      {
      float depth; // 用于存储深度值的变量
      float3 normals; // 用于存储法线的变量
      float4 depthNormal = tex2D(_CameraDepthNormalsTexture, i.uv); // 对深度+法线纹理进行采样(xy是法线消息,zw是深度消息)
      DecodeDepthNormal(depthNormal, depth, normals); // 一次处理得到深度值和法线消息
      depth = DecodeFloatRG(depthNormal.zw); // 单独得到深度值
      normals = DecodeViewNormalStereo(depthNormal); // 单独得到法线消息
      }

通过对这些宏和函数的介绍,大家只要记住,直接采样出来的深度和法线信息是不会直接使用的,
我们需要将他们通过内置函数进行转换,得到最终我们会使用的观察空间下的深度和法线信息

SAMPLE_DEPTH_TEXTURE

它是用于从深度纹理中进行采样的宏,相比直接用 tex2D​ 进行采样,它在内部会帮助我们适配各种不同的平台,
因为不同平台对深度纹理的采样规则会有所不同,因此我们使用它。
它采样得到的深度值是裁剪空间下的 z 分量转换到 0~1 之间的结果

1
2
3
4
5
6
fixed4 frag(v2f i) : SV_Target
{
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // 使用基本深度纹理采样,得到的结果是非线性的
float viewDepth = LinearEyeDepth(depth); // 将非线性的深度值转换到观察空间下
float linearDepth = Linear01Depth(depth); // 将非线性的深度值转换为[0,1]区间内的线性深度值
}

通过 SAMPLE_DEPTH_TEXTURE​ 得到的深度值是非线性的,所谓的非线性值的是指在透视摄像机的裁剪空间中​**深度值分部不均匀**

  • 当深度值接近裁剪面近时,深度值变化迅速,精度高
  • 当深度值接远裁剪面近时,深度值变化缓慢,精度低

更直观的解释:一个相机在观察一个3D场景时,近处的物体移动一点,视觉上变化很大,所以需要更高的精度来记录这种变化。
而远处的物体移动同样的距离,视觉上的变化很小,因此可以使用较低的精度来记录

因此为了让我们在 Shader 中利用深度值进行的计算更加准确,我们需要获得线性的深度值,只需要把裁剪空间下的深度值转换到观察空间下,便可以得到线性的深度值
Unity Shader 中提供了如下的内置函数:LinearEyeDepth​ 和 Linear01Depth​ 都可以得到观察空间下的线性深度值,两个方法区别在于取值范围

  • LinearEyeDepth​:得到的是像素到摄像机的实际距离
  • Linear01Depth​:得到的是实际距离被压缩到 0~1 之间的值

DecodeDepthNormal

DecodeDepthNormal​ 函数内部其实也是执行的 DecodeFloatRG​ 和 DecodeViewNormalStereo​ 函数,
它的作用就是得到观察空间下的对应像素的 法线 和 线性 深度值 (0~1)

你可以一次性的获得两个信息,也可以选择分别调用 DecodeFloatRG​ 和 DecodeViewNormalStereo​ 单独获取深度和法线信息

1
2
3
4
5
6
7
8
9
fixed4 frag(v2f i) : SV_Target
{
float depth; // 用于存储深度值的变量
float3 normals; // 用于存储法线的变量
float4 depthNormal = tex2D(_CameraDepthNormalsTexture, i.uv); // 对深度+法线纹理进行采样(xy是法线消息,zw是深度消息)
DecodeDepthNormal(depthNormal, depth, normals); // 一次处理得到深度值和法线消息
depth = DecodeFloatRG(depthNormal.zw); // 单独得到深度值
normals = DecodeViewNormalStereo(depthNormal); // 单独得到法线消息
}

函数中具体做的事情,就是利用法线的 xy算出 z,得到最终的法线信息;
然后,将裁剪空间下的非线性深度值 转换为观察空间下线性的范围为 0~1 的深度值