[OpenGL] 根据深度图重建世界坐标的两种方式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ZJU_fish1996/article/details/87211119

        这个计算是比较常用的,写延迟渲染demo的文章已经做过一次推导,这里再单独拿出来介绍一下。

方法一 :使用透视变换后的非线性深度

        经过透视变换,我们将得到非线性深度。这个深度实际上是一个伪深度,因为透视变换后的深度是一个定值,处于近裁剪面处,为了存储这么一个定值浪费一个8位的空间是没有必要的。所以该深度是为作它用特别存储的一个非线性深度。

        由于OpenGL硬件会自动做深度测试的操作,所以我们可以直接使用深度缓存来完成我们的操作。在一些不支持RTT(渲染到目标)的硬件上可以采用这种方法。当然,我们也可以自己将深度写入纹理。

        记录非线性深度

        以下是写入非线性深度纹理的代码,我们将深度存储在NormalAndDepth纹理的alpha通道。

        vertex shader

#version 450 core

uniform mat4 ModelMatrix;
uniform mat4 IT_ModelMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ProjectMatrix;

attribute vec4 a_position;
attribute vec3 a_normal;

varying vec3 v_normal;
varying vec2 v_depth;

void main()
{
    gl_Position = ModelMatrix * a_position;
    gl_Position = ViewMatrix * gl_Position;
    gl_Position = ProjectMatrix * gl_Position;

    v_depth = gl_Position.zw;
    mat3 M = mat3(IT_ModelMatrix[0].xyz, IT_ModelMatrix[1].xyz, IT_ModelMatrix[2].xyz);
    v_normal = M * a_normal;
}

        fragment shader

#version 450 core

uniform float zFar;

varying vec3 v_normal;
varying vec2 v_depth;

layout(location = 0) out vec4 NormalAndDepth;

void main(void)
{
    NormalAndDepth.xyz = (normalize(v_normal) + 1)/2;
    NormalAndDepth.w = (v_depth.x/v_depth.y + 1)/2);
}

        此处,经过透视变换后,z的取值范围是[-1, 1]  (准确来说是那些没有被裁剪的像素的取值范围) 。为了将其写入纹理,我们把它转换为[0, 1]之间的值。

        用非线性深度求解世界坐标

        为了求解像素的世界坐标,我们先来确定当前像素投影变换后的坐标。

        首先,我们通过解码深度纹理中记录的w,可以得到投影空间下的pos.z = 2 * w - 1 ...... (1) 

        由于投影变换直接投射到了近裁剪面上(即屏幕),我们可以直接使用屏幕坐标来得到投影空间的x,y坐标。

        又由于我们将场景直接绘制到了一张面片上,该面片的uv坐标如下分布,取值范围为[0,1],而投影空间x,y坐标取值范围为[-1,1],所以我们做一个简单的线性映射就能得到投影后的x,y坐标:

         

        pos.x = v_texcoord.x * 2 - 1 ...... (2)

        pos.y = v_texcoord.y * 2 - 1 ...... (3)

        计算出了投影空间的坐标后,我们只需乘上视图投影矩阵的逆矩阵,就能还原世界坐标了。

        以下为利用非线性深度求解世界坐标的代码:


vec3 ComputeWorldPos(float depth)
{
    vec4 pos;
    pos.w = 1;
    pos.z = depth * 2 - 1;
    pos.x = v_texcoord.x * 2 - 1;
    pos.y = v_texcoord.y * 2 - 1;
 
    vec4 worldPos = Inverse_ViewProjMatrix * pos;
    return worldPos .xyz/worldPos .w;
}
 
void main(void)
{
    float pixelDepth = texture2D(NormalAndDepth, v_texcoord).w;
    vec3 worldPos = ComputeWorldPos(pixelDepth);
    
    // ...
}

方法二:使用视图变换后的线性深度

        在经过视图变换后,我们得到的深度仍然是线性深度,所以我们也可以利用这一数据进行世界坐标的计算。一般而言,如果我们方便自定义深度纹理的话,更推荐这种做法,因为在线性的深度下,世界坐标的分布比较均匀,在光照计算上,离视点较远的地方不容易出现走样,在表现上会更好。

        记录线性深度

        以下是写入线性深度纹理的代码,我们将深度存储在NormalAndDepth纹理的alpha通道。

        vertex shader

#version 450 core

uniform mat4 ModelMatrix;
uniform mat4 IT_ModelMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ProjectMatrix;

attribute vec4 a_position;
attribute vec3 a_normal;

varying vec3 v_normal;
varying vec2 v_depth;

void main()
{
    gl_Position = ModelMatrix * a_position;
    gl_Position = ViewMatrix * gl_Position;

    v_depth = gl_Position.zw;
    gl_Position = ProjectMatrix * gl_Position;

    mat3 M = mat3(IT_ModelMatrix[0].xyz, IT_ModelMatrix[1].xyz, IT_ModelMatrix[2].xyz);
    v_normal = M * a_normal;
}

         fragment shader

#version 450 core

uniform float zFar;

varying vec3 v_normal;
varying vec2 v_depth;
layout(location = 0) out vec4 NormalAndDepth;

void main(void)
{
    NormalAndDepth.xyz = (normalize(v_normal) + 1)/2;
    NormalAndDepth.w = -v_depth.x / v_depth.y / zFar;
}

        关于以上代码,有几点要注意的地方。

        (1) 为了能够将深度存储在纹理中,我们需要将其归一化。此处采取的方式为视图空间的z值除以远裁剪面zFar。当然,如果此处我们使用(z - zNear) / (zFar - zNear),得到的结果在[0,1]区间会分布得更均匀,具体的计算方式可以 自己选择。

        (2) 我们对z值做了一个相反数的操作,这是因为我们是基于OpenGL右手坐标系来构建相机坐标的。此时,相机坐标系的z轴与视线方向平行,但与视线方向相反,也就是由观察点指向眼睛。此时,处在视线中的物体,相机坐标系下的z轴为负数。为了压缩到[0,1]区间,需要做取反操作。

        用线性深度求解世界坐标(方法一)

        

        在这里,我们需要用到视锥体进行计算,如上图,我们需要求解点p的世界坐标。zNear、zFar、fov的概念如图所示,特别需要注意的是,fov是竖直方向的张角,而并非水平方向的张角,因此上图演示的为视锥体的纵切面。

        此外,我们还使用了一个参数,aspect。它在视锥体中的含义为宽高比(aspect = w / h)。

        接下来,我们以y坐标的推导为例子:

        

        首先,我们把之前编码的深度w解码,可以直接得到视图变换后的z值: z = - w * zFar...... (1) 

        假设图中红线部分的长度为t, 可得 t = |z| * tan (fov/2) ......(2)

        有一个显然的结论是,紫色点在相机空间下y坐标为0,在它左边的为负数,右边的为正数。那么我们想要求点p的y坐标的话,只需要求蓝色线段占红色线段的百分比ratio。由相似三角形可得它等价于近裁剪面上,屏幕空间坐标y到屏幕中心 / 屏幕的一半。由于我们最终把场景绘制在了一个面片上,屏幕空间坐标y我们可以用texcoord.y来表示。

        ratio = (texcoord.y - 0.5) / 0.5 = 2 * texcoord.y - 1 ...... (3)

        p.y = t * ratio = |z| * tan(fov/2) * (2 * texcoord.y - 1) = w * zFar *  tan(fov/2) * (2 * texcoord.y - 1) ...... (4)

        自此,在式(4)中,我们推导出了相机空间下的y坐标。同理,可以推导出x坐标。

        得到相机空间下的x,y,z坐标后,再除以视图矩阵的逆,即可得到世界坐标。

        最终,我们得到的计算世界坐标的代码如下:

vec3 ComputeWorldPos(float depth)
{
    vec4 pos;
    pos.z = -depth;
    pos.w = 1;
    float x = 2 * v_texcoord.x - 1;
    float y = 2 * v_texcoord.y - 1;
    pos.x = tan(fov/2) * x * depth * aspect;
    pos.y = tan(fov/2) * y * depth;

    vec4 worldPos = Inverse_ViewMatrix * pos;
    return worldPos.xyz / worldPos.w;
}

void main(void)
{
    vec4 result = texture2D(NormalAndDepth, v_texcoord);
    float depth = result.w * zFar;
    vec3 worldPos = ComputeWorldPos(depth);

    // ...
}

        用线性深度求解世界坐标(方法二) 

       此外,我还在网上看到了一个更一般的解法,它的优点是利用了顶点的硬件插值,使得片元着色器的计算量大大减少。

       基本思路是:在顶点着色器中,记录远裁剪面处,对应屏幕四个点的相机空间的x,y坐标,此时在片元着色器中,由于硬件插值,我们就有了屏幕空间所有点对应的远裁剪面坐标。(也就是从视点发出的,经过当前屏幕点的射线在远裁剪面上的交点的坐标)。

       之后,我们使用这个值直接乘以深度纹理读取的w,就能直接得到当前像素点的相机空间坐标(利用相似三角形和相机空间坐标性质推导而出的)。

       以上仅是一个简单的描述,没有上述结论的具体推导,但推导并不难。对于这一方法,我也给出了代码实现,以供参考。

       vertex shader

#version 450 core
uniform float fov;
uniform float zFar;
uniform float aspect;
attribute vec4 a_position;
attribute vec2 a_texcoord;

varying vec2 v_texcoord;
varying vec2 farPlanePos;

void main()
{
    gl_Position = a_position;
    v_texcoord = a_texcoord;
    float t = tan(fov/2);
    farPlanePos.x = (v_texcoord.x * 2 - 1) * zFar * t * aspect;
    farPlanePos.y = (v_texcoord.y * 2 - 1) * zFar * t;
}

         fragment shader

// .....
varying vec2 farPlanePos;

void main(void)
{
    float pixelDepth = texture2D(NormalAndDepth, v_texcoord).w;   
    vec4 pos = vec4(vec3(farPlanePos.x, farPlanePos.y,-zFar) * pixelDepth , 1);
    vec4 ret = Inverse_ViewMatrix * pos;
    vec3 worldPos = ret.xyz / ret.w;
// .....
}

        

        最终,打印一下世界空间的坐标,根据颜色可以验证和真实世界坐标基本是吻合的。

猜你喜欢

转载自blog.csdn.net/ZJU_fish1996/article/details/87211119
今日推荐