OpenGL深入探索——廣告牌(Billboard)和幾何著色器
背景
在前面的幾章內容裡,我們已經認識了頂點著色器和片元著色器,但事實上,我們遺漏了一個很重要的著色器——幾何著色器(GS)。微軟在 DirectX10 中使用了這個著色器,後來被整合到了 OpenGL3.2 的核心中。VS 會對每一個頂點執行一次, FS 會對每一個片元執行一次,GS 則會對每一個基本圖元執行一次。也就是說,如果我們繪製一些三角形,那麼 GS 的每一次呼叫都會接收到一個三角形;如果我們繪製一些線,那麼 GS 的每一次呼叫都會接收到一個線,等等。這就給 GS 提供了一個看待模型的獨特的視野,開發人員可以知道頂點和頂點之間拓撲關係,並基於此開發一些新的技術。頂點著色器將一個頂點作為輸入,一個頂點作為輸出(或者說,它不可能憑空建立或者銷燬頂點),而對於傳遞到 GS 中的圖元,GS 有特殊的能力
- 改變新傳遞進來的圖元的拓撲結構。GS 可以接收任何拓撲型別的圖元,但是隻能輸出點列表、折線(line strip)和三角帶(triangle strips);
- GS 需要一個圖元作為輸入,在處理過程中他可以將這個圖元整個丟棄或者輸出一個或更多的圖元(也就是說它可以產生比它得到的更多或更少的頂點)。這個能力被叫做幾何增長(growing geometry)。這一章我們將會看看它有哪些優勢。
三角形列表中每三個頂點構成一組。比如頂點 0-2 形成第一個三角形,頂點 3-5 形成第二個三角形,依次進行。我們可以簡單地通過將頂點數除以 3(捨去餘數)來計算三角形的數量。三角帶的繪製更加高效,因為這個過程不是每新增 3 個頂點才繪製一個新三角形的,大多數時候我們只需要新增一個頂點就可以繪製出一個新三角形。構建第一個三角形,我們用三個頂點( 0-2 )。當我們新增第四個頂點的時候,就得到了第二個三角形,這個三角形是由頂點 1-3 構成的。當你新增第五個頂點的時候,你就會得到第三個三角形,這個三角形是由頂點 2-4 構成的。以此類推。因此,從第二個三角形開始,每新增一個新的頂點,就可以和先前的兩個頂點一起構建一個新的三角形,例如:
如你所見,9 個頂點就可以創建出 7 個三角形。如果這是三角形列表,我們只能建立 3 個三角形。
三角帶有一條關於三角形內部環繞順序的重要性質——奇數三角形的環繞順序是反向的。這就意味著如下的順序:[0,1,2],[1,3,2], [2,3,4], [3,5,4],以此類推。下面的圖片顯示了這個順序:
這下我們就理解了 GS 的主要原理了,讓我們來看一下它如何幫助我們實現一個非常有用並且廣受歡迎的技術—— billboard 。Billboard 是一個始終朝向相機的四邊形。當相機在場景中發生運動時,Billboard 也隨之轉動,因此,從 billboard 到相機的向量一直垂直於 billboard 的表面。這和我們真實世界中高速公路上的廣告牌是一個道理,總是儘可能地面朝行駛的車輛。一旦我們得到面向相機的四邊形,就很容易將怪獸、樹等等的圖片貼在這個四邊形之上了,並且建立大量朝相機的場景物體。森林需要大量的樹,我們常使用
Billboard 來建立森林,營造森林的效果。由於 billboard 上的紋理總是面朝相機,所以玩家誤以為所有物體都有真實的深度,而實際上呢,這些物體都僅僅是一個面而已。每一個 billboard 只需要四個頂點,因此它比實實在在製作出來的模型佔用的資源會少很多。
這一課中我們建立一個頂點緩衝區,並且在這個頂點快取中存放 billboard 在世界座標系中的位置,每一個位置僅僅是一個單獨的點( 3D 向量)。我們將這個點的資訊傳遞給幾何著色器,並用這些位置座標組建出一個四邊形。也就是說,輸入給 GS 的拓撲結構是點列表,而輸出的拓撲結構是三角帶。利用三角帶的原理,我們用 4 個頂點建立一個四邊形:
GS 負責對四邊形進行處理,使之始終朝向相機,同時他會為每個頂點生成合適的紋理座標。片元著色器只需要從紋理上取樣就能得到最後的顏色資訊。
我們來看一下如何使 billboard 一直朝向相機。在下面的這張圖片中,黑點代表相機,紅點代表 billboard 的位置。每一個點都在世界座標系下,雖然看起來就好像它們像是位於平行於 XZ 平面的一個平面上(當然也沒必要如此),實際上任意兩個點都行。
接下來我們建立一個從 billboard 指向相機的向量:
接下來我們新增(0,1,0)向量:
現在對這兩個向量做一個叉乘,結果是一個垂直於這兩個向量建立的平面的向量,我們可以通過這個向量對這個點進行擴充套件並創建出四邊形。這個四邊形將會垂直於從 billboard 到相機的向量,這就是我們想要的。由上面的情況可知,我們能得到以下內容(黃色的向量就是叉乘的結果):
有一件事情經常困擾開發人員,就是計算叉乘時的順序(A X B還是 B X A)。這兩種方法產生的向量是相向的。我們有必要提前知道向量的結果,因為我們需要輸出頂點,這樣從相機的方位來看,這兩個三角形生成的正方形才會是順時針的。這裡我們使用左手定則——如果你站在 billboard 的位置,你的食指指向相機的方位,中指指向天空,然後你的大拇指將會沿著“食指”和“中指”叉乘的結果的方向(兩個手指保持夾緊)。這一節,我們將叉乘的結果稱為“右”向量,因為從相機方位看我們手,是指向右側的。“中指”叉乘“食指”會產生一個“左”向量。 (我們使用左手定則,因為我們使用的是左手座標系(Z軸指向螢幕裡)。左手座標系非常合適)。
(billboard_list.h:27)
class BillboardList
{
public:
BillboardList();
~BillboardList();
bool Init(const std::string& TexFilename);
void Render(const Matrix4f& VP, const Vector3f& CameraPos);
private:
void CreatePositionBuffer();
GLuint m_VB;
Texture* m_pTexture;
BillboardTechnique m_technique;
};
BillboardList 類封裝了建立 billboard 的所需要的介面。Init()函式中包含將要貼在 billboard 上的紋理影象的檔名。Render()函式在主渲染迴圈中呼叫,並且負責 billboard 物件的渲染。這個函式需要兩個引數:檢視矩陣和投影矩陣的組合矩陣、相機在世界座標系下的位置。因為 billboard 就定義在世界座標系下,所有我們直接跳過了世界座標系的變換。這個類有三個私有屬性:頂點緩衝用來儲存 billboard 的位置,一個指標指向給 billboard 需要的紋理貼圖,一個 billboard technique 類物件中包含渲染相關的著色器程式。
(billboard_list.cpp:80)
void BillboardList::Render(const Matrix4f& VP, const Vector3f& CameraPos)
{
m_technique.Enable();
m_technique.SetVP(VP);
m_technique.SetCameraPosition(CameraPos);
m_pTexture->Bind(COLOR_TEXTURE_UNIT);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, m_VB);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vector3f), 0); // position
glDrawArrays(GL_POINTS, 0, NUM_ROWS * NUM_COLUMNS);
glDisableVertexAttribArray(0);
}
這個函式用於啟用 billboard technique 並對 billboard 物件進行渲染,在函式中首先設定了 OpenGL 的相關狀態,之後呼叫繪製函式對這些點進行繪製(這些點在經過幾何著色器之後會被裝換為一個正方形面)。在這個例子中,billboard 的位置嚴格地按照行列排列,這就是我們為什麼可以通過將其行列相乘來獲得點的數量。注意,我們在繪製的時候使用點模式(GL_POINTS)進行繪製,在幾何著色器中需要於此對應。
(billboard_technique.h:24)
class BillboardTechnique : public Technique
{
public:
BillboardTechnique();
virtual bool Init();
void SetVP(const Matrix4f& VP);
void SetCameraPosition(const Vector3f& Pos);
void SetColorTextureUnit(unsigned int TextureUnit);
private:
GLuint m_VPLocation;
GLuint m_cameraPosLocation;
GLuint m_colorMapLocation;
};
這就是 billboard technique 類的介面。它需要三個引數來完成這項工作:視口和投影矩陣的組合矩陣、世界座標系下相機的位置和 billboard 用到的紋理繫結的紋理單元的。
(billboard.vs)
#version 330
layout (location = 0) in vec3 Position;
void main()
{
gl_Position = vec4(Position, 1.0);
}
這是 billboard 渲染的頂點著色器,由於大部分工作都會在 GS 中完成, 所以 VS 中所做的工作非常簡單,而且由於頂點快取中只有位置向量,而且這個位置向量同時也是定義在世界座標系之下的,所有我們只需要將它們傳遞到 GS 中即可。
(billboard.gs:1)
#version 330
layout (points) in;
layout (triangle_strip) out;
layout (max_vertices = 4) out;
Billboard 技術的核心在 GS。首先我們使用 “layout” 關鍵詞宣告一些全域性變數。我們告訴管線輸入的圖元拓撲結構是點列表,輸出圖元的拓撲是三角帶。我們還告訴管線生成的頂點數不會多於四個頂點。這個關鍵詞是用來告訴驅動程式 GS 中會產生的頂點的最大數目。提前知道這個限度,使得驅動程式可以針對一些特殊場景對 GS 效能進行優化。因為我們知道最終我們會將一個點擴充套件成一個四邊形,所以我們聲明瞭最大定點數目是 4。
(billboard.gs:7)
uniform mat4 gVP;
uniform vec3 gCameraPos;
out vec2 TexCoord;
由於傳入 GS 中的點的位置資訊本來就是位於世界座標系下的,因此我們只需要一個 VP(view 和 projection)矩陣。它還需要一個相機位置來計算如何讓 billboard 朝向它。同時在 GS 中為 FS 生成了紋理座標,因此我們需要宣告一個紋理座標變數。
void main()
{
vec3 Pos = gl_in[0].gl_Position.xyz;
上面這一行是 GS 中特有的一部分。由於 GS 的執行時針對一個完整的圖元的,所以我們可以訪問這個圖元的所有頂點,這是通過內建變數 “gl_in” 做到的。這個變數是一個結構體陣列(陣列中每個元素都是一個結構體),陣列中每個元素都包含了寫入到 gl_position 中的位置資訊和其他一些從 VS 中輸出的資料。為了獲取某個頂點的資訊,我們可以使用這個頂點在圖元中的索引來得到。在這個特殊的例子中,輸入拓撲是點列表,因此每個圖元中只有一個單獨的點,我們使用 “gl_in[0]” 來獲取它。如果輸入拓撲是三角形,我們也可以寫成 “gl_in[1]” 或 “gl_in[2]” 。在這裡我們只需要位置向量的前三個分量,我們可以用 “.xyz” 方法獲取。
vec3 toCamera = normalize(gCameraPos - Pos);
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = cross(toCamera, up);
這裡我們使用在“背景”中介紹的技術使 billboard 在每一幀的渲染中都朝向相機。我們將從 billboard 到相機的向量與豎直向上的向量進行叉乘。當我們從相機看向 billboard 時,前面叉乘的結果是朝向右邊的。我們現在就藉助於這個向量,繞著 billboard 的位置來建立一個四邊形。
Pos -= (right * 0.5);
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(0.0, 0.0);
EmitVertex();
Pos.y += 1.0;
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(0.0, 1.0);
EmitVertex();
Pos.y -= 1.0;
Pos += right;
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(1.0, 0.0);
EmitVertex();
Pos.y += 1.0;
gl_Position = gVP * vec4(Pos, 1.0);
TexCoord = vec2(1.0, 1.0);
EmitVertex();
EndPrimitive();
}
頂點緩衝區中的存放的點的是四邊形底部的中心。我們需要通過這個點形成兩個面向相機的三角形。我們從四邊形左下角開始。首先我們通過前面計算出的“右”向量得到平面的左下角的點,之後我們通過視口矩陣和投影矩陣將其變換到裁剪座標系下,同時將這個頂點的紋理座標設定為(0,0),因為我們希望將紋理完整的貼在這個平面上。為了將新產生的頂點傳遞到管線的下一階段,我們呼叫內建函式 EmitVertex()。一旦我們呼叫了這個函式 gl_Position 變數中的資料就無效了,所以我們需要給其傳一個新值。常規的方法是,我們形成四邊形的左上角和右下角。這是第一個朝向相機的三角形。由於 GS 輸出的圖元的拓撲結構是三角帶,在構建第二個三角形的時候,我們只需要再多新增一個點。使用新頂點和前一個三角形的最後兩個頂點(這兩個頂點構成之後形成四邊形的對角線)構成了第二個三角形。這第四個頂點並且也是最後一個頂點是四邊形的右上角。為了終止三角帶,我們呼叫內建函式 EndPrimitive()。
(billboard.fs)
#version 330
uniform sampler2D gColorMap;
in vec2 TexCoord;
out vec4 FragColor;
void main()
{
FragColor = texture2D(gColorMap, TexCoord);
if (FragColor.r == 0 && FragColor.g == 0 && FragColor.b == 0)
{
discard;
}
}
FS 中的處理非常簡單的——它的大部分工作是藉助於 GS 中的紋理座標進行紋理取樣。這這裡我們使用了 OpenGL 中一個新的功能——內建關鍵字 “discard” ,在某些情況下,我們可以希望將某個片元完全丟棄,我們就可以使用這個關鍵字。這一課中使用的紋理影象是一個黑色背景下的怪物,理想情況下我們不希望我們渲染出來的 billboard 出現黑色的背景,所以我們對片元的顏色進行判斷——如果 billboard 的顏色是黑色,我們就丟棄這個畫素。這樣最終渲染出來的就只剩下怪物的影象了,試著將 “discard” 註釋掉,看看有什麼不同。
操作結果: