學(xué)習(xí)代碼地址
OpenGL(ES)學(xué)習(xí)一:準(zhǔn)備
OpenGL(ES)學(xué)習(xí)二:繪制一個三角形
就像學(xué)習(xí)編程的hello world一樣,畫一個三角形幾乎是必備經(jīng)過。
渲染pipeline
pipeline我的理解是該翻譯成“流水線”,而不是常見的”管線“。因?yàn)樗囊馑季褪窍窳魉€一樣工作,是把輸入的數(shù)據(jù)一步步變成屏幕上的像素的過程。而”管線“很容易聯(lián)想到管道,就想偏了?!绷魉€“更讓人注意到它本質(zhì)是一個流程,是一個過程,而不是一個...管子?

- 對于任何一個模型,不管3D、2D,最初都是點(diǎn)的數(shù)據(jù)。比如一個正方形,就是4個角的坐標(biāo),立方體,就是8個點(diǎn)的坐標(biāo),一個游戲里的復(fù)雜人物,實(shí)際是許多的點(diǎn)拼接起來的,搜一下”三維模型“圖片就有直觀感受。所以整個流程輸入的是頂點(diǎn)的數(shù)據(jù),即Vertex Data.
- 而最終呈現(xiàn)給用戶的是顯示屏上的圖像,而圖像是一個個像素構(gòu)成,所以輸出的是每一個像素的顏色。整個流程要做的就是:怎么把一個個坐標(biāo)數(shù)據(jù)變成屏幕上正確的像素顏色呢?
- 這張圖片還是很直觀的。而藍(lán)色部分就是現(xiàn)代OpenGL可以讓我們編寫參與的部分。shader譯作”著色器“,它是流程中的一段子程序,負(fù)責(zé)處理某一個階段的任務(wù),就像流水線上有很多不同的機(jī)器和人,它們負(fù)責(zé)一部分工作。
- Vertex shader是第一個shader,它負(fù)責(zé)處理輸入的頂點(diǎn)數(shù)據(jù),比如坐標(biāo)變換
- Geometry shader和Tesselation shader,不是必須的。
- Fragment shader這時接受的已經(jīng)不是頂點(diǎn),而是fragment,有碎片的意思,它就對應(yīng)著一個像素單位。這一階段主要就是要計(jì)算顏色,比如光照計(jì)算:在有N個光源的時候,這個fragment的顏色是什么,光的顏色、物體本身的顏色、這個fragment朝向等都要考慮。
所以要繪制一個三角形,需要提供3個點(diǎn)的數(shù)據(jù)以及編寫vertex shader和fragment shader.
加載shader
shader是一個子程序,它有自己的語言glsl,也需要編譯才能使用。glsl和c類似,如繪制三角形需要的vertex shader:
const GLchar *vertexShaderSource =
"#version 330 core \n\
layout (location = 0) in vec3 position; \n\
void main(){ \n\
gl_Position = vec4(position, 1.0f); \n\
} \n\
";
先忽略掉”\n\“,這只是為了多行輸入字符串,shader的內(nèi)容從#version開始。
有了shader的代碼,下一步就是把shader代價(jià)加載到它的編譯器里:
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, 0);
glCompileShader(vertexShader);
先用glCreateShader生成一個shader對象,然后通過glShaderSource把shader代碼提供給這個shader,最后編譯這個shader: glCompileShader。
如果shader代碼寫錯了,編譯之后就會報(bào)錯,所以這時需要檢查下shader的編譯情況:
GLint succeed;
GLchar infoLog[256];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &succeed);
if (!succeed) {
glGetShaderInfoLog(vertexShader, sizeof(infoLog),NULL, infoLog);
std::cout<< "compile vertex shader error: "<< infoLog << std::endl;
return -1;
}
先使用glGetShaderiv獲取shader的編譯狀態(tài),這種函數(shù)方式也是常見的:
- iv表示int value, 帶這種后綴的用來區(qū)分返回值或傳入值的類型
- 然后有一個參數(shù)用來表示獲取什么值,這里是GL_COMPILE_STATUS,表示獲取shader的編譯狀態(tài)。
如果編譯狀態(tài)為失敗,就獲取log信息,查看哪里出錯:glGetShaderInfoLog。
fragment shader使用同樣的方式加載進(jìn)來:
const GLchar *fragmentShaderSource =
"#version 330 core \n\
out vec4 color; \n\
void main(){ \n\
color = vec4(1.0f, 0.0f, 0.0f, 1.0f); \n\
} \n\
";
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, 0);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &succeed);
if (!succeed) {
glGetShaderInfoLog(fragmentShader, sizeof(infoLog),NULL, infoLog);
std::cout<< "compile fragment shader error: "<< infoLog << std::endl;
return -1;
}
program
shader加載完后,不同的shader需要連接到一起,測試是否可以一起使用;還需要由這些shder生成可執(zhí)行文件(executable)給渲染流程。
所以這時需要一個shader容器,或說管理者,即program。
program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
先生成一個program,然后使用glAttachShader把一起使用的所有shader都綁定到這個program上;最后使用glLinkProgram把鏈接program。
最后在渲染時,使用glUseProgram(program);指定使用的program。
準(zhǔn)備數(shù)據(jù):VBO和VAO
準(zhǔn)備好shader后,處理流程的邏輯已經(jīng)準(zhǔn)備好,缺的就是數(shù)據(jù)了。
VBO是vertex buffer object,是用來存儲頂點(diǎn)數(shù)據(jù)的緩沖區(qū)對象. Buffer Object具體是什么?
Buffer Objects are OpenGL Objects that store an array of unformatted memory allocated by the OpenGL context (aka: the GPU).
就是在GPU中申請的一塊內(nèi)存,用于存放數(shù)據(jù)。之前有直接模式:每次繪制都要把數(shù)據(jù)提交過去。后來發(fā)展成為”頂點(diǎn)數(shù)組“,數(shù)據(jù)存放在電腦內(nèi)存中,繪制的時候提供位置索引。再到現(xiàn)在的buffer object,數(shù)據(jù)存放在GPU端,這樣進(jìn)一步加快了數(shù)據(jù)傳遞。
-
先生成一個buffer object
GLuint VBO;
glGenBuffers(1, &VBO);
2. 然后綁定:
```
glBindBuffer(GL_ARRAY_BUFFER, VBO);
在生成VBO后,其實(shí)它和任何其他的Buffer object沒有任何的區(qū)別,所以還需要做的就是:誰來使用這個數(shù)據(jù),以及怎么使用。glBindBuffer就是指定誰來使用的問題.使用`GL_ARRAY_BUFFER `表示這個buffer用來存儲頂點(diǎn)屬性數(shù)據(jù)。
頂點(diǎn)屬性是什么?
在返回vertex shader的代碼:
layout (location = 0) in vec3 position;
頂點(diǎn)的坐標(biāo)position就是屬性之一,這個shader配合VBO,那么position的數(shù)據(jù)就從這個buffer object讀取。
-
輸入數(shù)據(jù)
GLfloat vertices[] = {
-0.5f, -0.3f, 0.0f,
0.5f, -0.3f, 0.0f,
0.0f, 0.8f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
先把數(shù)據(jù)輸入,glBufferData會給第一個參數(shù)對應(yīng)的buffer object那里創(chuàng)建并初始化數(shù)據(jù),因?yàn)橹敖壎℅L_ARRAY_BUFFER到VBO,所以VBO輸入了vertices的數(shù)據(jù)。
4. 讀取數(shù)據(jù)的方式
```
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GL_FLOAT), vertices);
glEnableVertexAttribArray(0);
```
默認(rèn)數(shù)據(jù)對頂點(diǎn)屬性是不可訪問的,使用`glEnableVertexAttribArray `開啟,參數(shù)就是shader代碼里屬性的位置,因?yàn)閌layout (location = 0) in vec3 position;`,position這個屬性的location設(shè)為了0,所以這里就是開啟position的讀取能力。
`glVertexAttribPointer`比較關(guān)鍵的函數(shù),決定了怎么讀取數(shù)據(jù),這個函數(shù)原型是:
```
void glVertexAttribPointer( GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid * pointer);
```
* index指定這是描述哪個屬性的,傳入0,表示描述的是position這個屬性讀取數(shù)據(jù)的方式。
* size是每次讀取的數(shù)據(jù)大小
* type是數(shù)據(jù)類型,這個配合size一起決定每次讀取多大的內(nèi)存。這里傳入3和GL_FLOAT,也就是每個頂點(diǎn)的position讀取3個浮點(diǎn)數(shù)。
* normalized 是指數(shù)據(jù)是否需要被歸一化,所謂”normalize“,就是把數(shù)值映射到[-1,1](有符號數(shù))或[0,1],有符號數(shù)。這里不需要,傳入GL_FALSE.
* stride 有很多的頂點(diǎn)都從這里讀取數(shù)據(jù),讀完一個后,下一個從哪里開始讀取,stride就是跳過的距離.比如:12 34 56 78,讀完3位置的內(nèi)存后,如果stride設(shè)為4,那么就跳到7開始讀取下一個數(shù)據(jù)。因?yàn)橐粋€頂點(diǎn)3個浮點(diǎn)數(shù),而且緊貼著就是下一個,所以傳入3*sizeof(GL_FLOAT)。如果傳入0,也是可以的,因?yàn)閭魅?時,就是讀取完上一個,從結(jié)尾的位置開始讀下一個。
* pointer 這個用來指定讀取開始位置的偏移。比如:12 34 56 78,12和56存的是屬性1的數(shù)據(jù),34和78存儲的是屬性2的數(shù)據(jù),那么屬性2讀取的開始位置就不是buffer object的開頭,有一段偏移。
經(jīng)過上面的一系列操作,頂點(diǎn)屬性知道了在哪里讀取數(shù)據(jù)(VBO),也知道了如何讀取數(shù)據(jù),并且數(shù)據(jù)也輸入到了VBO里。
5. 最后還有VAO,即Vertex Array Object。每繪制一個物體,上面的步驟就要走一遍,除了頂點(diǎn)數(shù)據(jù),可能還有索引數(shù)據(jù)。而VAO就是把這些狀態(tài)(哪些屬性可以讀取數(shù)據(jù),這些屬性怎么讀取,索引數(shù)據(jù)是哪些等)打包一起。繪制的時候調(diào)用一句`glBindVertexArray(VAO1);`那么和VAO1關(guān)聯(lián)的所有狀態(tài)都會啟用,如果接著調(diào)用`glBindVertexArray(VAO2);`就可以又馬上切換到VAO2的所有數(shù)據(jù)。應(yīng)該是為了方便編碼而設(shè)計(jì)的。
因?yàn)橛辛薞AO,可以把上面的數(shù)據(jù)處理都放到準(zhǔn)備階段,即渲染循環(huán)之前,而不是每次循環(huán)都去處理。渲染循環(huán)里只需要`glBindVertexArray`切換需要的VAO就可以。在準(zhǔn)備階段,哪些狀態(tài)會被VAO綁定?
glBindVertexArray(VAO1);
//數(shù)據(jù)處理1
glBindVertexArray(VAO2);
//數(shù)據(jù)處理2
glBindVertexArray(0);
數(shù)據(jù)處理1位置做的所有操作的都會綁定到VAO1上,而數(shù)據(jù)處理2做的處理都會到VAO2上,也就是現(xiàn)在哪個VAO被綁定,就是作用在誰上。
###渲染循環(huán)
glUseProgram(program);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram使用program,啟用program關(guān)聯(lián)的所有shader. glBindVertexArray啟用VAO關(guān)聯(lián)的所有數(shù)據(jù)和讀取方式。數(shù)據(jù)和邏輯都有了,glDrawArrays繪制。
* GL_TRIANGLES 表示繪制三角形,這里用來指定繪制的圖元類型,所有復(fù)雜物體都是基本圖形構(gòu)成的,還有點(diǎn)(GL_POINTS)、線(GL_LINES)等.
* 0和3指定繪制時使用的點(diǎn)的數(shù)據(jù)范圍,從第0個開始,總共3個。因?yàn)橹焕L制一個三角形,所以使用3個點(diǎn)就可以了。
###回到shader
vertex shader
version 330 core
layout (location = 0) in vec3 position;
void main(){
gl_Position = vec4(position, 1.0f);
}
* `#version 330 core `聲明版本,這里是3.3,core表示使用core profile.另一種是:compatibility。compatibility是兼容模式,會保留之前的函數(shù),而core會拋棄那些已經(jīng)禁用的函數(shù)。學(xué)習(xí)就直接從core profile開啟吧。
* `layout (location = 0) in vec3 position;`聲明一個vec3類型的變量,vec是vector的縮寫,即向量。vec3是3元向量,比如rgb、xyz坐標(biāo)都是。layout和location用來指定這個屬性的位置,配合VBO數(shù)據(jù)讀取。
* main函數(shù)是主函數(shù),在這里做頂點(diǎn)的處理。gl_Position是默認(rèn)變量,是用來輸出頂點(diǎn)數(shù)據(jù)給下一個階段的。這里main函數(shù)里,只是把vec3變成vec4.
fragment shader
version 330 core
out vec4 color;
void main(){
color = vec4(1.0f, 0.0f, 0.0f, 1.0f);
}
* #version同上
* color 定義一個顏色,vec4是包含rgba4個分量,out關(guān)鍵字用來表示這個變量用來輸出到下一個階段。如果用in就是從上一個階段輸入進(jìn)來。
* fragment shader會輸出第一個被賦值的out vec4,作為像素顏色。
###OpenGL ES的區(qū)別
在繪制一個三角形的問題上,基本沒有區(qū)別,除了shader的代碼有兩處不同:
* 版本聲明里的core修改為es,版本改為300.
* es因?yàn)槭墙o嵌入式設(shè)備設(shè)計(jì)的,大概對內(nèi)存和性能有更嚴(yán)格的考驗(yàn),需要執(zhí)行數(shù)據(jù)的精度。聲明精度有兩種方式:
* 聲明一個默認(rèn)精度:`precision mediump float;`所有使用的float都為中等精度。
* 或者對某個變量特別指定精度: `out mediump vec4 color;`