OGL(教程35)——延遲渲染1
原文地址:http://ogldev.atspace.co.uk/www/tutorial35/tutorial35.html
從17節開始,到現在對燈光的處理就是所謂的向前渲染或者向前著色。這是最直接的方式,我們在頂點著色器中對物體的各個頂點做變換(對法線進行變化,把位置變換到裁剪空間),然後再FS中對畫素進行光照計算。對於每個物件的每個畫素只能呼叫一次FS,我們需要對FS提供所有的光源資訊,然後在計算這個畫素的光照的時候全部考慮進去。這種方式簡單,但是也有自身的缺點。如果場景高度負責(比如當今流行的遊戲中),它有大量的物體,還有大量的深度複雜性(螢幕上的一個畫素會被多個物體覆蓋),這樣對此畫素的計算會浪費很多CPU。比如,如果深度複雜度為4,那就意味著對3個畫素的計算是浪費掉的,因為只有最上面的一個畫素才最終有效。我們可以對物體進行排序,但是對複雜的物體,其效果不是很理想。
向前渲染的另外一個問題是,在很多燈的情況下也會遇到問題。在這種情況下,光源對於很小的區域有效果,否則會承受不住壓力。但是我們的FS計算,是針對所有燈的,即使它離畫素很遠。你可以嘗試計算畫素和光源的位置,但是會額外增加計算量,還會增加FS的分支。向前渲染的FS部分,對多盞燈不能勝任。
延遲渲染是針對上面的問題,被當今很多遊戲所採用的流行方案。延遲渲染的一個核心是,把幾何計算(位置和法線計算)和光照計算解耦。並不是把所有的物體都一股腦的處理,從頂點緩衝到最終的幀緩衝,我們把它分為兩個主要的階段。第一個階段,我們執行VS,但是我們不把處理的屬性傳給FS以備光照計算使用,我們把它傳入到G Buffer中。GBuffer從邏輯上講是一組2D貼圖,每個貼圖對應一個頂點屬性。我們分離屬性,然後利用OpenGL所謂的MRT(多個渲染目標技術)一次繪製所有的屬性到多張貼圖。由於我們在FS中寫入屬性,那麼在GBuffer中的最終結果是被光柵化插值之後的頂點屬性。我們把這個階段叫做幾何階段。每個物件都在這個階段處理。由於深度測試,當幾何階段完成之後,出現在GBuffer中的,都是離攝像機最近的的點的差值之後的屬性。這就意味著所有無關的畫素深度測試失敗,然後被丟棄,只有留在GBuffer中的畫素才是最終會被計算光照的畫素。下面是典型的GBuffer的一幀圖:
在第二個階段,稱之為光照階段,我們遍歷GBuffer中的畫素,我們從不同的貼圖取樣畫素的屬性,然後像之前我們處理光照的方式計算光照。由於留下的畫素都是離攝像機最近的點,所以我們對一個畫素只會進行一次光照計算。
我們怎麼遍歷GBuffer中的畫素呢?最簡單的方式是渲染一個和螢幕大小相同的四邊形。但是還有更好的方法。之前說過,由於光源的影響範圍有限,我們假設很多畫素與它無關。當一個光源的對畫素的影響足夠小,最好忽略這個光源對其的影響,這也是從效能考慮。在向前渲染中沒有有效的方式處理。但是在延遲渲染中,我們可以計算光源影響的一個球體(對於點光源來說是這樣,對於聚光燈,我們使用cone角度)。球體代表了光源影響的區域,在球體之外,我們忽略光源的影響。我們可以用一個粗略的球體模型,它只有很少的幾個面,來表現那個點的光源所影響的範圍。VS只做位置的變換,把位置變換到裁剪空間。FS只會對相關的畫素進行處理,並對相關的畫素做光照計算。有些人會計算出最小的包含球體的四邊形,這個計算是從光源視角計算。渲染這個四邊形不耗時,因為只有兩個三角形而已。這個方法可有效限制計算畫素的個數。
我們將分為三步介紹延遲渲染,分為三個章節進行。
1、本節我們將會使用MRT技術填充GBuffer。我們會輸出到GBuffer到螢幕。
2、下一節,我們將會增加光照通道,在延遲渲染中看看光照如何計算。
3、最後一節,我們將會學習模板緩衝,來阻止距離較遠的點光源。
程式碼註釋:
Source walkthru
(gbuffer.h:28)
class GBuffer
{
public:
enum GBUFFER_TEXTURE_TYPE {
GBUFFER_TEXTURE_TYPE_POSITION,
GBUFFER_TEXTURE_TYPE_DIFFUSE,
GBUFFER_TEXTURE_TYPE_NORMAL,
GBUFFER_TEXTURE_TYPE_TEXCOORD,
GBUFFER_NUM_TEXTURES
};
GBuffer();
~GBuffer();
bool Init(unsigned int WindowWidth, unsigned int WindowHeight);
void BindForWriting();
void BindForReading();
private:
GLuint m_fbo;
GLuint m_textures[GBUFFER_NUM_TEXTURES];
GLuint m_depthTexture;
};
GBuffer誒包含所有延遲渲染階段需要的貼圖。我們有針對針對頂點屬性的貼圖,也有稱當深度緩衝的貼圖。我們之所以需要深度緩衝貼圖,因為我們將會在FBO中封裝所有的貼圖,所以預設的深度緩衝沒有用。FBO已經在23節講述,這裡不再提及。
GBuffer類有兩個方法,將會在執行時反覆呼叫。BindForWriting()繫結貼圖到目標,這個發生在幾何階段。BindForReading()繫結FBO作為輸入,所以它的結果可以輸出到螢幕。
(gbuffer.cpp:48)
bool GBuffer::Init(unsigned int WindowWidth, unsigned int WindowHeight)
{
// Create the FBO
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
// Create the gbuffer textures
glGenTextures(ARRAY_SIZE_IN_ELEMENTS(m_textures), m_textures);
glGenTextures(1, &m_depthTexture);
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_textures) ; i++) {
glBindTexture(GL_TEXTURE_2D, m_textures[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, WindowWidth, WindowHeight, 0, GL_RGB, GL_FLOAT, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, m_textures[i], 0);
}
// depth
glBindTexture(GL_TEXTURE_2D, m_depthTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT,
NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_depthTexture, 0);
GLenum DrawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 };
glDrawBuffers(ARRAY_SIZE_IN_ELEMENTS(DrawBuffers), DrawBuffers);
GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (Status != GL_FRAMEBUFFER_COMPLETE) {
printf("FB error, status: 0x%x\n", Status);
return false;
}
// restore default FBO
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
return true;
}
這個就是我們如何初始化GBuffer的。我們首先建立了FBO物件、還有各種貼圖,還有深度緩衝。頂點屬性貼圖在迴圈中進行初始化,迴圈體中做的是:
1、建立貼圖的儲存區域(沒有初始化它)
2、繫結貼圖到FBO作為目標
初始化深度貼圖被顯示呼叫,因為他有不同的格式,它被繫結到不同的FBO物件。
為了做MRT,我們需要寫入所有的貼圖。我們通過提供一個數組的附著地點給glDrawBuffers()函式。這個陣列允許我們有一些靈活性,如果我們把GL_COLOR_ATTACHMENT6 作為第一個索引,FS寫入的第一個輸出變數,此變數會繫結到GL_COLOR_ATTACHMENT6。本節我們不考慮效率,只是一個接一個的繫結。
最終,我們檢測FBO的狀態,確保所有的東西都是正確的額,然後重置預設的FBO(未來的變換不會影響到GBuffer)。此時GBuffer已經可以被使用了。
(tutorial35.cpp:105)
virtual void RenderSceneCB()
{
CalcFPS();
m_scale += 0.05f;
m_pGameCamera->OnRender();
DSGeometryPass();
DSLightPass();
RenderFPS();
glutSwapBuffers();
}
我們現在從上往下看執行過程。上面的函式主迴圈函式。它做的並不多。它處理一些全域性的事務,比如幀率計算,還有展示,攝像機的更新等等。主要的工作室執行幾何階段,接著是光照階段。如之前提到的,本節只處理幾何階段。
(tutorial35.cpp:122)
void DSGeometryPass()
{
m_DSGeomPassTech.Enable();
m_gbuffer.BindForWriting();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Pipeline p;
p.Scale(0.1f, 0.1f, 0.1f);
p.Rotate(0.0f, m_scale, 0.0f);
p.WorldPos(-0.8f, -1.0f, 12.0f);
p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
p.SetPerspectiveProj(m_persProjInfo);
m_DSGeomPassTech.SetWVP(p.GetWVPTrans());
m_DSGeomPassTech.SetWorldMatrix(p.GetWorldTrans());
m_mesh.Render();
}
我們開始幾何階段,是通過開啟合適的計算,然後設定GBuffer物件以備寫入。這樣之後,我們清除GBuffer。現在所有的東西都準備好了,我們開始變換,然後是渲染網格。在真實的遊戲中,我們會一個接一個渲染多個網格。最終我們會得到頂點的各個屬性。
(tutorial35.cpp:141)
void DSLightPass()
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
m_gbuffer.BindForReading();
GLsizei HalfWidth = (GLsizei)(WINDOW_WIDTH / 2.0f);
GLsizei HalfHeight = (GLsizei)(WINDOW_HEIGHT / 2.0f);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_POSITION);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
0, 0, HalfWidth, HalfHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_DIFFUSE);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
0, HalfHeight, HalfWidth, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_NORMAL);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
HalfWidth, HalfHeight, WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_TEXCOORD);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
HalfWidth, 0, WINDOW_WIDTH, HalfHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
(geometry_pass.vs)
#version 330
layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;
uniform mat4 gWVP;
uniform mat4 gWorld;
out vec2 TexCoord0;
out vec3 Normal0;
out vec3 WorldPos0;
void main()
{
gl_Position = gWVP * vec4(Position, 1.0);
TexCoord0 = TexCoord;
Normal0 = (gWorld * vec4(Normal, 0.0)).xyz;
WorldPos0 = (gWorld * vec4(Position, 1.0)).xyz;
}
#version 330
in vec2 TexCoord0;
in vec3 Normal0;
in vec3 WorldPos0;
layout (location = 0) out vec3 WorldPosOut;
layout (location = 1) out vec3 DiffuseOut;
layout (location = 2) out vec3 NormalOut;
layout (location = 3) out vec3 TexCoordOut;
uniform sampler2D gColorMap;
void main()
{
WorldPosOut = WorldPos0;
DiffuseOut = texture(gColorMap, TexCoord0).xyz;
NormalOut = normalize(Normal0);
TexCoordOut = vec3(TexCoord0, 0.0);
}