深度平台差异

裁剪空间深度值

裁剪空间坐标(也称为投影后空间坐标)在 Direct3D 类和 OpenGL 类平台之间有所不同:

  • Direct3D :裁剪空间深度 Z值为 $[1,0]$。

  • OpenGL:裁剪空间深度 Z 值为 $[-1, 1]$。

  • 在着色器代码内,可使用内置宏 UNITY_NEAR_CLIP_VALUE 来获取基于平台的近平面值。UNITY_NEAR_CLIP_VALUE 定义为近剪裁平面的值。 Direct3D 为 1.0,OpenGL 为–1.0

  • 在脚本代码内,使用 GL.GetGPUProjectionMatrix 将 Unity 的坐标系(遵循 OpenGL 类约定)转换为 Direct3D 类坐标(如果这是平台所期望的)。

深度 (Z) 方向

[!hint] 现代平台 :使用了 [[06 深度测试#^9bb785|Reversed direction技术]],相比传统平台翻转了 Z 值
DirectX 11,DirectX12,PS4,Xbox One,和 Metal:

  • 裁剪空间的 Z 值范围是 $[near,0]$(near 表示近平面距离,在远平面处减小到 0.0)。
  • NDC 的 Z 值范围为 $[1,0]$,对应 ZBuffer 的取值范围也为 $[1,0]$
  • Unity 定义了 UNITY_REVERSED_Z 宏定义,用于判断是否是使用翻转 z 方向的平台

[!quote] 传统平台

  • 在旧版 Direct3D 类平台上,范围是 $[0,far]$(表示在近平面处为 0.0,在远平面处增加到远平面距离)。
    • 对应 NDC 的 Z 值值范围为 $[0,1]$
  • 在 OpenGL 类平台上,裁剪空间的 Z 值范围是 $[-near,far]$。
    • 对应 NDC 的 Z 值值范围为 $[-1,1]$。
    • 由于深度值应该是 0~1 的数,所以 Unity 对其将其转换为 $[0,1]$ 存入 ZBuffer

[!summary] Unity 深度纹理和 NDC 的深度的关系

  • 以 Unity OpenGL 平台为例, NDC 的的取值范围为 $[-1, 1]$ ,而深度纹理的取值范围为 $[0,1]$,两者的关系为 $d = Z_{ndc} * 0.5 + 0.5$。
  • 以 Unity DX 平台为例, NDC 经过 Reverse-Z 的取值范围为 $[1, 0]$ ,深度纹理的取值范围为 $[1,0]$,两者的关系其实也符合为 $d = Z_{ndc} * 0.5 + 0.5$。
  • 我们可以进行跨平台处理: [[#跨平台采样深度纹理]],让所有平台的 ZBuffer 范围都是 $[0,1]$ 或 $[1,0]$

跨平台采样深度纹理

我们做东西肯定要考虑跨平台,前面提到了不同平台生成的深度图是不同的,如 DirctX 近到远是 1 到 0,OpenGL 近到远是 0 到 1,那么怎么统一采样的值呢?根据前面的介绍我们知道 DirctX 等平台之所以是 1 到 0 是因为 unity 为其做了反转,那么我们再把它们转回来不就得了么。而对于这些进行了深度反转的平台,unity 都定义了名为 UNITY_REVERSED_Z 的宏,
如果想要各个平台 Zbuffer 都是 $[0,1]$:

title:方法一
1
2
3
4
float depth = tex2D(_CameraDepthTexture, uvSS).r;
# if defined(UNITY_REVERSED_Z)
depth = 1.0f - depth;
# endif

如果想要各平台 Zbuffer 都是 $[1,0]$:

title:方法二
1
2
3
4
5
6
7
8
#if UNITY_REVERSED_Z
// 具有 REVERSED_Z 的平台(如 D3D)的情况。
real depth = SampleSceneDepth(uvSS);
#else
// 没有 REVERSED_Z 的平台(如 OpenGL)的情况。
// 调整 Z 以匹配 OpenGL 的 NDC
real depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(uvSS));
#endif

使用裁剪空间

如果要手动使用裁剪空间 (Z) 深度,则可能还需要使用以下宏来抽象化平台差异:

1
float clipSpaceRange01 = UNITY_Z_0_FAR_FROM_CLIPSPACE(rawClipSpace);

注意:此宏不会改变 OpenGL 或 OpenGL ES 平台上的裁剪空间,因此在这些平台上,此宏返回“-near”1(近平面)到 far(远平面)之间的值。

投影矩阵

如果处于深度 (Z) 发生反转的平台上,则 GL.GetGPUProjectionMatrix() 返回一个还原了 z 的矩阵。但是,如果要手动从投影矩阵中进行合成(例如,对于自定义阴影或深度渲染),您需要通过脚本按需自行还原深度 (Z) 方向。

以下是执行此操作的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var shadowProjection = Matrix4x4.Ortho(...); //阴影摄像机投影矩阵
var shadowViewMat = ... //阴影摄像机视图矩阵
var shadowSpaceMatrix = ... //从裁剪空间到阴影贴图纹理空间

//当引擎通过摄像机投影计算设备投影矩阵时,
//"m_shadowCamera.projectionMatrix"被隐式反转
m_shadowCamera.projectionMatrix = shadowProjection;

//"shadowProjection"在连接到"m_shadowMatrix"之前被手动翻转,
//因为它被视为着色器的其他矩阵。
if(SystemInfo.usesReversedZBuffer)
{
shadowProjection[2, 0] = -shadowProjection[2, 0];
shadowProjection[2, 1] = -shadowProjection[2, 1];
shadowProjection[2, 2] = -shadowProjection[2, 2];
shadowProjection[2, 3] = -shadowProjection[2, 3];
}
m_shadowMatrix = shadowSpaceMatrix * shadowProjection * shadowViewMat;

深度 (Z) 方向检查工具

  • 使用 SystemInfo.usesReversedZBuffer 可确认所在平台是否使用反转深度 (Z)。

理论与推导

[!note] 深度纹理
Depth Texture = 深度纹理 = 深度图
上面保存了深度缓冲区的值,是非线性深度,使用时要先转换成线性深度

[!note] 约定
本节采用 OpenGL 标准进行推导

  1. 列向量
  2. 模型空间、世界空间、观察空间是右手坐标系,而裁剪空间与 NDC 是左手坐标系
  3. Camera 的 LookAt (Forward)方向为 $- z$ 轴方向
  4. NDC 空间范围 $[-1,1]^3$
  5. OpenGL 使用距离值表示 $n、f$。$n$ 被映射到 $-1$,$f$ 被映射到 $1$

深度是指像素到摄像机的距离,观察空间的深度为线性深度,NDC 空间的深度为非线性深度

当我们想要精确表达物体的深度差异或者重建像素世界坐标位置,就需要使用将非线性深度转化为线性深度。

非线性深度

假如在 MV 变换后,观察空间(View Space) 下的某个点对应的齐次坐标为 $(x,y,z,1)$,那么经过透视投影变换和 GPU 裁剪后转换到齐次裁剪空间(Clip Space),变换过程如下:(该变换同样适用于 Unity,Unity 与 OpenGL 投影矩阵相同)

![[02 空间变换#^bgahra]]

我们只关注深度,即 $\displaystyle z’=-(\frac{f+n}{f-n})z-\frac{2fn}{f-n}$

然后进行齐次除法转换到NDC 空间
$$ z’’=\frac{z’}{w’}=(\frac{f+n}{f-n})+\frac{2fn}{(f-n)z}\tag{1}$$
$z’’$ 值范围为 $[-1,1]$ ,而 ZBuffer 中存储的值应该为 $[0,1]$,所以我们将 NDC 空间深度值($z’’$ ) 的范围转换到 $[0,1]$:($NonLinearDepth$ 与 $\displaystyle \frac{1}{z}$ 相关,是非线性的,即非线性深度
$$NonLinearDepth = z’’\times0.5+0.5=\frac f{f-n}+\frac{2fn}{(f-n)\color\red{z}}\tag{2}$$
带入 $z=n, z=f$ 可得近平面 $NonLinearDepth$ 为 $1$,远平面 $NonLinearDepth$ 为 $0$

Pasted image 20230708123806

范围转换前后对比,横轴为 z 值。可以看出靠近摄像机的十个单位占了 90%的深度缓冲区精度,故离摄像机越远的值精度越低

线性深度

Pasted image 20230708110316

观察空间的深度为线性深度

Pasted image 20230708152129

线性深度受 far 的影响

线性深度分为两种:

  1. $LinearEyeDepth$:观察空间下的线性深度值,取值范围$[n, f]$
  2. $Linear01Depth$:把线性深度归一化到$[0,1]$,我们通常会使用这个线性深度
    $$
    \begin{aligned}&LinearEyeDepth=-Pview.z\\&Linear01Depth=\frac{-Pview.z-n}{f-n}or\frac{-Pview.z}{f}\end{aligned}
    $$

由上一节方程(1)(2)可得:
$$ \begin{cases}z’’=(\frac{f+n}{f-n})+\frac{2fn}{(f-n)z} \
z’’ = NonLinearDepth\times2-1\end{cases}$$
联立可以求出
$$z=\frac1{(\frac{f-n}{fn}*NonlinearDepth-\frac1n)}$$
由于世界空间以 $-Z$ 为正方向,所以求深度需要取反得到正数:
$$LinearEyeDepth=\frac{1}{(\frac{n-f}{fn}*NonlinearDepth+\frac1n)}$$

然后将 $LinearEyeDepth$ 除以 $f$ 即可得到归一化的线性深度 $Linear01Depth$
$$
\begin{aligned}Linear01Depth&=(\frac{1}{(\frac{n-f}{fn}*NonlinearDepth+\frac{1}{n})}\text{-n})/(f\text{-n})\\or&=\frac{1}{(\frac{n-f}{n}*NonlinearDepth+\frac{f}{n})}\end{aligned}
$$

Pasted image 20230708152153

曲线对比图

深度纹理重建像素的世界空间坐标

使用 VP 逆矩阵重建

设 NDC 空间上的点 $P_{ndc}$ 映射到屏幕空间上为点 $P(x, y)$,$P (x, y)$ 点对应的屏幕 uv 为 $(u, v)$。
设 ${P.x=u}{Width}, {P.y=v}{Height}$

1. 由屏幕空间转换到 NDC 空间
从 NDC 空间到屏幕空间,点 P 相对于左下角坐标的比例是不变的,可以列出等式:

Pasted image 20230708152930

$P_{ndc}. z$ 是 NDC 空间的深度,由前文, $P_{ndc}.z=2*NonlinearDepth-1$

则:
$$
\begin{array}{lcr}P_{ndc}.x=2u-1\ P_{ndc}.y=2v-1\ P_{ndc}.z=2*NonlinearDepth-1\ P_{ndc}.w=1.0\end{array}
$$

2. 由 NDC 空间转换到齐次裁剪空间
齐次除法可知 $\displaystyle \frac{Pclip}{Pclip.w}=Pndc$,则
$$\displaystyle P_{clip}=P_{ndc}*P_{clip}.w \tag{1}$$
⭐3. 由齐次裁剪空间转换到世界空间
因为 $P_{clip}$ 是由 $P_{world}$ 经过 $VP$ 矩阵变换得来,我们将 $VP$ 矩阵写作 $M$ ,则 $MP_{world} = P_{clip}$,带入(1)
$$P_{world}=M^{-1}P_{clip}=M^{-1}P_{ndc}*P_{clip}.w\tag{2}$$

因为 $P_{world}=(x,y,z,1)$ ,我们将其 $w$ 分量分量带入(2)
$$P_{world}.w=({M^{-1}}P_{ndc}).w*P_{clip}.w=1$$

$$P_{clip}.w={\frac1{(M^{-1}P_{ndc}).w}}\tag{3}$$

将(3)带入(2)即可得出世界空间坐标:
$$P_{world}=\frac{M^{-1}P_{ndc}}{(M^{-1}P_{ndc}).w}$$
代码实现红线标记处即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1 脚本获取VP逆矩阵
Matrix4x4 ViewProjectionMatrix = renderingData.cameraData.camera.projectionMatrix * renderingData.cameraData.camera.worldToCameraMatrix;

Matrix4x4 ViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
m_blitMaterial.SetMatrix("_ViewProjectionInverseMatrix", ViewProjectionInverseMatrix);

//2 片元着色器中计算
//获取屏幕空间UV
float2 ScreenUV = GetNormalizedScreenSpaceUV(i.positionCS);

//用屏幕UV采样屏幕深度纹理得到像素的非线性深度
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, ScreenUV).r;

// NDC空间坐标
float4 currentPosNDC = float4(ScreenUV.x * 2 - 1, ScreenUV.y * 2 - 1, depth * 2 - 1, 1);

//得到世界空间坐标
float4 D = mul(_ViewProjectionInverseMatrix, currentPosNDC);
float4 currentPosWS = D / D.w;

使用摄像机射线构建

使用 VP 逆矩阵的方法需要在片元着色器中进行矩阵乘法,通常会影响性能。本节介绍的方法性能更好。
首先对图像空间下的视锥体射线(从摄像机出发,指向图像上某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后把该射线和线性化后的观察空间下的深度相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。

Unity3D Shader系列之深度纹理重建世界坐标_textrue3d unity 切片重建
在某些情况下,我们需要屏幕后处理阶段得到像素点对应的世界坐标。如下图,我们在屏幕后处理阶段,想要知道屏幕空间中 $A1$ 点对应的世界坐标 $A$ 点。那么 $A$ 点该怎么求呢?
Pasted image 20230709122334

  • @ 已知条件:

    1. $O$ 点的世界坐标(即相机的世界坐标):_WorldSpaceCameraPos
    2. $OD$ 的长度(观察空间的线性深度值 linearEyeDepth) :可以采样深度纹理得到
    3. 透视相机的各项参数:
      • 近、远裁剪平面的值
      • 视口角度 $FOV$
      • 横纵比 $Aspect$
  • ! $A$ 点的世界坐标 = $O$ 点的世界坐标 + $\overrightarrow{OA}$,我们要做的就是求 $\overrightarrow{OA}$

  • % 主要步骤如下:

  1. 求 $\mathrm{\overrightarrow{OLT},\overrightarrow{OLB},\overrightarrow{ORT},\overrightarrow{ORB}}$( $\overrightarrow {OLT}$ 即从相机指向 $LT$ 的向量)
    • 当顶点为 $LT$ 点(即屏幕左上角)时将 $\overrightarrow {OLT}$ 向量的值放置在顶点着色器输出结构体中
    • 当顶点为 $LB$ 点(即在屏幕左下角)时将 $\overrightarrow {OLB}$ 放置在顶点着色器输出结构体中
    • 当顶点为 $RT$ 点(即在屏幕右上角)时将 $\overrightarrow {ORT}$ 放置在顶点着色器输出结构体中
    • 当顶点为 $RB$ 点(即在屏幕右下角)时将 $\overrightarrow {ORB}$ 放置在顶点着色器输出结构体中
      Pasted image 20230709123140|500
  2. 利用 GPU 硬件的插值(顶点着色器的输出结构体会在三角形遍历阶段进行重心坐标插值,然后将插值后的值传递给片元着色器使用),得到 $\overrightarrow {OA1}$
  3. 利用三角形的相似关系,可以得到 $\overrightarrow {OA}$

步骤 1

  • @ 求 $\mathrm{\overrightarrow{OLT},\overrightarrow{OLB},\overrightarrow{ORT},\overrightarrow{ORB}}$

Pasted image 20230709121439

为了方便计算,我们可以先计算两个向量——$toTop$ 和 $toRight$, 它们是起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量。它们的计算公式如下:
$$
halfHeight=Near\times\tan\biggl(\frac{FOV}2\biggr)
$$
$$
to Top = camera.up \times halfHeight
$$
$$
toRight =camera.right \times halfHeight \cdot aspect
$$

$camera.up$ 是单位向量,指向摄像机正上方,只是用来确定向量方向

得到这两个辅助向量后,就可以计算 4 个角(图中的 TL、TR、BL、BR)相对于摄像机的方向了,只需要简单的向量运算:
$$
\begin{gathered}
\overrightarrow{OLT}=camera.forward \cdot Near+to Top-to Right\
\overrightarrow{OLB}=camera.forward·Near-toTop-toRight \
\overrightarrow{ORT}=camera.forward·Near+toTop+toRight \
\overrightarrow{ORB}=camera,forward\cdot Near-toTop+toRight
\end{gathered}
$$
注意这四个向量不仅包含了方向信息,它们的模对应了四个点到摄像机的距离。

步骤 2

  • @ 利用 GPU 硬件的插值,得到 $\overrightarrow {OA1}$

这一步是这种方法的核心。只有真正理解了这一步,才可以说是真正理解了这种方法。
我们先要明白什么是屏幕后处理。相机渲染完场景中所有物体后会得到一张渲染纹理,但是我们不直接把这张渲染纹理显示在屏幕上,而是额外对这张渲染纹理的每一个像素点进行处理一遍(这个过程就叫做屏幕后处理),然后将屏幕后处理的结果递到屏幕上。
屏幕后处理一般是通过额外渲染一个与屏幕大小相同的矩形网格来实现的。该网格只有 2 个三角面,共 4 个顶点,如下图。对每个像素的额外处理则会放到片元着色器中,具体处理的是哪一个像素用 uv 坐标来得到。
Pasted image 20230709124035

我们知道,在渲染流水线中,GPU 会在三角形设置阶段对顶点着色器输出结构体中的值进行重心坐标插值,然后再传递给片元着色器,就像下图这样。
Pasted image 20230709124046
也就是说,我们在步骤 1 中传递的 $\mathrm{\overrightarrow{OLT},\overrightarrow{OLB},\overrightarrow{ORT},\overrightarrow{ORB}}$ 经过 GPU 硬件的插值后,在片元着色器中将会得到(方向和长度通过重心坐标插值都能得到)。
这一步根本不用写代码,GPU 硬件已经实现了。

步骤 3

  • @ 利用三角形的相似关系,可以得到 $\overrightarrow {OA}$

我们得到的线性深度值 linearEyeDepth 并非是摄像机的欧氏距离,而是在 $z$ 方向的距离。
Pasted image 20230709124513

如图,以世界空间中的的 $A$ 点为例
$|OA|$ 是 A 点到摄像机的距离
$|OD|$ 为 A 点在观察空间的线性深度
$|OD1|$ 是相机的近裁剪平面距离

$\overrightarrow {OA1}$ 在第二步插值得到 ,用图中公式即可求得 $\overrightarrow {OA}$

代码(待验证)

首先在 C #中传递需要用到的向量 。(这里是用 RendererFeature 写的后处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {  
Matrix4x4 view = renderingData.cameraData.GetViewMatrix();
Matrix4x4 proj = renderingData.cameraData.GetProjectionMatrix();
Matrix4x4 vp = proj * view;

// 将camera view space 的平移置为0,用来计算world space下相对于相机的vector
Matrix4x4 cview = view;
cview.SetColumn(3, new Vector4(0.0f, 0.0f, 0.0f, 1.0f));
Matrix4x4 cviewProj = proj * cview;

// 计算viewProj逆矩阵,即从裁剪空间变换到世界空间
Matrix4x4 cviewProjInv = cviewProj.inverse;

// 计算世界空间下,近平面四个角的坐标
var near = renderingData.cameraData.camera.nearClipPlane;
// Vector4 topLeftCorner = cviewProjInv * new Vector4(-near, near, -near, near);
// Vector4 topRightCorner = cviewProjInv * new Vector4(near, near, -near, near); // Vector4 bottomLeftCorner = cviewProjInv * new Vector4(-near, -near, -near, near); Vector4 topLeftCorner = cviewProjInv.MultiplyPoint(new Vector4(-1.0f, 1.0f, -1.0f, 1.0f));
Vector4 topRightCorner = cviewProjInv.MultiplyPoint(new Vector4(1.0f, 1.0f, -1.0f, 1.0f));
Vector4 bottomLeftCorner = cviewProjInv.MultiplyPoint(new Vector4(-1.0f, -1.0f, -1.0f, 1.0f));

// 计算相机近平面上方向向量
Vector4 cameraXExtent = topRightCorner - topLeftCorner;
Vector4 cameraYExtent = bottomLeftCorner - topLeftCorner;

near = renderingData.cameraData.camera.nearClipPlane;

mMaterial.SetVector(mCameraViewTopLeftCornerID, topLeftCorner);
mMaterial.SetVector(mCameraViewXExtentID, cameraXExtent);
mMaterial.SetVector(mCameraViewYExtentID, cameraYExtent);
mMaterial.SetVector(mProjectionParams2ID, new Vector4(1.0f / near, renderingData.cameraData.worldSpaceCameraPos.x, renderingData.cameraData.worldSpaceCameraPos.y, renderingData.cameraData.worldSpaceCameraPos.z));
}

在 Shader 中,进行还原。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 根据线性深度值和屏幕UV,还原世界空间下,相机到顶点的位置偏移向量
half3 ReconstructViewPos(float2 uv, float linearEyeDepth) {
// Screen is y-inverted
uv.y = 1.0 - uv.y;

float zScale = linearEyeDepth * _ProjectionParams2.x; // divide by near plane
float3 viewPos = _CameraViewTopLeftCorner.xyz + _CameraViewXExtent.xyz * uv.x + _CameraViewYExtent.xyz * uv.y;
viewPos *= zScale;

return viewPos;
}

half4 Fragment(Varyings input) : SV_Target {
// 采样深度缓冲区
float rawDepth = SampleSceneDepth(input.uv);
// 得到线性深度 [n, f]
float linearDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
// 还原偏移向量
float3 vpos = ReconstructViewPos(input.uv, linearDepth);
// 计算世界空间坐标
float3 wpos = _WorldSpaceCameraPos + vpos;

half3 color = wpos;
return half4(color, 1.0);
}

Unity 深度法线纹理

获取深度纹理

Unity 深度纹理存储了高精度的深度值,范围是 $[1,0]$,是非线性深度。

  • @ 1 首先要开启 URP Asset ->Depth Texture 并设置 Depth Texture Mode 为 Force Prepass 或 Depth Priming Mode 设置为 Auto 或Force
    Pasted image 20230707140918|450
    Pasted image 20230717212059|500
    ⭐方法二:RenderFeature 的 SetupRenderPasses 中设置 ConfigureInput,这样就可以采样到深度纹理了。

    1
    m_renderPass.ConfigureInput(ScriptableRenderPassInput.Depth);
  • @ 2 采样深度纹理,计算线性深度
    _CameraDepthTexture:深度纹理
    _ZBufferParams:用于线性化 Z 缓冲区值。x 是 (1-near/far),y 是 (far/near),z 是 (x/far),w 是 (y/far)。
    LinearViewDepth: 把深度纹理的采样结果转换成观察空间下的深度值,返回范围在 $[near, far]$ 的线性深度值
    Linear01Depth:返回范围在 $[0,1]$ 的线性深度值

title:手写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//声明深度纹理
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);

//获取屏幕空间UV
float2 ScreenUV = i.positionCS.xy / _ScreenParams.xy;


//用屏幕UV采样屏幕深度纹理得到非线性深度,转换成0-1线性深度图
float depthColor = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, ScreenUV).r;
float linearDepthColor = Linear01Depth(depthColor,_ZBufferParams);

//计算模型深度,转换成线性深度图
float depth = i.positionCS.z;
float linearDepth = Linear01Depth(depth,_ZBufferParams);
title:⭐直接调用api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//内部声明了深度纹理_CameraDepthTexture
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

//获取屏幕UV
//深度纹理是一个全屏(full-screen)纹理,它的尺寸和我们的屏幕相同,所以用屏幕UV采样,其他全屏纹理同理
float2 ScreenUV = GetNormalizedScreenSpaceUV(i.positionCS);


//从深度纹理中采样深度
#if UNITY_REVERSED_Z
// 具有 REVERSED_Z 的平台(如 D3D)的情况。
float depth = SampleSceneDepth(ScreenUV);
#else
// 没有 REVERSED_Z 的平台(如 OpenGL)的情况。
float depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(uvSS));
#endif

//转换成线性深度图
float linearDepthColor = Linear01Depth(depth,_ZBufferParams);
  • @ 3 深度图内对象添加 pass
    要想对象在深度纹理中显示,对象的 shader 需要加一个 pass:
    可以直接添加 Lit 内置的 pass,这种方法会打破 SRPBatcher 的,可以自己写一个来匹配 SRPBatcher 条件
    fold title:Lit-DepthOnly
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    Pass
    {
    Name "DepthOnly"
    Tags
    {
    "LightMode" = "DepthOnly"
    }

    // -------------------------------------
    // Render State Commands
    ZWrite On
    ColorMask R
    Cull[_Cull]

    HLSLPROGRAM
    #pragma target 2.0

    // -------------------------------------
    // Shader Stages
    #pragma vertex DepthOnlyVertex
    #pragma fragment DepthOnlyFragment

    // -------------------------------------
    // Material Keywords
    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

    // -------------------------------------
    // Unity defined keywords
    #pragma multi_compile_fragment _ LOD_FADE_CROSSFADE

    //--------------------------------------
    // GPU Instancing
    #pragma multi_compile_instancing
    #include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DOTS.hlsl"

    // -------------------------------------
    // Includes
    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
    ENDHLSL
    }
fold title:手写-DepthOnly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//景深Pass
Pass
{
//Pass名称
Name "DepthOnly"

//渲染目标
Tags{"LightMode" = "DepthOnly"}

//开启深度写入
ZWrite On

//深度测试 正向前后排序
ZTest LEqual
//Greater > , GEqual >= , Less < , LEqual <= , Equal == , NotEqual != ,Always(永远渲染), Never(从不渲染);

//只输出深度数据,以节省带宽
ColorMask 0


HLSLPROGRAM //URP 程序块开始

//指向顶点函数
#pragma vertex vert

//指向面渲染函数
#pragma fragment frag

//URP函数命令库
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

//源数据
struct VertexInput
{
//原始物体空间顶点数据
float4 positionOS : POSITION;//数据来源于(寄存器POSITION)
};

//顶点输出
struct VertexOutput
{
//物体裁切空间坐标
float4 positionCS : SV_POSITION;//数据目标(寄存器SV_POSITION)

//世界空间顶点
float3 positionWS : TEXCOORD0;//数据目标(寄存器TEXCOORD0~7)
};

//顶点函数
VertexOutput vert(VertexInput v)
{
//声明输出变量o
VertexOutput o;

//输入物体空间顶点数据
VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz);

//获取裁切空间顶点
o.positionCS = positionInputs.positionCS;

//获取世界空间顶点
o.positionWS = positionInputs.positionWS;

//输出
return o;
}

//面渲染函数
float4 frag(VertexOutput i) : SV_Target
{
//输出物体的世界空间顶点到摄像机距离
return distance(i.positionWS.xyz , _WorldSpaceCameraPos.xyz);
}

ENDHLSL
}

获取深度+法线纹理

  • @ 1 开启深度法线纹理
    方法一:(不推荐,消耗大)首先添加 SSAO RenderFeature,Source->Depth Normals
    Pasted image 20230717212229|500

    这里也可单选 depth,来实现获取深度纹理

⭐方法二:RenderFeature 的 SetupRenderPasses 中设置 ConfigureInput,这样就可以采样到法线纹理了。

1
m_renderPass.ConfigureInput(ScriptableRenderPassInput.Normal);
  • @ 2 采样深度+法线纹理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //在shader脚本中定义
    TEXTURE2D(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture);
    TEXTURE2D(_CameraNormalsTexture); SAMPLER(sampler_CameraNormalsTexture);

    //或者直接引用,内部声明了纹理和采样器
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"

    //⭐深度使用方法和上节一样


    //⭐法线使用方法:
    //屏幕空间uv
    float2 ScreenUV = GetNormalizedScreenSpaceUV(i.positionCS);
    //采样即可
    float3 normal = SampleSceneNormals(ScreenUV);
  • @ 3 法线图内对象添加 pass
    要想对象在法线纹理中显示,对象的 shader 需要加一个 pass:
    可以直接添加 Lit 内置的 pass,这种方法会打破 SRPBatcher 的,可以自己写一个来匹配 SRPBatcher 条件

    fold title:DepthNormals
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    // This pass is used when drawing to a _CameraNormalsTexture texture
    Pass
    {
    Name "DepthNormals"
    Tags
    {
    "LightMode" = "DepthNormals"
    }

    // -------------------------------------
    // Render State Commands
    ZWrite On
    Cull[_Cull]

    HLSLPROGRAM
    #pragma target 2.0

    // -------------------------------------
    // Shader Stages
    #pragma vertex DepthNormalsVertex
    #pragma fragment DepthNormalsFragment

    // -------------------------------------
    // Material Keywords
    #pragma shader_feature_local _NORMALMAP
    #pragma shader_feature_local _PARALLAXMAP
    #pragma shader_feature_local _ _DETAIL_MULX2 _DETAIL_SCALED
    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

    // -------------------------------------
    // Unity defined keywords
    #pragma multi_compile_fragment _ LOD_FADE_CROSSFADE

    // -------------------------------------
    // Universal Pipeline keywords
    #include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/RenderingLayers.hlsl"

    //--------------------------------------
    // GPU Instancing
    #pragma multi_compile_instancing
    #include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DOTS.hlsl"

    // -------------------------------------
    // Includes
    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitDepthNormalsPass.hlsl"
    ENDHLSL
    }
fold title:手写-DepthNormals
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//深度法线Pass
Pass
{
//Pass名字
Name "DepthNormals"

//渲染目标深度法线
Tags{"LightMode" = "DepthNormals"}

//开启深度写入,这样就可以在模型渲染种获得正确层Mask
ZWrite On

//深度测试 正向前后排序
ZTest LEqual

HLSLPROGRAM //URP 程序块开始

//指向顶点函数
#pragma vertex vert

//指向面渲染函数
#pragma fragment frag

//URP函数命令库
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

//源数据
struct VertexInput
{
//原始物体空间顶点数据
float4 positionOS : POSITION;//数据来源于(寄存器POSITION)

//顶点中的物体空间法线数据
float4 normalOS : NORMAL;//数据来源于(寄存器NORMAL)

//顶点中的物体空间切线数据
float4 tangentOS : TANGENT;//数据来源于(寄存器TANGENT)
};

//顶点输出
struct VertexOutput
{
//物体裁切空间坐标
float4 positionCS : SV_POSITION;//数据目标(寄存器SV_POSITION)

//世界空间顶点
float3 normalWS : TEXCOORD0;//数据目标(寄存器TEXCOORD0~7)
};

//顶点函数
VertexOutput vert(VertexInput v)
{
//声明输出变量o
VertexOutput o;

//输入物体空间顶点数据
VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz);

//获取裁切空间顶点
o.positionCS = positionInputs.positionCS;


//输入物体空间法线数据
VertexNormalInputs normalInputs = GetVertexNormalInputs(v.normalOS.xyz, v.tangentOS);

//获取世界空间法线
o.normalWS = normalInputs.normalWS;

return o;
}


//面渲染函数
float4 frag(VertexOutput i) : SV_Target
{
//输出世界法线
return float4 (i.normalWS,1);
}

ENDHLSL //URP 程序块结束
}

深度纹理重建像素的世界空间位置

202377141633

代码

[[03 深度法线纹理#^hl5tio|原理手算]]
运用内置 API 更方便!

title:使用深度纹理和屏幕空间UV坐标来重建像素的世界空间位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"


//用深度纹理和屏幕空间uv重建像素的世界空间位置
//屏幕空间uv
float2 ScreenUV = GetNormalizedScreenSpaceUV(i.positionCS);
//float2 ScreenUV = i.positionCS.xy / _ScaledScreenParams.xy;

//从深度纹理中采样深度,跨平台统一返回[1,0]深度值
#if UNITY_REVERSED_Z
// 具有 REVERSED_Z 的平台(如 D3D)的情况。
float depth = SampleSceneDepth (ScreenUV);
#else
// 没有 REVERSED_Z 的平台(如 OpenGL)的情况。
float depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(ScreenUV));
#endif

// 重建世界空间位置,注意,这里的深度为非线性深度
float3 rebuildPosWS = ComputeWorldSpacePosition(ScreenUV, depth, UNITY_MATRIX_I_VP);

//在远裁剪面附近将颜色设置为黑色。
// #if UNITY_REVERSED_Z
// if(depth < 0.0001)
// return half4(0,0,0,1);
// #else
// if(depth > 0.9999)
// return half4(0,0,0,1);
// #endif

步骤

  1. 包含文件:DeclareDepthTexture. hlsl 文件包含用于对摄像机深度纹理进行采样的实用程序: SampleSceneDepth 返回 [0, 1] 范围内的 $Z$ 值。

    title:包含文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

    //包含如下:已经声明了相机深度纹理,我们只需要传入屏幕空间uv调用采样函数
    TEXTURE2D_X_FLOAT(_CameraDepthTexture);
    SAMPLER(sampler_CameraDepthTexture);

    float SampleSceneDepth(float2 uv)
    {
    return SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(uv)).r;
    }

    float LoadSceneDepth(uint2 uv)
    {
    return LOAD_TEXTURE2D_X(_CameraDepthTexture, uv).r;
    }
  2. 在片元着色器中计算用于采样深度纹理的屏幕空间 UV 坐标,像素位置除以渲染目标分辨率 _ScaledScreenParams_ScaledScreenParams.xy 属性会考虑渲染目标的任何缩放,例如动态分辨率。

    title:用深度纹理和屏幕空间uv重建像素的世界空间位置
    1
    2
    3
    //屏幕空间uv  
    float2 ScreenUV = GetNormalizedScreenSpaceUV(i.positionCS);
    //float2 ScreenUV = i.positionCS.xy / _ScaledScreenParams.xy; 等价
  3. 在片元着色器中,使用 SampleSceneDepth 函数对深度缓冲区进行采样。

    title:从深度纹理中采样深度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #if UNITY_REVERSED_Z
    // 具有 REVERSED_Z 的平台(如 D3D)的情况。
    //返回[1,0]的深度值
    real depth = SampleSceneDepth(ScreenUV);
    #else
    // 没有 REVERSED_Z 的平台(如 OpenGL)的情况。
    // 调整 Z 以匹配 OpenGL 的 NDC([-1, 1])
    real depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(ScreenUV));
    #endif
  4. 用像素的 UV 和 Z 坐标重建世界空间位置。

    title:重建世界空间位置
    1
    float3 rebuildPosWS = ComputeWorldSpacePosition(ScreenUV, depth, UNITY_MATRIX_I_VP);

    ComputeWorldSpacePosition :根据屏幕空间 UV 和深度 ($Z$) 值计算世界空间位置
    UNITY_MATRIX_I_VP 是一个逆观察投影矩阵,可将点从裁剪空间变换为世界空间。

  5. 对于未渲染几何图形的区域,深度缓冲区可能没有任何有效值。以下代码会在这些区域绘制黑色。

    1
    2
    3
    4
    5
    6
    7
    8
    //在远裁剪面附近将颜色设置为黑色。
    #if UNITY_REVERSED_Z
    if(depth < 0.0001)
    return half4(0,0,0,1);
    #else
    if(depth > 0.9999)
    return half4(0,0,0,1);
    #endif

    不同的平台对远裁剪面使用不同的 Z 值(0 == far,或 1 == far)。UNITY_REVERSED_Z 常量让代码可以正确处理所有平台。

  6. 对象要在重建的世界坐标中显示,需要添加深度法线纹理pass

深度纹理重建法线

在了解如何用深度还原位置信息之后还原法线就非常容易了,其实对于一个着色点,只需要求出他的上下左右的位置信息,然后利用叉乘来近似计算该点的法线即可,伪代码如下

1
2
3
4
5
6
7
8
vec3 P  = GetViewPos(v2f_TexCoords);
vec3 Pl = GetViewPos(v2f_TexCoords + vec2(-xOffset,0));
vec3 Pr = GetViewPos(v2f_TexCoords + vec2(xOffset,0));
vec3 Pu = GetViewPos(v2f_TexCoords + vec2(0,yOffset));
vec3 Pd = GetViewPos(v2f_TexCoords + vec2(0,-yOffset));
vec3 leftDir = min(P - Pl, Pr - P) ? P - Pl : Pr - P//求出最小的变换量
vec3 upDir = min(P - Pd, Pu - P) ? P - Pd : Pu - P//求出最小的变换量
vec3 normal = normalize(cross(leftDir,upDir))

求最小的变换向量是为了让法线的变换根据平滑一点,如果觉得采样太多也只需要采用x,y方向各一个点即可。

应用

深度相交高亮

世界空间位置差

做交接边的思路:通过深度重建世界坐标,
Pasted image 20230731225948

Pasted image 20230801143415|500

要求深度图只渲染不透明物体,这样深度图记录的是 B 点的深度,进而重建出 B 点的世界坐标。

1
float3 posDistance = saturate(distance(rebuildPosWS, i.positionWS) / _MaxDepth);

观察空间深度差

1
float DepthDifference = saturate(linearEyeDepth - i.positionCS.w / _MaxDepth);

护盾/能量场

TA101_作业_温斯顿的能量盾_3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//深度交接出白边
//rebuildPosWS一般来说>=i.positionWS
float3 posDistance = saturate(distance(rebuildPosWS, i.positionWS) / _DepthFadeDistance);

float3 whiteEdge = 1 - posDistance;

//计算过渡颜色
float3 FadeColor = pow(whiteEdge, _DepthFadePower) * _DepthFadeScale * _DepthFadeColor.rgb;

//计算边颜色
float3 edge = step(_WhiteEdgeWidth, whiteEdge); //Step得出离地近的边
float3 edgeColor = edge * _WhiteEdgeColor.rgb;

//混合颜色
float3 DepthEdgeColor = lerp(FadeColor, edgeColor, edge);


//护盾外部菲涅尔,内部无菲涅尔
if (facing > 0)
{
return float4(DepthEdgeColor + fresnelColor, 1);
}
else
{
return float4(DepthEdgeColor, 1);
}

全局雾效

思路是让雾的浓度随着深度值的增大而增大,然后进行的原图颜色和雾颜色的插值:

1
2
3
4
5
6
7
8
9
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv.xy);
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearDepth = Linear01Depth(depth);
float fogDensity = saturate(linearDepth * _FogDensity);
fixed4 finalColor = lerp(col, _FogColor, fogDensity);
return finalColor;
}

完整代码点这里

aa8f778ba81f5d48c2e06800891e7134_MD5

Fog 场景

扫描线

深度扫线

  1. 深度图中存的是深度值,减去一个对应的扫描线深度(整个深度图都的深度都减去这个值)。这样小于等于这个扫描线深度的部分值小于等于 0:
    Pasted image 20230802161949|450

  2. 取绝对值,扫描线位置就会变黑。加一个 saturate 防止过曝
    Pasted image 20230802162032

  3. 除以 _LineWidth_LineWidth <1 时候越小,扫描线越细

  4. 最后 lerp 混合原图:

    1
    2
    3
    float linearEyeDepth = LinearEyeDepth(depth,_ZBufferParams);
    float v = saturate(abs(linearEyeDepth-_ScanDepth)/_LineWidth);
    return lerp(_ScanLineColor_,color,v);

8a5f6b329c528a4a855e12d23f41a880_MD5

重建世界坐标画线

重建的世界坐标取小数+取余数,即可

得到世界坐标后,因为我们的坐标轴取值范围是从 -∞到 +∞,而颜色的范围只是 0-1 之间,如果对世界坐标使用 frac 取小数就可以得到只在 0-0.99 的值了

1
return float4(frac(rebuildPosWS),1);

61fb3efd59dc2c6cf9b00ff81ee5c9b4_MD5

取余数

对于在 0-1 之间均匀变换的我们想得到它的边界位置,所以直接来个 step 函数,我们只取 0.98 到 1.0 之间的值为 1,其他的值为 0,我们把它输出出来。是不是有那味了,线框就直接出来了。

89da9b0159e843562f581518306b433d_MD5

但是这个线框的颜色红蓝绿(为什么是红蓝绿?是因为它对应 XYZ 三个轴向)很乱而且不好看,我们想要自由控制颜色,我们定义三个颜色,去分别控制 XYZ 方向的线框颜色,并把它输出。

248dd835198f5ef05b6c64552a6603ca_MD5

重建世界坐标扫线

042402eab80e213473b1cba5fbad412b_MD5

1
2
3
4
5
6
//x方向
float mask = saturate(pow(abs(frac(rebuildPosWS.x*_ScanSpace-_Time.y*_TimeSpeed)),_ScanPower))*_ScanScale;
//y方向
float mask = saturate(pow(abs(frac(rebuildPosWS.y*_ScanSpace-_Time.y*_TimeSpeed)),_ScanPower))*_ScanScale;
//z方向
float mask = saturate(pow(abs(frac(rebuildPosWS.z*_ScanSpace-_Time.y*_TimeSpeed)),_ScanPower))*_ScanScale;

水淹

利用上面提到的第二种重建世界空间坐标的方法得到世界空间坐标,判断该坐标的 Y 值是否在给定阈值下,如果是则混合原图颜色和水的颜色:

1
2
3
4
5
float3 rebuildPosWS = ComputeWorldSpacePosition(ScreenUV, depth, UNITY_MATRIX_I_VP);
if(rebuildPosWS.y<_WaterHeight)
{
return lerp(finalColor,_WaterColor,_WaterColor.a);
}

f72d713427cce38f7905b4f065e792dc_MD5