之前的屏幕后处理效果都只是在屏幕颜色图像上进行各种操作来实现的。然而,很多时候我们不仅需要当前屏幕的颜色信息,还希望得到深度和法线信息。例如,在进行边缘检测时,直接利用颜色信息会使检测到的边缘信息受物体纹理和光照等外部因素的影响,得到很多我们不需要的边缘点。一种更好的方法是,我们可以在深度纹理和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息
,通过这样的方式检测出来的边缘更加可靠。
由于被存储在一张纹理中,深度纹理里的深度值范围是[0, 1],而且通常是非线性分布的。这些深度值来自于顶点变换后得到的归一化的设备坐标(Normalized Device Coordinates , NDC)。一个模型要想最终被绘制在屏幕上,需要把它的顶点从模型空间变换到齐次裁剪坐标系下,这是通过在顶点着色器中乘以MVP变换矩阵得到的。在变换的最后一步,我们需要使用一个投影矩阵来变换顶点,当我们使用的是透视投影类型的摄像机时,这个投影矩阵就是非线性的。
在得到NDC后,深度纹理中的像素值就可以很方便地计算得到了,这些深度值就对应了NDC中顶点坐标的z分量的值。由于NDC中z分量的范围在[-1, 1],为了让这些值能够存储在一张图像中,需要将其映射到[0,1]。
延迟渲染路径
时,深度纹理可以从G-buffer访问到。单独的深度纹理
:Unity会直接获取深度缓存。或是用着色器替换技术,选取需要的不透明物体,并使用LightMode被设置为ShadowCaster的Pass来得到深度纹理。如果Shader中不包含这样一个Pass,那么这个物体就不会出现在深度纹理中。深度纹理的精度通常是24位或16位,这取决于使用的深度缓存的精度。深度+法线纹理
:Unity会创建一张和屏幕分辨率相同、精度为32位(每个通道为8位)的纹理,其中观察空间下的法线信息会被编码进纹理的R和G通道,而深度信息会被编码进B和A通道。法线信息的获取在延迟渲染中是可以非常容易就得到的,Unity只需要合并深度和法线缓存即可。而在前向渲染中,默认情况下是不会创建法线缓存的,因此Unity底层使用了一个单独的Pass把整个场景再次渲染一遍来完成。这个Pass被包含在Unity内置的一个Unity Shader中,我们可以在内置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormalTexture.shader文件中找到这个用于渲染深度和法线信息的Pass。在脚本中设置摄像机的depthTextureMode
然后在Shader中访问对应变量即可。
//【1】深度纹理
camera.depthTextureMode = DepthTextureMode.Depth;//在Shader中声明 _ CameraDepthTexture
//【2】深度+法线纹理
camera.depthTextureMode = DepthTextureMode.DepthNormals;//在Shader中声明_CameraDepthNormalsTexture
//【3】深度 和 深度+法线纹理
camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;
在Unity 5中,我们还可以在摄像机的Camera组件上看到当前摄像机是否需要渲染深度或深度+法线纹理。
(1)使用tex2D函数
采样:当在Shader中访问到深度纹理_CameraDepthTexture后,绝大多数情况下,我们就可以使用当前像素的纹理坐标对它进行采样。
(2)使用SAMPLE_DEPTH_TEXTURE
宏对深度纹理进行采样:在某些平台(例如PS3和PSP2)上,我们需要一些特殊处理。Unity为我们提供了一个统一的宏SAMPLE_DEPTH_TEXTURE,用来处理这些由于平台差异造成的问题。
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
(3)使用SAMPLE_DEPTH_TEXTURE_PROJ
宏 ( 类似的还有SAMPLE_DEPTH_TEXTURE_LOD
)
float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,UNITY_PROJ_COORD(i.srcPos));
(1)公式推导
当通过纹理采样得到深度值后,这些深度值往往是非线性的,这种非线性来自于透视投影使用的裁剪矩阵
。然而,在我们的计算过程中通常是需要线性的深度值,也就是说,我们需要把投影后的深度值变换到线性空间下,例如视角空间下的深度值
。当我们使用透视投影的裁剪矩阵 P c l i p P_{clip} Pclip对视角空间下的一个顶点进行变换后,裁剪空间下顶点的z和w分量为:
其中,Far和Near分别是远近裁剪平面的距离。然后,我们通过齐次除法就可以得到NDC下的z分量:
深度纹理中的深度值是通过下面的公式由NDC计算而得的: d = 0.5 ⋅ z n d c + 0.5 d=0.5· z_{ndc}+0.5 d=0.5⋅zndc+0.5。由上面的这些式子,我们可以推导出用d表示而得的 z v i s w z_{visw} zvisw的表达式:
由于在Unity使用的视角空间中,摄像机正向对应的z值均为负值,因此为了得到深度值的正数表示,我们需要对上面的结果取反,最后得到的结果如下:
它的取值范围就是视锥体深度范围,即[Near, Far]。如果我们想得到范围在[0, 1]之间的深度值,只需要把上面得到的结果除以Far即可。这样,0就表示该点与摄像机位于同一位置,1表示该点位于视锥体的远裁剪平面上。结果如下:
(2)Unity函数
Unity提供了两个辅助函数来为我们进行上述的计算过程:
LinearEyeDepth()
:负责把深度纹理的采样结果转换到视角空间下的深度值,也就是我们上面得到的 z v i e w ′ z'_{view} zview′。Linear01Depth()
:返回一个范围在[0,1]的线性深度值,也就是我们上面得到的 z 01 z_{01} z01。这两个函数内部使用了内置的_ZBufferParams变量来得到远近裁剪平面的距离。使用tex2D函数
对_CameraDepthNormalsTexture
进行采样,得到里面存储的深度和法线信息。Unity提供了辅助函数来为我们对这个采样结果进行解码,从而得到深度值和法线方向。这个函数是DecodeDepthNormal()
,它在UnityCG.cginc里被定义:
inline void DecodeDepthNormal(float4 enc,out float depth, out float3 normal){
depth = DecodeFloatRG(enc.zw);
normal = DecodeViewNormalStereo(enc);
}