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

《Unity Shader入门精要》笔记(二十三)

[复制链接]
发表于 2022-10-18 07:57 | 显示全部楼层 |阅读模式
本文为《Unity Shader入门精要》第十三章《使用深度和法线纹理》的第一节内容《获取深度和法线纹理》。

本文相关代码,详见:
原书代码,详见原作者github:
<hr/>1. 概念及原理

深度纹理实际是一张渲染纹理,里面存储的是高精度的深度值,数值的范围是[0, 1],且是非线性分布的。


  • 为什么是非线性分布的?
顶点从模型空间变换到其次裁剪坐标系下,是通过在顶点着色器中乘以MVP矩阵得到的,在变换的最后一步,如果使用了透视投影类型的相机,这个投影矩阵就是非线性的。

Unity透视投影对顶点的变换过程:



左图:观察空间下视椎体的结构及响应的顶点位置;中图:顶点着色器阶段输出的顶点变换结果;右图:底层硬件进行透视除法后得到的归一化的设备坐标;

注意:
投影过程建立在Unity对坐标系的假定上(观察空间为右手坐标系),使用列矩阵在矩阵右侧进行相乘,且得到NDC后z分量范围在[-1, 1]之间;
在类似DirectX这样的图形接口中,变换后z分量在[0, 1]之间。

Unity正交投影对顶点的变换过程:



正交投影使用的变换矩阵是线性的。

在得到NDC后,深度纹理中的深度值就对应了NDC中顶点坐标的z分量的值。由于z分量范围在[-1, 1],所以需要进行映射,将其转为[0, 1]:
// depth:深度纹理中的深度值
// z:NDC坐标中z分量的值
depth = 0.5 * z + 0.5


  • Unity如何得到一张深度纹理?

    • 当使用延迟渲染路径(包括:遗留的延迟渲染路径)时

延迟渲染会把深度信息渲染到G-buffer中,可直接访问深度缓存得到。


    • 未使用延迟渲染时

通过一个单独的Pass渲染得到深度和法线纹理。
Unity使用着色器替换(Shader Replacement)技术选择渲染类型(SubShader的RenderType标签)为Opaque的物体,判断他们的渲染队列是否小于等于2500(内置的Background、Geometry和AlphaTest渲染队列都在这个范围)。如果满足条件,就把它们渲染到深度和法线纹理中。


  • 如果只需要生成一张深度纹理
深度纹理的精度通常是24位或16位;
Unity会直接获取深度缓存,或是使用着色器替换技术,选取需要的不透明物体,并使用投射阴影时使用的Pass(LightMode为ShadowCaster的Pass)来得到深度纹理。如果Shader中不包含这样的纹理,这个物体就不会出现在深度纹理中,也不能向其他物体投射阴影。

  • 如果需要生成一张深度纹理+法线纹理
Unity会创建一张和屏幕分辨率相同、精度为32位(RGBA每个通道为8位)的纹理,观察空间的法线信息被写入R、G通道,深度信息被写进B、A通道;
延迟渲染中法线信息很容易获得,Unity只需要合并深度和法线缓存即可;
而前向渲染中,默认不会创建法线缓存,因此Unity底层使用了一个单独的Pass把整个场景再次渲染一遍,整个Pass在buildin_shaders-xxx/DefaultResources/Camera-DepthNormalTexture.shader文件中可以找到。

2 如何获取

在C#脚本中设置camera的深度纹理模式:
// 让摄像机产生一张深度纹理
camera.depthTextureMode = DepthTextureMode.Depth;
// 让摄像机产生一张深度+法线纹理
camera.depthTextureMode = DepthTextureMode.DepthNormals;
// 让摄像机产生一张深度纹理和深度+法线纹理
camera.depthTextureMode  |=  DepthTextureMode.Depth;
camera.depthTextureMode  |=  DepthTextureMode.DepthNormals;

再在Shader中通过声明_CameraDepthNormalsTexture变量来访问它。

绝大多数情况下,可以直接使用tex2D函数对纹理采样即可,但在某些平台(如:PS3、PSP2),需要进行一些特殊处理。不过Unity提供了统一的宏SAMPLE_DEPTH_TEXTURE用来处理平台差异造成的问题:
// i.uv是float2类型的变量,对应当前像素的纹理坐标
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);

类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJ、SAMPLE_DEPTH_TEXTURE_LOD,可以在Unity内置的HLSLSupport.cginc文件中找到。


  • 关于SAMPLE_DEPTH_TEXTURE_PROJ
接受两个参数:深度纹理、float3或float4类型的纹理坐标;
内部使用了tex2Dproj这样的函数进行投影纹理采样,纹理坐标的前两个分量会除以最后一个分量,再进行纹理采样;
如果提供了第四个分量,还会进行一次比较,通常用于阴影的实现。
SAMPLE_DEPTH_TEXTURE_PROJ的第二个参数通常有顶点着色器输出差值得到的屏幕坐标:
// i.scr是顶点着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标
float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos));

经过透视投影裁剪矩阵处理后的深度纹理,其深度值是非线性的,而计算过程中需要线性的深度值,就需要把投影后的深度值变换到线性空间下,例如:视角空间下的深度值。


  • 如何进行这个转换?
倒推顶点变换的过程。

以透视投影为例,推导如何由深度纹理中的深度信息计算得到视角空间下的深度值:
由4.6.7节可知,当使用透视投影的矩阵Pclip对视角空间下 一个顶点进行变换后,裁剪空间下顶点的z和w分量为:



注:图片中的Zvisw实则为Zview,是原书作者笔误所致,下同。

Far和Near分别是远近裁剪平面的距离,然后通过齐次除法可以得到NDC下的z分量:



由上一小节可知,深度纹理中的深度值是通过下面的公式由NDC计算得到:
d = 0.5 * z + 0.5

由上面这些式子可以推导出用d表示得到的zview的表达式:



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



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



上述的计算过程,Unity封装到了辅助函数中,分别是:LinearEyeDepth、Linear01Depth。

  • LinearEyeDepth
负责把深度纹理的采样结果转换到视角空间下的深度值——Z'view。

  • Linear01Depth
返回一个范围在[0, 1]的线性深度值——Z01。
这2个函数内部都是用了内置的_ZBufferParams变量来得到远近裁剪平面的距离。

如果需要获取深度+法线纹理,可直接使用tex2D函数对_CameraDepthNormalsTexture进行采样,得到里面存储的深度和法线信息。
Unity提供了辅助函数对这个采样结果进行解码——DecodeDepthNormal,其在UnityCG.cginc里的定义:
// enc: 对深度+法线纹理的采样结果,是Unity对其编码后的结果
// xy分量存储视角空间下的法线信息,zw分量是深度信息
// depth:解码后的深度值,范围在[0, 1],为线性深度值
// normal:解码后的视角空间下的法线方向
inline void DecodeDepthNormal(float4 enc, out float depth, out float3 normal)
{
    depth = DecodeFloatRG(enc.zw);
    normal = DecodeViewNormalStereo(enc);
}

3 查看深度和法线纹理

使用帧调试器(Frame Debugger)查看深度纹理和深度+法线纹理:



左:深度纹理,右:法线纹理

帧调试器看到的深度纹理是非线性空间的深度值,而深度+法线纹理都是由Unity编码后的结果,有时线性空间下的深度信息或解码后的发现方向更加有用,此时可在片元着色器中做转换或解码逻辑:
// 输出线性深度值
float  depth  =  SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,  i.uv);
float  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)调小,使之刚好覆盖场景的所在区域即可。若裁剪平面的距离过大,会导致距离相机较近的物体会被映射到非常小的深度值,导致看起来全黑(场景为封闭区域比较常见);相反若场景为开放区域,物体距离相机较远,则会导致画面几乎全白。



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

以上是本次笔记的所有内容,下一篇我们将学习《再谈运动模糊》。

写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder

本帖子中包含更多资源

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

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

本版积分规则

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

GMT+8, 2024-11-15 10:46 , Processed in 0.089812 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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