Jim's GameDev Blog

通过深度图重建坐标

2016-12-6

从 Depth-Map 重建坐标的两个方法:

重建相机空间坐标

从 Normalized Device Coordinates(NDC)转换到 Camera Space,其实就是一个矩阵的乘法,简单来说只需要将 NDC 坐标和 Projection Matrix 的逆相乘即可得到 Camera Space 的坐标。坐标重建的过程一般会在 Fragment 中进行,这样会产生大量的计算消耗,为了减少这些消耗,可以使用一种取巧的方法来避免。

\[\begin{bmatrix} {cot{\theta \over 2} \over Aspect} & 0 & 0 & 0 \\ 0 & cot{\theta \over 2} & 0 & 0 \\ 0 & 0 & -{f+n \over f-n} & -{2nf \over f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix}\]

从 Projection Matrix 可以看出,Camera Space 的坐标的 xy 分量在变换的时候,只有第一行和第二行对角线上的两个值参与了(因为气他值是0,可以不考虑),也就是 ProjMat[0][0]、ProjMat[1][1]。所以完全没必要求 Projection Matrix 的逆矩阵。下面我们先正向推导一遍,即从 Camera Space 转换到 NDC。

Matrix4x4 projectionMatrix = camera.projectionMatrix;
Vector3 posInCamSpace = myPos;
Vector3 posInNDC = projectionMatrix.MultiplyPoint(posInCamSpace);

MultiplyPoint 这个方法是引擎封装好了的,具体的实现是这样的:

public Vector3 MultiplyPoint(Matrix4x4 mat, Vector3 v)
{
    Vector3 result;
    result.x = mat.m00 * v.x + mat.m01 * v.y + mat.m02 * v.z + mat.m03;
    result.y = mat.m10 * v.x + mat.m11 * v.y + mat.m12 * v.z + mat.m13;
    result.z = mat.m20 * v.x + mat.m21 * v.y + mat.m22 * v.z + mat.m23;
    float num = mat.m30 * v.x + mat.m31 * v.y + mat.m32 * v.z + mat.m33;
    num = 1 / num;
    result.x *= num;
    result.y *= num;
    result.z *= num;
    return result;
}

下面我们将所有相乘后为 0 的值去掉,并且去除 z 分量(因为 z 分量的值可以直接从 Depth-Map 中获取,不需要重建),最后简化为:

public Vector3 MultiplyPoint(Matrix4x4 mat, Vector3 v)
{
    Vector3 result;
    result.x = mat.m00 * v.x;
    result.y = mat.m11 * v.y;
    float num = mat.m32 * v.z;
    num = 1 / num;
    result.x *= num;
    result.y *= num;
    return result;
}

要做这个的逆运算就很方便了:

posInCamSpace.x = posInNDC.x * zLinearEyeDepth / ProjMat[0][0];
posInCamSpace.y = posInNDC.y * zLinearEyeDepth / ProjMat[1][1];

在 Shader 中写为:

float2 proj0011 = float2(unity_CameraProjection[0][0], unity_CameraProjection[1][1]);
posInCamSpace.xy = posInNDC.xy * zLinearEyeDepth / proj0011;

优化为:

posInCamSpace.xy = poseInNDC.xy * _OneOverProj0011 * zLinearEyeDepth;

重建世界空间坐标

img

如图中的相机视锥体,首先求出蓝色的四条向量,这四条蓝色向量通过顶点传入 vertex shader,再通过 fragment shader 进行插值,插值后的效果入下图:

img

从中挑选出一个向量来说明,如何计算:

img img

顶视图

从图中可以很明显的推导出:

\[ {YellowL \over YellowL + GreenL} = {\overrightarrow{BlackV} \over \overrightarrow{BlackV} + \overrightarrow{BlueV}} \]

又因为:

\[ YellowL + GreenL = 1 \]

所以最终简化为:

\[ \overrightarrow{BlackV} = YellowL * \left( \overrightarrow{BlackV} + \overrightarrow{BlueV} \right) \]

其中,$$$ YellowL $$$ 就是从 Depth-Map 中取出的值(01 空间),$$$ \overrightarrow{BlackV}+\overrightarrow{BlueV} $$$ 就是四条蓝色向量经过 fragment shader 插值后的向量。所以:

\[\begin{align} \overrightarrow{BlackV} &= YellowL * \overrightarrow{interpolatedRay} \\ wPos &= camWPos + \overrightarrow{BlackV} \end{align}\]

还有一种类似的方法,Unity Shader入门精要 中所使用的。基本思想与这里介绍的类似,差异在于求四条蓝色向量以及 Depth-Map 采样后没有线性化到 01 空间。