目前,我們在着色器中要傳遞多個uniform變量時,總是使用多個uniform,然後在主程序中設置這些變量的值;同時如果要在多個shader之間共享變量,例如投影矩陣projection和視變換矩陣view的話,仍然需要爲不同shader分別設置這些uniform變量。本節將爲大家介紹interface block,以及基於此的uniform buffer object(UBO),這些技術將簡化着色器中變量的傳遞和共享問題。本節示例程序均可以從我的github下載。
本節內容參考自:
1.www.learningopengl.com Advanced GLSL
2.GLSL Tutorial – Uniform Blocks
3.《OpenGL 4.0 Shading Language Cookbook》-Using Uniform Blocks and Uniform Buffer Objects
interface block
interfac
block是一組GLSL着色器裏面的輸入、輸出、uniform等變量的集合,有一些類似於C語言中的struct,但是不像struct那樣簡單明瞭,還有一些其他的選項包含在裏面。通過使用interface block,我們可以將着色器中的變量以組的形式來管理,這樣書寫更整潔。
interface block的聲明形式爲:
storage_qualifier block_name
{
<define members here>
} instance_name;
- 1
- 2
- 3
- 4
其中storage_qualifier指明這個block的存儲限定符,限定符可以使用in, out, uniform, 或者buffer(GLSL4.3支持)等,block_name則給定名稱,而instance_name給定實例名稱。例如,我們之前在實現點光源的過程中,頂點着色器和片元着色器之間需要傳遞法向量、紋理座標等變量,將他們封裝到一個block中,代碼顯得更緊湊。頂點着色器中輸出變量定義形式如下:
// 定義輸出interface block
out VS_OUT
{
vec3 FragPos;
vec2 TextCoord;
vec3 FragNormal;
}vs_out;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
而在片元着色器中,要以相同的block_name接受,實例名稱則可以不同,形式可以定義爲:
// 定義輸入interface block
in VS_OUT
{
vec3 FragPos;
vec2 TextCoord;
vec3 FragNormal;
}fs_in;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
如果指定了instance_name,則在片元着色器中引用這些變量時需要加上instance_name前綴,例如:
// 環境光成分
vec3 ambient = light.ambient * vec3(texture(material.diffuseMap, fs_in.TextCoord));
- 1
- 2
反之如果沒有指定instance_name,則這個block中的變量將和uniform一樣是全局的,可以直接使用。如果沒有給定instance_name,則需要注意,interface block中給定的變量名不要和uniform給定的重複,否則造成重定義錯誤,例如下面的定義將造成重定義錯誤:
uniform MatrixBlock
{
mat4 projection;
mat4 modelview;
};
uniform vec3 modelview; // 重定義錯誤 和MatrixBlock中衝突
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
相比於之前以分散形式書寫這些變量,interface block能夠讓你更合理的組織變量爲一組,邏輯更清晰。主程序部分未變,實現的點光源效果相同,這裏還是給出效果圖如下:
從上面可以看出,interface block確實解決了我們一直想要合理組織着色器中變量的問題。這是我們提到的第一個問題。
UBO的概念
本節開始提到的第二個問題是,如何在多個着色器之間簡潔的共享變量。GLSL中可以通過uniform buffer來實現。uniform buffer的實現思路爲: 在多個着色器中定義相同的uniform block(就是上面的interface block,使用uniform限定符定義),然後將這些uniform block綁定到對應的uniform buffer object,而uniform buffer object中實際存儲這些需要共享的變量。着色器中的uniform block和主程序中的uniform buffer object,是通過OpenGL的綁定點(binding points)連接起來的,它們的關係如下圖所示(來自www.learningopengl.com Advanced GLSL):
使用時,每個shader中定義的uniform block有一個索引,通過這個索引連接到OpenGL的綁定點x;而主程序中創建uniform buffer object,傳遞數據後,將這個UBO綁定到對應的x,此後shader中的uniform block就和OpenGL中的UBO聯繫起來,我們在程序中操作UBO的數據,就能夠在不同着色器之間共享了。例如上圖中,着色器A和B定義的Matrices的索引都指向綁定點0,他們共享openGL的uboMatrices這個UBO的數據。同時着色器A的Lights和着色器B的Data,分別指向不同的UBO。
UBO的使用
在上面我們介紹了UBO的概念,下面通過實例瞭解UBO的實際使用。UBO的實現依賴於着色器中uniform block的定義,uniform block的內存佈局四種形式:shared, packed, std140, and std430(GLSL4.3以上支持),默認是shared內存佈局。本節我們重點學習shared和std140這兩種內存佈局形式,其他的形式可以在需要時自行參考OpenGL規範。
-
shared 默認的內存佈局 採用依賴於具體實現的優化方案,但是保證在不同程序中具有相同定義的block擁有相同的佈局,因此可以在不同程序之間共享。要使block能夠共享必須注意block具有相同定義,同時所有成員顯式指定數組的大小。同時shared保證所有成員都是激活狀態,沒有變量被優化掉。
-
std140 這種方式明確的指定alignment的大小,會在block中添加額外的字節來保證字節對齊,因而可以提前就計算出佈局中每個變量的位移偏量,並且能夠在shader之間共享;不足在於添加了額外的padding字節。稍後會介紹字節對齊和padding相關內容。
下面通過兩個簡單例子,來熟悉std140和默認的shared內存佈局。這個例子將會在屏幕上通過4個着色器繪製4個不同顏色的立方體,在着色器之間共享的是投影矩陣和視變換矩陣,以及爲了演示shared layout而添加的混合顏色的示例。
layout std140
字節對齊的概念
字節對齊的一個經典案例就是C語言中的結構體變量,例如下面的結構體:
struct StructExample {
char c;
int i;
short s;
};
- 1
- 2
- 3
- 4
- 5
你估計它佔用內存大小多少字節? 假設在int 佔用4字節,short佔用2個字節,那麼整體大小等於 1+ 4+ 2 = 7字節嗎?
答案是否定的。在Windows平臺測試,當int佔用4個字節,short佔用2個字節是,實際佔用大小爲12個字節,這12個字節是怎麼算出來的呢? 就是用到了字節補齊的概念。實際上上述結構體的內存佈局爲:
struct StructExample {
char c; // 0 bytes offset, 3 bytes padding
int i; // 4 bytes offset
short s; // 8 bytes offset, 2 bytes padding
}; // End of 12 bytes
- 1
- 2
- 3
- 4
- 5
內存佈局如下圖所示:
字節對齊的一個重要原因是爲了使機器訪問更迅速。例如在32字長的地址的機器中,每次讀取4個字節數據,所以將字節對齊到上述地址 0x0000,0x0004和0x0008, 0x000C將使讀取更加迅速。否則例如上面結構體中的int i將跨越兩個字長(0x0000和0x0004),需要兩次讀取操作,影響效率。當然關於爲什麼使用字節對齊的更詳細分析,感興趣地可以參考SO Purpose of memory alignment。
關於字節對齊,我們需要知道的幾個要點就是(參考自wiki Data structure alignment):
-
一個內存地址,當它是n字節的倍數時,稱之爲n字節對齊,這裏n字節是2的整數冪。
-
每種數據類型都有它自己的字節對齊要求(alignment),例如char是1字節,int一般爲4字節,float爲4字節對齊,8字節的long則是8字節對齊。
-
當變量的字節沒有對齊時,將額外填充字節(padding)來使之對齊。
上面的結構體中,int變量i需要4字節對齊,因此在char後面填充了3個字節,同時結構體變量整體大小需要滿足最長alignment成員的字節對齊,因此在short後面補充了2個字節,總計達到12字節。
關於字節對齊這個概念,介紹到這裏,希望瞭解更多地可以參考The Lost Art of C Structure Packing。
std140的字節對齊
std140內存佈局同樣存在字節對齊的概念,你可以參考官方文檔獲取完整描述。常用標量int,float,bool要求4字節對齊,4字節也被作爲一個基礎值N,這裏列舉幾個常用的結構的字節對齊要求:
類型 | 對齊基數(base alignment) |
---|---|
標量,例如 int bool | 每個標量對齊基數爲N |
vector | 2N 或者 4N, vec3的基數爲4N. |
標量或者vector的數組 | 每個元素的基數等於vec4的基數. |
矩陣 | 以列向量存儲, 列向量基數等於vec4的基數. |
結構體 | 元素按之前規則,同時整體大小填充爲vec4的對齊基數 |
例如一個複雜的uniform block定義爲:
layout (std140) uniform ExampleBlock
{
// // base alignment // aligned offset
float value; // 4 // 0
vec3 vector; // 16 // 16 (must be multiple of 16 so 4->16)
mat4 matrix; // 16 // 32 (column 0)
// 16 // 48 (column 1)
// 16 // 64 (column 2)
// 16 // 80 (column 3)
float values[3]; // 16 // 96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
int integer; // 4 // 148
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
上面的註釋給出了它的字節對齊,其中填充了不少字節,可以根據上面表中給定的對齊基數提前計算出來,在主程序中可以設置這個UBO的變量:
GLuint exampleUBOId;
glGenBuffers(1, &exampleUBOId);
glBindBuffer(GL_UNIFORM_BUFFER, exampleUBOId);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_DYNAMIC_DRAW); // 預分配空間 大小可以提前根據alignment計算
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferBase(GL_UNIFORM_BUFFER, 1, exampleUBOId); // 綁定點爲1
// step4 只更新一部分值
glBindBuffer(GL_UNIFORM_BUFFER, exampleUBOId);
GLint b = true; // 布爾變量在GLSL中用4字節表示 因此這裏用int存儲
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); // offset可以根據UBO中alignment提前計算
glBindBuffer(GL_UNIFORM_BUFFER, 0);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
說明: 上面最終計算出的大小爲152,UBO整體不必滿足vec4的字節對齊要求。152 /4 = 38,滿足N的對齊要求即可。
從上面可以看到,當成員變量較多時,這種手動計算offset的方法比較笨拙,可以事先編寫一個自動計算的函數庫,以減輕工作負擔。
std140的簡單例子
下面通過一個簡單例子來熟悉UBO的使用。
Step1: 首先我們在頂點着色器中定義uniform block如下:
#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
uniform mat4 model; // 因爲模型變換矩陣一般不能共享 所以單獨列出來
// 定義UBO
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
}; // 這裏沒有定義instance name,則在使用時不需要指定instance name
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
Step2 在主程序中設置着色器的uniform block索引指向到綁定點0:
// step1 獲取shader中 uniform buffer 的索引
GLuint redShaderIndex = glGetUniformBlockIndex(redShader.programId, "Matrices");
GLuint greeShaderIndex = glGetUniformBlockIndex(greenShader.programId, "Matrices");
...
// step2 設置shader中 uniform buffer 的索引到指定綁定點
glUniformBlockBinding(redShader.programId, redShaderIndex, 0); // 綁定點爲0
glUniformBlockBinding(greenShader.programId, greeShaderIndex, 0);
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
這裏爲了演示代碼中重複寫出了4個着色器,實際中可以通過vector裝入這4個着色器簡化代碼。
Step3: 創建UBO,並綁定到綁定點0
我們需要傳入2個mat4矩陣,由於mat4中每列的vec4對齊,因此兩個mat4中沒有額外的padding,大小即爲2*sizeof(mat4)。
GLuint UBOId;
glGenBuffers(1, &UBOId);
glBindBuffer(GL_UNIFORM_BUFFER, UBOId);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_DYNAMIC_DRAW); // 預分配空間
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, UBOId, 0, 2 * sizeof(glm::mat4)); // 綁定點爲0
- 1
- 2
- 3
- 4
- 5
- 6
Step4: 更新UBO中的數據
這裏使用前面介紹的glBufferSubData更新UBO中數據,例如更新視變換矩陣如下:
glm::mat4 view = camera.getViewMatrix(); // 視變換矩陣
glBindBuffer(GL_UNIFORM_BUFFER, UBOId);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
- 1
- 2
- 3
- 4
通過上面的步驟,我們完成了着色器中unifrom block和UBO的連接,實現了投影矩陣和視變換矩陣在4個着色器之間的共享,繪製4個立方體如下圖所示:
驗證ExampleBlock
這裏在着色器中添加一段代碼測試下上面那個複雜的ExampleBlock的內容,我們在主程序中設置boolean變量爲true,在着色器中添加一個判斷,如果boolean爲true,則輸出白色立方體:
if(boolean)
{
color = vec4(1.0, 1.0, 1.0, 1.0);
}
- 1
- 2
- 3
- 4
最終顯示獲得了4個全是白色的立方體,效果如下:
這就驗證了上述計算出那個複雜ExampleBlock的大小爲152,boolean變量位移偏量爲144是正確的。
layout shared
同std140內存佈局方式不一樣,shared方式的內存佈局依賴於具體實現,因此我們無法提前根據某種字節對齊規範計算出UBO中變量的位移偏量和整體大小,因此在使用shared方式時,我們需要多次利用OpenGL的函數來查詢UBO的信息。
這裏在着色器中定義一個用於混合顏色的uniform block:
#version 330 core
// 使用默認shared方式的UBO
uniform mixColorSettings {
vec4 anotherColor;
float mixValue;
};
out vec4 color;
void main()
{
color = mix(vec4(0.0, 0.0, 1.0, 1.0), anotherColor, mixValue);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在出程序中首先查詢UBO整體大小,預分配空間:
GLuint colorUBOId;
glGenBuffers(1, &colorUBOId);
glBindBuffer(GL_UNIFORM_BUFFER, colorUBOId);
// 獲取UBO大小 因爲定義相同 只需要在一個shader中獲取大小即可
GLint blockSize;
glGetActiveUniformBlockiv(redShader.programId, redShaderIndex,
GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);
glBufferData(GL_UNIFORM_BUFFER, blockSize, NULL, GL_DYNAMIC_DRAW); // 預分配空間
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferBase(GL_UNIFORM_BUFFER, 1, colorUBOId); // 綁定點爲1
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
然後,通過查詢UBO中成員變量的索引和位移偏量來設置變量值:
// 通過查詢獲取uniform buffer中各個變量的索引和位移偏量
const GLchar* names[] = {
"anotherColor", "mixValue"
};
GLuint indices[2];
glGetUniformIndices(redShader.programId, 2, names, indices);
GLint offset[2];
glGetActiveUniformsiv(redShader.programId, 2, indices, GL_UNIFORM_OFFSET, offset);
// 使用獲取的位移偏量更新數據
glm::vec4 anotherColor = glm::vec4(0.0f, 1.0f, 1.0f, 1.0f);
GLfloat mixValue = 0.5f;
glBindBuffer(GL_UNIFORM_BUFFER, colorUBOId);
glBufferSubData(GL_UNIFORM_BUFFER, offset[0], sizeof(glm::vec4), glm::value_ptr(anotherColor));
glBufferSubData(GL_UNIFORM_BUFFER, offset[1], sizeof(glm::vec4), &mixValue);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
和上面std140定義的uniform block一起工作,產生的混合顏色效果如下圖所示:
從上面可以看到,使用shared佈局時,當變量較多時,這種查詢成員變量索引和位移偏量的工作顯得比較麻煩。