[OpenGL] 根據深度圖重建世界座標的兩種方式

        這個計算是比較常用的,寫延遲渲染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;
// .....
}

        

        最終,打印一下世界空間的座標,根據顏色可以驗證和真實世界座標基本是吻合的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章