OpenGL學習: uniform blocks(UBO)在着色器中的使用

目前,我們在着色器中要傳遞多個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):

uniform buffer

使用時,每個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個立方體如下圖所示:

layout std140

驗證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佈局時,當變量較多時,這種查詢成員變量索引和位移偏量的工作顯得比較麻煩。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章