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

Unity Shader系列文章:Unity Shader目录-初级篇

Unity Shader系列文章:Unity Shader目录-中级篇

参考文章:使用深度纹理,计算像素的世界坐标
背后的原理:

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

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

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

下图显示了在使用正交摄像机时投影变换的过程。同样,变换后会得到一个范围为 [-1, 1]的立方体。正交投影使用的变换矩阵是线性的。

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

在得到NDC后,深度纹理中的像素值就可以很方便地计算得到了,这些深度值就对应了NDC中顶点坐标的z分量的值。由于NDC中 z分量的范围在[-1, I], 为了让这些值能够存储在一张图像中,需要使用下面的公式对其进行映射:
d = 0.5⋅Z_{ndc} + 0.5
其中,d对应了深度纹理中的像素值,Z_{ndc}对应了NDC坐标中的z分量的值。

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

在 Unity中,可以选择让一个摄像机生成一张深度纹理或是一张深度+法线纹理。当选择前者,即只需要 张单独的深度纹理时, Unity 会直接获取深度缓存或是按之前讲到的着色器替换技术,选取需要的不透明物体,并使用它投射阴影时使用的 Pass (即 LightMode 被设置为ShadowCaster Pass)来得到深度纹理。如果 Shader 中不包含这样一个 Pass, 那么这个物体就不会出现在深度纹理中(当然,它也不能向其他物体投射阴影)。深度纹理的精度通常24 位或 16 位,这取决于使用的深度缓存的精度。如果选择生成一张深度+法线纹理, Unity 创建一张和屏幕分辨率相同、精度为 32 位(每个通道为 位)的纹理,其中观察空间下的法线信息会被编码进纹理的 通道,而深度信息会被编码进 通道。法线信息的获取在延迟渲染中是可以非常容易就得到的, Unity 只需要合并深度和法线缓存即可。而在前向渲染中,默认情况下是不会创建法线缓存的,因此 Unity 底层使用了一个单独的 Pass 把整个场景再次渲染一遍来完成。这个 Pass 被包含在 Unity 内置的一个 Unity Shader 中,可以在内置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormaITexture.shader 文件中找到这个用于渲染深度和法线信息的 Pass。

如何获取:

获取深度纹理,先设置摄像机的 depthTextureMode:

camera . depthTextureMode = DepthTextureMode.Depth;

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

同理,如果需要获取获取深度+法线纹理,设置摄像机的 depthTextureMode为:

camera .depthTextureMode = DepthTextureMode.DepthNormals;

然后在 Shader 中通过声明_CameraDepthN ormalsTexture 变量来访问它。

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

camera.depthTextureMode I= DepthTextureMode.Depth;
camera.depthTextureMode I= DepthTextureMode.DepthNormals;

在 Shader 中访问到深度纹理_CameraDepthTexture 后,我们就可以使用当前像素的纹理坐标对它进行采样。绝大多数情况下,我们直接使用 tex2D 函数采样即可,但在某些平台(例如 PS3 PSP2) 上,我们需要 一些特殊 处理 Unity 为我们提供了一个统一的宏SAMPLE_DEPTH_TEXTURE, 用来处理这些由于平台差异造成的问题。而我们只需要在 Shader中使用 SAMPLE_DEPTH_TEXTURE 宏对深度纹理进行采样,例如:

float d = SAMPLE DEPTH TEXTURE (CameraDepthTexture, i. uv) ;

其中,i.scrPos是在顶点着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标。上述这些宏的定义,可以在Unity 内置的HLSLSupport.cginc文件中找到。

当通过纹理采样得到深度值后,这些深度值往往是非线性的,这种非线性来自于透视投影使用的裁剪矩阵。然而,在我们的计算过程中通常是需要线性的深度值,也就是说,我们需要把投影后的深度值变换到线性空间下,例如视角空间下的深度值,我们只需要倒推顶点变换的过程即可。下面以透视投影为例,推导如何由深度纹理中的深度信息计算得到视角空间下的深度值。

当我们使用透视投影的裁剪矩阵P_{clip}对视角空间下的一个顶点进行变换后,裁剪空间下顶点的z和w分量为:
Z_{clip} = -Z_{view}\frac{Far + Near}{Far - Near}-\frac{2⋅Near⋅Far}{Far - Near}
W_{clip} = -Z_{view}

其中,Far和Near分别是远近裁剪平面的距离。然后,我们通过齐次除法就可以得到NDC下的z分量:
Z_{ndc} = \frac{Z_{clip}}{W_{clip}} = \frac{Far + Near}{Far - Near}+\frac{2⋅Near⋅Far}{(Far - Near)⋅Z_{view}}

而深度纹理中的深度值是通过下面的公式由NDC 计算而得的:
d = 0.5⋅Z_{ndc} + 0.5
由上面的这些式子,可以推导出用d表示而得的Z_{view}的表达式:
Z_{view} = \frac{1}{\frac{Far-Near}{Near⋅Far}d-\frac{1}{Near}}

由于在Unity 使用的视角空间中,摄像机正向对应的z值均为负值,因此为了得到深度值的正数表示,我们需要对上面的结果取反,最后得到的结果如下:
Z_{view} = \frac{1}{\frac{Near-Far}{Near⋅Far}d+ \frac{1}{Near}}

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容