unity 从深度纹理重建世界坐标

根据深度纹理获取世界坐标很常用,可以实现很多有用的功能,比如简单的雾 / 扫描线等

主要参考

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;

书中使用的是近裁平面推导,好像使用原裁平面更好推导
总结起来就是

  1. 先计算相机到屏幕四个角的射线
  2. 在顶点着色器中根据 uv 的值选择对应的射线
  3. 在像素着色器中计算世界坐标
// 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 上的后处理就可以对比计算结果


默认相机
使用第一种方式计算
使用第二种方式计算
相机设置
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容