【Unity3D】屏幕深度和法线纹理简介
1 前言
1)深度纹理和法线纹理的含义
深度纹理本质是一张图片,图片中每个像素反应了屏幕中该像素位置对应的顶点 z 值相反数(观察坐标系),之所以用 “反应了” 而不是 “等于”(或 “对应” ),因为深度纹理中颜色的值域是 [0, 1],而顶点 z 值相反数不一定在该区间,另外顶点 z 值相反数与深度纹理不是线性关系(透视投影引起的)。
法线纹理本质也是一张图片,图片中每个像素的 R、G 值对应该点法线向量的 x、y 值(观察空间),z 值通过公式 z = sqrt(x * x + y * y) 计算得到。
2)深度纹理和法线纹理的作用
深度纹理和法线纹理可以用于边缘检测特效、全局雾化特效、激光雷达特效等场景。
2 顶点映射
1)顶点映射过程
在空间和变换中,讲述了顶点是如何从模型空间逐步映射到屏幕上的像素点。这里再简单描述下顶点映射过程,模型中的顶点先后经历了模型变换、观察变换、投影变换、齐次除法(或透视除法)、屏幕映射,逐步映射到屏幕空间。

2)顶点映射的线性阶段和非线性阶段
光栅化发生在投影变换和齐次除法(或透视除法)之间,它是顶点映射的一个重大转折点,主要体现在以下两点:
- 光栅化对三角形内部进行线性插值,使得光栅化后的顶点数远大于光栅化前的顶点数;
- 光栅化及之前的变换都是线性变换,光栅化之后进行了齐次除法(或透视除法),使得顶点映射不再保有线性性质(这主要是相机的透视效果引起的)。
3)顶点映射各阶段值域
在空间和变换中,介绍了每次变换后顶点的各个分量的值域,这里再简单描述下:经过投影变换后,顶点 x、y、z 坐标都映射在区间 [-w, w];经过齐次除法后,顶点 x、y、z 坐标都映射在区间 [-1, 1](DirectX *台上 z 坐标映射在区间 [0, 1]),该空间被称为归一化的设备空间(Normalized Device Coordinates, NDC);经过屏幕映射后,x、y 坐标分别映射在区间 [0, pixelWidth]、 [0, pixelHeight],z 坐标保持不变。
4)NDC 到深度纹理
归一化的设备空间中顶点坐标 z 分量值域是 [-1, 1],而颜色 R、G、B、A 分量值域都是 [0,1],因此需要进行以下映射,其中 z 为 NDC 空间中顶点坐标 z 值,c 为映射的深度纹理的 R 通道值。

3 深度纹理和法线纹理的来源
1)前向渲染生成深度和法线纹理
当使用前向渲染(Forward Rendering)路径时,Unity 会选取所有不透明物体(RenderType 为 Opaque,Queue 为 Background、Geometry 或 AlphaTest,即 Queue <= 2500)生成深度和法线纹理。对于深度纹理,Unity 使用着色器替换技术,在 FallBack 中寻找 LightMode 为 ShadowCast 的 Pass 进行阴影投射(详见阴影原理及应用),同时生成深度纹理;对于法线纹理,Unity 底层会使用一个单独的 Pass 把整个场景再渲染一遍,生成法线纹理(Camera-DepthNormalTexture.shader)。
2)延时渲染生成深度和法线纹理
当使用延时渲染(Deferred Rendering)路径时,Unity 会将深度和法线信息渲染到 G-buffer(Geometric Buffer,几何缓冲区)中。
3)深度&法线纹理编码
用户可以设置只生成深度纹理还是生成深度&法线纹理(深度和法线信息编码在一张纹理中),当设置深度&法线纹理时,Unity 会创建一张和屏幕分辨率相同、精度为 32 位(每个通道 8 位)的纹理,其中观察空间下的法线信息会被编码进 R、G 通道,深度信息会被编码进 B 和 A 通道。
4 深度值和法线向量的获取
4.1 设置深度纹理模式
在 C# 脚本中可以设置深度纹理模式,DepthTextureMode.Depth 模式下会生成一张深度纹理,在 Shader 中可以通过 _CameraDepthTexture 变量获取;DepthTextureMode.DepthNormals 模式下会生成一张深度&法线纹理,在 Shader 中可以通过 _CameraDepthNormalsTexture 变量获取。
camera.depthTextureMode = DepthTextureMode.None; // 不渲染深度纹理和法线纹理
camera.depthTextureMode = DepthTextureMode.Depth; // 渲染深度纹理
camera.depthTextureMode = DepthTextureMode.DepthNormals; // 渲染深度&法线纹理
// 渲染两张纹理, 一张深度纹理, 一张深度&法线纹理
camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;
在 Inspector 面板相机组件的最下方可以看到设置的属性,如下:

4.2 从 _CameraDepthTexture 中获取深度
如果生成了深度纹理,深度纹理会保存在内置变量 _CameraDepthTexture 中。
1)深度纹理采样
// 非线性的深度(即计算的深度值与实际深度值不是线性关系)
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // tex2D(_CameraDepthTexture, i.uv).r
// 观察空间中的线性的深度, 值域: [Near, Far], 公式: 1.0 / (_ZBufferParams.z * depth + _ZBufferParams.w)
float linearDepth = LinearEyeDepth(depth);
// 观察空间中的线性且归一化的深度, 值域: [0, 1], 公式: 1.0 / (_ZBufferParams.x * depth + _ZBufferParams.y)
float linear01Depth = Linear01Depth(depth);
SAMPLE_DEPTH_TEXTURE 内部使用 tex2D 进行采样,类似的宏还有 SAMPLE_DEPTH_TEXTURE_PROJ、SAMPLE_DEPTH_TEXTURE_LOD,它们在 HLSLSupport.cgin 文件中有定义,如下:
# define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)
# define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)
# define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(sampler, uv).r)
float4 tex2Dproj(sampler2D s, in float3 t) { return tex2D(s, t.xy / t.z); }
float4 tex2Dproj(sampler2D s, in float4 t) { return tex2D(s, t.xy / t.w); }
float4 tex2Dlod(sampler2D x, in float4 t) { return x.t.SampleLevel(x.s, t.xy, t.w); }
说明: SAMPLE_DEPTH_TEXTURE 得到的深度不是线性的深度,即 SAMPLE_DEPTH_TEXTURE 返回的深度值与实践的深度值不是线性关系。
2)LinearEyeDepth 函数源码分析
LinearEyeDepth 函数源码(见 UnityCG.cgin 文件)如下,_ZBufferParams.z = (Near - Far) / (Near · Far),_ZBufferParams.w = 1 / Near(_ZBufferParams 为内置变量,详见→Shader常量、变量、结构体、函数,Near、Far 分别为*裁剪*面和远裁剪*面离相机的距离),因此 LinearEyeDepth 内部实现等价于注释部分。
// 公式: Near * Far / ((Near - Far) * z + Far), 值域: [Near, Far]
inline float LinearEyeDepth(float z)
{ // 观察空间中的线性的深度, z为纹理采样的非线性的深度
return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}
通过第 2 节顶点映射过程,我们可以得出以下方程组关系,其中,z1 为观察空间中顶点坐标 z 值,z2、w2 分别为裁剪空间中顶点坐标 z 值和 w 值,z3 为归一化的设备空间(NDC)中顶点坐标 z 值,z4 为纹理空间中顶点坐标 z 值,depth 为观察空间中顶点的深度值,公式 1 和公式 2 由空间和变换中透视投影得到,公式 3 是齐次除法(或透视除法)(z3 值域为 [-1, 1]),公式 4 是归一化处理(z4 值域为 [0, 1]),公式 5 是将深度值取正(观察空间中顶点坐标都是负值,取反后使得深度值为正)。

进一步计算得到 z4 与 depth 的关系如下:

计算反函数得到 depth 与 z4 的关系如下,结果与代码中注释一致。

3)Linear01Depth 函数源码分析
Linear01Depth 函数源码(见 UnityCG.cgin 文件)如下,_ZBufferParams.x = (Near - Far) / Near,_ZBufferParams.y = Far / Near (_ZBufferParams 为内置变量,详见→Shader常量、变量、结构体、函数,Near、Far 分别为*裁剪*面和远裁剪*面离相机的距离),因此 Linear01Depth 内部实现等价于注释部分。
// 公式: Near / ((Near - Far) * z + Far), 值域: [0, 1]
inline float Linear01Depth(float z)
{ // 观察空间中的线性且归一化的深度, z为纹理采样的非线性的深度
return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
本节继续沿用 2)中变量,假设归一化的线性深度为 depth01,depth 的值域为 [Near, Far],depth01 的值域为 [0, 1],因此 depth01 = (depth - Near) / (Far - Near),由于 Near 一般取值较小(Unity 中默认值为 0.3)、Far 取值较大(Unity 中默认值为 1000),depth01 和 depth 的关系可以简化为:depth01 = depth / Far,进一步计算得到 depth 与 z4 的关系如下,结果与代码中注释一致。

说明:Linear01Depth 归一化的结果是一个*似结果,即值域并不是 [0,1],而是 [Near / Far, 1],由于 Near 一般取值较小(Unity 中默认值为 0.3)、Far 取值较大(Unity 中默认值为 1000),Near / Far *似为 0。
4.3 从 _CameraDepthNormalsTexture 中获取深度和法线
如果生成了深度&法线纹理,深度&法线纹理会保存在 _CameraDepthNormalsTexture 中。
1)深度&法线纹理采样
inline void DecodeDepthNormal(float4 enc, out float depth, out float3 normal)
{ // 深度&法线采样, enc为tex2D采样结果, depth、normal为解码后的深度和法线
depth = DecodeFloatRG(enc.zw); // 观察空间中的线性且归一化的深度
normal = DecodeViewNormalStereo(enc); // 观察空间中的法线向量
}
2)DecodeFloatRG 函数源码
inline float DecodeFloatRG(float2 enc)
{
float2 kDecodeDot = float2(1.0, 1 / 255.0);
return dot(enc, kDecodeDot);
}
3)DecodeViewNormalStereo 函数源码
inline float3 DecodeViewNormalStereo(float4 enc4)
{
float kScale = 1.7777;
float3 nn = enc4.xyz * float3(2 * kScale, 2 * kScale, 0) + float3(-kScale, -kScale, 1);
float g = 2.0 / dot(nn.xyz, nn.xyz);
float3 n;
n.xy = g * nn.xy;
n.z = g - 1;
return n;
}
5 查看深度纹理和法线纹理
为了不让深度值映射到一个比较小的区域(接* 0 或接* 1),使得深度纹理图呈现黑色或白色,需要调整远裁剪*面的值。
5.1 帧调试器查看深度纹理和法线纹理
1)设置深度纹理模式
DepthNormalTest.cs
using UnityEngine;
[ExecuteInEditMode] // 编辑态可以查看脚本运行效果
public class DepthNormalTest : MonoBehaviour {
private void OnEnable() {
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
}
}
说明:DepthNormalTest 脚本组件需要挂在相机上。
场景渲染如下:

2)查看深度纹理
通过 Window → Analysis → Frame Debug 打开帧调试器,单击 Enable 按钮开始调试,如下:

深度纹理如下:

3)查看深度&法线纹理
在帧调试器中调整帧渲染事件,找到最后一个渲染目标为 Camera DepthNormalsTexture 的事件,显示深度&法线纹理如下:

5.2 代码查看线性的深度和法线纹理
1)设置深度纹理模式和材质的 Shader
LinearDepthNormalsTexture.cs
using UnityEngine;
[ExecuteInEditMode] // 编辑态可以查看脚本运行效果
[RequireComponent(typeof(Camera))] // 需要相机组件
public class LinearDepthNormalsTexture : MonoBehaviour {
private Material material = null; // 材质
private void Start() {
material = new Material(Shader.Find("MyShader/LinearDepthNormalsTexture"));
material.hideFlags = HideFlags.DontSave;
}
private void OnEnable() {
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
}
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null) {
Graphics.Blit(null, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
2)基于 _CameraDepthTexture 的深度纹理
LinearDepthNormalsTexture.shader
Shader "MyShader/LinearDepthNormalsTexture" { // 线性深度和法线纹理
SubShader{
Pass {
// 深度测试始终通过, 关闭深度写入
ZTest Always ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert_img // 使用内置的vert_img顶点着色器
#pragma fragment frag
sampler2D _CameraDepthTexture; // 深度纹理
fixed4 frag(v2f_img i) : SV_Target{ // v2f_img为内置结构体, 里面只包含pos和uv
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // 非线性的深度(即计算的深度值与实际深度值不是线性关系)
float linear01Depth = Linear01Depth(depth); // 观察空间中的线性且归一化的深度
return fixed4(linear01Depth, 0, 0, 1);
}
ENDCG
}
}
FallBack off
}
运行后效果如下:

3)基于 _CameraDepthNormalsTexture 的深度纹理
LinearDepthNormalsTexture.shader
Shader "MyShader/LinearDepthNormalsTexture" { // 线性深度和法线纹理
SubShader{
Pass {
// 深度测试始终通过, 关闭深度写入
ZTest Always ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert_img // 使用内置的vert_img顶点着色器
#pragma fragment frag
sampler2D _CameraDepthNormalsTexture; // 深度&法线纹理
fixed4 frag(v2f_img i) : SV_Target{ // v2f_img为内置结构体, 里面只包含pos和uv
fixed4 tex = tex2D(_CameraDepthNormalsTexture, i.uv);
float depth = DecodeFloatRG(tex.zw); // 观察空间中的线性且归一化的深度
return fixed4(depth, 0, 0, 1);
}
ENDCG
}
}
FallBack off
}
运行后效果同第 2)节。
4)基于 _CameraDepthNormalsTexture 的法线纹理
LinearDepthNormalsTexture.shader
Shader "MyShader/LinearDepthNormalsTexture" { // 线性深度和法线纹理
SubShader{
Pass {
// 深度测试始终通过, 关闭深度写入
ZTest Always ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert_img // 使用内置的vert_img顶点着色器
#pragma fragment frag
sampler2D _CameraDepthNormalsTexture; // 深度&法线纹理
fixed4 frag(v2f_img i) : SV_Target{ // v2f_img为内置结构体, 里面只包含pos和uv
fixed4 tex = tex2D(_CameraDepthNormalsTexture, i.uv);
float3 normal = DecodeViewNormalStereo(tex); // 观察空间中的法线向量
return fixed4(normal * 0.5 + 0.5, 1);
}
ENDCG
}
}
FallBack off
}
运行效果如下:

5)基于 _CameraDepthNormalsTexture 的深度&法线纹理
LinearDepthNormalsTexture.shader
Shader "MyShader/LinearDepthNormalsTexture" { // 线性深度和法线纹理
SubShader{
Pass {
// 深度测试始终通过, 关闭深度写入
ZTest Always ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert_img // 使用内置的vert_img顶点着色器
#pragma fragment frag
sampler2D _CameraDepthNormalsTexture; // 深度&法线纹理
fixed4 frag(v2f_img i) : SV_Target{ // v2f_img为内置结构体, 里面只包含pos和uv
return tex2D(_CameraDepthNormalsTexture, i.uv);
}
ENDCG
}
}
FallBack off
}
运行效果如下:

声明:本文转自【Unity3D】屏幕深度和法线纹理简介。
【Unity3D】屏幕深度和法线纹理简介的更多相关文章
- Unity Shader入门精要学习笔记 - 第13章 使用深度和法线纹理
线纹理的代码非常简单,但是我们有必要在这之前首先了解它们背后的实现原理. 深度纹理实际上就是一张渲染纹理,只不过它里面存储的像素值不是颜色值而是一个高精度的深度值.由于被存储在一张纹理中,深度纹理里的 ...
- 《Unity shader入门精要》复习<第13章 关于NDC坐标和深度/法线纹理>
分为三个地方讲解. NDC(Normalize Device Coordinates)归一化的设备坐标 NDC坐标是世界空间坐标通过MVP变换之后再进行归一化得到的坐标.只需要再一步变换就能得到屏幕空 ...
- Unity3D 屏幕空间雪场景Shader渲染
笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解 ...
- 【Unity Shaders】法线纹理(Normal Mapping)的实现细节
写在前面 写这篇的目的是为了总结我长期以来的混乱.虽然题目是"法线纹理的实现细节",但其实我想讲的是如何在shader中编程正确使用法线进行光照计算.这里面最让人头大的就是各种矩阵 ...
- 【Unity Shader】(四) ------ 纹理之法线纹理、单张纹理及遮罩纹理的实现
笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题. [Unity Shader](三) ----- ...
- Unity3d用户手册用户指南 电影纹理(Movie Texture)
http://www.58player.com/blog-2327-952.html 电影纹理(Movie Texture) 注意:这只是专业/高级功能. 桌面 电影纹理是从视频文件创建的动画纹理 ...
- Unity3D学习笔记12——渲染纹理
目录 1. 概述 2. 详论 3. 问题 1. 概述 在文章<Unity3D学习笔记11--后处理>中论述了后处理是帧缓存(Framebuffer)技术实现之一:而另外一个帧缓存技术实现就 ...
- Unity3d 屏幕空间人体皮肤知觉渲染&次表面散射Screen-Space Perceptual Rendering & Subsurface Scattering of Human Skin
之前的人皮渲染相关 前篇1:unity3d Human skin real time rendering 真实模拟人皮实时渲染 前篇2:unity3d Human skin real time ren ...
- Unity加载模块深度解析(纹理篇)
在游戏和VR项目的研发过程中,加载模块所带来的效率开销和内存占用(即“加载效率”.“场景切换速度”等)经常是开发团队非常头疼的问题,它不仅包括资源的加载耗时,同时也包含场景物件的实例化和资源卸载等.在 ...
- (转)分布式深度学习系统构建 简介 Distributed Deep Learning
HOME ABOUT CONTACT SUBSCRIBE VIA RSS DEEP LEARNING FOR ENTERPRISE Distributed Deep Learning, Part ...
随机推荐
- Laravel - blade 模板继承的使用
1. 模板文件 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&qu ...
- android应用申请加入电池优化白名单
首先,在 AndroidManifest.xml 文件中配置一下权限: 1 <uses-permission android:name="android.permission.REQU ...
- Kubernerts - 概览
1. Kubernerts K8s,是用于自动部署.扩容和管理容器化应用程序的开源系统 1.1 特性 自动化上线与回滚 分步骤针对应用或者配置更改上线,监控应用的运行状态同时不会终止所有实例,若出现问 ...
- [转帖]max_allowed_packet 与 SQL 长度的关系
https://www.oceanbase.com/knowledge-base/oceanbase-database-1000000000210013 适用版本:V2.1.x.V2.2.x.V3.1 ...
- [转帖]SPEC CPU 2017 单线程整数性能测试与总结 (2022)
https://zhuanlan.zhihu.com/p/574105237 x86处理器的整数性能在过去4年间取得了长足的进步 x86处理器移动端性能缩水非常严重 ARM公版的旗舰级处理器相比前代进 ...
- [转帖]实战演练 | Navicat 数据生成功能
https://zhuanlan.zhihu.com/p/631823381 数据生成的目的是依据某个数据模型,从原始数据通过计算得到目标系统所需要的符合该模型的数据.数据生成与数据模型是分不开的,数 ...
- Jmeter学习之一_连接与测试Redis
Jmeter学习之一_连接与测试Redis 简介 下载: https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.6.zip 注意事项: D ...
- [转帖]-O1,-O2,-O3编译优化知多少
1.从.c文件到可执行文件,其间经历了几步? 高级语言是偏向人,按照人的思维方式设计的,机器对这些可是莫名奇妙,不知所谓.那从高级语言是如何过渡到机器语言的呢?这可是一个漫长的旅途呀! 其中,得经历这 ...
- [转帖]将nginx.conf文件的内容拆分成多个
nginx的如果有多个server模块都配置在同一个nginx.conf文件会显得比较臃肿,后续维护起来也会比较困难,所以可以将内容写入到多个配置文件中然后在nginx.conf文件中通过includ ...
- 解锁前端新潜能:如何使用 Rust 锈化前端工具链
前言 近年来,Rust的受欢迎程度不断上升.首先,在操作系统领域,Rust 已成为 Linux 内核官方认可的开发语言之一,Windows 也宣布将使用 Rust 来重写内核,并重写部分驱动程序. ...