OpenGL ES 2.0 知識串講 (8) ——OpenGL ES 詳解II(傳入繪製信息)

出處:電子設備中的畫家|王爍 於 2017 年 7 月 13 日發表,原文鏈接(http://geekfaner.com/shineengine/blog9_OpenGLESv2_8.html)

 

上節回顧

上一節講述瞭如何通過 OpenGL ES 給 GPU 關聯一套可以使用的 shader,這 一套 shader 是被放在一個 program 中當作一個整體供 GPU 使用的。那麼 GPU 繪製圖片不止是需要這套 shader,還需要給這套 shader 傳遞一些必要的輸入參數, 比如想要繪製圖片的頂點位置,形狀,顏色等等信息,那麼這一節,將學習如何通過 OpenGL ES API 把這些繪製所需要的信息傳遞給 GPU。


繪製所需要的信息

想要繪製一幅圖片,最起碼需要預先想好要繪製什麼形狀的圖片,比如是繪製一個三角形還是一個圓形,這個圖片大小有多大,以及這個圖片應該是什麼顏色的。所以需要通過 OpenGL ES 的 API 把這些信息,比如圖片的頂點位置信息、頂點顏色信息等傳給 GPU。那麼這一節將詳細講解 OpenGL ES 中負責傳入繪製信息的 API。


OpenGL ES API 詳解

void glGenBuffers(GLsizei n, GLuint * buffers);

我們的目的是把數據傳給 GPU,專業一點的說法就是把 CPU 對應內存中的數據傳給 GPU 的內存中。比如在程序中分配一塊內存,在這塊內存中寫入要傳給 GPU 的東西,這塊內存是位於 CPU 對應的內存中。那麼傳遞的方式有兩種, 一種是直接把 CPU 內存中的數據賦值給 GPU 中對應的變量,另外一種是把 CPU 中的數據打包,然後在 GPU 中創建一個 buffer object,將 CPU 中打包的數據傳給 GPU 中的 buffer object,然後再在 GPU 中對這個 buffer 進行訪問和使用。第二種方法雖然代碼量會稍微多一點,但是會更加節省空間和時間消耗。之所以節約的原因有兩個,第一,由於我們要傳給 GPU 中的數據分爲很多種,比如頂點座標, 頂點顏色等,如果通過第一種方式傳遞,那麼我們要進行多次傳遞,而從 CPU 到 GPU 的傳遞是耗時又耗資源,所以如果我們把 CPU 內存中的數據打包,一次性把數據全部傳給 GPU 中的一個 buffer object 中,然後 GPU 只需要根據 buffer object 中數據偏移,就可以將該 buffer object 的某一塊分配給某個變量,另外一塊分配給另外一個變量了。原因二:可能一套頂點顏色會在多次繪製的時候,被 GPU 使用多次,比如先繪製一個三角形,要把頂點顏色傳進去一次,然後又繪製一個顏色完全一樣的三角形,那麼又要把頂點顏色傳進去一次,這樣就需要傳遞兩次數據,而如果用 buffer object,這個 buffer object 只要不被刪除,則會一直存在於 GPU 中,那麼 GPU 就可以多次使用這塊 buffer 了。

編寫程序要在保證功能的同時,注意對空間和時間的節約,所以一般都會使用第二種方法。而第二種方法,首先需要一個 buffer object。而 glGenBuffers 這 個 API,就是用於先創建 buffer object 的 name,然後再通過 API glBindBuffer 創建一個 buffer object。先說 glGenBuffers,glBindBuffer 這個 API 一會再進行說明。

這個函數的第一個輸入參數的意思是該 API 會生成 n 個 buffer object name, 當 n 小於 0 的時候,出現 INVALID_VALUE 的錯誤。第二個輸入參數用於保存被創建的 buffer object name。這些 buffer object name 其實也就是一些數字,而且假如一次性生成多個 buffer object name,那麼它們沒有必要必須是連續的數字。 buffer object name 是 uint 類型,而且 0 已經被預留了,所以肯定是一個大於 0 的整數。

這個函數沒有輸出參數。當創建成功的時候,會在第二個參數 buffers 中生成 n 個之前沒有使用過的 buffer objects 的 name。然後這些 name 會被標記爲已使用,而這個標記只對 glGenBuffers 這個 API 有效,也就是再通過這個 API 生成更多的 buffer object name 的時候,不會使用之前創建的這些 buffer objects name。 所以回憶一下,這一步其實只是創建了一些 buffer object name,而沒有真正的創建 buffer object。而只有在這些 buffer object name 被 glBindBuffer 進行 bind 之後, 纔會真正的創建對應的 buffer object。

void glBindBuffer(GLenum target, GLuint buffer);

上一個 API glGenBuffers 只創建了一些 bufferobject 的 name,然後在glBindBuffer 這個 API 再創建一個 buffer object。 這個函數的第一個輸入參數的意思是指定 buffer object 的類型,就好像shader 分爲 vertex shader 和 fragment shader 一樣。buffer object 也是分類型的, 在 OpenGL ES2.0 中,buffer object 分爲 VBO vertex buffer object 和 IBO index buffer object 兩種。那麼在這裏,第一個輸入參數必須是 GL_ARRAY_BUFFER 或者 GL_ELEMENT_ARRAY_BUFFER 。 其 中 GL_ARRAY_BUFFER 對應的是 VBO , GL_ELEMENT_ARRAY_BUFFER 對應的是 IBO。如果傳入其他的參數,就會報 INVALID_ENUM 的錯誤。第二個輸入參數爲剛纔 glGenBuffers 得到的 buffer object name。

這個函數沒有輸出參數,假如傳入的 buffer 是剛被創建的 buffer object name,而且它還沒有被創建和關聯一個 buffer object,那麼通過這個 API,就會生成一個制定類型的 buffer object,且與這個 buffer object name 關聯在一起,之後指定某個 buffer object name 的時候,也就相當於指定這個 buffer object。新創建的 buffer object 是一個空間爲 0,且初始狀態爲默認值的 buffer,初始狀態爲 GL_STATIC_DRAW。然後創建和關聯完畢之後,也就會把這個 buffer object 當作是當前 GPU 所使用的 VBO 或者 IBO 了。如果傳入的 buffer 已經有關聯的 buffer object 了,那麼只是把該 buffer object 指定爲當前 GPU 所使用的 VBO 或者 IBO。然後 GPU 之前使用的 VBO 或者 IBO 就不再是處於被使用狀態了。

所以回憶一下,通過 glGenBuffers 創建一些 buffer object name,然後通過 glBindBuffer,給 buffer object name 創建和關聯一個 buffer object,同時,通過這個 API,還將參數 buffer 對應的 buffer object 設置爲目前 GPU 所使用的 VBO 或者 IBO。雖然 GPU 中可以存放大量的 buffer object,但是同一時間一個 thread 的一 個 context 中只能有一個 VBO 和一個 IBO 是被使用着的。之後關於 buffer 的操作, 比如查詢 buffer 的狀態等,就會直接操作 VBO 或者 IBO,而不會在使用 buffer object name 了。所以,如果想使用某個 buffer object,必須先通過 glBindBuffer 這個 API,把這個 buffer 推出來,設置爲 GPU 當前的 VBO 或者 IBO。

初始狀態下 VBO 或者 IBO 都是與 buffer object name 爲 0 的 buffer object 綁 定的,然而其實 buffer object name 爲 0 的 buffer object 不是一個合法的 buffer object,所以如果當這個時候,對 VBO 或者 IBO 進行操作或者查詢等,就會報 GL_INVALID_OPERATIO 的錯誤。

一個 buffer object 可以多次與不同的 target 進行綁定,即使重複綁定也沒關係,因爲 GPU driver 的開發人員會想辦法去進行優化的。

當一個 buffer object 被 GPU 使用之後,任何對這個 buffer object 的操作都會影響其與 GPU 綁定的結果。比如一個 buffer object 被 GPU 使用之後,然後將該 buffer object 刪除,那麼原來本 context 中所有使用這個 buffer object 的 BO,就都會進行 reset,然後相當於與 buffer object name 爲 0 的 buffer object 進行綁定, 也就相當於重新回到沒有綁定任何 buffer 的狀態;而其他 context 或者其他線程如果也使用這個 buffer object,那麼刪除的時候不會有任何改變,但是一旦使用了這個被刪除了的 buffer,就會導致 undefine 的結果、GL Error、繪製中斷甚至是程序終止。

當 VBO 與 buffer object name 不爲 0 的 buffer object 綁定之後,也就說明了在 GPU 中對 shader 中的 attribute 賦值將通過 VBO,而非直接通過把 CPU 內存中的數據賦值給 GPU 中對應的變量了。通過 OpenGL ES 給 GPU 中正在使用的 shader 中的 attribute 賦值是通過 glVertexAttribPoint 這個 API,這個 API 一會會詳細進行講解,現在先簡單的說一下。如果通過 VBO 給 shader 中的 attribute 賦值,則這個 API 的最後一個參數只需要傳遞一個偏移量即可,否則的話,則需要傳入一個包含真正數據的數組或者指向真正數據的指針。可以通過 glGet 這個 API,參數 爲 GL_ARRAY_BUFFER_BINDING 這個參數查詢 VBO 綁定的 buffer object 的 name, 也可以通過 glGetVertexAttribiv , 參數爲 GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING,進行查詢。

當 IBO 與 buffer object name 不爲 0 的 buffer object 綁定之後,也就說明了 通過 glDrawElement 這個 API 進行繪製的時候,則這個 API 的最後一個參數只需要傳遞一個偏移量即可,否則的話,則需要傳入一個包含真正數據的數組或者指向真正數據的指針。這個 API 的其他功能,將在下一節進行說明。可以通過 glGet 這個 API,參數爲 GL_ELEMENT_ARRAY_BUFFER_BINDING 這個參數查詢 IBO 綁定 的 buffer object 的 name。

buffer object name 和對應的 buffer object 和 shader 以及 program 一樣,都屬於一個 namespace,也就是可以被多個 share context 進行共享。

void glBufferData(GLenumtarget, GLsizeiptr size, const GLvoid * data, GLenum usage);

創建了 buffer object 之後,就需要給這個 buffer object 賦予數據了,而 glBufferData 這個 API 就是通過 OpenGL ES,把 CPU 端保存的數據傳遞給 GPU 端, 保存在指定的 buffer object 中。

這個函數的第一個輸入參數的意思是指定 buffer object 的類型,可以是 GL_ARRAY_BUFFER 或者 GL_ELEMENT_ARRAY_BUFFER,剛纔已經說過了,在 bindbuffer 之後,對 buffer object 的操作,都會直接通過 target 進行操作,而不再通過 buffer object name 了(除非是對 buffer object 進行刪除),由於一個 context 只能有一個 VBO 和一個 IBO,所以在這裏通過 target 指定我們是操作 VBO 還是 IBO,就能精確的指定到實際操作的是哪個 buffer object。如果傳入其他的參數, 就會報 INVALID_ENUM 的錯誤。第二個和第三個輸入參數的意思是:data 是 CPU 中一塊指向保存實際數據的內存,而 size 爲 data 以機器單元爲單位的大小,size 不能爲負,否則會報 INVALID_VALUE 的錯誤。如果 data 不爲 null,那麼 data 中 的數據會被從 CPU 端 copy 到 GPU 端的 buffer object 中。如果 data 爲 null,那麼執行完這個 API 之後,依然會給 buffer object 分配 size 大小的內存,但是沒有對這塊內存進行初始化,也就是其中存儲的將是 undefine。data 中保存的數值一定要按照 CPU 端的對齊要求進行對齊。第四個參數剛纔在 bindbuffer 創建 buffer object 的時候提到過,指的是 buffer object 的 usage,也就是 buffer object 的數據存儲方式 ,剛纔說了,buffer object 的 usage 初始狀態下爲 GL_STATIC_DRAW。解 釋一下什麼是 GL_STATIC_DRAW。GL_STATIC_DRAW 意思就是程序暗示這個 buffer object 只會被賦值一次,然後在 GPU 中可能會多次讀取調用。除了 GL_STATIC_DRAW 之外,還有兩種 usage,分別是 GL_DYNAMIC_DRAW,意思就是程序暗示這個 buffer object 可能會被賦值多次,且在 GPU 中可能會多次讀取調用。 以及 GL_STREAM_DRAW,意思就是程序暗示這個 buffer object 只會被賦值一次, 然後在 GPU 中也只會被讀取調用少量的幾次。usage 的用法只適用於 performance 的暗示,通過傳入這個暗示,GPU driver 可以更好的選擇存儲 buffer object 的方 式和位置,更好的優化 buffer object 的讀寫效率。而 usage 並不影響使用的功能, 無論 usage 使用哪個,這個 buffer object 都可以被正常使用。如果傳入除了這三個 enum 之外的其他參數,就會報 INVALID_ENUM 的錯誤。 這個函數沒有輸出參數,但是有以下幾種情況會出錯,除了剛纔說的傳入的target 或者 usage 不合法會報 INVALID_ENUM 的錯,以及 size 爲負會報 GL_INVALID_VALUE 之外,還有如果 target 沒有對應一個合法的 buffer object,或 者說 target 對應的是不合法的 buffer object 0,那麼會報 GL_INVALID_OPERATION 的錯誤。而且由於這裏需要在 GPU 分配一大塊內存,所以可能會出現內存不夠的情況,而當內存不夠的時候,會報 GL_OUT_OF_MEMORY 的錯誤。在這裏解釋 一下 GL_OUT_OF_MEMORY ,出現這個錯誤代碼,說明了,GPU 中沒有足夠的內存去執行這個命令。這種錯誤,除了 GPU driver 中被打上了標記之外,還會導致 GL 的狀態爲 undefine。

如果在執行這個 API 之前 buffer object 中就存有數據,那麼會先把之前的數據全部刪掉,再把新的數據裝入 buffer object 中。執行完這個 API,會將 buffer object 的狀態更新,BUFFER_USAGE 設置爲這個 API 傳入的第四個參數, BUFFER_SIZE 會被設置爲傳入的第二個參數。

void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid * data);

這個 API 的功能和剛纔的 glBufferData 類似,顧名思義,剛纔那個 API 是給 buffer object 傳入數據,這個 glBufferSubData 是給 buffer object 的一部分傳入數據。

這個函數的第一個輸入參數和 glBufferData 的第一個輸入參數一樣,用於指定 bufferobject 的類型,如果傳入非 GL_ARRAY_BUFFER 或者 GL_ELEMENT_ARRAY_BUFFER 的參數,就會報 INVALID_ENUM 的錯誤。如果 target 沒有對應一個合法的 buffer object,或者說 target 對應的是不合法的 buffer object 0,那麼會報 GL_INVALID_OPERATION 的錯誤。 第二個和第三個和第四個輸入參數的意思是:以 buffer object 的開始爲起點,進行 offset 個位置的偏移,從這個位置開始,長度爲 size 個單位的這麼一塊空間,使用 data 指向的一塊 CPU 中的內存數據進行覆蓋。offset 和 size 的單位都是機器單元。如果 offset 或者 size 爲 負,或者 offset + size 大於 BUFFER_SIZE,那麼就會出現 INVALID_VALUE 的錯誤。 data 中保存的數值一定要按照 CPU 端的對齊要求進行對齊。

這個函數沒有輸出參數。用於更新 buffer object 的一部分,或者是全部數據。 由於 buffer object 的空間已經通過 glBufferData 分配好了,所以在這裏不會分配新的空間,也就不會出現 GL_OUT_OF_MEMORY 的錯誤。

有一點需要注意:通過這個 API 也可以覆蓋 buffer object 的全部數據,只需要將 offset 設置爲 0,size 設置爲 GL_BUFFER_SIZE,data 指向一組新的數據即可。 如果我們想更新 buffer object 中的全部內容的時候,理論上也可以直接通過再調用一次 glBufferData 進行重新賦值。但是建議使用 glBufferSubData 來操作這樣的事情。因爲 glBufferData 會牽扯到內存的重新分配,這樣也會比較耗費資源,而 glBufferSubData 則不會牽扯到內存的重新分配。

void glDeleteBuffers(GLsizei n, const GLuint * buffers);

當 buffer 不再被需要的時候,則可以通過 glDeleteBuffers 這個 API 把 buffer object name 刪除。

這個函數輸入參數的意思是該 API 會刪除 n 個 buffer object,當 n 小於 0 的 時候,出現 INVALID_VALUE 的錯誤。buffers 保存的就是這些 buffer object 的變量名。如果傳入的變量名爲 0,或者對應的不是一個合法的 buffer object,那麼 API 就會被忽略掉。

這個函數沒有輸出參數。當 buffer object 被刪除之後,其中的內容也會被刪掉,名字也會被釋放,可以被 glGenBuffers 重新使用。如果被刪除的 buffer 正在處於 bind 狀態,那麼就相當於先執行了一次 glBindBuffer 把對應的 binding 變成 binging 0,也就相當於什麼都沒有 bind 了,然後再進行刪除。

void glBindAttribLocation(GLuint program, GLuint index, const GLchar *name);

在說 GLSL 語法的時候介紹過 attribute,這個是 shader 中的一個存儲修飾符, 只存在於 VS 中。OpenGL ES 可以通過 API 得到指定 VS 中某個 attribute 的位置, 然後通過這個位置以及另外一個 OpenGL ES API,將數據傳輸到 VS 中。主要是用於傳入頂點座標、顏色等信息的。所以 attribute 有一個 OpenGL ES 可以獲取到的 location,然而這個 location 可以通過 OpenGL ES 的 API 直接指定,如果沒有指定的話,就會在 linkProgram 的時候,由 GPU 給 shader 中所有開發者自定義,且沒有被指定 location 的 active 的 attribute,分配一個 index。而 glBindAttribLocation 就是通過 OpenGL ES 的 API 給 attribute 指定 index,也可以說是指定 location。而 獲取 attribute location 的 API 叫做 glGetAttribLocation,把數據傳輸給 Attribute 的 API 叫做 glVertexAttribPointer。現在先來說 glBindAttribLocation,而另外兩個 API 一會再說。

這個函數的第一個輸入參數爲 program object,如果我們傳入一個不合法的 program object 數值,就會出現 INVALID_VALUE 的 error。第二個參數爲一個無符號數值,這個數值將會是這個 attribute 的 index 或者說是 attribute 的 location。 但是這個數值是有限制的,它不能大於或者等於 GL_MAX_VERTEX_ATTRIBS 的值, 否則會出現 GL_INVALID_VALUE 的錯誤,同時也會導致 link 失敗。然而區別於 shader 或者 program 以及 buffer object 的名字,shader 或者 program 或者 buffer object 的名字爲無符號的非 0 整數,然後 attribute 的 location 則可以是 0。第三個參數是一個字符串,用於保存 program 對應的 vertex shader 中的一個 attribute 的變量名。在這裏暫時不會檢測這個變量名是否真實有效,但是會檢測這個變量名不能以 gl_作爲前綴。在 GLSL 的語法中也說過,gl_作爲前綴的變量名,是被 GLSL 語法預留的,自定義的變量不能使用 gl_作爲前綴。如果使用了的話,則會 出現 GL_INVALID_OPERATION 的錯誤。GPU 會在執行這個 API 的時候把 name copy 過去了,也就是說,當執行完這個 API 之後,name 這個用於保存 attribute name 的字符就可以被 free 了。

這個函數沒有輸出參數。除了剛纔說的那些錯誤情況,還有,假如給一個 matrix 的 attribute bindlocation,但是由於 matrix 需要不止一個 location,比如 mat4 就需要 4 個 location,而且是需要 4 個連續的 location,那麼第二個參數 index 就是matrix第一列的location,index + 1就是matrix第二列的location,index + 2 就是 matrix 第三列的 location,index + 3 就是 matrix 第四列的 location,如果沒有 4 個連續的 location,linkprogram 也有可能會失敗。

這個 API 執行之後,會在下一次 link program 的時候生效,也就是,假如, 之前這個 program 已經被 link 一次了,attributeA 已經被自動分配了一個 location 1。然後再用這個 API 把 Attribute A 與 location 2 綁定,執行完這個 API 之後, attribute A 的 location 還是 1,使用 OpenGL ES API 去訪問這個 attribute 依然要用 location 1 去訪問。然而等這個 program 再次被 link 之後,attribute 的 location 才變成了 2。也就是說 program 再次被 link 之前,這個 API 的執行,在開發者看來是沒有任何功效的。

我們知道這個 API 需要在 linkprogram 之前執行,那麼其實它甚至可以在program attachShader 之前執行,也就是當 program 還沒有關聯的 shader 的時候 就可以執行了。因爲我們執行這個命令的時候只會判斷第三個參數是否有以 gl_ 作爲前綴,並不關心它是否是 program 對應 shader 中一個真實存在,或者是否是一個 active 的 attribute。當然,如果第三個參數其實不是一個 active 的 attribute 的話,那麼在 linkprogram 的時候,會直接忽略掉這次 bind 的。

這種 bind 屬於 GL 的當前狀態,也就是說它並不僅僅是一個 program 的行爲。 比如我們對一個 program A 的 attribute A 綁定爲 location 1,假如我們突然通過 glUseProgram 開始使用 program B 了,不再使用 program A 了,那麼假如 program B 中也有 Attribute A,那麼這個 Attribute A 的 location 也爲 1,這個需要特別注意的。

程序也可以給多個 attribute bind 同一個 location,這種現象叫做 aliasing,但是前提是被使用的 program 的 shader 中,只有其中的一個 attribute 是 active 的。 否則就會出現 link error。compiler 和 linker 默認認爲沒有這種 aliasing,並也相應的做了一頂的優化。如果一個 attribute 被 bind 了兩遍 location,那麼前一遍就會失效。所以你沒有辦法給一個 attribute bind 兩個 location。

不允許通過這個 API 去給內置的 attribute bind location,因爲如果需要的話它們會被自動 bind 的。

GLint glGetAttribLocation(GLuint program, const GLchar*name);

無論是通過 glBindAttribLocation 給 attribute 指定一個 location,還是通過 linkprogram GPU 自動給 attribute 分配一個 location,總歸 attribute 是有一個 location。那麼 glGetAttribLocation 這個 API,就是 OpenGL ES 去獲取指定 VS 中某個 attribute 的位置。獲取到這個位置之後,纔可以通過 OpenGL ES 的其他 API 對 這個 attribute 進行操作。

這個函數的第一個輸入參數爲 program object,如果我們傳入一個不合法的 program object 數值,或者這個 program 沒有被成功 link,那麼就會出現 GL_INVALID_OPERATION 的 error。其實我原本也以爲應該會出現 INVALID_VALUE 的錯誤,按照之前的慣例應該會出現 INVALID_VALUE 的錯誤,但是不管是 OpenGL ES 的 Spec 還是 khronos 的網站,都是說會出現 GL_INVALID_OPERATION 的錯誤, 那麼這裏大家要注意一下。第二個參數是一個字符串,用於保存 program 對應的 vertex shader 中的一個 attribute 的變量名。同樣的,在這裏暫時不會檢測這個變量名是否真實有效,同時也不會檢測是否以"gl_"作爲前綴。所以在這裏 name 只 需要是一個字符串即可。

這個函數有輸出參數,如果 name 指定的 attribute 爲這個 program 中的一個 active 的 attribute,則輸出該 program 最近一次被 link 的時候該 program 中 name 指定的這個 attribute 的 location。如果 program 一共被 link 了兩次,第一次 attribute 的 location 爲 1,第二次 link 的時候 location 爲 2,然後再執行一次 glBindAttribLocation 把 attribute 的 location 設置爲 3.那麼這個時候執行這個 API, 獲取這個 attribute 的 location 就是 2。如果 attribute 爲一個 matrix,那麼則返回 matrix 第一列的 location。如果 name 以“gl_”作爲前綴,或者指定的並非是一個 active 的 attribute,或者甚至是一個在當前 program 並不存在的 attribute,或者這個 API 執行的時候出現了以上說過的那些錯誤,那麼就會返回-1。由於 0 也 是一個合法的 attribute 的 location,所以這裏返回的是-1。

void glEnableVertexAttribArray(GLuint index);

獲取了 attribute 的 location 之後,在 OpenGL ES 以及 GPU 真正使用這個 attribute 之前,還需要通過 glEnableVertexAttribArray 這個 API,對這個 attribute 進行 enable。如果不 enable 的話,這個 attribute 的值無法被訪問,比如無法通過 OpenGL ES 給這個 Attribute 賦值。更嚴重的是,如果不 enable 的話,由於 attribute 的值無法訪問,GPU 甚至在通過 glDrawArray 或者 glDrawElement 這 2 個 API 進行繪製的時候都無法使用這個 attribute。

這個函數的輸入參數爲 attribute 的 location,剛纔說了,當 OpenGL ES 獲取 到 attribute 的 location 之後,就可以通過這個 location 來對 attribute 進行操作。 但是這個數值是有限制的,它不能大於或者等於 GL_MAX_VERTEX_ATTRIBS 的值, 否則會出現 GL_INVALID_VALUE 的錯誤,正常的 attribute location 應該是小於這個值的,所以如果寫了超過這個值的話,肯定是錯誤的。

這個函數沒有輸出參數。默認狀態下所有開發者自定義的 attribute 都是 disable 的,如果想使用,一定要先 enable。

void glDisableVertexAttribArray (GLuint index);

有了 enable,就一定要有 disable。當繪製結束之後,就可以把沒用了的 attribute 通過 glDisableVertexAttribArray disable , 將指定 program 中 的某個 attribute 的開關關閉。關閉後,在繪製的時候,GPU 就無法訪問到 attribute 對應的值。 這個函數的輸入參數也是 attribute 的 location,它不能大於或者等於 GL_MAX_VERTEX_ATTRIBS 的值,否則會出現 GL_INVALID_VALUE 的錯誤。

這個函數沒有輸出參數。

void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);

剛纔做了那麼多準備工作,獲取到 attribute 的 location,然後 enable attribute, 目的就是往 attribute 傳值。而最主要的傳值 API 就是 glVertexAttribPoint,用處是 從 OpenGL ES 向 VS 中傳輸數據。在準備通過這個 API 傳值之前,那些實際的值可能存放在 CPU 中,比如以一個指針或者數據的形式存放着,也有可能存放在 GPU 端,通過剛纔創建並賦值了的 buffer object 保存着。不管存放在哪裏,都可以通過這個 API 給 attribute 賦值。然後被賦值後的 attribute 就代表着若干個頂點的座標或者顏色等等其他信息。

這個函數的第一個輸入參數是 attribute 的 location,它不能大於或者等於 GL_MAX_VERTEX_ATTRIBS 的值,否則會出現 GL_INVALID_VALUE 的錯誤。第二個參數是 size,回憶一下說 GLSL 語法的時候說過的東西,當時說假如想繪製一個三角形,那麼需要設定三個頂點,這個三個頂點都會經過 VS 的運算得到三個頂點真正的世界座標值,然後對這三個頂點進行光珊化,會生成組成三角形的無數個頂點,這些頂點都會經過 PS,通過 PS 的運算得到這些頂點的顏色值。所以 VS 被執行了三遍,這三遍都是使用 VS 這個函數,不同的是這三次函數的輸入不同, 導致輸出結果不同。而 VS 這個函數的輸入就是 attribute 等值。所以通過 glVertexAttribPoint 傳給 attribute 的值,就是會分爲多個單元,每個頂點認領一個單元,每個單元包含 size 個變量。size 默認爲 4,也就是說如果想要繪製一個三角形,那麼就需要傳進來三個單元,乘以每個單元 4 個值,也就是要通過這個 API 傳進來 12 個值。然後每個頂點的 attribute 依次認領 4 個值,這樣三個頂點的 attribute 就準備好了。size 只能是 1、2、3、4,也比較容易理解,因爲 GLSL 中的變量最大也就是 vec4 和 mat4。而如果是 mat4 的 attribute,還會根據列被拆分成 attribute,所以一個 attribute 最多也就 4 個分量,如果 size 沒有使用這四個值其中的任何一個,就會出現 INVALID_VALUE 的錯誤。第三個參數是 type,用於指定存儲的數據的數據類型,比如 GL_BYTE,GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_FIXED, 或者是 GL_FLOAT ,默認是 GL_FLOAT,如果 type 不是這些值中的一個,那麼出現 INVALID_ENUM 的錯誤。第四個參數 normalized 意思是如果賦值給 attribute 的值爲定點值或者整數值,在傳遞給 attribute,轉化爲 float 值的時候,是否需要被歸一化,歸一化就是帶符號的值轉化爲(-1,1), 不帶符號的值轉化爲(0,1)。true 的話爲需要歸一化,false 的話就是不需要歸一化。一般我傳遞頂點或者顏色座標都是直接使用歸一化之後的值傳遞,這樣就不需要歸一化了,減少 GPU 的負擔。第五個參數 stride 是間隔的意思,舉個例子, 剛纔爲了畫三角形,需要傳入 12 個值。那麼可以直接創建一個 12 個值的數組傳入,也可以創建一個 15 個值的數組傳入,其中第 5、10、15 這 4 個值爲無用值, 第 1234、6789、1112131415 這 12 個值是有用的,然後把 size 寫爲 4,stride 寫 爲 5,GPU 就知道 5 個值爲一個單元進行讀取,然後前四個爲有效值,使用前四個對 attribute 進行賦值。這就是 stride 的功能。如果直接創建 12 個值的數組傳入,size 寫爲 4,stride 也爲 4,或者 stride 可以爲 0。因爲爲 0 的話,GPU 也知道先給第一個 attribute 賦值 size 個值,然後緊挨着的 size 個值賦值給第二個 attribute。stride 默認爲 0,如果 stride 小於 0,則出現 INVALID_VALUE 的錯誤。 最後一個參數 pointer 非常重要,它分爲兩種情況,假如實際數據保存在 CPU 端, 那麼 pointer 就是一個指向實際數據存放位置的指針或者數組地址。如果實際數據保存在 GPU 的 VBO 中,那麼 pointer 就傳入一個偏移,意思就是從 VBO 的某一位開始,從之後的那些數值讀取 stride 或者 size 爲一個單元,將 size 個數值爲有效數據,頂點數個單元的值作爲 attribute 的值。

這個函數沒有輸出參數。

void glVertexAttrib*f(GLuint index, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);

除了上面那個 API glVertexAttribPoint 用於給 attribute 賦值之外,還有一些簡單的給 attribute 賦值的 API,這些 API 就是 glVertexAttrib*,*中包含三部分內容, 首先是一個數字,可以是 1 或者 2 或者 3 或者 4,第二個是個字母,可以有 f, 也可以沒有 f,第三個是 v,可以有 v,也可以沒有 v。先說這組 API 和剛纔那個 API 的區別,剛纔那個 API,我們針對三個頂點可以傳入三組不同的單元,導致三個頂點的 attribute 都不同,而使用這組 API,所有頂點的 attribute 都一樣了。 所以通過這個 API,指定了一個 attribute 之後,也就最多傳入了四個值用於給所有頂點的 attribute 賦值。

通過剛纔 API 的說明,知道了每個 attribute 最多由 4 個值組成,那麼假如 attribute 由 1 個值組成,那麼這裏使用 glVertexAttrib1f 傳入 1 個值,那麼正好這個值就賦給了這個 attribute,但是假如 attribute 由 4 個值組成,而使用 glVertexAttrib1f 傳入 1 個值,那麼其他三個值就使用默認值,第二個和第三個爲 0,第四個位 1;依此類推,如果使用 glVertexAttrib2f 傳入 2 個值,那麼其他兩個值就使用默認值,第三個爲 0,第四個位 1;如果使用 glVertexAttrib3f 傳入 3 個值,那麼其他一個值就使用默認值,第四個位 1。如果使用帶 f 結尾的 API, 比如 glVertexAttrib4f,意思也就是傳入的值爲 float 類型,如果使用帶 v 結尾的 API,比如 glVertexAttrib4fv,和使用 glVertexAttrib4f 唯一的區別也就是傳入參數不同,使用 glVertexAttrib4f,需要傳入四個值,使用 glVertexAttrib4fv,只需要傳入一個包含四個值的指針即可。

這個函數的第一個輸入參數是 attribute 的 location,它不能大於或者等於 GL_MAX_VERTEX_ATTRIBS 的值,否則會出現 GL_INVALID_VALUE 的錯誤。後面的輸入參數,根據 API 的名稱不同,傳入的參數不同。總之是會根據 API 的變量名 傳入對應的參數。

這個函數沒有輸出參數。使用這組 API,雖然會導致每個頂點的 Attribute 都一樣,但是每個頂點一樣可以支持 MAX_VERTEX_ATTRONS 個 Attribute。也支持 matrix 的 attribute。

GLint glGetUniformLocation(GLuint program, const GLchar *name);

在說 glLinkProgram 的時候,說過 linkprogram 的時候,會給沒有指定 index 的 attribute 分配一個 location,也會把所有 shader 中開發者自定義的 active 的 uniform 初始化爲 0,並且分配給其一個地址。而每次 relink 之後,uniform 的 location 就會被釋放掉,然後再重新分配一個新的 location,當然新的 location 和 舊的 location 也有可能相同。而 glGetUniformLocation 這個 API 就是用於獲取 uniform 的 location 的。

關於 uniform 的定義和用法,在 GLSL 語法中已經詳細說過了,在這裏再簡單的講幾點,一是 uniform 是存在於 VS 和 PS 之中的,區別於 attribute 只存在與 VS 中,二是 uniform 針對不同的點值是一樣的,區別於 attribute 針對每個點可以一樣(比如用 glVertexAttrib*傳入的時候),也可以不一樣(比如用 glVertexAttribPoint)。三、uniform 值是隻讀的。四、若 compiler 和 linker 認爲 unform 會被使用或者不確定是否會被使用,都會認爲 uniform 是 active 的。五、它們的值一旦被 load 進來,program 開始 use 之後就一直保持着,直到 program relink。

這個函數的第一個輸入參數爲 program object,如果傳入一個不合法的 program object 數值,那麼就會出現 INVALID_VALUE 的 error。如果這個 program 沒有被成功 link,那麼就會出現 GL_INVALID_OPERATION 的 error。第二個參數是一個字符串,用於保存 program 對應的一個 uniform 的變量名。同樣的,在這裏暫時不會檢測這個變量名是否真實有效,同時也不會檢測是否以"gl_"作爲前綴。 所以在這裏 name 只需要是一個字符串即可,但是要注意,這個字符串不能包含空格。

這個函數有輸出參數,如果 name 以“gl_”作爲前綴,或者指定的並非是一個 active 的 uniform,或者甚至是一個在當前 program 並不存在的 attribute,或 者這個 API 執行的時候出現了以上說過的那些錯誤,那麼就會返回-1。還記得 attribute 可以是 vec4 或者 mat4。而 unform 甚至可以是一個數組,一個結構體等等,這邊不能將 name 設置爲結構體、結構體數組或者 vector、matrix 的一部分, 以直接獲取它們對應的地址,只能通過.或者[]的形式傳入數組或者結構體中的一個成員,然後獲取到該成員的地址。比如數組中第一個元素的地址就用數組名加 [0]來充當 name,來獲取第一個元素地址。而數組的地址也就是它第一個元素的地址,或者直接把數組名通過 name 傳入獲取的地址。

void glUniform*iv(GLint location, GLsizei count, const GLint *value);

這個是給 uniform 進行賦值,有點類似 glVertexAttrib*,都是在獲取到 location 之後進行賦值。但是又存在一定的區別。

glUniform*這一套 API 一共分爲 3 種,根據 API 的名稱不同,傳入的參數不同。

第一種是直接以值的形式把數據傳遞給 Uniform,比如 glUniform1f, glUniform4f,glUniform4i 等。這個類型的 API 主要是用於當 shader 中的 uniform 爲 float、int、vec 類型,且不是 array 的情況,給 uniform 賦值的。

這種 API 的第一個輸入參數是 attribute 的 location,它必須是一個合法的 uniform 的 location 或者-1,否則會出現 GL_INVALID_OPERATION 的錯誤。如果 location 爲-1,那麼這個 API 就會被忽略掉,也不會改變任何 uniform 的值。

後面的參數根據 API 的不同而不同,比如 glUniform1f,它就一共只有 2 個參 數,剛纔已經說了第一個參數,而第二個參數就是一個數值,這個數值就是用於 給 uniform 賦予的值。

而比如 glUniform4f,它一共就有 5 個參數,除了剛纔所說的第一個參數,剩下的四個參數,就是用於給 uniform 賦予的值。

第二種是以指針形式把數據傳給 uniform,比如 glUniform1fv、glUniform4fv、 glUniform4iv 等。這個類型的 API 主要是用於當 shader 中的 uniform 爲 float、int、 vec 類型,而且允許 uniform 是 array 的情況,給 uniform 賦值的。

這個類型的 API 輸入參數就都一樣了。第一個輸入參數也是 attribute 的 location,它必須是一個合法的 uniform 的 location 或者-1,否則會出現 GL_INVALID_OPERATION 的錯誤。

第二個參數和第三個參數的意思是,count 代表將要改變指定的這個 uniform 數組一共由幾個數組元素組成,可以是把數組全部賦值,或者部分賦值,而 value 就是指向實際存放要賦予給 uniform 的值的地址。如果 count 大於 1,但是指向 的 uniform 卻不是一個 array,則會出現 GL_INVALID_OPERATION 的錯誤,且對 unform 的賦值無效。如果 count 小於 0,則出現 GL_INVALID_VALUE 的錯誤。

比如 glUniform1fv,而指定的 uniform 是一個具有三個元素的數組,那麼第二個參數 count 爲 3,value 則是一個指向 3*1 個 float 值的一塊內存的指針。

再比如 glUniform4iv,而指定的 uniform 是一個具有五個元素的數組,那麼 第二個參數 count 爲 5,value 則是一個指向 5*4 個 int 值的一塊內存的指針。如果 value 指向一塊更大的內存,那麼超過 5*4 個 int 值之外的值就被忽略了。

第三種是以指針的形式把數據傳給 uniform,比如 glUniformMatrix2fv、 glUniformMatrix4fv。它和第二種 API 的區別在於這個類型的 API 主要用於當 shader 中的 uniform 爲 matrix 類型,它也支持 uniform 爲 array。

這個類型的 API 輸入參數也都一樣了。第一個輸入參數也是 attribute 的 location,它必須是一個合法的 uniform 的 location 或者-1,否則會出現 GL_INVALID_OPERATION 的錯誤。

第二個參數和第四個參數的意思是,count 代表指定的這個 uniform 一共由 幾個數組元素組成,而 value 就是指向實際存放要賦予給 uniform 的值的地址。

比如 glUniformMatrix2fv,而指定的 uniform 是一個具有 3 個元素的數組,那麼第二個參數 count 爲 3,value 則是一個指向 3*2*2 個 float 值的一塊內存的指針。

再比如 glUniformMatrix4fv,而指定的 uniform 是一個具有五個元素的數組, 那麼第二個參數 count 爲 5,value 則是一個指向 5*4*4 個 float 值的一塊內存的指針。

第三個參數 transpose 必須爲 GL_FALSE,意思就是指定給 matrix 賦值的時候, 是以列爲單位進行賦值,第一列賦值完畢再賦值第二列。如果使用別的參數,就 會出現 GL_INVALID_VALUE 的錯誤,而且給 uniform 的賦值失敗。

所以總結一下 glUniform*和 glAttrib*,區別一共有 3 個,1.attribute 針對每個點,值都可以不一樣,而 uniform 針對每個點,值都一樣。2.attribute 如果是 array,那麼賦值也要以數字元素爲單位進行賦值,一次只能賦值給一個數組元素, 而 uniform 爲 array 的話,可以一次性把這個 array 中的值全部賦值了。3.雖然 attribute 和 uniform 都支持 matrix 類型,但是通過 glVertexAttribPoint 或者 glVertexAttrib 給 attribute 傳值的時候,一次雖然可以傳遞多個單元,但是每個單元最多隻能傳遞 4 個 float,而 glUniform 可以直接傳入一個 matrix 類型的值。

在第一種和第二種 API 中,分別有一個 API 是用於給 sample 賦值的, glUniform1i 和 glUniform1iv,分別用於給 sample 以及 sample 數組賦值。我們知道 sample 是特殊的 uniform,相當於變量類型爲 int 的 uniform,用於保存 texture object。如果通過其他 API,會因爲 size 或者 type 不同,而出現 GL_INVALID_OPERATION 的錯誤。

剛纔說到了 float 和 int 類型,但是如果 uniform 爲 bool 類型,那麼不管是用 glUniform*i 或者 glUniform*f 都可以。GL 會自動轉化類型。如果輸入參數爲 0 或 者 0.0,則爲 false,否則的話則爲 true。

說了這麼多了 glUniform*的 API,在傳值的時候,一定要嚴格根據 uniform 的 尺寸和類型來選擇適合的 API,假如 size 不匹配,比如 uniform 爲 vec3,但是使用了 glUniform2fv,則會出現 GL_INVALID_OPERATION 的錯誤。而假如類型不匹配,除了 bool 的 uniform 會自動轉化,其他都不會自動轉化,比如 uniform 爲 vec3, 但是使用了 glUniform3iv,則會出現 GL_INVALID_OPERATION 的錯誤。出現了這樣 的錯誤,uniform 的賦值也會無效。

這個函數沒有輸出參數。除了出現以上所說的那些錯誤,還有,如果當前沒有 program 被使用,那麼則會出現 GL_INVALID_OPERATION 的錯誤。

這些 uniform 會保持這些賦予的值,直到 program 再次被 link,因爲再次 link 的時候,這些 uniform 值又會被初始化爲 0。

以上這些 API 就是 OpenGL ES 往 GPU 的 Shader 中傳遞 attribute 和 uniform 信息的主要 API。那麼 OpenGL ES 中關於 attribute 和 uniform 的 API 除此之外, 還有很多 API 是用於查詢服務的,比如 glGetBufferParameteriv 這個 API 是用於查詢指定 buffer 的參數,查詢的對象只能是當前被使用的 VBO 或者 IBO,查詢的是 buffer 的 GL_BUFFER_SIZE 或者是 GL_BUFFER_USAGE。比如 API glIsBuffer 用於查詢傳入的參數,是否是一個合法的 buffer。比如 API glGetVertexAttrib 和 glGetActiveAttrib,是用於查詢 attribute 的屬性的,而 API glGetVertexAttribPointerv 是用於查詢 attribute 的地址的。比如 API glGetActieUniform,是用於查詢 uniform 的屬性,而 API glGetUniform 用於查詢指定 uniform 的值。

傳入了頂點信息,準備好了shader,下面就可以使用API,觸發GPU開始渲染了。

void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);

我們在EGL中,通過eglCreateWindowSurface的時候,創建了一塊繪製buffer,這塊buffer的大小爲屏幕分辨率的大小。然而雖然這塊buffer的大小已經確定了,但是並不代表我們使用這塊buffer進行繪製的時候,一定要完全按照這個buffer的大小去繪製。就好比我們有一塊A4的畫紙,那麼我們想畫一個太陽和一間房子,我們會分兩次去進行繪製,第一次會在畫紙的左上角先繪製太陽,第二次會在畫紙的右下角繪製一間房子。那麼第一次繪製的時候,我們只要把繪製區域設定在左上角即可,而第二次繪製,即把繪製區域設定在右下角。而glViewPort這個API,就是用於設定繪製區域的。

設定繪製區域會對繪製出來的結果產生很大的影響,記得我們在通過glVertexAttribPoint給shader中的attribute傳值的時候,假設這個attribute剛好是用於表示頂點座標的變量。那麼我們說到在傳值的時候可以選擇是否歸一化,我也說過我一般傳進去的值本身就是歸一化好的值,對於頂點座標,就好比我傳入了一個(0.5, 0.5, 0, 1),這個點看上去就是畫紙的中間點,然而假如我們的屏幕分辨率爲1080*720,那麼我們通過egl生成的繪製buffer的大小就是1080*720,然後假如我們通過glViewport設置的繪製區域爲從畫紙的左下角開始,寬高爲1080*720。那麼繪製區域就是整個畫紙,而我們這個(0.5, 0.5, 0, 1)的點就是座標爲(540,360)的點。然而假如我們通過glViewport設置的繪製區域爲從畫紙的中間點開始,寬高爲540*360。那麼繪製區域就是畫紙的右上角部分,而我們這個(0.5, 0.5, 0, 1)的點就是座標爲(810,540)的點。再舉個例子,假如我們想要繪製(0.5, 0.5, 0, 1)這個點到(0.5, 1, 0, 1)這個點的一條線,而我們通過glViewport設置的繪製區域爲從畫紙的左下角開始,寬高爲1080*720。也就是繪製區域就是整個畫紙,那麼我們畫的這條線從(540, 360), 到(540, 720),長度爲360個單位。假如我們通過glViewport設置的繪製區域爲從畫紙的中間點開始,寬高爲540*360。那麼繪製區域就是畫紙的右上角部分,那麼我們畫的這條線從(810, 540), 到(810, 720),長度爲180個單位。長度是剛纔的一半。

所以繪製區域不同,直接導致了繪製的位置以及繪製圖片的大小。

這個函數的前兩個輸入參數代表着繪製區域的左下角頂點在整個繪製buffer中的位置,初始值爲(0, 0)。後兩個輸入參數代表着繪製區域的寬和長,初始值爲屏幕分辨率的尺寸(所以如果沒有調用這個API,則相當於調用了glviewport(0, 0, screen_width, screen_height))。後面兩個代表寬和長的參數是有限制的,不能爲負,否則會出現GL_INVALID_VALUE的錯誤。當然它們也不能太大,GPU中有對它們設定了最大值,可以通過glGet這個API,參數爲GL_MAX_VIEWPORT_DIMS進行查詢。如果超過了的話,GPU會自動對這個值進行clamp。當然GL_MAX_VIEWPORT_DIMS肯定會大於或者等於屏幕實際分辨率的尺寸的。

這個函數沒有輸出參數。被歸一化之後的頂點座標會根據輸入參數計算出最終在屏幕上的頂點座標。比如歸一化之後的頂點座標爲(0.5, 0.5),繪製區域爲從(0, 0)到(1080, 720),那麼最終顯示在屏幕上的點爲0.5*1080 + 0.5 = 540.5,0.5*720 + 0.5 = 360.5。

通過以上的API,我們給繪製圖片已經做了充足的準備工作,傳入了需要傳入的值,設定好了繪製區域,那麼下面我們會說三個繪製API。

void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);

glClear,用於清理繪製buffer。因爲我們通過egl創建的繪製buffer,其實也就是一塊內存,但是這個內存在剛被創建好的時候,並不會被初始化,那麼根據平臺不同,裏面保存的東西可能不同,就好比我們雖然準備了一張畫紙,但是這個畫紙上面可能原本就有東西,這些東西是不確定的,所以我們先要把這張畫圖塗上底色,而我們可能希望這個畫紙的底色是白色,也有可能希望是黑色的,或者其他顏色,而glClearColor這個API,就是設定一種顏色,作爲清理繪製buffer所使用的顏色。

這個函數一共有4個輸入參數,分別是rgba四個值,用於確定一種特定的顏色,供清理繪製buffer使用的。這4個輸入參數初始值都爲0。在輸入的時候這4個值可以隨意填寫,但是會被閉區間[0,1]進行clamp。

這個函數沒有輸出參數,通過這個API確定的顏色,會被用於glClear,用於清理繪製buffer使用。

void glClear(GLbitfield mask);

這是我們將要學習的第一個繪製API,用於使用預先設定的值去清理繪製buffer,比如我們想要清理繪製buffer的顏色,那麼通過剛纔的glclearcolor API,我們已經確定好一種顏色了,然後通過這個API,就可以調用GPU,對繪製buffer的顏色根據剛纔設定的顏色進行清理。

這個函數的輸入參數爲一個mask值,用處是指定繪製buffer中的哪一塊被清理,因爲繪製buffer中主要分爲三塊buffer,color buffer、depth buffer、stencil buffer。剛纔我們已經學習瞭如何設置默認的清理繪製buffer的顏色,然後我們只要這裏傳入GL_COLOR_BUFFER_BIT,就可以使用那個顏色去清理繪製buffer了。而還有另外兩個API,glClearDepth和glClearStencil,是用於設定默認的清理繪製buffer的depth和stencil值,然後在這裏需要傳入GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT,去清理繪製buffer的depth和stencil。如果傳入除了這三個值之外的其他值,就會出現GL_INVALID_VALUE的錯誤。由於depth和stencil並非每個OpenGL ES程序都會需要的了,這些API我們將放在之後的課程再進行講解。

這個函數沒有輸出參數。API會根據傳入的參數去清理繪製buffer中的對應模塊,比如清理color、depth、stencil模塊,可以只清理一個模塊,也可以通過一個API調用,清理多個模塊。但是假如繪製buffer原本就沒有包含該模塊,比如很多GPU的格式是不支持stencil的,那麼glClear stencil就會沒有任何效果。還有很多別的GL狀態會影響glclear的結果,比如scissor test,scissor的意思就是在繪製區域中再設定一個小的繪製區域,假如屏幕分辨率爲1080,720,viewport爲(0,0)到(1080, 720),然後當OpenGL ES開啓了scissor之後,再通過glScissor設定小的繪製區域爲(0,0)到(1,1),那麼再執行glClear,就不會clear一整塊繪製buffer,而是隻會clear那一小塊繪製區域。再比如glColorMask,glColorMask是限定了繪製buffer中的那些顏色分量可以被寫,默認是四個顏色分量都可以寫,但是也可以限定只能r通道可寫,然後假如glClearColor設定爲(1,1,1,1),也就是準備clear成白色,但是由於只有r通道可寫,那麼其實是會被clear成紅色。初次之外,還有dither也會影響到glClear

glClear也會clear multisample的color/depth/stencil buffer。

void glColorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);

當PS結束後,Mask可以限制color、depth、stencil是否可以寫入對應的buffer。比如這個glColorMask API就是用於控制color buffer R\G\B\A通道的寫入權限的。

這個函數一共有4個輸入參數,分別是rgba四個值,用於確定RGBA四個通道是否有寫入權限,如果傳入的爲GL_TRUE,則說明該通道有寫入權限。初始狀態下,默認RGBA四個通道都是可寫的。當傳入GL_FALSE的時候,所有的color buffers的R通道將都不可寫。無法只控制某個通道的權限,要麼就一次性控制四個通道。

這個函數沒有輸出參數。

void glDrawArrays(GLenum mode, GLint first, GLsizei count);

如果說剛纔的glClear只是清理一下繪製buffer,雖然也是對繪製buffer的操作,改變了繪製buffer中的顏色等值,那麼這個glDrawArray,就是一個使用我們傳入的頂點信息,在繪製buffer中真正進行繪製的API。還記得我們在說attribute的時候,說過如果我們想繪製一個三角形,那麼需要傳入三個點的位置、顏色等信息。這些信息是會保存在attribute中的。所以通過對attribute的賦值,GPU那邊已經保存了三個頂點的信息。然後我們就需要通過glDrawArray這個API傳入參數,告知GPU,使用這三個頂點其中的哪幾個頂點進行繪製,而且把用於繪製的點,如何進行圖元裝配。

先說這個函數的第二個和第三個輸入參數。它們的意思是,通過這個API繪製,會使用到GPU中保存的那些頂點中,從第first個頂點開始,到first+count個頂點結束,使用count個頂點作爲繪製的點。如果GPU中還保存有其他的頂點,那麼那些頂點在這次繪製中就會被忽略掉。其中first和count不能小於0,否則會導致undefine的行爲發生,且會產生GL_INVALID_VALUE的錯誤。這個函數的第一個輸入參數爲一個枚舉值,用於指定通過這些頂點去繪製什麼樣的圖元。可以是GL_POINTS,意思就是會把這些頂點在圖紙上繪製成一個個的點,也可以是GL_LINE_STRIP, 意思就是一個連續的線段,第一個頂點作爲第一條線段的起點,第二個頂點作爲第一條線段的終點和第二條線段的起點,以此類推,第i個頂點作爲第i-1條線段的終點和第i條線段的起點。最後一個頂點,作爲之前一條線段的終點。如果只是用一個頂點去繪製,那麼這個繪製API就毫無意義。這樣,最終,繪製出來的要麼是什麼都沒繪製出來,要麼是一個線段,要麼是一個折線。也可以是GL_LINE_LOOP,這樣的話和GL_LINE_STRIP基本一樣,唯一的區別就是最後一個頂點會和第一個頂點連在一起。 如果只是用一個頂點去繪製,那麼這個繪製API就毫無意義。這樣,最終,繪製出來的要麼是什麼都沒繪製出來,要麼是一個線段,要麼是一個封閉的折線。 GL_LINES,也是用於繪製線段的,但是和剛纔兩個不同,這次繪製的線段不是連續的。指定這種格式之後,會把第1個和第二個點組成一個線段,第三個和第四個點組成一個線段,而這兩條線段是獨立的,依次類推,如果一共有2i或者2i+1個點,就會畫出i個線段。可以看出,如果用於繪製的頂點數爲奇數,那麼最後一個頂點會被忽略掉。GL_TRIANGLE_STRIP,用於繪製三角形,會把第一第二第三個點連成第一個三角形,注意連接的順序,123和321的連接方法,在圖形學中是完全不一樣的,圖形學會通過這種順時針還是逆時針來判斷該圖元爲正面還是背面,之後會被用於剔除使用。而剔除這些特性屬於並非每個OpenGL ES程序都會需要的了,這些API我們將放在之後的課程再進行講解。 在這裏我們先要知道連接的順序很重要。所以在這裏,是把123組成一個三角形,然後324再組成一個三角形,這個三角形與第一個三角形是有一條邊是共享的,然後345再組成一個三角形,這個三角形與第二個三角形有一條邊是共享的。所以通過這個格式,我們得到的是一堆共享邊的三角形,且它們要麼全部是順時針,要麼全部是逆時針的畫法。如果只是用一個或者兩個頂點去繪製,那麼這個繪製API就毫無意義。 GL_TRIANGLE_FAN,也是用於繪製三角形,而且顧名思義,是要繪製成一個扇形三角形。那麼會把123組成一個三角形,134組成第二個三角形,145組成第三個三角形,以此類推,把1,i-1,i組成最後一個三角形,而這些三角形也是都有一條邊是共享的。 所以通過這個格式,我們得到的也是一堆共享邊的三角形,而且它們要麼全部是順時針,要麼全部是逆時針的畫法。如果只是用一個或者兩個頂點去繪製,那麼這個繪製API就毫無意義。 最後一種格式GL_TRIANGLES。可想而知,也是用於繪製三角形,且繪製的是獨立的三角形。也就是123組成一個三角形,456組成另外一個三角形。3i-2,3i-1,3i組成第i個三角形,假如繪製頂點的數量不是三的整數倍,那麼多餘的一個或者兩個頂點,就會被忽略掉。這種格式,畫出的三角形就不一定全部是順時針或者全部是逆時針了。如果mode不是以上這些參數,則會出現GL_INVALID_ENUM的錯誤。

這個函數沒有輸出參數。由於繪製圖片需要的不只是頂點座標顏色信息,還需要頂點的法向量等信息,而這些信息經常使用默認值。所以,如果沒有被賦值的這些信息,或者沒有被通過glEnableVertexAttribArray enable的attribute,就會使用默認值去參與繪製。

如果當前被use的program不是一個合法的program,則繪製的結果爲undefined。但是不會出現其他錯誤。

還有一種可能性會導致出錯,我們已經知道了egl會給GL創建一塊可以繪製使用的繪製buffer。其實OpenGL ES還可以自己創建一塊繪製buffer,我們稱之爲FBO和RBO,關於FBO和RBO也有很多的API,但是由於這些API就並非每個OpenGL ES程序都會需要的了,這些API我們將放在之後的課程再進行講解。而如果使用OpenGL ES自己創建的繪製buffer去繪製,但是假如OpenGL ES創建的繪製buffer有誤,那麼就會出現GL_INVALID_FRAMEBUFFER_OPERATION的錯誤。在這裏我們解釋一下GL_INVALID_FRAMEBUFFER_OPERATION,GL_INVALID_FRAMEBUFFER_OPERATION是最後一種glError的標記了,其他的我們之前都說過了,當OpenGL ES程序從一塊沒有complete的framebuffer中讀取,或者往裏面繪製的時候,就會報這個錯誤。而這種錯誤可以被忽略,除了這個API要執行的操作沒有成功執行,以及GPU driver中被打上了標記之外,其他方面不會產生任何影響。關於這塊framebuffer是否complete,可以通過glCheckFramebufferStatus這個API查詢,如果查詢的結果不是GL_FRAMEBUFFER_COMPLETE,則說明被查詢的framebuffer不是complete的。

void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid * indices);

這個API和剛纔的glDrawArrays功能基本一樣,唯一的區別就是選取繪製所使用的頂點的方式不同。假如GPU中一共保存有10個頂點的信息,那麼通過glDrawArrays,我們會從這10個頂點中選取一個起點,然後再確定一個長度,所以取到的是連續的幾個頂點。而glDrawElements更加靈活一些,開發者可以通過它指定一些不連續的頂點,所以需要傳入一個數組,這個數組保存的就是頂點的index,比如我們可以傳入一個1357,也就是選擇第一第三第五第七個頂點,第一個繪製頂點爲1,依次類推,用於繪製。GPU中其他的頂點,會在這次繪製中就會被忽略掉。

這個函數的第一個輸入參數和glDrawArrays的第一個輸入參數一樣,用於指定將要繪製出來圖元的mode種類。如果mode不是以上這些參數,則會出現GL_INVALID_ENUM的錯誤。剛纔我們已經說的很詳細了,這裏就不詳細說了。第二個輸入參數和glDrawArrays的第三個輸入參數一樣,count,用於指定使用多少個頂點作爲繪製的點。count不能小於0,否則會導致undefine的行爲發生,且會產生GL_INVALID_VALUE的錯誤。最後一個參數 indices 非常重要,它分爲兩種情況,假如實際數據保存在 CPU 端, 那麼 indices 就是一個指向實際數據存放位置的指針或者數組地址。如果實際數據保存在 GPU 的 IBO 中,那麼 indices 就傳入一個偏移,意思就是從 IBO 的某一位開始,從之後的那些數值讀取 count 個數值作爲 indices 的值。而第三個輸入參數的意思是,存放頂點index內存中,所使用的變量類型爲type。type必須是GL_UNSIGNED_BYTE或者GL_UNSIGNED_SHORT。否則則會出現 GL_INVALID_ENUM 的錯誤。

這個函數沒有輸出參數。錯誤的方式也和glDrawArrays基本一樣。

如果當前被use的program不是一個合法的program,則繪製的結果爲undefined。但是不會出現其他錯誤。

假如繪製buffer有誤,那麼就會出現GL_INVALID_FRAMEBUFFER_OPERATION的錯誤。

void glLineWidth(GLfloat width);

根據上面兩個APIglDrawArraysglDrawElements的第一個參數mode,如果mode爲GL_LINE_LOOP、GL_LINE_STRIP、GL_LINES。那麼光柵化出來的就是線段,而線段的粗細就是由glLineWidth這個API設定的。

這個函數只有一個輸入參數width,默認爲1。如果小於或者等於0,則會出現GL_INVALID_VALUE的錯誤。

這個函數沒有輸出。實際上最終的線寬爲floor(width),也就是說,如果傳入爲1.9,那麼實際寬度爲1。而如果傳入爲0.8,floor後爲0,但是實際寬度依然取爲1。如果 ∆ X> ∆ Y,那麼width的線寬體現在列上,也就是縱向尺寸爲width。反之,則體現在行上。

還有就是,支持多少尺寸的線寬,由硬件決定,只有尺寸1的線寬是強制要求必須支持的。其他尺寸是否支持,只能通過API glGet,傳入GL_ALIASED_LINE_WIDTH_RANGE進行查詢。實際上最終的線寬也會被clamp到這個支持的尺寸之內。

如果通過GL_LINE_WIDTH 查詢,返回的則是glLineWidth這個API設置的數字,而非實際的數字(比如floor之後的數字)。

void glFrontFace(GLenum mode);

根據上面兩個APIglDrawArraysglDrawElements的第一個參數mode,如果mode爲GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN、GL_TRIANGLE。那麼光柵化出來的就是三角形,繪製出來的結果同樣也收到其他一些因素的影響,比如這個API glFrontFace以及glCullFace。這兩個API會根據頂點的組合,決定剔除一些三角形不去進行繪製。可以把三角形分爲front三角形和back三角形。而glFrontFace這個API就是用於算出哪些三角形爲front三角形,而哪些爲back三角形。

這個計算過程需要借用一個算法,其中x和y爲第i個頂點的座標,i⊕1爲i+1 mod n,n爲頂點的總數。i從0開始,到n-1結束。

opengles

這個函數只有一個輸入參數 dir ,當dir爲GL_CCW的時候,對上述算法取反,之後得到的結果如果爲正,則該三角形爲front。反之,則會back。而如果dir爲GL_CW的話,則對上述算法的結果,直接判斷其是否爲正,如果爲正,則該三角形爲front。反之則爲back。默認爲GL_CCW。如果dir不是這兩個值,則會出現 GL_INVALID_ENUM 的錯誤。

其實算法比較複雜,一般情況下GL_CCW的時候,逆時針的三角形爲front,順時針的三角形爲back。GL_CW則相反。(Patrick:我覺得spec應該寫反了,GL_CCW的話應該是順時針爲front)

這個函數沒有輸出。得到的三角形是否爲front/back會和API glCullFace關聯使用。

void glCullFace(GLenum mode);

由於back三角形一般都是不可見的,剔除這些不可見的三角形對性能會提高。所以一般會將這些不可見的三角形剔除掉。glCullFace這個API就是用於選擇剔除哪些三角形。而剔除這個功能,則是通過glEnableglDisable這兩個API通過傳入 GL_CULL_FACE 來開關。

這個函數只有一個輸入參數mode,mode可以爲 GL_FRONT, GL_BACK, and GL_FRONT_AND_BACK。否則,則會出現GL_INVALID_ENUM 的錯誤。當傳入GL_FRONT的時候,則剔除front三角形,當傳入GL_BACK的時候,則剔除back三角形,當傳入GL_FRONT_AND_BACK則全部剔除,只會繪製點和線段,無法繪製任何三角形。剔除功能只有GL_CULL_FACE被打開的時候生效,如果GL_CULL_FACE被關閉,則全部不剔除。默認情況下剔除功能是關閉的,剔除mode爲GL_BACK。

截至到現在,OpenGL ES最重要的,每個OpenGL ES程序中都會使用到的API已經被我們說完了,比如如何創建和使用shader和program,比如如何給GPU傳入attribute繪製信息,如何給GPU傳入uniform信息,確定繪製區域以及啓動繪製命令。

除了這些之外,還有很多一樣也是很重要的API,比如這個課時我們提到的,通過OpenGL ES創建繪製buffer,通過API給GPU傳遞scissor、colormask、blend、depthstencil等信息,以及衆多查詢API和非常非常重要的一個模塊,紋理等。這些API我們會在之後的課程中,進行一一介紹。

本節教程就到此結束,下一節將學習如何通過 OpenGL ES 創建和設置紋理信息,希望大家繼續閱讀我之後的教程。

謝謝大家,再見!

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