OpenGL(ES)學(xué)習(xí)二:繪制一個三角形

學(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ì)是一個流程,是一個過程,而不是一個...管子?


OpenGL pipeline
  • 對于任何一個模型,不管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ù)傳遞。

  1. 先生成一個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讀取。

  1. 輸入數(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;`
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄結(jié)構(gòu): 第一步,明確要干嘛 第二步,怎么去畫(純理論) 第三步,怎么去畫(實(shí)戰(zhàn)) 第四步,練練手 第一步,明確...
    半紙淵閱讀 8,301評論 18 57
  • OpenGL學(xué)習(xí)大致的理解 OpenGL為什么會涉及這么多操作順序。這是因?yàn)?,和我們現(xiàn)在使用的C++、JAVA這種...
    wo不懂閱讀 5,535評論 10 8
  • 你好,三角形 圖形渲染管線(Pipeline) 3D坐標(biāo)轉(zhuǎn)為2D坐標(biāo)的處理過程是由OpenGL的圖形渲染管線(Pi...
    IceMJ閱讀 7,640評論 2 13
  • 《當(dāng)我放過自己的時候》是馬德的一本書,是我前些日子在機(jī)場打發(fā)時間買的,原因不外乎自己剛分手,心里憋屈著呢久久放不下...
    般若觀閱讀 860評論 9 18
  • 2011年2月18日,早晨。我必須及時記下我這時的感受。我的早餐剛剛吃完,在制作的時候我突然想到了最近一個...
    張玉新關(guān)東漢子閱讀 1,026評論 3 5

友情鏈接更多精彩內(nèi)容