根据深度纹理获取世界坐标很常用,可以实现很多有用的功能,比如简单的雾 / 扫描线等
主要参考
https://forum.unity.com/threads/reconstructing-world-pos-from-depth-imprecision.228936/
准备工作
在 unity 中开启深度贴图
创建一个脚本用来放在 Camera 组件上
void Awake()
{
cameraComponent.depthTextureMode = DepthTextureMode.Depth; // 生成获得深度纹理
// cameraComponent.depthTextureMode = DepthTextureMode.DepthNormals; // 生成深度和法线纹理
}
根据搜索总结了下,重建世界坐标基本有两种计算方法
第一种最简单最容易理解的就是在 shader 中对深度纹理采样,然后对深度坐标进行矩阵变换,乘以投影变换矩阵和相机变换矩阵的逆矩阵,就可以还原得到世界坐标
// in C#
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (postEffectMaterial == null)
{
Graphics.Blit(src, dest);
}
else
{
//main problem encountered is camera.projectionMatrix = ??????? worked but further from camera became more inaccurate
//had to use GL.GetGPUProjectionMatrix( ) seems to stay pretty exact now
Matrix4x4 viewMat = currentCamera.worldToCameraMatrix;
Matrix4x4 projMat = GL.GetGPUProjectionMatrix(currentCamera.projectionMatrix, false);
//Matrix4x4 viewProjMat = currentCamera.projectionMatrix * currentCamera.worldToCameraMatrix;
//有的文章里直接使用的currentCamera.projectionMatrix,如果远离原点的话会计算不准确
Matrix4x4 viewProjMat = (projMat * viewMat);
Shader.SetGlobalMatrix("_ViewProjInv", viewProjMat.inverse);
Graphics.Blit(src, dest, postEffectMaterial);
}
}
// in fragment shader:
uniform float4x4 _ViewProjInv;
float4 GetWorldPositionFromDepth( float2 uv_depth )
{
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv_depth);
float4 H = float4(uv_depth.x*2.0-1.0, (uv_depth.y)*2.0-1.0, depth, 1.0);
float4 D = mul(_ViewProjInv,H);
return D/D.w;
}
第一种方法原理比较简单,但是效率不是很高,毕竟是逐像素计算,
第二种方法可以使用屏幕射线差值的方法,《unity shader入门精要》中有非常详细的推导和解释
物体的世界坐标为相机的世界坐标+物体在相机坐标系的坐标
相机坐标系的坐标 = 点的方向 * 点到相机的距离
点的方向可以通过计算剪裁空间四个角的射线得到
点到相机的距离为
distance = ray * depth / far; far = 1;
distance = ray * depth;
书中使用的是近裁平面推导,好像使用原裁平面更好推导
总结起来就是
- 先计算相机到屏幕四个角的射线
- 在顶点着色器中根据 uv 的值选择对应的射线
- 在像素着色器中计算世界坐标
// in C#
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (postEffectMaterial == null)
{
Graphics.Blit(src, dest);
}
else
{
float aspect = currentCamera.aspect;
float near = currentCamera.nearClipPlane;
float far = currentCamera.farClipPlane;
float halfFovTan = Mathf.Tan(currentCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
Vector3 up = transform.up;
Vector3 right = transform.right;
Vector3 forward = transform.forward;
Vector3 forwardVector = forward * far;
Vector3 upVector = up * far * halfFovTan;
Vector3 rightVector = right * far * halfFovTan * aspect;
Vector3 topLeft = forwardVector - rightVector + upVector;
Vector3 topRight = forwardVector + rightVector + upVector;
Vector3 bottomLeft = forwardVector - rightVector - upVector;
Vector3 bottomRight = forwardVector + rightVector - upVector;
Matrix4x4 viewPortRay = Matrix4x4.identity;
viewPortRay.SetRow(0, topLeft);
viewPortRay.SetRow(1, topRight);
viewPortRay.SetRow(2, bottomLeft);
viewPortRay.SetRow(3, bottomRight);
postEffectMaterial.SetMatrix("_ViewPortRay", viewPortRay);
Graphics.Blit(src, dest, postEffectMaterial);
}
}
# in vertex shader
v2f vertex_depth(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy;
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;
}
o.interpolateRay = _FrustumCornerRay[index];
return o;
}
# in fragment shader
fixed4 frag_depth(v2f i) : SV_Target
{
float4 color = tex2D(_MainTex, i.uv);
float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
float linear01Depth = Linear01Depth(depthTextureValue);
float3 worldPos = _WorldSpaceCameraPos + linear01Depth * i.interpolateRay.xyz;
return fixed4(worldPos, 1.0);
}
由于是在顶点着色器计算线段的差值,所有效率会比逐像素计算要高
为了方便验证计算结果,可以在场景中简单摆放几个物体,使用简单的材质输出世界坐标作为颜色,然后在 camera 组件上添加屏幕后处理的脚本绘制世界坐标,开启 / 禁用 camera 上的后处理就可以对比计算结果
默认相机
使用第一种方式计算
使用第二种方式计算
相机设置