unity shader之——使用深度和法线纹理

下面会学习如何在unity中获取深度纹理和法线纹理来实现特定的屏幕后处理效果。

一、获取深度和法线纹理

1.1 背后原理

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

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

d=0.5*Zndc+0.5

其中,d对应了深度纹理中的像素值,Zndc对应了NDC坐标中的z分量的值。

unity是怎么得到这样一张深度纹理的呢?在unity中,深度纹理可以直接来自于真正的深度缓存,也可以是由一个单独的Pass渲染而得,这取决于使用的渲染路径和硬件。通常讲,当时用延迟渲染路径时,深度纹理理所当然可以访问到,因为延迟渲染会把这些信息渲染到G-buffer中。而当无法直接获取深度缓存时,深度和法线纹理是通过一个单独的Pass渲染而得的。具体实现是,unity会使用着色器替换技术选择那些渲染类型(RenderType标签)为Opaque的物体,判断它们使用的渲染队列是否小于等于2500(内置的background、Geometry和AlphaTest渲染队列均在此范围内),如果满足条件,就把它渲染到深度和法线纹理中。想要物体能够出现在深度和法线纹理中,就必须在shader中中设置正确的RenderType标签。

在unity中,我们可以选择让一个摄像机生成一张深度纹理或是一张深度+法线纹理:

1. 当选择前者,即只需要一张单独的深度纹理时,unity会直接获取深度缓存或按之前讲到的着色器替换技术,选取需要的不透明物体,并使用它投射阴影时使用的Pass(即LightMode被设置为ShadowCaster的Pass)来得到深度纹理。如果shader中不包含这样一个pass,那么这个物体就不会出现在深度纹理中(当然也不能向其他物体投射阴影)。深度纹理的精度通常是24位或16位,这取决于使用的深度缓存的精度。

2. 如果选择生成一张深度+法线纹理,unity会创建一张和屏幕分辨率相同、精度为32位(每个通道为8位)的纹理,其中观察空间下的法线信息会被编码进纹理的R和G通道,而深度信息会被编码进B和A通道,法线信息的获取在延迟渲染中是可以非常容易就得到的,unity只需要合并深度和法线缓存即可。而在前向渲染中,默认情况下是不会创建法线缓存的,因此unity底层使用了一个单独的Pass把整个场景再次渲染一遍来完成。这个Pass被包含在unity一个内置shader中(Camera-DepthNormalTexture.shader),可以找到这个用于渲染深度和法线信息的Pass。

1.2 如何获取

在unity中,获取深度纹理是非常简单的,只需要告诉unity把深度纹理给自己,然后在shader中访问特定的纹理属性即可。这个与unity沟通的过程是通过在脚本中设置摄像机的depthTextureMode来完成的,通过下面代码获取深度纹理:

camera.depthTexureMode = DepthTextureMode.Depth;

一旦设置好了上面的摄像机模式后,我们就可以在shader中通过声明_CameraDepthTexture变量来访问它,这个过程非常简单,但我们需要知道这两行代码的背后,unity为我们做了许多工作,前边的理论里也说了。

同理,如果想要获取深度+法线纹理,我们只需要在代码中这样设置:

camera.depthTexureMode = DepthTextureMode.DepthNormals;

然后在shader中通过声明_CameraDepthNormalsTexture变量来访问它。

我们还可以组合这些模式,让一个摄像机同时产生一张深度和深度+法线纹理:

camera.depthTexureMode |= DepthTextureMode.Depth;
camera.depthTexureMode |= DepthTextureMode.DepthNormals;

在unity5中,我们还可以在摄像机的Camera组件上看到当前摄像机是否需要渲染深度或深度+法线纹理,当在shader中访问到深度纹理_CameraDepthTexture后,我们就可以使用当前像素纹理坐标对它进行采样。在大多数情况下我们直接使用tex2D函数采样即可,但在某些平台(例如PS3和PSP2)上,我们需要一些特殊处理。unity为我们提供了一个统一的宏SAMPLE_DEPTH_TEXTURE宏对深度纹理进行采样,例如:

float d=SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);

其中,i.uv是一个float2类型的变量,对应了当前像素的纹理坐标。类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJ和SAMPLE_DEPTH_TEXTURE_LOD。SAMPLE_DEPTH_TEXTURE_PROJ宏同样接受两个参数——深度纹理和一个float3或float4类型的纹理坐标,它的内部使用了tex2Dproj这样的函数进行投影纹理采样,纹理坐标的前两个分量首先会除以最后一个分量,再进行纹理采样。如果提供了第四个分量,还会进行一次比较,通常用于阴影的实现中。SAMPLE_DEPTH_TEXTURE_PROJ的第二个参数通常是由顶点着色器输出插值而得的屏幕坐标,例如:

float d=SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,UNITY_PROJ_COORD(i.srcPos));

其中,i.scrPos是在顶点着色器中通过调用ComputerScreenPos(o.pos)得到的屏幕坐标。

当通过纹理采样得到深度值后,这些深度值往往是非线性的,这种非线性来自于透视投影使用的裁剪矩阵。然而在我们的计算过程中通常是需要线性的深度值,也就是说需要把投影后的深度值变换到线性空间下,例如视角空间下的深度值,unity提供了两个辅助函数来实现——LinearEyeDepth和Linear01Depth。LinearEyeDepth负责把深度纹理的采样结果转换到视角空间下的深度值,而Linear01Depth则会返回一个范围在[0,1]的线性深度值,这两个函数内部使用了内置的_ZBufferParams变量来得到远近裁剪平面的距离。

如果我们需要获取深度+法线纹理,可以直接使用tex2D函数对_CameraDepthNormalsTexture进行采样,得到里面存储的深度和法线信息。unity提供了辅助函数来对我们这个采样结果进行解码,从而得到深度值和法线方向。这个函数是DecodeDepthNormal,它在UnityCG.cginc里被定义:

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

DecodeDepthNormal的第一个参数是对深度+法线纹理的采样结果,这个采样结果是unity对深度和法线信息编码后的结果,它的xy分量存储的是视角空间下的法线信息,而深度信息被编码进了zw分量。通过调用DecodeDepthNormal函数对采样结果解码后,我们就可以得到解码后的深度值和法线。这个深度值是范围在[0,1]的线性深度值而得到的法线这是视角空间下的法线方向。同样,我们也可以通过调用DecodeFloatRG和DecodeViewNormalStereo来解码深度+法线纹理中的深度和法线信息。

至此我们已经学会了如何在unity获取及使用深度法线纹理,下面会学习如果使用他们实现各种屏幕特效。

1.3 查看深度和法线纹理

很多时候我们希望可以查看生成的深度和法线纹理,以便对shader进行调试,unity5提可以用Frame Debugger查看摄像机生成的深度和法线纹理:

unity shader之——使用深度和法线纹理

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

使用帧调试器查看到的深度纹理是非线性空间的深度值,而深度+法线纹理都是由Unity编码后的结果,有时显示出线性空间下的深度值信息或解码后的法线方向会更有用。此时我们可以自行在片元着色器中输出转换或解码后的深度和法线值,

可以使用类似下面的代码输出线性深度值:

float depth=SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
flaot linearDepth=Linear01Depth(depth);
return fixed4(linearDepth,linearDepth,linearDepth,1.0);

或是输出法线方向:

fixed3 normal=DecodeViewNormalStereo(tex2D(_CameraDepthNormalsTexture,i.uv).xy);
return fixed4(normal*0.5+0.5,1.0);

在查看深度纹理时,我们得到的画面可能几乎是全黑或全白的,这时候可以把摄像机的远裁剪平面的距离(Unity默认1000)调小,使视锥体的范围刚好覆盖场景所在区域,这是因为由于投影变换时需要覆盖从近裁剪平面的所有深度区域,当远裁剪平面的距离过大时,会导致摄像机较近的距离会被映射到非常小的深度值,如果场景是一个封闭的区域,那么就会导致画面看起来几乎是全黑的,相反如果场景是一个开放的区域,且物体距离摄像机的距离较远,则会导致画面几乎是全白的。

本文转自 CSDN,为博主 小橙子0 原创文章,
遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/cgy56191948/article/details/101518769

推荐阅读