版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2018.01.16 |
前言
OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。下面內容來自歡迎來到OpenGL的世界。
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
21. OpenGL 圖形庫的使用(二十一)—— 高級OpenGL之深度測試
22. OpenGL 圖形庫的使用(二十二)—— 高級OpenGL之模板測試Stencil testing
23. OpenGL 圖形庫的使用(二十三)—— 高級OpenGL之混合Blending
24. OpenGL 圖形庫的使用(二十四)—— 高級OpenGL之面剔除Face culling
25. OpenGL 圖形庫的使用(二十五)—— 高級OpenGL之幀緩沖Framebuffers
26. OpenGL 圖形庫的使用(二十六)—— 高級OpenGL之立方體貼圖Cubemaps
27. OpenGL 圖形庫的使用(二十七)—— 高級OpenGL之高級數據Advanced Data
高級GLSL
這一小節并不會向你展示非常先進非常酷的新特性,也不會對場景的視覺質量有顯著的提高。但是,這一節會或多或少涉及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
是必須的步驟。這已經是它的全部功能了。
1. 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;
}
結果就是,當我們遠離這些點的時候,它們會變得更大:
你可以想到,對每個頂點使用不同的點大小,會在粒子生成之類的技術中很有意思。
2. gl_VertexID
gl_Position
和gl_PointSize
都是輸出變量,因為它們的值是作為頂點著色器的輸出被讀取的。我們可以對它們進行寫入,來改變結果。頂點著色器還為我們提供了一個有趣的輸入變量,我們只能對它進行讀取,它叫做gl_VertexID。
整型變量gl_VertexID
儲存了正在繪制頂點的當前ID。當(使用glDrawElements
)進行索引渲染的時候,這個變量會存儲正在繪制頂點的當前索引。當(使用glDrawArrays)不使用索引進行繪制的時候,這個變量會儲存從渲染調用開始的已處理頂點數量。
雖然現在它沒有什么具體的用途,但知道我們能夠訪問這個信息總是好的。
片段著色器變量
在片段著色器中,我們也能訪問到一些有趣的變量。GLSL提供給我們兩個有趣的輸入變量:gl_FragCoord
和gl_FrontFacing
。
1. 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時,它一定在窗口的左側,所以我們給它一個不同的顏色。
我們現在會計算出兩個完全不同的片段著色器結果,并將它們顯示在窗口的兩側。舉例來說,你可以將它用于測試不同的光照技巧。
2. gl_FrontFacing
片段著色器另外一個很有意思的輸入變量是gl_FrontFacing
。在面剔除教程中,我們提到OpenGL能夠根據頂點的環繞順序來決定一個面是正向還是背向面。如果我們不(啟用GL_FACE_CULL
來)使用面剔除,那么gl_FrontFacing
將會告訴我們當前片段是屬于正向面的一部分還是背向面的一部分。舉例來說,我們能夠對正向面計算出不同的顏色。
gl_FrontFacing
變量是一個bool,如果當前片段是正向面的一部分那么就是true,否則就是false。比如說,我們可以這樣子創建一個立方體,在內部和外部使用不同的紋理:
#version 330 core
out 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
就沒有意義了。
3. 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 core
layout (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 core
out 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 core
layout (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
來表示。
和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,前提條件是兩個著色器都定義了相同的Matrices Uniform
塊。
為了將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
了。下面的代碼顯式地設置了Lights Uniform
塊的綁定點。
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字節的,所以我們將它存為一個integer
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
同樣的步驟也能應用到Uniform塊中其它的uniform變量上,但需要使用不同的范圍參數。
一個簡單的例子
所以,我們來展示一個真正使用Uniform緩沖對象的例子。如果我們回頭看看之前所有的代碼例子,我們不斷地在使用3個矩陣:投影、觀察和模型矩陣。在所有的這些矩陣中,只有模型矩陣會頻繁變動。如果我們有多個著色器使用了這同一組矩陣,那么使用Uniform緩沖對象可能會更好。
我們會將投影和模型矩陣存儲到一個叫做Matrices
的Uniform塊中。我們不會將模型矩陣存在這里,因為模型矩陣在不同的著色器中會不斷改變,所以使用Uniform緩沖對象并不會帶來什么好處。
#version 330 core
layout (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 uboMatrices
glGenBuffers(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緩沖對象的優勢就會很大地體現出來了。
你可以在這里找到uniform例子程序的完整源代碼。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <learnopengl/shader_m.h>
#include <learnopengl/camera.h>
#include <learnopengl/model.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;
// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;
// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);
// tell GLFW to capture our mouse
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// configure global opengl state
// -----------------------------
glEnable(GL_DEPTH_TEST);
// build and compile shaders
// -------------------------
Shader shaderRed("8.advanced_glsl.vs", "8.red.fs");
Shader shaderGreen("8.advanced_glsl.vs", "8.green.fs");
Shader shaderBlue("8.advanced_glsl.vs", "8.blue.fs");
Shader shaderYellow("8.advanced_glsl.vs", "8.yellow.fs");
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float cubeVertices[] = {
// positions
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
};
// cube VAO
unsigned int cubeVAO, cubeVBO;
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &cubeVBO);
glBindVertexArray(cubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// configure a uniform buffer object
// ---------------------------------
// first. We get the relevant block indices
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");
// then we link each shader's uniform block to this uniform binding point
glUniformBlockBinding(shaderRed.ID, uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID, uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID, uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);
// Now actually create the buffer
unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// define the range of the buffer that links to a uniform binding point
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
// store the projection matrix (we only do this once now) (note: we're not using zoom anymore by changing the FoV)
glm::mat4 projection = glm::perspective(45.0f, (float)SCR_WIDTH / (float)SCR_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);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
// --------------------
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// set the view and projection matrix in the uniform block - we only have to do this once per loop iteration.
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);
// draw 4 cubes
// RED
glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f)); // move top-left
shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// GREEN
shaderGreen.use();
model = glm::mat4();
model = glm::translate(model, glm::vec3(0.75f, 0.75f, 0.0f)); // move top-right
shaderGreen.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// YELLOW
shaderYellow.use();
model = glm::mat4();
model = glm::translate(model, glm::vec3(-0.75f, -0.75f, 0.0f)); // move bottom-left
shaderYellow.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// BLUE
shaderBlue.use();
model = glm::mat4();
model = glm::translate(model, glm::vec3(0.75f, -0.75f, 0.0f)); // move bottom-right
shaderBlue.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &cubeVAO);
glDeleteBuffers(1, &cubeVBO);
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
Uniform緩沖對象比起獨立的uniform有很多好處。第一,一次設置很多uniform會比一個一個設置多個uniform要快很多。第二,比起在多個著色器中修改同樣的uniform,在Uniform緩沖中修改一次會更容易一些。最后一個好處可能不會立即顯現,如果使用Uniform緩沖對象的話,你可以在著色器中使用更多的uniform。OpenGL限制了它能夠處理的uniform數量,這可以通過GL_MAX_VERTEX_UNIFORM_COMPONENTS
來查詢。當使用Uniform緩沖對象時,最大的數量會更高。所以,當你達到了uniform的最大數量時(比如再做骨骼動畫(Skeletal Animation)
的時候),你總是可以選擇使用Uniform緩沖對象。
后記
未完,待續~~~