現在在我們的"hello world"程序中,我們已經加載了我們的緩沖和紋理,并且編譯和鏈接了我們的著色器程序。終于到最后一步了--讓我們來渲染我們的圖片。 渲染作業綜述 渲染可能需要很多的參數。除了所有的緩沖,紋理,著色器,以及它所涉及到的uniform參數,還有許多的其它控制渲染作業的設置我沒提到。OpenGL的方法是將這些設置做成了一個狀態機,而不是提供一個完整的帶所有標記作為參數的"draw"函數,或者一個需要你去填充各個域的結構體。當你使用glBindTexture,glBindBuffer以及類似的方法綁定一個對象的時候,你不僅是使這些對象可以修改,你還將它們綁定到了當前渲染作業的狀態。并且有狀態操作函數可以設置當前著色器,賦值到uniform參數和描述頂點數組的結構。當你最后使用glDrawElements將一個作業提交時,OpenGL取當前狀態機的一個快照并將它添加到GPU的命令隊列,它將在當GPU可用時被執行。同時,你可以改變OpenGL狀態以及將更多任務加到隊列中,而不用等待之前的作業完成。一旦你將作業排隊完畢,你可以讓窗口系統"切換緩沖",這個操作將會等待所有的排隊作業完成然后將結果顯示在窗口中。 讓我們寫一些代碼設置渲染作業狀態: 激活著色器程序并賦值uniform static void render(void) { glUseProgram(g_resources.program); 我們首先通過傳遞鏈接的程序對象的名字給glUseProgram來激活我們的著色器對象。一旦程序激活,我們可以開始對我們的uniform變量進行賦值。如果你回憶下我們的片元著色器的代碼,我們需要給float fade_factor和一個叫做textures的sampler2D數組進行賦值。 glUniform1f(g_resources.uniforms.fade_factor, g_resources.fade_factor); OpenGL提供了一組glUniform*函數用于給uniform變量賦值,其中每一個對應GLSL程序中的一種uniform變量類型。這些函數都是glUniform{dim}{type}的形式,其中dim表示vector類型的大小(int或float的uniform是1,vec2是2,等等),type表示組元的類型:要么是i表示integer,要么是f表示float。我們的fade_factor uniform是一個簡單的float,因此我們通過調用glUniform1f給它賦值,傳入uniform的位置以及新的值作為參數。 glActiveTexture(GLTEXTURE0); glBindTexture(GLTEXTURE2D, gresources.textures[0]); glUniform1i(g_resources.uniforms.textures[0], 0); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, g_resources.textures[1]); glUniform1i(g_resources.uniforms.textures[1], 1); 將紋理賦值給samplers有一點點復雜。GPU只有數量有限的紋理單元可以提供給紋理數據給渲染作業。我們必須將我們的紋理對象綁定到這些紋理單元,然后將紋理單元的索引賦值給我們的sampler uniform變量,如果它們是int的話。我們綁定的GL_TEXTURE_*目標名必須對應于sampler uniform的類型。在這里,GLTEXTURE2D對應于我們的textures變量使用的sample2D類型。glActiveTexture設置當前活躍的紋理單元。glBindTexture其實是使用活躍紋理單元作為一個隱含參數(其它的紋理對象操作的函數像glTexParameteri和glTexImage2D也是操作綁定到當前活躍的紋理單元的紋理)。一旦我們綁定紋理單元之后,我們可以使用glUniform1i對它的索引進行賦值。 設置紋理數組 glBindBuffer(GL_ARRAY_BUFFER, g_resources.vertex_buffer); glVertexAttribPointer( g_resources.attributes.position, /* attribute */ 2, /* size */ GL_FLOAT, /* type */ GL_FALSE, /* normalized? */ sizeof(GLfloat)*2, /* stride */ (void*)0 /* array buffer offset */ ); glEnableVertexAttribArray(g_resources.attributes.position); 接下來,我們告訴OpenGL我們使用的紋理數組的格式。我們通過調用glVertexAttribPointer設置每一個頂點屬性格式,這個函數告訴OpenGL在渲染時從頂點數組中讀出屬性值。glVertexAttribPointer使用屬性位置,關聯的屬性變量的元素大小和類型(對于我們的position屬性,大小為2,類型為GLFLOAT),屬性值之間的字節數(稱為stride),以及當前第一個屬性在當前綁定的GLARRAY_BUFFER中的偏移作為參數。由于歷史原因,offset是作為一個指針傳遞的,但它實際上被當作integer值使用,因此我們傳遞一個整形的0并傳換為void*類型。 在我們這里,我們的頂點數組只由單個vec2 position屬性組成;如果我們有多個屬性值,屬性值可以是交錯的,像是一個結構體數組,或者是分別存儲在不同的數組里。靈活的glVertexAttribPointer讓我們可以選擇這兩種情況中每個屬性如何選擇stride和offset去適應它們的存儲布局;改變GLARRAYBUFFER綁定不影響由我們已經設置過的屬性數組指針使用的緩沖。 (上面我沒有提到的normalized?參數是跟頂點數組中的整型的數組一起使用的。如果為true,元素將從它們的integer類型的范圍進行映射,比如0-255用于unsigned byte,0.0-1.0用于符點數,像圖片中的顏色組分。如果為false,它們的整型值將被保存。像我們這樣使用的已經是符點數的元素,該參數沒有任何作用。) 提交渲染作業 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_resources.element_buffer); glDrawElements( GL_TRIANGLE_STRIP, /* mode */ 4, /* count */ GL_UNSIGNED_SHORT, /* type */ (void*)0 /* element array buffer offset */ ); glDrawElements是設置繪圖管線動作為函數。我們告訴它我們使用哪種三種角組裝模式,使用多少頂點組裝三角形,我們元素數組的組成類型,以及當前綁定的第一個要渲染的元素在GLELEMENTARRAY_BUFFER內部的偏移,這也是一個實際上是integer的指針參數。它將獲取指向的元素數組的索引,將它們跟當前綁定的著色器程序,uniform變量,紋理單元,我們剛剛設置的頂點屬性指針集合在一起,綁定成為一個渲染作業,并將這個作業放到GPU隊列中。 清理工作 glDisableVertexAttribArray(g_resources.attributes.position); "Always leave things the way you found them",Bill Brasky曾經建議過。OpenGL狀態機的缺點就是所有的綁和設置都是全局地持久的,即使調用glDrawElements之后。這意味著我們必須注意整個程序生命期中,我們的OpenGL代碼是怎樣和其它的OpenGL代碼交互的。盡管在這個程序中還沒有其它的OpenGL代碼與之交互,我們仍然應該養成一個好的習慣。尤其要注意頂點屬性:在涉及到多個著色器程序和多個頂點數組的復雜程序中,不正確地使用頂點屬性可能會造成glDrawElements去使用無效的GPU數據,導致錯誤的輸出或者段錯誤。只在需要的時候去使用頂點數組是一個好習慣。這里,我們對position禁用頂點屬性。 你也可能會想,每次渲染時,我們重新綁定了所有相同的對象,設置了所有的相同的uniform值(除了fade_factor),并且重新激活了所有的同樣的頂點屬性。如果狀態設置在glDrawElements調用之間是持久的,從技術上講在進入glutMainLoop之后,我們可以幾乎完全沒必要要每幀都進行設置,并且每次渲染只更新混色因子并調用glDrawElements。但是,在你每次期望的時候都設置好狀態,這是個好主意。 顯示我們完成的場景 glutSwapBuffers(); } 我們只有一個渲染作業需要等待,因此當我們提交作業并清理之后,我們可以立即執行同步。GLUT函數glutSwapBuffers等待所有的運行中的作業完成,然后用我們的雙緩沖的framebuffer交換顏色緩沖,在下一幀時將當前可見的緩沖移到要渲染的"后面",然后將我們剛剛渲染好的圖象推到前面,在我們的窗口中顯示新渲染好的場景。我們的渲染流程完成了! 讓場景動起來 static void update_fade_factor(void) { int milliseconds = glutGet(GLUT_ELAPSED_TIME); g_resources.fade_factor = sinf((float)milliseconds * 0.001f) * 0.5f + 0.5f; glutPostRedisplay(); } 為了讓圖片動起來,我們的glutIdleFunc回調函數不停地更新我們給fadefactor賦值的uniform。GLUT維護一個毫秒級的計時器,我們可以使用glutGet(GLUTELAPSED_TIME)訪問到;我們使用標準C語言的sinf函數來得到一個平滑的,周期性的0到1之前的數。每次我們更新混色因子,我們調用glutPostRedisplay,這會強制我們的渲染回調函數去執行,更新窗口。 再次編譯運行程序 這是我們最后一次編譯和運行整個程序,使用所有我們的新的代碼。構建和執行的命令看起來很像上次我們構建的空函數版本,但是這次,你將編譯真正的hello-gl.c和util.c源文件。如果你使用Makefiles,你可以這樣編譯默認的目標: make -f Makefile.MacOSX # or Makefile.Unix or Makefile.Mingw nmake /f Nmakefile.Windows 一旦編譯后,程序假定它的圖片和著色器資源是在當前目錄的,因此最好從包含可執行文件,圖片,著色器代碼的目錄用命令行運行它。最后我們終于可以曬一下我們的成果了: 結論 必須承認從一個簡單的"hello world"已經起了很遠了。但是這里我們所創建的框架是非常靈活的;你可以替換成你自己的圖片并調整著色器代碼在圖片取樣之前對它們進行變換或者進一步處理,都不需要重新編譯C。下一章中,我們將繼續頂點著色器來展示基本的3D變換和投影。 如果你很感興趣,這個時候你也可以停下來,自己看一下OpenGL標準,注意,OpenGL 2標準仍然包含了很多我沒有提到的過時的特性。我強烈推薦你看OpenGL 3.1之后的版本,一定要看看核心標準部分而不是為了兼容的部分。盡管OpenGL 3之后相對于OpenGL 2添加了很多新的特征,所有的OpenGL 2中的基本的API也仍然是新版本的基本部分。 OpenGL ES 2也是值得一看的。它大部分由我這里提到的OpenGL 2之后的一個子集;所有的我前面提到的OpenGL API也都是在OpenGL ES 2中的。OpenGL ES還對移動平臺添加了一些額外的特性,比如浮點數支持以及離線著色器編譯,這是桌面版標準中所有提供的。如果你想試一下OpenGL ES開發,它是Android NDK和iPhone SDK的部分。在Windows下,Google的ANGLE項目還提供一個OpenGL ES2在DirectX上的實現。 |