這一小節并不會向你展示非常先進非常酷的新特性,也不會對場景的視覺品質有顯著的提高。但是,這一節會或多或少涉及GLSL的一些有趣的地方以及一些很棒的技巧,它們可能在今後會幫助到你。簡單來說,它們就是在組合使用OpenGL和GLSL建立程式時的一些最好要知道的東西,和一些會讓你生活更加輕松的特性。
我們将會讨論一些有趣的内建變量(Built-in Variable),管理着色器輸入和輸出的新方式以及一個叫做Uniform緩沖對象(Uniform Buffer Object)的有用工具。
GLSL的内建變量
着色器都是最簡化的,如果需要目前着色器以外地方的資料的話,我們必須要将資料傳進來。我們已經學會使用頂點屬性、uniform和采樣器來完成這一任務了。然而,除此之外,GLSL還定義了另外幾個以
gl_
為字首的變量,它們能提供給我們更多的方式來讀取/寫入資料。我們已經在前面教程中接觸過其中的兩個了:頂點着色器的輸出向量gl_Position,和片段着色器的gl_FragCoord。
我們将會讨論幾個有趣的GLSL内建輸入和輸出變量,并會解釋它們能夠怎樣幫助你。注意,我們将不會讨論GLSL中存在的所有内建變量,如果你想知道所有的内建變量的話,請檢視OpenGL的wiki。
頂點着色器變量
我們已經見過gl_Position了,它是頂點着色器的裁剪空間輸出位置向量。如果你想在螢幕上顯示任何東西,在頂點着色器中設定gl_Position是必須的步驟。這已經是它的全部功能了。
gl_PointSize
我們能夠選用的其中一個圖元是GL_POINTS,如果使用它的話,每一個頂點都是一個圖元,都會被渲染為一個點。我們可以通過OpenGL的glPointSize函數來設定渲染出來的點的大小,但我們也可以在頂點着色器中修改這個值。
GLSL定義了一個叫做gl_PointSize輸出變量,它是一個float變量,你可以使用它來設定點的寬高(像素)。在頂點着色器中修改點的大小的話,你就能對每個頂點設定不同的值了。
在頂點着色器中修改點大小的功能預設是禁用的,如果你需要啟用它的話,你需要啟用OpenGL的GL_PROGRAM_POINT_SIZE:
glEnable(GL_PROGRAM_POINT_SIZE);
一個簡單的例子就是将點的大小設定為裁剪空間位置的z值,也就是頂點距觀察者的距離。點的大小會随着觀察者距頂點距離變遠而增大。
void main(){
gl_Position = projection * view * model * vec4(aPos, 1.0);
gl_PointSize = gl_Position.z;
}
結果就是,當我們遠離這些點的時候,它們會變得更大:

你可以想到,對每個頂點使用不同的點大小,會在粒子生成之類的技術中很有意思。
gl_VertexID
gl_Position和gl_PointSize都是輸出變量,因為它們的值是作為頂點着色器的輸出被讀取的。我們可以對它們進行寫入,來改變結果。頂點着色器還為我們提供了一個有趣的輸入變量,我們隻能對它進行讀取,它叫做gl_VertexID。
整型變量gl_VertexID儲存了正在繪制頂點的目前ID。當(使用glDrawElements)進行索引渲染的時候,這個變量會存儲正在繪制頂點的目前索引。當(使用glDrawArrays)不使用索引進行繪制的時候,這個變量會儲存從渲染調用開始的已處理頂點數量。
雖然現在它沒有什麼具體的用途,但知道我們能夠通路這個資訊總是好的。
片段着色器變量
在片段着色器中,我們也能通路到一些有趣的變量。GLSL提供給我們兩個有趣的輸入變量:gl_FragCoord和gl_FrontFacing。
gl_FragCoord
在讨論深度測試的時候,我們已經見過gl_FragCoord很多次了,因為gl_FragCoord的z分量等于對應片段的深度值。然而,我們也能使用它的x和y分量來實作一些有趣的效果。
gl_FragCoord的x和y分量是片段的視窗空間(Window-space)坐标,其原點為視窗的左下角。我們已經使用glViewport設定了一個800x600的視窗了,是以片段視窗空間坐标的x分量将在0到800之間,y分量在0到600之間。
通過利用片段着色器,我們可以根據片段的視窗坐标,計算出不同的顔色。gl_FragCoord的一個常見用處是用于對比不同片段計算的視覺輸出效果,這在技術示範中可以經常看到。比如說,我們能夠将螢幕分成兩部分,在視窗的左側渲染一種輸出,在視窗的右側渲染另一種輸出。下面這個例子片段着色器會根據視窗坐标輸出不同的顔色:
void main(){
if(gl_FragCoord.x < 400)
FragColor = vec4(1.0, 0.0, 0.0, 1.0); else
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
因為視窗的寬度是800。當一個像素的x坐标小于400時,它一定在視窗的左側,是以我們給它一個不同的顔色。
我們現在會計算出兩個完全不同的片段着色器結果,并将它們顯示在視窗的兩側。舉例來說,你可以将它用于測試不同的光照技巧。
gl_FrontFacing
片段着色器另外一個很有意思的輸入變量是gl_FrontFacing。在面剔除教程中,我們提到OpenGL能夠根據頂點的環繞順序來決定一個面是正向還是背向面。如果我們不(啟用GL_FACE_CULL來)使用面剔除,那麼gl_FrontFacing将會告訴我們目前片段是屬于正向面的一部分還是背向面的一部分。舉例來說,我們能夠對正向面計算出不同的顔色。
gl_FrontFacing變量是一個bool,如果目前片段是正向面的一部分那麼就是
true
,否則就是
false
。比如說,我們可以這樣子建立一個立方體,在内部和外部使用不同的紋理:
#version 330 coreout vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D frontTexture;
uniform sampler2D backTexture;void main(){
if(gl_FrontFacing)
FragColor = texture(frontTexture, TexCoords); else
FragColor = texture(backTexture, TexCoords);
}
如果我們往箱子裡面看,就能看到使用的是不同的紋理。
注意,如果你開啟了面剔除,你就看不到箱子内部的面了,是以現在再使用gl_FrontFacing就沒有意義了。
gl_FragDepth
輸入變量gl_FragCoord能讓我們讀取目前片段的視窗空間坐标,并擷取它的深度值,但是它是一個隻讀(Read-only)變量。我們不能修改片段的視窗空間坐标,但實際上修改片段的深度值還是可能的。GLSL提供給我們一個叫做gl_FragDepth的輸出變量,我們可以使用它來在着色器内設定片段的深度值。
要想設定深度值,我們直接寫入一個0.0到1.0之間的float值到輸出變量就可以了:
gl_FragDepth = 0.0; // 這個片段現在的深度值為 0.0
如果着色器沒有寫入值到gl_FragDepth,它會自動取用
gl_FragCoord.z
的值。
然而,由我們自己設定深度值有一個很大的缺點,隻要我們在片段着色器中對gl_FragDepth進行寫入,OpenGL就會(像深度測試小節中讨論的那樣)禁用所有的提前深度測試(Early Depth Testing)。它被禁用的原因是,OpenGL無法在片段着色器運作之前得知片段将擁有的深度值,因為片段着色器可能會完全修改這個深度值。
在寫入gl_FragDepth時,你就需要考慮到它所帶來的性能影響。然而,從OpenGL 4.2起,我們仍可以對兩者進行一定的調和,在片段着色器的頂部使用深度條件(Depth Condition)重新聲明gl_FragDepth變量:
layout (depth_<condition>) out float gl_FragDepth;
condition
可以為下面的值:
條件 | 描述 |
---|---|
| 預設值。提前深度測試是禁用的,你會損失很多性能 |
| 你隻能讓深度值比 更大 |
| 更小 |
| 如果你要寫入 ,你将隻能寫入 的值 |
通過将深度條件設定為
greater
或者
less
,OpenGL就能假設你隻會寫入比目前片段深度值更大或者更小的值了。這樣子的話,當深度值比片段的深度值要小的時候,OpenGL仍是能夠進行提前深度測試的。
下面這個例子中,我們對片段的深度值進行了遞增,但仍然也保留了一些提前深度測試:
#version 420 core // 注意GLSL的版本!out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;void main(){
FragColor = vec4(1.0);
gl_FragDepth = gl_FragCoord.z + 0.1;
}
注意這個特性隻在OpenGL 4.2版本或以上才提供。
接口塊
到目前為止,每當我們希望從頂點着色器向片段着色器發送資料時,我們都聲明了幾個對應的輸入/輸出變量。将它們一個一個聲明是着色器間發送資料最簡單的方式了,但當程式變得更大時,你希望發送的可能就不隻是幾個變量了,它還可能包括數組和結構體。
為了幫助我們管理這些變量,GLSL為我們提供了一個叫做接口塊(Interface Block)的東西,來友善我們組合這些變量。接口塊的聲明和struct的聲明有點相像,不同的是,現在根據它是一個輸入還是輸出塊(Block),使用in或out關鍵字來定義的。
#version 330 corelayout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out VS_OUT
{
vec2 TexCoords;
} vs_out;void main(){
gl_Position = projection * view * model * vec4(aPos, 1.0);
vs_out.TexCoords = aTexCoords;
}
這次我們聲明了一個叫做vs_out的接口塊,它打包了我們希望發送到下一個着色器中的所有輸出變量。這隻是一個很簡單的例子,但你可以想象一下,它能夠幫助你管理着色器的輸入和輸出。當我們希望将着色器的輸入或輸出打包為數組時,它也會非常有用,我們将在下一節讨論幾何着色器(Geometry Shader)時見到。
之後,我們還需要在下一個着色器,即片段着色器,中定義一個輸入接口塊。塊名(Block Name)應該是和着色器中一樣的(VS_OUT),但執行個體名(Instance Name)(頂點着色器中用的是vs_out)可以是随意的,但要避免使用誤導性的名稱,比如對實際上包含輸入變量的接口塊命名為vs_out。
#version 330 coreout vec4 FragColor;
in VS_OUT
{
vec2 TexCoords;
} fs_in;
uniform sampler2D texture;void main(){
FragColor = texture(texture, fs_in.TexCoords);
}
隻要兩個接口塊的名字一樣,它們對應的輸入和輸出将會比對起來。這是幫助你管理代碼的又一個有用特性,它在幾何着色器這樣穿插特定着色器階段的場景下會很有用。
Uniform緩沖對象
我們已經使用OpenGL很長時間了,學會了一些很酷的技巧,但也遇到了一些很麻煩的地方。比如說,當使用多餘一個的着色器時,盡管大部分的uniform變量都是相同的,我們還是需要不斷地設定它們,是以為什麼要這麼麻煩地重複設定它們呢?
OpenGL為我們提供了一個叫做Uniform緩沖對象(Uniform Buffer Object)的工具,它允許我們定義一系列在多個着色器中相同的全局Uniform變量。當使用Uniform緩沖對象的時候,我們隻需要設定相關的uniform一次。當然,我們仍需要手動設定每個着色器中不同的uniform。并且建立和配置Uniform緩沖對象會有一點繁瑣。
因為Uniform緩沖對象仍是一個緩沖,我們可以使用glGenBuffers來建立它,将它綁定到GL_UNIFORM_BUFFER緩沖目标,并将所有相關的uniform資料存入緩沖。在Uniform緩沖對象中儲存資料是有一些規則的,我們将會在之後讨論它。首先,我們将使用一個簡單的頂點着色器,将projection和view矩陣存儲到所謂的Uniform塊(Uniform Block)中:
#version 330 corelayout (location = 0) in vec3 aPos;
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;void main(){
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
在我們大多數的例子中,我們都會在每個渲染疊代中,對每個着色器設定projection和view Uniform矩陣。這是利用Uniform緩沖對象的一個非常完美的例子,因為現在我們隻需要存儲這些矩陣一次就可以了。
這裡,我們聲明了一個叫做Matrices的Uniform塊,它儲存了兩個4x4矩陣。Uniform塊中的變量可以直接通路,不需要加塊名作為字首。接下來,我們在OpenGL代碼中将這些矩陣值存入緩沖中,每個聲明了這個Uniform塊的着色器都能夠通路這些矩陣。
你現在可能會在想
layout (std140)
這個語句是什麼意思。它的意思是說,目前定義的Uniform塊對它的内容使用一個特定的記憶體布局。這個語句設定了Uniform塊布局(Uniform Block Layout)。
Uniform塊布局
Uniform塊的内容是儲存在一個緩沖對象中的,它實際上隻是一塊預留記憶體。因為這塊記憶體并不會儲存它具體儲存的是什麼類型的資料,我們還需要告訴OpenGL記憶體的哪一部分對應着着色器中的哪一個uniform變量。
假設着色器中有以下的這個Uniform塊:
layout (std140) uniform ExampleBlock
{ float value;
vec3 vector;
mat4 matrix; float values[3]; bool boolean; int integer;
};
我們需要知道的是每個變量的大小(位元組)和(從塊起始位置的)偏移量,來讓我們能夠按順序将它們放進緩沖中。每個元素的大小都是在OpenGL中有清楚地聲明的,而且直接對應C++資料類型,其中向量和矩陣都是大的float數組。OpenGL沒有聲明的是這些變量間的間距(Spacing)。這允許硬體能夠在它認為合适的位置放置變量。比如說,一些硬體可能會将一個vec3放置在float邊上。不是所有的硬體都能這樣處理,可能會在附加這個float之前,先将vec3填充(Pad)為一個4個float的數組。這個特性本身很棒,但是會對我們造成麻煩。
預設情況下,GLSL會使用一個叫做共享(Shared)布局的Uniform記憶體布局,共享是因為一旦硬體定義了偏移量,它們在多個程式中是共享并一緻的。使用共享布局時,GLSL是可以為了優化而對uniform變量的位置進行變動的,隻要變量的順序保持不變。因為我們無法知道每個uniform變量的偏移量,我們也就不知道如何準确地填充我們的Uniform緩沖了。我們能夠使用像是glGetUniformIndices這樣的函數來查詢這個資訊,但這超出本節的範圍了。
雖然共享布局給了我們很多節省空間的優化,但是我們需要查詢每個uniform變量的偏移量,這會産生非常多的工作量。通常的做法是,不使用共享布局,而是使用std140布局。std140布局聲明了每個變量的偏移量都是由一系列規則所決定的,這顯式地聲明了每個變量類型的記憶體布局。由于這是顯式提及的,我們可以手動計算出每個變量的偏移量。
每個變量都有一個基準對齊量(Base Alignment),它等于一個變量在Uniform塊中所占據的空間(包括填充量(Padding)),這個基準對齊量是使用std140布局的規則計算出來的。接下來,對每個變量,我們再計算它的對齊偏移量(Aligned Offset),它是一個變量從塊起始位置的位元組偏移量。一個變量的對齊位元組偏移量必須等于基準對齊量的倍數。
布局規則的原文可以在OpenGL的Uniform緩沖規範這裡找到,但我們将會在下面列出最常見的規則。GLSL中的每個變量,比如說int、float和bool,都被定義為4位元組量。每4個位元組将會用一個
N
來表示。
類型 | 布局規則 |
---|---|
标量,比如int和bool | 每個标量的基準對齊量為N。 |
向量 | 2N或者4N。這意味着vec3的基準對齊量為4N。 |
标量或向量的數組 | 每個元素的基準對齊量與vec4的相同。 |
矩陣 | 儲存為列向量的數組,每個向量的基準對齊量與vec4的相同。 |
結構體 | 等于所有元素根據規則計算後的大小,但會填充到vec4大小的倍數。 |
和OpenGL大多數的規範一樣,使用例子就能更容易地了解。我們會使用之前引入的那個叫做ExampleBlock的Uniform塊,并使用std140布局計算出每個成員的對齊偏移量:
layout (std140) uniform ExampleBlock
{ // 基準對齊量 // 對齊偏移量
float value; // 4 // 0
vec3 vector; // 16 // 16 (必須是16的倍數,是以 4->16)
mat4 matrix; // 16 // 32 (列 0)
// 16 // 48 (列 1)
// 16 // 64 (列 2)
// 16 // 80 (列 3)
float values[3]; // 16 // 96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
int integer; // 4 // 148};
作為練習,嘗試去自己計算一下偏移量,并和表格進行對比。使用計算後的偏移量值,根據std140布局的規則,我們就能使用像是glBufferSubData的函數将變量資料按照偏移量填充進緩沖中了。雖然std140布局不是最高效的布局,但它保證了記憶體布局在每個聲明了這個Uniform塊的程式中是一緻的。
通過在Uniform塊定義之前添加
layout (std140)
語句,我們告訴OpenGL這個Uniform塊使用的是std140布局。除此之外還可以選擇兩個布局,但它們都需要我們在填充緩沖之前先查詢每個偏移量。我們已經見過
shared
布局了,剩下的一個布局是
packed
。當使用緊湊(Packed)布局時,是不能保證這個布局在每個程式中保持不變的(即非共享),因為它允許編譯器去将uniform變量從Uniform塊中優化掉,這在每個着色器中都可能是不同的。
使用Uniform緩沖
我們已經讨論了如何在着色器中定義Uniform塊,并設定它們的記憶體布局了,但我們還沒有讨論該如何使用它們。
首先,我們需要調用glGenBuffers,建立一個Uniform緩沖對象。一旦我們有了一個緩沖對象,我們需要将它綁定到GL_UNIFORM_BUFFER目标,并調用glBufferData,配置設定足夠的記憶體。
unsigned int uboExampleBlock;
glGenBuffers(1, &uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // 配置設定152位元組的記憶體glBindBuffer(GL_UNIFORM_BUFFER, 0);
現在,每當我們需要對緩沖更新或者插入資料,我們都會綁定到uboExampleBlock,并使用glBufferSubData來更新它的記憶體。我們隻需要更新這個Uniform緩沖一次,所有使用這個緩沖的着色器就都使用的是更新後的資料了。但是,如何才能讓OpenGL知道哪個Uniform緩沖對應的是哪個Uniform塊呢?
在OpenGL上下文中,定義了一些綁定點(Binding Point),我們可以将一個Uniform緩沖連結至它。在建立Uniform緩沖之後,我們将它綁定到其中一個綁定點上,并将着色器中的Uniform塊綁定到相同的綁定點,把它們連接配接到一起。下面的這個圖示展示了這個:
你可以看到,我們可以綁定多個Uniform緩沖到不同的綁定點上。因為着色器A和着色器B都有一個連結到綁定點0的Uniform塊,它們的Uniform塊将會共享相同的uniform資料,uboMatrices,前提條件是兩個着色器都定義了相同的MatricesUniform塊。
為了将Uniform塊綁定到一個特定的綁定點中,我們需要調用glUniformBlockBinding函數,它的第一個參數是一個程式對象,之後是一個Uniform塊索引和連結到的綁定點。Uniform塊索引(Uniform Block Index)是着色器中已定義Uniform塊的位置值索引。這可以通過調用glGetUniformBlockIndex來擷取,它接受一個程式對象和Uniform塊的名稱。我們可以用以下方式将圖示中的Lights Uniform塊連結到綁定點2:
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
注意我們需要對每個着色器重複這一步驟。
從OpenGL 4.2版本起,你也可以添加一個布局辨別符,顯式地将Uniform塊的綁定點儲存在着色器中,這樣就不用再調用glGetUniformBlockIndex和glUniformBlockBinding了。下面的代碼顯式地設定了LightsUniform塊的綁定點。
layout(std140, binding = 2) uniform Lights { ... };
接下來,我們還需要綁定Uniform緩沖對象到相同的綁定點上,這可以使用glBindBufferBase或glBindBufferRange來完成。
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// 或glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
glBindbufferBase需要一個目标,一個綁定點索引和一個Uniform緩沖對象作為它的參數。這個函數将uboExampleBlock連結到綁定點2上,自此,綁定點的兩端都連結上了。你也可以使用glBindBufferRange函數,它需要一個附加的偏移量和大小參數,這樣子你可以綁定Uniform緩沖的特定一部分到綁定點中。通過使用glBindBufferRange函數,你可以讓多個不同的Uniform塊綁定到同一個Uniform緩沖對象上。
現在,所有的東西都配置完畢了,我們可以開始向Uniform緩沖中添加資料了。隻要我們需要,就可以使用glBufferSubData函數,用一個位元組數組添加所有的資料,或者更新緩沖的一部分。要想更新uniform變量boolean,我們可以用以下方式更新Uniform緩沖對象:
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);int b = true; // GLSL中的bool是4位元組的,是以我們将它存為一個integerglBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
同樣的步驟也能應用到Uniform塊中其它的uniform變量上,但需要使用不同的範圍參數。
一個簡單的例子
是以,我們來展示一個真正使用Uniform緩沖對象的例子。如果我們回頭看看之前所有的代碼例子,我們不斷地在使用3個矩陣:投影、觀察和模型矩陣。在所有的這些矩陣中,隻有模型矩陣會頻繁變動。如果我們有多個着色器使用了這同一組矩陣,那麼使用Uniform緩沖對象可能會更好。
我們會将投影和模型矩陣存儲到一個叫做Matrices的Uniform塊中。我們不會将模型矩陣存在這裡,因為模型矩陣在不同的着色器中會不斷改變,是以使用Uniform緩沖對象并不會帶來什麼好處。
#version 330 corelayout (location = 0) in vec3 aPos;
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;void main(){
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
這裡沒什麼特别的,除了我們現在使用的是一個std140布局的Uniform塊。我們将在例子程式中,顯示4個立方體,每個立方體都是使用不同的着色器程式渲染的。這4個着色器程式将使用相同的頂點着色器,但使用的是不同的片段着色器,每個着色器會輸出不同的顔色。
首先,我們将頂點着色器的Uniform塊設定為綁定點0。注意我們需要對每個着色器都設定一遍。
unsigned int uniformBlockIndexRed = glGetUniformBlockIndex(shaderRed.ID, "Matrices");unsigned int uniformBlockIndexGreen = glGetUniformBlockIndex(shaderGreen.ID, "Matrices");unsigned int uniformBlockIndexBlue = glGetUniformBlockIndex(shaderBlue.ID, "Matrices");unsigned int uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");
glUniformBlockBinding(shaderRed.ID, uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID, uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID, uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);
接下來,我們建立Uniform緩沖對象本身,并将其綁定到綁定點0:
unsigned int uboMatricesglGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
首先我們為緩沖配置設定了足夠的記憶體,它等于glm::mat4大小的兩倍。GLM矩陣類型的大小直接對應于GLSL中的mat4。接下來,我們将緩沖中的特定範圍(在這裡是整個緩沖)連結到綁定點0。
剩餘的就是填充這個緩沖了。如果我們将投影矩陣的視野(Field of View)值保持不變(是以錄影機就沒有縮放了),我們隻需要将其在程式中定義一次——這也意味着我們隻需要将它插入到緩沖中一次。因為我們已經為緩沖對象配置設定了足夠的記憶體,我們可以使用glBufferSubData在進入渲染循環之前存儲投影矩陣:
glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
這裡我們将投影矩陣儲存在Uniform緩沖的前半部分。在每次渲染疊代中繪制物體之前,我們會将觀察矩陣更新到緩沖的後半部分:
glm::mat4 view = camera.GetViewMatrix();
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
Uniform緩沖對象的部分就結束了。每個包含了Matrices這個Uniform塊的頂點着色器将會包含儲存在uboMatrices中的資料。是以,如果我們現在要用4個不同的着色器繪制4個立方體,它們的投影和觀察矩陣都會是一樣的。
glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f)); // 移動到左上角shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// ... 繪制綠色立方體// ... 繪制藍色立方體// ... 繪制×××立方體
唯一需要設定的uniform隻剩model uniform了。在像這樣的場景中使用Uniform緩沖對象會讓我們在每個着色器中都剩下一些uniform調用。最終的結果會是這樣的:
因為修改了模型矩陣,每個立方體都移動到了視窗的一邊,并且由于使用了不同的片段着色器,它們的顔色也不同。這隻是一個很簡單的情景,我們可能會需要使用Uniform緩沖對象,但任何大型的渲染程式都可能同時激活有上百個着色器程式,這時候Uniform緩沖對象的優勢就會很大地展現出來了。