|
本文为《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
延迟渲染会把深度信息渲染到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。
负责把深度纹理的采样结果转换到视角空间下的深度值——Z&#39;view。
返回一个范围在[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) |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|