Unity3D中根据深度值重建像素点对应的世界坐标
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
1 深度图介绍
就是将深度信息(Z坐标值)保存在了一张贴图上的R通道上,因为R通道的值范围是[0, 1],所以我们可以用ndc空间下的Z坐标值做下处理就能变成[0, 1]范围了( (Zndc+1)*0.5
2 C#接口取得深度图纹理的方法
Shader.GetGlobalTexture("_CameraDepthTexture")
3 在NDC空间下深度值的计算公式
4 从片元着色器中根据深度图反推出片元世界坐标且输出的shader
Shader "zznewclear13/DepthToPositionShader"
{
Properties
{
[Toggle(REQUIRE_POSITION_VS)]
_Require_Position_VS("Require Position VS", float) = 0
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/core.hlsl"
#pragma multi_compile _ REQUIRE_POSITION_VS
sampler2D _CameraDepthTexture;
struct Attributes
{
float4 positionOS : POSITION;
float2 texcoord : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
Varyings Vert(Attributes input)
{
Varyings output = (Varyings)0;
// 根据顶点在其模型坐标系下的值,计算得到在裁剪空间中的齐次坐标
VertexPositionInputs vertexPositionInputs =
GetVertexPositionInputs(input.positionOS.xyz);
// vertexPositionInputs.positionCS的是齐次裁剪空间坐标,即一个 float4 值,
// 形式为 (x, y, z, w),其中 w 是用于透视除法的分量。透视除法会在后续的处理
// 步骤中进行,得到标准化设备坐标(NDC),最终决定物体在屏幕上的位置。
// 在透视除法之前,齐次裁剪空间的 x 和 y 值可以超出任何特定范围,因为它们与
// w 是关联的。最终,它们会通过透视除法得到以下标准化的范围
// 透视除法后(标准化设备坐标)的取值范围:
// x / w 和 y / w 的取值范围为 [-1, 1]。
// -1 表示屏幕的最左边(x 方向)或最下方(y 方向)。
// 1 表示屏幕的最右边(x 方向)或最上方(y 方向)。
output.positionCS = vertexPositionInputs.positionCS;
output.texcoord = input.texcoord;
return output;
}
float4 Frag(Varyings input) : SV_TARGET
{
// 根据片元的在裁剪空间中的坐标值,算出片元对应的在屏幕空间的坐标值
// _ScreenParams的xy分量是渲染目标纹理的宽度和高度(以像素为单位),
// z分量是1.0 + 1.0/宽度,w为1.0 + 1.0/高度。所以屏幕空间的坐标值为
//【齐次裁剪空间坐标】的坐标值的xy,分别除以渲染目标纹理的高宽
// 因为input.positionCS.xy的取值范围是[0,渲染目标纹理的宽]和
// [0,渲染目标纹理的高],故而positionSS的xy分量,经过上一步的运算后,
// 分别得到的取值范围是[0,1]。就是一个有效的纹理坐标uv值
float2 positionSS = input.positionCS.xy * (_ScreenParams.zw - 1);
// 利用纹理坐标uv值,对深度图纹理进行采样,得到的值,就是当前屏幕空间中
// 片元对应的深度值,即NDC空间中的坐标值z分量
// _CameraDepthTexture是Unity3D的内置着色器变量
float depth = tex2D(_CameraDepthTexture, positionSS).r;
// 因为NDC空间的取值范围是:
// X和Y轴的取值范围:[-1, 1]。
// -1表示屏幕的最左侧或最底部。1表示屏幕的最右侧或最顶部。
// Z轴的取值范围是[0, 1]。0和1分别表示相机的近截平面和远截平面
// 而positionSS的取值范围是[0,1],所以需要做【乘2减1】的操作,将其映射到
// [-1,1]的取值范围内。
float3 positionNDC = float3(positionSS * 2 - 1, depth);
#if UNITY_UV_STARTS_AT_TOP
positionNDC.y = -positionNDC.y;
#endif
// 得到裁剪空间的NDC坐标之后,就可以反向地推出片元对应的屏幕空间了
#if REQUIRE_POSITION_VS
// UNITY_MATRIX_I_P是投影矩阵的逆矩阵(inverse projection matrix)
float4 positionVS = mul(UNITY_MATRIX_I_P, float4(positionNDC, 1));
// 当你通过逆投影矩阵将点从 clip space 变换回 view space 时,生成的点仍
// 然是齐次坐标 (x, y, z, w),因此需要除以 w 来将其转换回标准的三维欧几里
// 得坐标。这是因为:在齐次坐标系下,w 分量影响了 x、y 和 z 分量的比例。
// 如果不除以 w,这些分量的值将无法正确反映物体在视图空间中的位置,导致不正
// 确的几何表示。view space中的坐标应该是三维欧几里得坐标,而不是齐次坐标。
// 透视除法确保得到的 x'、y'、z' 是正确的三维位置。
positionVS /= positionVS.w;
// 再通过观察矩阵的逆矩阵,将顶点变换回世界坐标
float4 positionWS = mul(UNITY_MATRIX_I_V, positionVS);
#else
float4 positionWS = mul(UNITY_MATRIX_I_VP, float4(positionNDC, 1));
positionWS /= positionWS.w;
#endif
return positionWS;
}
float4 DepthFrag(Varyings input) : SV_TARGET
{
return 0;
}
ENDHLSL
SubShader
{
Tags{ "RenderType" = "Opaque" }
LOD 100
Pass
{
Tags{"LightMode"="UniversalForward"}
ZWrite On
ZTest LEqual
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
ENDHLSL
}
Pass
{
Tags{"LightMode" = "DepthOnly"}
ZWrite On
ZTest LEqual
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment DepthFrag
ENDHLSL
}
}
}
5 在Unity3D默认渲染管线中在后处理阶段用深度图重建世界坐标
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class PostEffectUseDepthTex : MonoBehaviour
{
private Camera m_Camera;
private Material m_GetWorldPosByDepthMat;
void Start()
{
m_Camera = GetComponent<Camera>();
m_GetWorldPosByDepthMat =
new Material(Shader.Find("My/DepthTex/GetWorldPosByDepth"));
}
void OnRenderImage(RenderTexture src, RenderTexture dst)
{
// 用摄像机的观察矩阵和投影矩阵相乘,然后得出其逆矩阵
// 将其传递给shader的_ClipToWorldMatrix,即从齐次裁剪空间变换回世界空间中
var VPMatrix = m_Camera.projectionMatrix * m_Camera.worldToCameraMatrix;
var clipToWorldMatrix = VPMatrix.inverse;
m_GetWorldPosByDepthMat.SetMatrix("_ClipToWorldMatrix", clipToWorldMatrix);
Graphics.Blit(src, dst, m_GetWorldPosByDepthMat);
}
}
下面是shader代码
// 后处理中根据深度图重建世界坐标, 注意: 只适用于后处理OnRenderImage那边,即使用内置管线
Shader "My/DepthTex/GetWorldPosByDepth"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
}
SubShader
{
ZTest Always
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float2 uv_depth : TEXCOORD1; // 用来对深度图进行采样的坐标
};
sampler2D _CameraDepthTexture; //深度图
sampler2D _MainTex; //后处理提供的屏幕图
float4 _MainTex_TexelSize;
float4x4 _ClipToWorldMatrix; //裁剪空间到世界空间转换矩阵, 需要c#部分提供
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
// 因为传递进来的就是一个覆盖全屏幕的矩形,所以顶点的纹理采样uv
// 就可以直接用作对深度图的纹理采样UV
o.uv_depth = v.uv;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 对深度图纹理进行采样
float depth = SAMPLE_DEPTH_TEXTURE(
_CameraDepthTexture, i.uv_depth);
// 在Unity中,UNITY_REVERSED_Z 是一个内置宏,用来指示是否使用了反向Z缓冲(Reversed Z-buffering)。
// Unity引擎从Unity 5.5开始支持反向Z缓冲,这种方式可以在远平面处提高深度缓冲的精度。
// 通常,在深度缓冲中,靠近相机的物体会有较大的深度值,远离相机的物体会有较小的深度值。
// 然而,在启用了反向Z缓冲的情况下,Z值的方向是反的:近处的物体会有较小的Z值,远处的物
// 体会有较大的Z值。这种反转可以减少Z-fighting(Z轴上的对象深度接近时的渲染问题),
// 特别是在需要较远的远裁剪面时提高精度。
// 在Shader中使用时,可以通过这个宏来决定如何处理深度值。例如:
#if defined(UNITY_REVERSED_Z)
depth = 1 - depth;
#endif
// 后处理的那张贴图就对应整个屏幕, 所以可以直接用片元的纹理映射坐标,即齐次裁剪空间下的坐标,
// 对应创建创建ndc坐标(参见前面的shader)
float4 ndcPos = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1,
depth * 2 - 1, 1);
//ndcPos和clipPos只是差一个倍数(就是w分量)
float4 worldPos = mul(_ClipToWorldMatrix, ndcPos);
worldPos /= worldPos.w;
//用世界坐标做相关处理
fixed4 c = tex2D(_MainTex, i.uv);
return c;
}
ENDCG
}
}
FallBack Off
}