OpenGL ES之你好,三角形
OpenGL基礎詞彙理解
1.單詞術語:
(1) 頂點陣列物件:Vertex Array Object,VAO
(2) 頂點緩衝物件:Vertex Buffer Object,VBO
(3)索引緩衝物件:Element Buffer Object,EBO或Index Buffer Object,IBO
2. 圖形渲染管線 :指的是一堆原始圖形資料途經一個輸送管道,期間經過各種變化處理最終出現在螢幕的過程。主要分為2部分:(1)把3D座標轉化為2D座標,(2)把2D座標轉變為實際的有顏色的畫素。
3. 著色器 : 在GPU上為每一個(渲染管線)階段執行各自的小程式,從而在圖形渲染管線中快速處理你的資料。用 OpenGL著色器語言 ( GLSL )寫的。
圖形渲染管線的各個階段的抽象展示
下面是一個圖形渲染管線的每個階段的抽象展示。要注意藍色部分代表的是我們可以注入自定義的著色器的部分。

show.png
- 首先,我們以陣列的形式傳遞3個3D座標作為圖形渲染管線的輸入,用來表示一個三角形,這個陣列叫做 頂點資料 (Vertex Data);頂點資料是一系列頂點的集合。一個 頂點 (Vertex)是一個3D座標的資料的集合。而頂點資料是用 頂點屬性 (Vertex Attribute)表示的,它可以包含任何我們想用的資料,但是簡單起見,我們還是假定每個頂點只由一個3D位置和一些顏色值組成的吧。
當我們談論一個“位置”的時候,它代表在一個“空間”中所處地點的這個特殊屬性;同時“空間”代表著任何一種座標系,比如x、y、z三維座標系,x、y二維座標系
-
圖形渲染管線的第一個部分是 頂點著色器 (Vertex Shader),它把一個單獨的頂點作為輸入。
-
圖元裝配階段將頂點著色器輸出的所有頂點作為輸入(如果是GL_POINTS,那麼就是一個頂點),並所有的點裝配成指定圖元的形狀;
-
圖元裝配階段的輸出會傳遞給 幾何著色器 (Geometry Shader)。幾何著色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。
-
幾何著色器的輸出會被傳入 光柵化階段 (Rasterization Stage),這裡它會把圖元對映為最終螢幕上相應的畫素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器執行之前會執行 裁切 (Clipping)。裁切會丟棄超出你的檢視以外的所有畫素,用來提升執行效率。
OpenGL中的一個片段是OpenGL渲染一個畫素所需的所有資料。
- 片段著色器 的主要目的是計算一個畫素的最終顏色,這也是所有OpenGL高階效果產生的地方。通常,片段著色器包含3D場景的資料(比如光照、陰影、光的顏色等等),這些資料可以被用來計算最終畫素的顏色。
頂點輸入
開始繪製圖形之前,我們必須先給OpenGL輸入一些頂點資料。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有座標都是3D座標(x、y和z)。OpenGL不是簡單地把所有的3D座標變換為螢幕上的2D畫素;OpenGL僅當3D座標在3個軸(x、y和z)上都為-1.0到1.0的範圍內時才處理它。所有在所謂的 標準化裝置座標 (Normalized Device Coordinates)範圍內的座標才會最終呈現在螢幕上(在這個範圍以外的座標都不會顯示)。
GLfloat vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f,0.5f, 0.0f };
如下圖:

三角形.png
-
頂點著色器:會在GPU上建立記憶體用於儲存我們的頂點資料,還要配置OpenGL如何解釋這些記憶體,並且指定其如何傳送給顯示卡。頂點著色器接著會處理我們在記憶體中指定數量的頂點。
-
頂點緩衝物件(Vertex Buffer Objects, VBO):它會在GPU記憶體(通常被稱為視訊記憶體)中儲存大量頂點,使用這些緩衝物件的好處是我們可以一次性的傳送一大批資料到顯示卡上,而不是每個頂點發送一次。
-
glGenBuffers函式:生成頂點緩衝物件
GLuint VBO; glGenBuffers(1, &VBO);
- glBindBuffer函式把新建立的緩衝繫結到GL_ARRAY_BUFFER目標上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
- glBufferData函式:把之前定義的頂點資料複製到緩衝的記憶體中,是一個專門用來把使用者定義的資料複製到當前繫結緩衝的函式。它的第一個引數是目標緩衝的型別:頂點緩衝物件當前繫結到GL_ARRAY_BUFFER目標上。第二個引數指定傳輸資料的大小(以位元組為單位);用一個簡單的sizeof計算出頂點資料大小就行。第三個引數是我們希望傳送的實際資料:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
第四個引數指定了我們希望顯示卡如何管理給定的資料。它有三種形式:
(1)GL_STATIC_DRAW :資料不會或幾乎不會改變。
(2)GL_DYNAMIC_DRAW:資料會被改變很多。
(3)GL_STREAM_DRAW :資料每次繪製時都會改變。
三角形的位置資料不會改變,每次渲染呼叫時都保持原樣,所以它的使用型別最好是GL_STATIC_DRAW。如果,比如說一個緩衝中的資料將頻繁被改變,那麼使用的型別就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,這樣就能確保顯示卡把資料放在能夠高速寫入的記憶體部分。
頂點著色器
1.用著色器語言GLSL(OpenGL Shading Language)編寫頂點著色器,然後編譯這個著色器
attribute vec4 vPosition; void main(void) { gl_Position = vPosition; }
在頂點著色器中宣告所有的輸入頂點屬性(Input Vertex Attribute)。現在我們只關心位置(Position)資料,所以我們只需要一個頂點屬性。GLSL有一個向量資料型別,它包含1到4個float分量,包含的數量可以從它的字尾數字看出來。
向量(Vector)
簡明地表達了任意空間中的位置和方向,並且它有非常有用的數學屬性。在GLSL中一個向量有最多4個分量,每個分量值都代表空間中的一個座標,它們可以通過vec.x、vec.y、vec.z和vec.w來獲取。注意vec.w分量不是用作表達空間中的位置的(我們處理的是3D不是4D),而是用在所謂透視除法(Perspective Division)上。
為了設定頂點著色器的輸出,我們必須把位置資料賦值給預定義的gl_Position變數,它在幕後是vec4型別的
編譯著色器
- 我們把需要建立的著色器型別以引數形式提供給glCreateShader。由於我們正在建立一個頂點著色器,傳遞的引數是GL_VERTEX_SHADER。
//Create the shader object GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
- 下一步我們把這個著色器原始碼附加到著色器物件上,然後編譯它:
//Load the shader source const char * shaderStringUTF8 = [vertexShaderString UTF8String]; glShaderSource(vertexShader, 1, &shaderStringUTF8, NULL); //Compile the shader glCompileShader(vertexShader);
glShaderSource
函式把要編譯的著色器物件作為第一個引數。第二引數指定了傳遞的原始碼字串數量,這裡只有一個。第三個引數是頂點著色器真正的原始碼,第四個引數我們先設定為NULL。
- 檢測在呼叫glCompileShader後編譯是否成功了,如果沒成功的話,你還會希望知道錯誤是什麼,這樣你才能修復它們
//Check the compile status GLint compiled = 0; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &compiled); if (!compiled) { GLint infoLen = 0; glGetShaderiv(vertexShader, GL_INFO_LOG_LENGTH, &infoLen); if (infoLen > 1) { char *infoLog = malloc(sizeof(char)* infoLen); glGetShaderInfoLog(vertexShader, infoLen, NULL, infoLog); NSLog(@"Error compiling shader:\n%s\n",infoLog); free(infoLog); } glDeleteShader(vertexShader); return; }
片段著色器
void main() { gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0); }
片段著色器只需要一個輸出變數,這個變數是一個4分量向量,它表示的是最終的輸出顏色,我們應該自己將其計算出來。這裡我們命名為gl_FragColor。
- 編譯片段著色器的過程與頂點著色器類似,只不過我們使用GL_FRAGMENT_SHADER常量作為著色器型別:
//Create the shader object GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //Load the shader source const char * shaderStringUTF8 = [fragmentShaderString UTF8String]; glShaderSource(fragmentShader, 1, &shaderStringUTF8, NULL); //Compile the shader glCompileShader(fragmentShader);
著色器程式
著色器程式物件(Shader Program Object)是多個著色器合併之後並最終連結完成的版本。如果要使用剛才編譯的著色器我們必須把它們 連結 (Link)為一個著色器程式物件,然後在渲染物件的時候啟用這個著色器程式。已啟用著色器程式的著色器將在我們傳送渲染呼叫的時候被使用。當連結著色器至一個程式的時候,它會把每個著色器的輸出連結到下個著色器的輸入。當輸出和輸入不匹配的時候,你會得到一個連線錯誤。
- 建立一個程式物件很簡單:
// Create the program object GLuint shaderProgram = glCreateProgram();
glCreateProgram
函式建立一個程式,並返回新建立程式物件的ID引用。現在我們需要把之前編譯的著色器附加到程式物件上,然後用 glLinkProgram
連結它們:
glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram);
呼叫 glUseProgram
函式,用剛建立的程式物件作為它的引數,以啟用這個程式物件:
glUseProgram(shaderProgram);
在 glUseProgram
函式呼叫之後,每個著色器呼叫和渲染呼叫都會使用這個程式物件(也就是之前寫的著色器)了。
對了,在把著色器物件連結到程式物件以後,記得刪除著色器物件,我們不再需要它們了:
glDeleteShader(vertexShader); glDeleteShader(fragmentShader);
連結頂點屬性

頂點緩衝資料.png
- 位置資料被儲存為32位(4位元組)浮點值。
- 每個位置包含3個這樣的值。
- 在這3個值之間沒有空隙(或其他值)。這幾個值在陣列中
緊密排列
(Tightly Packed) - 資料中第一個值在緩衝開始的位置。
有了這些資訊我們就可以使用glVertexAttribPointer
函式告訴OpenGL該如何解析頂點資料(應用到逐個頂點屬性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
- 它可以把頂點屬性的位置值設定為0。因為我們希望把資料傳遞到這一個頂點屬性中,所以這裡我們傳入0(GLKVertexAttribPosition)。
- 第二個引數指定頂點屬性的大小。頂點屬性是一個
vec3
,它由3個值組成,所以大小是3 - 第三個引數指定資料的型別,這裡是GL_FLOAT(GLSL中vec*都是由浮點數值組成的)
- 下個引數定義我們是否希望資料被標準化(Normalize)。如果我們設定為
GL_TRUE
,所有資料都會被對映到0(對於有符號型signed資料是-1)到1之間。我們把它設定為GL_FALSE
- 第五個引數叫做
步長
(Stride),它告訴我們在連續的頂點屬性組之間的間隔。由於下個組位置資料在3個float
之後,我們把步長設定為3 * sizeof(float)
。要注意的是由於我們知道這個陣列是緊密排列的(在兩個頂點屬性之間沒有空隙)我們也可以設定為0來讓OpenGL決定具體步長是多少(只有當數值是緊密排列時才可用)。一旦我們有更多的頂點屬性,我們就必須更小心地定義每個頂點屬性之間的間隔,我們在後面會看到更多的例子(譯註: 這個引數的意思簡單說就是從這個屬性第二次出現的地方到整個陣列0位置之間有多少位元組) - 最後一個引數的型別是
void*
,所以需要我們進行這個奇怪的強制型別轉換。它表示位置資料在緩衝中起始位置的偏移量
(Offset)。由於位置資料在陣列的開頭,所以這裡是0。我們會在後面詳細解釋這個引數。 -
glEnableVertexAttribArray
,以頂點屬性位置值作為引數,啟用頂點屬性;頂點屬性預設是禁用的
// 0. 複製頂點陣列到緩衝中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 1. 設定頂點屬性指標 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 2. 當我們渲染一個物體時要使用著色器程式 glUseProgram(shaderProgram);
要想繪製我們想要的物體,OpenGL給我們提供了glDrawArrays函式,它使用當前啟用的著色器,之前定義的頂點屬性配置,和VBO的頂點資料(通過VAO間接繫結)來繪製圖元。
glEnableVertexAttribArray(_positionSlot); glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), NULL); glDrawArrays(GL_TRIANGLES, 0, 3);
大家一定好奇 _positionSlot
幹嘛的,其實他是頂點著色器裡面的vPosition,用來傳引數值的
// Get the attribute position slot from program _positionSlot = glGetAttribLocation(shaderProgram, "vPosition");
glDrawArrays函式第一個引數是我們打算繪製的OpenGL圖元的型別。由於我們在一開始時說過,我們希望繪製的是一個三角形,這裡傳遞GL_TRIANGLES給它。第二個引數指定了頂點陣列的起始索引,我們這裡填0。最後一個引數指定我們打算繪製多少個頂點,這裡是3(我們只從我們的資料中渲染一個三角形,它只有3個頂點長)。如圖,三角形誕生了!

你好,三角形.png