1. 程式人生 > >OGL(教程23)——陰影對映1

OGL(教程23)——陰影對映1

原文地址:http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html

背景知識:
陰影的概念和光照的概念是密不可分的,因為你需要一個燈來釋放陰影。有很多方式來產生陰影,在接下來的兩節中我們將會學習最基礎的最簡單的一個——陰影對映。

當到達了光柵化和陰影的時候,你會問自己——這個畫素是否在陰影中。讓我們換個方法——從光源到畫素的路徑是否經過其他物體?如果是——這個畫素則在陰影中。這裡假設其他物體不是透明的。如果不是——這個畫素即不在陰影中。這種方式,此問題和之前的章節中的問題很類似——怎樣確定看到近的物體,當一個物體和另外一個物體重疊時。如果我們把攝像機放在光源的起點,兩個問題變為了一個。我們希望距離遠的深度測試時失敗(如果此畫素比另外一個畫素要遠),那麼此畫素在陰影中。只有那些深度測試通過的畫素,才在光源中。這個是光源直接照射,路徑上沒有其他物體。這既是陰影對映的實質。

所以看起來像深度測試能幫著我們決定一個畫素是否在陰影中,但是存在一個問題。攝像機和光源並不總是處在同一個位置。深度測試通常是用來從攝像機的角度來解決可見性問題。所以當光源處在很遠的位置,如何利用攝像機來解決陰影偵測。解決的方案是,渲染場景兩次。第一次是從光源的位置。渲染的結果不會到達顏色緩衝,但是最近點的深度值會儲存在應用程式建立的深度緩衝中(這個深度緩衝不是由GLUT自動建立的)。第二次繪製,是通常所謂的在攝像機位置渲染場景。我們建立的深度緩衝被繫結到片段著色器以備讀取。對於每個畫素我們都從深度緩衝中讀取深度資訊,我們同樣計算從光源位置處的深度資訊。某些情況下,這兩個深度資訊是相同的。這種情況下,此畫素離光源最近,所以它的深度值寫入了深度緩衝。如果是這樣的話,那麼此畫素在光源中,將會被正常著色。如果深度值是不同的,那麼說明從光源的位置來看,有其他的畫素覆蓋了此畫素。在這種情況下,我們在顏色計算的時候新增影子係數,來模擬影子效果。看下圖:
在這裡插入圖片描述

我們的場景由兩個物體組成——平面和立方體。光源處在左上角的位置,照射著立方體。首先,我們從光源的位置渲染場景。注意點A、B和C。當渲染到B點的時候,它的深度值寫入到了深度緩衝。這是因為在光源和B點之間沒有其他的東西。所以此時B點就是距離光源最近的點。但是A和C點就需要進行比較,到底誰會寫入深度緩衝呢。由於兩個點在同一條直線上,所以光柵化之後,兩個點在螢幕的同一個位置。深度測試的時候C點戰勝了A點,C的深度值寫入深度緩衝。

然後,從攝像機的角度渲染表面和立方體。在光照shader中做所有的事情之外,我們也要計算畫素到光源的距離,然後和深度緩衝比較。當光柵化點B的時候,這兩個值近乎相等(有些略微的不同是因為浮點數插值計算的時候精度問題)。因此,我們偵測B點不在陰影之內。當光柵化A點的時候,從深度緩衝去除的深度值遠遠小於A點到光源的距離。因此我們偵測A點在陰影之內,所以要加入陰影係數處理,以使它變得暗一些。

這就是陰影對映演算法的原理(第一次從光源渲染場景叫做陰影對映)。我們將會分兩個步驟學習它。本節我們將學習如何渲染到陰影對映。渲染某個東西,如深度、顏色等,到一個應用建立的貼圖,這個過程叫做渲染到紋理。我們將會使用我們已經熟悉的一個簡單的紋理對映技術來把這個陰影對映展示在螢幕上。這是一個很好的除錯陰影是否正確的過程。下一節,我們將會看到如何使用陰影對映圖來判斷畫素是否在陰影中。

本節的原始碼中,包含一個四邊形網格,用來展示陰影對映圖。四邊形由兩個三角形組成,紋理座標也被設定用來覆蓋整個貼圖空間。當四邊形被渲染的時候,紋理座標被光柵器插值,於是整個紋理會顯示在螢幕上。

程式碼註釋:

(shadow_map_fbo.h:50)

class ShadowMapFBO
{
    public:
        ShadowMapFBO();

        ~ShadowMapFBO();

        bool Init(unsigned int WindowWidth, unsigned int WindowHeight);

        void BindForWriting();

        void BindForReading(GLenum TextureUnit);

    private:
        GLuint m_fbo;
        GLuint m_shadowMap;
};

OpenGL中的3D渲染管線的最終的結果是一個叫做幀緩衝物件的東東,FBO(framebuffer object)。
幀緩衝物件可以用於顏色緩衝、深度緩衝還有其他的額外的緩衝。當glutInitDisplayMode()被呼叫的時候,他會根據指定的引數建立預設的幀緩衝物件。幀緩衝是由作業系統管理的,而且不能被OpenGL刪除。處理預設的幀緩衝外,應用程式可以建立自己的FBO物件。這些物件可以在應用程式控制下,來應用各種各樣的技術。ShadowMapFBO 封裝了FBO物件相關的簡單介面,用來實現陰影對映。在這個類中,包含兩個OpenGL的控制代碼。m_fbo代表事實上FBO。FBO物件包含了幀快取的所有狀態。一旦這個對下被建立,且被正確配置,我們就可以很容易的繫結到一個不同的物件。注意到僅僅只有預設的幀快取 可以被用來展示東西到螢幕。由應用程式建立的幀緩衝只能用於離屏渲染。這個是中間的渲染通道,比如我們的陰影對映緩衝,然後才會被用於真實渲染。

就本身而言,幀緩衝物件只是一個佔位符。為了讓其能夠有用,我們需要附加貼圖到一個或者多個可以用的附著點。貼圖包含實際的幀緩衝儲存空間。OpenGL定義下面的附著點:

  1. COLOR_ATTACHMENTi——貼圖繫結到這個型別,將會接收來自片段著色器的顏色。i表面可以有多個貼圖同時繫結到顏色附著點。這個機制可以在片段著色器中開啟同時渲染多個顏色緩衝。
  2. DEPTH_ATTACHMENT——貼圖繫結到這個型別,將會接收的是深度測試。
  3. STENCIL_ATTACHMENT——貼圖繫結到這個型別,將會稱當模板緩衝。模板緩衝限制了光柵化的區域,而且可以用於很多其他技術。
  4. DEPTH_STENCIL_ATTACHMENT——這個是深度和模板的結合,因為這兩個經常會同時使用。
    對於陰影對映技術,我們只需要深度緩衝。成員m_shadowMap是一個貼圖的控制代碼,他將會被繫結到DEPTH_ATTACHMENT附著點。ShadowMapFBO提供了一些方法,他們將會在主迴圈中使用。我們將會在渲染陰影對映之前呼叫BindForWriting(),而在第二次渲染的時候,呼叫BindForReading()。
(shadow_map_fbo.cpp:43)

glGenFramebuffers(1, &m_fbo);

這裡我們建立了FBO。和貼圖和緩衝一樣,我們指定了GLuints陣列的地址和大小。此陣列用控制代碼填充。

shadow_map_fbo.cpp:46)

glGenTextures(1, &m_shadowMap);
glBindTexture(GL_TEXTURE_2D, m_shadowMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

接著,我們建立了貼圖,稱當的是陰影對映圖。通常,這個是標準的2D貼圖。

  1. 內部格式是GL_DEPTH_COMPONENT。這個和之前用到的函式不同,它的內部顏色型別是GL_RGB。GL_DEPTH_COMPONENT 意味著一個單精度的浮點數,它代表了標準化的深度。
  2. glTexImage2D 最後一個引數是空。這就意味著我們不提供任何資料初始化緩衝。這個意味著,我們想要每幀都包含深度資訊值,每幀都有些不同。每當我們想開始新的幀的時候,我們會呼叫glClear()來清空緩衝。
  3. 我們告訴OpenGL,當貼圖座標超出邊界的時候,限制在[0,1]區間。
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);

我們已經建立了一個FBO,還有一個貼圖物件,它已經配置好引數以備陰影映射了。仙子我們需要把貼圖物件和FBO繫結。第一件事情,我們需要繫結FBO。這個會使當前和將來的操作都應用於FBO物件。這個函式接收FBO控制代碼,和指定的目標。目標可以是GL_FRAMEBUFFER,GL_DRAW_FRAMEBUFFER ,GL_READ_FRAMEBUFFER。GL_READ_FRAMEBUFFER是在想從FBO中讀取畫素的時候使用,函式是glReadPixels。GL_DRAW_FRAMEBUFFER是當想渲染到FBO時使用。GL_FRAMEBUFFER 具備讀寫兩個功能,建議使用這個型別初始化FBO物件。當我們會在真正開始渲染的時候使用GL_DRAW_FRAMEBUFFER。

(shadow_map_fbo.cpp:55)

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap, 0);

這裡我們繫結陰影對映圖到FBO的深度緩衝附著點。最後一個引數是揭示了沒有mipmap層級被使用。mipmapping是貼圖對映特性,它代表不同的解析度,從高解析度(其mipmap=0),到其他低解析度(1-N)。這裡我們僅僅使用高解析度即可。第四個引數是陰影對映圖的控制代碼,如果這裡是0,那麼當前的貼圖就會和指定的附著點解耦(比如上面的深度緩衝)。

(shadow_map_fbo.cpp:58)

glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);

由於我們不想渲染顏色緩衝,只是深度資訊。我們只需要呼叫上面兩句程式碼即可。預設情況,顏色緩衝目標是被置為GL_COLOR_ATTACHMENT0,但是我們FBO不包含顏色緩衝。因此,最好告訴OpenGL我們真正的目的。有效的引數還可以是GL_NONE、GL_COLOR_ATTACHMENT0 到GL_COLOR_ATTACHMENTm ,這裡m是GL_MAX_COLOR_ATTACHMENTS -1。這些引數只對FBO物件有效。如果預設的幀緩衝使用引數GL_NONE,GL_FRONT_LEFT,GL_FRONT_RIGHT,GL_BACK_LEFT ,GL_BACK_RIGHT。這些允許你繪製前後左右到緩衝。我們把讀取緩衝設定為GL_NONE,記住我們不打算呼叫glReadPixel函式。這個主要是避免支援OpenGL3.x和OpenGL4.x的GPU會出現的問題。

(shadow_map_fbo.cpp:61)

GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);

if (Status != GL_FRAMEBUFFER_COMPLETE) {
    printf("FB error, status: 0x%x\n", Status);
    return false;
}

當配置FBO之後,最重要的事情是驗證OpenGL的狀態是否為完成。如果有錯誤要檢視。

(shadow_map_fbo.cpp:72)

void ShadowMapFBO::BindForWriting()
{
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
}

我們要渲染到陰影對映和渲染到預設緩衝之間做切換。第二次渲染的時候,我們將會繫結陰影對映。上面的函式在第一次渲染之前呼叫。

(shadow_map_fbo.cpp:78)

void ShadowMapFBO::BindForReading(GLenum TextureUnit)
{
    glActiveTexture(TextureUnit);
    glBindTexture(GL_TEXTURE_2D, m_shadowMap);
}

上面的函式在第二次渲染之前呼叫。注意到,我們綁定了貼圖物件,而不是FBO本身。這個函式接收了貼圖單元。貼圖單元必須和shader中的sampler2D統一變數保持一致。很重要的一點是,當glActiveTexture 接收的引數是索引列舉的時候,如GL_TEXTURE0, GL_TEXTURE1等等。shader需要的僅僅是0和1,等等。

(shadow_map.vs)

#version 330

layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;

uniform mat4 gWVP;

out vec2 TexCoordOut;

void main()
{
    gl_Position = gWVP * vec4(Position, 1.0);
    TexCoordOut = TexCoord;
}

我們將會對兩個渲染通道使用同樣的shader。頂點著色器兩個通道都會使用,但是片段著色器只有第二個通道使用。由於我們第一次渲染的時候,關閉了顏色緩衝,所以片段著色器將不會被使用。上面的頂點著色器很容易。它產生了裁剪空間座標。第一次渲染,貼圖座標是冗餘的,因為沒有片段著色器。但是,但是這個不影響,因為要公用頂點著色器。正如你看到的,從shader的角度,這裡的z通過或者不通過不再重要。兩次呼叫的不同之處在於,第一次渲染,傳遞的是光源視角的wvp矩陣,而第二次渲染是攝像機視角的wvp矩陣。第一次渲染,z被距離光源最近的深度值填充。而第二次渲染是被距離攝像機最近的點的深度值填充。第二次渲染,我們需要紋理座標,因為片段著色器需要從陰影對映圖取樣。

(shadow_map.fs)

#version 330

in vec2 TexCoordOut;
uniform sampler2D gShadowMap;

out vec4 FragColor;

void main()
{
    float Depth = texture(gShadowMap, TexCoordOut).x;
    Depth = 1.0 - (1.0 - Depth) * 25.0;
    FragColor = vec4(Depth);
}

這個是片段著色器,用來展示渲染通道的陰影對映圖。2D紋理座標用來從陰影對映圖中取樣。陰影對映圖是通過GL_DEPTH_COMPONENT 型別建立的內部格式。這意味著基本紋理單元是單精度浮點數,而不是顏色。這就是為什麼在取樣的時候使用.x的原因。透視矩陣有一個知道的行為,就是它把距離攝像機更近的點對映到[0,1]範圍。理論上z要高精度,因為距離攝像機越近,誤差越明顯。