找回密码
 立即注册
查看: 392|回复: 0

Unity Shader之获取深度和法线纹理

[复制链接]
发表于 2022-7-2 13:14 | 显示全部楼层 |阅读模式
本文主要参考《Unity Shader入门精要》的13.1节
在之前的文章中,我们学习的屏幕后处理效果都只是在屏幕颜色图像上进行各种操作来实现的。然而,很多时候我们不仅需要当前屏幕的颜色信息 ,还希望得到深度和法线信息
例如,在进行边缘检测时,直接利用颜色信息会使检测到的边缘信息受物体纹理和光照等外部因素的影响,得到很多我们不需要的边缘点。一种更好的方法是,我们可以在深度纹理和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息,通过这样的方式检测出来的边缘更加可靠。
在本文中,我们将学习如何在 Unity 中获取深度纹理和法线纹理来实现特定的屏幕后处理效果。
1. 获取深度和法线纹理的原理

深度纹理实际就是一张渲染纹理,只不过它里面存储的像素值不是颜色值,而是一个高精度的深度值。由于被存储在一张纹理中,深度纹理里的深度值范围是[0, 1], 而且通常是非线性分布的。
那么,这些深度值是从哪里得到的呢?总体来说,这些深度值来自于顶点变换后得到的归一化的设备坐标(Normalized Device Coordinates , NDC) 。回顾一下书的4.6节,一个模型要想最终被绘制在屏幕上,需要把它的顶点从模型空间变换到齐次裁剪坐标系下,这是通过在顶点着色器中乘以 MVP 变换矩阵得到的 。在变换的最后一步,我们需要使用一个投影矩阵来变换顶点,当我们使用的是透视投影类型的摄像机时,这个透视投影矩阵就是非线性的



在透视投影中,投影矩阵首先对顶点进行了缩放。在经过齐次除法后,透视投影的裁剪空间会变换到一个立方体。图中标注了 4个关键点经过投影矩阵变换后的结果

上图显示了 Unity 中透视投影对顶点的变换过程。最左侧的图显示了投影变换前,即观察空间下视锥体的结构及相应的顶点位置,中间的图显示了应用透视裁剪矩阵后的变换结果,即顶点着色器阶段输出的顶点变换结果 ,最右侧的图则是底层硬件进行了透视除法后得到的归一化的设备坐标。
需要注意的是,这里的投影过程是建立在 Unity 对坐标系的假定上的,也就是说,我们针对的是观察空间为右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换到 NDC 分量范围将在[-1, 1] 之间的情况。



在正交投影中,投影矩阵对顶点进行了缩放。在经过齐次除法后,正交投影的裁剪空间会变换到一个立方体。图中标注了4个关键点经过投影矩阵变换后的结果

正交投影使用的变换矩阵是线性的。
在得到 NDC 后,深度纹理中的像素值就可以很方便地计算得到了,这些深度值就对应了 NDC 中顶点坐标的 z 分量的值。由于 NDC 分量的范围在[-1, 1],为了让这些值能够存储在一张图像中,我们需要使用下面的公式对其进行映射:


其中, d 对应了深度纹理中的像素值, 对应了 NDC 坐标中的 z 分量的值。
那么 Unity 是怎么得到这样一张深度纹理的呢?在 Unity 中,深度纹理可以直接来自于真正的深度缓存,也可以是由一个单独的 Pass (LightMode 被设置为 ShadowCaster 的Pass)渲染而得,这取决于使用的渲染路径和硬件。
通常来讲,当使用延迟渲染路径(包括遗留的延迟渲染路径)时,深度纹理理所当然可以访问到,因为延迟渲染会把这些信息渲染到 G-buffer 。
而当无法直接获取深度缓存时,深度和法线纹理是通过一个单独的 Pass 渲染而得 。具体实现是 Unity 会使用着色器替换 (Shader Replacement) 技术选择那些渲染类型(即 SubShader 的 RenderType 标签)为 Opaque 的物体,判断它们使用的渲染队列是否小于等于 500 (内置的 Background,Geometry,AlphaTest 渲染队列均在此范围内),如果满足条件,就把它渲染到深度和法线纹理中。因此,要想让物体能够出现在深度和法线纹理中,就必须在 Shader 中设置正确的 RenderType 标签。
在 Unity 中,我们可以选择让一个摄像机生成一张深度纹理或是一张深度+法线纹理
当选择前者,即只需要一张单独的深度纹理时, Unity 会直接获取深度缓存或是按之前讲到的着色器替换技术,选取需要的不透明物体,并使用它投射阴影时使用的 Pass (即 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-DepthNormaITexture.shader 文件中找到这个用于渲染深度和法线信息的 Pass。
2. 如何获取

在 Unity 中,获取深度纹理是非常简单的,通过在脚本中设置摄像机的 depthTextureMode 来完成的,例如我们可以通过下面的代码来获取深度纹理:
camera.depthTextureMode = DepthTextureMode.Depth;
一旦设置好了上面的摄像机模式后,我们就可以在 Shader 中通过声明 _CameraDepthTexture 变量来访问它。
同理,如果想要获取深度+法线纹理,我们只需要在代码中这样设置:
camera.depthTextureMode = DepthTextureMode.DepthNormals;
然后在 Shader 中通过声明 _CameraDepthNormalsTexture 变量来访问它。
我们还可以组合这些模式,让一个摄像机同时产生一张深度和深度+法线纹理:
camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;当在 Shader 中访问到深度纹理 _CameraDepth Texture 后,我们就可以使用当前像素的纹理坐标对它进行采样。绝大多数情况下,我们直接使用 tex2D 函数采样即可,但在某些平台(例如 PS3 PSP2) 上,我们需要 一些特殊处理。Unity 为我们提供了一个统一的宏 SAMPLE_DEPTH_TEXTURE,用来处理这些由于平台差异造成的问题。而我们只需要在 Shader 中使用 SAMPLE_DEPTH_TEXTURE 宏对深度纹理进行采样,例如:
float d = SAMPLE_DEPTH_TEXTURE (CameraDepthTexture , i.uv) ;其中, i.uv 是一个 float2 类型的变量,对应了当前像素的纹理坐标。类似的宏还有 SAMPLE_DEPTH_ TEXTURE_PROJ 和 SAMPLE_DEPTH_TEXTURE_LOD。上述这些宏的定义,读者可以在Unity 内置的HLSLSupport.cginc文件中找到。
当通过纹理采样得到深度值后,这些深度值往往是非线性的,这种非线性来自于透视投影使用的裁剪矩阵。然而,在我们的计算过程中通常是需要线性的深度值,也就是说,我们需要把投影后的深度值变换到线性空间下,例如视角空间下的深度值。那么,我们应该如何进行这个转换呢?实际上,我们只需要倒推顶点变换的过程即可。(具体推导过程见书的13.1节)
推导出用d表示而得的  的表达式:


由于在Unity 使用的视角空间中,摄像机正向对应的z值均为负值,因此为了得到深度值的正数表示,我们需要对上面的结果取反,最后得到的结果如下:


它的取值范围就是视锥体深度范围,即[Near, Far]。如果我们想得到范围在[0, 1]之间的深度值,只需要把上面得到的结果除以Far即可。这样,0就表示该点与摄像机位于同一位置,1表示该点位于视锥体的远裁剪平面上。结果如下:


Unity提供了两个辅助函数来为我们进行上述的计算过程——LinearEyeDepth 和 Linear01Depth。LinearEyeDepth 负责把深度纹理的采样结果转换到视角空间下的深度值,也就是我们上面得到的  。而Linear01Depth则会返回一个范围在[1, 1]的线性深度值,也就是我们上面得到的 。这两个函数内部使用了内置的 _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);
}
DecodeDepthNormal 的第一个参数是对深度+法线纹理的采样结果,这个采样结果是 Unity 对深度和法线信息编码后的结果,它的 xy 分量存储的是视角空间下的法线信息,而深度信息被编码进了 zw 分量。通过调用 DecodeDepthNormal 函数对采样结果解码后,我们就可以得到解码后的深度值和法线。这个深度值是范围在[0, 1] 的线性深度值(这与单独的深度纹理中存储的深度值不同),而得到的法线则是视角空间下的法线方向。同样 也可以通过调用 DecodeFloatRG 和 DecodeViewNormalStereo 来解码深度+法线纹理中的深度和法线信息。
3. 查看深度和法线纹理

Unity 5 提供了一个方便的方法来查看摄像机生成的深度和法线纹理,这个方法就是帧调试器(Frame Debugger) 。下图显示了使用帧调试器查看到的深度纹理和深度+法线纹理。



使用 Frame Debugger 查看深度纹理(左)和深度+法线纹理(右)。如果当前摄像机需要生成深度和法线纹理,帧调试器的面板中就会出现相应的渲染事件。只要单击对应的事件就可以查看得到的深度和法线纹理

使用帧调试器查看到的深度纹理是非线性空间的深度值,而深度+法线纹理都是由 Unity 编码后的结果。有时,显示出线性空间下的深度信息或解码后的法线方向会更加有用。此时,我们可以自行在片元着色器中输出转换或解码后的深度和法线值,我们可以使用类似下面的代码来输出线性深度值:
float depth = SAMPLE_ DEPTH_TEXTURE (_CameraDepthTexture , i.uv);
float linearDepth = Linear01Depth(depth);
return fixed4(linearDepth, linearDepth, linearDepth, 1.0);
或是输出法线方向:
fixed3 normal= DecodeViewNorma1Stereo(tex2D(_CameraDepthNormalsTexture , i.uv) .xy) ;
return fixed4 (normal * 0.5 + 0.5, 1.0);
在查看深度纹理时,读者得到的画面有可能几乎是全黑或全白的。这时候读者可以把摄像机的远裁剪平面的距离 (Unity 默认为 1000)调小,使视锥体的范围刚好覆盖场景的所在区域。这是因为,由于投影变换时需要覆盖从近裁剪平面到远裁剪平面的所有深度区域,当远裁剪平面的距离过大时,会导致离摄像机较近的距离被映射到非常小的深度值,如果场景是一个封闭的区域 (如下图所示),那么这就会导致画面看起来几乎是全黑的。相反,如果场景是一个开放区域,且物体离摄像机的距离较远,就会导致画面几乎是全白的。



左边:线性空间下的深度纹理。右边:解码后并且被映射到[0, 1]范围内的视角空间下的法线纹理

Reference
[1] 《Unity Shader入门精要》—— 冯乐乐

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 05:21 , Processed in 0.090207 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表