UnityShader-重建世界坐标

UnityShader-重建世界坐标

最近在学习屏幕后处理相关的知识,其中很重要的一个技巧就是通过深度纹理重建像素的世界坐标,通过它可以实现很多酷炫的效果,比如烟雾效果,扫描效果等。我目前已知两种方法,一种是通过逆矩阵的方式重建,另一种是通过屏幕射线插值的方式实现,本文主要介绍第二种

通过屏幕射线插值重建

核心原理:计算摄像机的位置加上像素点到摄像机的偏移得到世界坐标

其中用到的数学原理如下图所示:

image-20210926192549702

我将用以下俯视图辅助解释其中的原理:

RebuildWorldPosition.drawio

如图,O为摄像机的位置,A为远裁剪平面的左上角,B为右上角,其它点的相对位置如图,其中T为一个世界空间中的像素点,为了得到T的坐标,我们需要先求出向量$$OT$$的值

根据相似三角形定理,可以得出$$OT = OE \times depth(T)$$,公式推导如下:
$$
\frac{\abs{OT}}{\abs{OE}} = \frac{\abs{OD}}{\abs{OC}} = \frac{depth(T)}{depth(E)} = \frac{depth(T)}{1}
$$

$$
\abs{OT} = \abs{OE} \times depth(T)
$$

又因为:
$$
OT \times OE = 0
$$
即$$OT$$平行于$$OE$$,因此有:
$$
OT = OE \times depth(T)
$$
所以,我们只要知道T点的深度值和向量$$OE$$,就能求得像度点 T 的世界坐标

T的线性深度值可以通过深度纹理采集,然后进行透视除法的逆获得,那么如何求得向量$$OE$$呢?

我们可以在顶点着色器中求得摄像机坐标到屏幕四边形四个顶点的向量,再通过GPU插值获得,这4个向量的求法很简单,利用到摄像机的几个参数即可,具体方法见代码

在《Unity Shader 入门精要》一书中也介绍了类似的重建深度坐标的方法,但其中使用的深度值是视角空间下的深度值,该深度值的范围是[Near, Far](视角空间下z坐标的范围),本文所使用的是01范围的深度值,是视角空间下的深度值除以Far后得到的,如果不小心混淆使用则得不到理想的效果!!

挂载到摄像机下脚本的关键代码如下:

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
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (RenderMaterial == null) // 用于重建世界坐标的材质
{
Graphics.Blit(source, destination);
}
else
{
var aspect = currentCamera.aspect;
var far = currentCamera.farClipPlane;
var right = transform.right;
var up = transform.up;
var forward = transform.forward;
var halfHeight = Mathf.Tan(currentCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);

// 计算远裁剪面处的xyz三方向向量
var rightVec = right * far * halfFovTan * aspect;
var upVec = up * far * halfFovTan;
var forwardVec = forward * far;

// 计算摄像机到远裁剪平面的四个顶点的向量
var topLeft = (forwardVec - rightVec + upVec);
var topRight = (forwardVec + rightVec + upVec);
var bottomLeft = (forwardVec - rightVec - upVec);
var bottomRight = (forwardVec + rightVec - upVec);

// 将计算得到的向量放到一个4维矩阵中储存
var frustumCorners = Matrix4x4.identity;
frustumCorners.SetRow(0, topLeft);
frustumCorners.SetRow(1, topRight);
frustumCorners.SetRow(2, bottomLeft);
frustumCorners.SetRow(3, bottomRight);

postEffectMat.SetMatrix("_FrustumCornersRay", frustumCorners);
Graphics.Blit(source, destination, RenderMaterial);
}
}

用于重建世界坐标的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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 顶点着色器,负责计算世界空间坐标,把向量存储到顶点信息中(实际上只有4个顶点,6个索引)
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.srcPos = ComputeScreenPos(o.pos);

o.uv = v.texcoord;
o.uv_depth = v.texcoord;

#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif

int index = 0;
if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
index = 0;
} else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
index = 1;
} else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
index = 2;
} else {
index = 3;
}

#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
index = 3 - index;
#endif

o.interpolatedRay = _FrustumCornersRay[index];

return o;
}

fixed4 frag(v2f i) : SV_Target {
// 获得线性深度值
float depth = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.srcPos)).r;
float linear01Depth = Linear01Depth(depth);

// 计算世界坐标
float3 worldPos = _WorldSpaceCameraPos + linear01Depth * i.interpolatedRay.xyz;

return fixed4(worldPos, 1);
}

UnityShader-重建世界坐标
http://example.com/2023/01/10/UnityShader-重建世界坐标/
作者
Chen Shuwen
发布于
2023年1月10日
许可协议