获取深度和法线纹理
背后的原理
深度纹理是一张渲染纹理,它里面存储的像素值不是颜色,而是一个高精度的深度值。深度值范围是[0, 1],非线性分布的。这些深度值来自于顶点变换后得到的归一化的设备坐标(NDC)。一个模型想要被绘制在屏幕上,需要把它的顶点从模型空间变换到齐次裁剪坐标系下,这是通过顶点着色器中乘以MVP变换矩阵得到的。在变换的最后一步,我们需要用一个投影矩阵来变换顶点,当我们使用的是透视投影类型的摄像机时,这个投影矩阵就是非线性的。
透视投影对顶点的变换过程。左侧显示投影变换前,观察空间下视锥体的结构及相应顶点位置。中间显示应用裁剪矩阵之后的变换结果,顶点着色器阶段输出的顶点变换结果。右侧是底层硬件进行了透视除法后得到的归一化的设备坐标。
使用正交摄像机时投影变换过程。同样会得到一个范围为[-1, 1]的立方体,正交投影使用的变换矩阵是线性的。
得到NDC之后,深度值就对应了NDC中顶点坐标的z分量的值。由于NDC中z分量值范围-1~1,为了存储在一张图像中,我们需要对其映射。
d=0.5\cdot z_{ndc}+0.5
d对应了深度纹理中的像素值,z_{ndc}对应了z分量的值。
在Unity中,深度纹理可以直接来自于真正的深度缓存,也可以是由一个单独的Pass渲染而得。Unity会使用着色器替换技术选择那些渲染类型(RenderType)标签为Opaque的物体,判断它们使用的渲染队列是否小于等于2500,如果满足条件,就把它渲染到深度和法线纹理中。
如何获取
只需要告诉Unity并在Shader中直接访问特定纹理属性。与Unity沟通过程是在脚本设置摄像机的depthTextureMode完成的,我们可以通过如下代码获取深度纹理值
camera.depthTextureMode = DepthTextureMode.Depth;
设置好上面摄像机后,就可以在Shader中通过声明_CameraDepthTexture变量来访问它。
如果想要获取深度+法线纹理,只需要在代码中这样设置
camera.depthTextureMode = DepthTextureMode.DepthNormals;
在Shader中通过声明_CameraDepthNormalTexture变量来访问它。
在Unity中,我们还可以在摄像机的Camera组件上看到当前摄像机是否需要渲染深度或深度+法线纹理。当在Shader中访问到深度纹理_CameraDepthTexture后,我们就可以使用当前像素的纹理坐标对它采样,Unity为我们提供一个统一的宏SAMPLE_DEPTH_TEXTURE来处理这些平台差异造成的问题。
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
i.uv是一个float2类型的变量,对应了当前像素的纹理坐标。SAMPLE_DEPTH_TEXTURE接受两个参数-深度纹理和一个float3或float4类型的纹理坐标。类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJ、SAMPLE_DEPTH_TEXTURE_LOD。SAMPLE_DEPTH_TEXTURE_PROJ的内部使用了tex2Dproj这样的函数进行投影纹理采样,纹理坐标的前两个分量首先会除以最后一个分量,再进行纹理采样。如果提供了第四个分量,还会进行一次比较,通常用于阴影的实现中。SAMPLE_DEPTH_TEXTURE_PROJ的第二个参数通常是由顶点着色器输出插值而得的屏幕坐标。
float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos));
其中,i.scrPos是在顶点着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标。
当通过纹理采样得到深度值后,这些深度值往往是非线性的,这种非线性来自透视投影使用的裁剪矩阵。但是,我们需要把投影后的深度值变换到线性空间下。我们只需要倒推顶点变换的过程即可。
Unity提供了两个辅助函数来为我们进行上述计算过程:LinearEyeDepth和Linear01Depth。LinearEyeDepth负责把深度纹理的采样结果转换到视角空间下的深度值,也就是z_{view}。Linear01Depth会返回一个范围在[0, 1]的线性深度值,也就是z_{01}。这两个函数使用了内置的_ZBufferParams变量来得到远近裁剪平面的距离。如果我们需要获取深度+法线纹理,可以直接使用tex2D函数对_CameraDepthNormalsTexture进行采样,得到里面存储的深度和法线信息。Unity提供了辅助函数来为我们对这个采样结果进行解码,从而得到深度值和法线方向。这个函数是DecodeDepthNormal,它的第一个参数是对深度+法线纹理的采样结果。这个采样结果是Unity对深度和法线信息编码后的结果,它的xy分量存储的是视角空间下的法线信息,而深度信息被编码进了zw分量。通过调用DecodeDepthNormal函数对采样结果解码后,我们就可以得到解码后的深度值和法线。这个深度值是范围在[0, 1]的线性深度值,而得到的法线则是视角空间下的法线方向。我们也可以用DecodeFloatRG和DecodeViewNormalStereo来解码深度+法线纹理中的深度和法线信息。