【OpenGL】详解第一个OpenGL程序
寫在前面
?
OpenGL能做的事情太多了!很多程序也看起來很復(fù)雜。很多人感覺OpenGL晦澀難懂,原因大多是被OpenGL里面各種語句搞得頭大,一會(huì)gen一下,一會(huì)bind一下,一會(huì)又active一下。搞到最后都不知道自己在干嘛,更有可能因?yàn)槟骋徊降捻樞蝈e(cuò)誤導(dǎo)致最后渲染出錯(cuò),又或者覺得記下這些操作的順序是非常煩人的一件事。那么,OpenGL為什么會(huì)長成這個(gè)樣子呢?這篇文章旨在通過一個(gè)最簡單的OpenGL程序開始,讓我們能夠“看懂”它,“記住”這些操作順序。
?
我們先來解釋一下OpenGL為什么會(huì)涉及這么多操作順序。這是因?yàn)?#xff0c;和我們現(xiàn)在使用的C++、C#這種面向?qū)ο蟮恼Z言不同,OpenGL中的大多數(shù)函數(shù)使用了一種基于狀態(tài)的方法,大多數(shù)OpenGL對象都需要在使用前把該對象綁定到context上。這里有兩個(gè)新名詞——OpenGL對象和Context。
?
Context
Context是一個(gè)非常抽象的概念,我們姑且把它理解成一個(gè)包含了所有OpenGL狀態(tài)的對象。如果我們把一個(gè)Context銷毀了,那么OpenGL也不復(fù)存在。
?
OpenGL對象
我們可以把OpenGL對象理解成一個(gè)狀態(tài)的集合,它負(fù)責(zé)管理它下屬的所有狀態(tài)。當(dāng)然,除了狀態(tài),OpenGL對象還會(huì)存儲(chǔ)其他數(shù)據(jù)。注意。這些狀態(tài)和上述context中的狀態(tài)并不重合,只有在把一個(gè)OpenGL對象綁定到context上時(shí),OpenGL對象的各種狀態(tài)才會(huì)映射到context的狀態(tài)。因此,這時(shí)如果我們改變了context的狀態(tài),那么也會(huì)影響這個(gè)對象,而相反地,依賴這些context狀態(tài)的函數(shù)也會(huì)使用存儲(chǔ)在這個(gè)對象上的數(shù)據(jù)。
?
因此,OpenGL對象的綁定既可能是為了修改該對象的狀態(tài)(大多數(shù)對象需要綁定到context上才可以改變它的狀態(tài)),也可能是為了讓context渲染時(shí)使用它的狀態(tài)。
?
畫了一個(gè)圖,僅供理解。圖中灰色的方塊代表各種狀態(tài),箭頭表示當(dāng)把一個(gè)OpenGL對象綁定到context上后,對應(yīng)狀態(tài)的映射。
?
前面提到過,OpenGL就是一個(gè)“狀態(tài)機(jī)”。那些各種各樣的API調(diào)用會(huì)改變這些狀態(tài),或者根據(jù)這些狀態(tài)進(jìn)行操作。但我們要注意的是,這只是說明了OpenGL是怎樣被定義的,但硬件是否是按狀態(tài)機(jī)實(shí)現(xiàn)的就是另一回事了。不過,這不是我們需要擔(dān)心的地方。
?
OpenGL對象包含了下面一些類型:Buffer Objects,Vertex Array Objects,Textures,Framebuffer Objects等等。我們下面會(huì)講到Vertex Array Objects這個(gè)對象。
?
這些對象都有三個(gè)相關(guān)的重要函數(shù):
void glGen*(GLsizei n?, GLuint *objects?);負(fù)責(zé)生成一個(gè)對象的name。而name就是這個(gè)對象的引用。
void glDelete*(GLsizei n?, const GLuint *objects?);負(fù)責(zé)銷毀一個(gè)對象。
void glBind*(GLenum target?, GLuint object?);將對象綁定到context上。
?
?
關(guān)于OpenGL對象還有很多內(nèi)容,這里就不講了。可以參見官方wiki。
?
?
?
在開始第一個(gè)程序之前,我們還要了解一些圖形名詞。
?
- 渲染(Rendering):計(jì)算機(jī)從模型到創(chuàng)建一張圖像的過程。OpenGL僅僅是其中一個(gè)渲染系統(tǒng)。它是一個(gè)基于光柵化的系統(tǒng),其他的系統(tǒng)還有光線追蹤(但有時(shí)也會(huì)用到OpenGL)等。
? - 模型(Models)或者對象(Objects):這里兩者的含義是一樣的。指從幾何圖元——點(diǎn)、線、三角形中創(chuàng)建的東西,由頂點(diǎn)指定。
? - Shaders:這是一類特殊的函數(shù),是在圖形硬件上執(zhí)行的。我們可以理解成,Shader是一些為圖形處理單元(GPU)編譯的小程序。OpenGL包含了編譯工具來把我們編寫的Shader源代碼編譯成可以在GPU上運(yùn)行的代碼。在OpenGL中,我們可以使用四種shader階段。最常見的就是vertex shaders——它們可以處理頂點(diǎn)數(shù)據(jù);以及fragment shaders,它們處理光柵化后生成的fragments。vertex shaders和fragment shaders是每個(gè)OpenGL程序必不可少的部分。
? - 像素(pixel):像素是我們顯示器上的最小可見元素。我們系統(tǒng)中的像素被存儲(chǔ)在一個(gè)幀緩存(framebuffer)中。幀緩存是一塊由圖形硬件管理的內(nèi)存空間,用于供給給我們的顯示設(shè)備。
?
?
驚鴻一瞥
?
?
我們的第一個(gè)程序(不完整)的運(yùn)行結(jié)果如下:
?
?
?
代碼如下(提示:這里可以粗略地看下中文注釋,后面會(huì)更詳細(xì)講述的):
/// // // triangles.cpp // /////-------------------------------------------------------------------- // // 在程序一開頭,我們包含了所需的頭文件, // 聲明了一些全局變量(但通常是不用全局變量在做的,這里只是為了說明一些基本問題) // 以及其他一些有用的程序結(jié)構(gòu) //#include <iostream> using namespace std;#include "vgl.h" #include "LoadShaders.h"enum VAO_IDs { Triangles, NumVAOs }; enum Buffer_IDs { ArrayBuffer, NumBuffers }; enum Attrib_IDs { vPosition = 0 };GLuint VAOs[NumVAOs]; GLuint Buffers[NumBuffers];const GLuint NumVertices = 6;//--------------------------------------------------------------------- // // init // // init()函數(shù)用于設(shè)置我們后面會(huì)用到的一些數(shù)據(jù).例如頂點(diǎn)信息,紋理等 //void init(void) {glGenVertexArrays(NumVAOs, VAOs);glBindVertexArray(VAOs[Triangles]);// 我們首先指定了要渲染的兩個(gè)三角形的位置信息.GLfloat vertices[NumVertices][2] = {{ -0.90, -0.90 }, // Triangle 1{ 0.85, -0.90 },{ -0.90, 0.85 },{ 0.90, -0.85 }, // Triangle 2{ 0.90, 0.90 },{ -0.85, 0.90 }};glGenBuffers(NumBuffers, Buffers);glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices),vertices, GL_STATIC_DRAW);// 然后使用了必需的vertex和fragment shadersShaderInfo shaders[] = {{ GL_VERTEX_SHADER, "triangles.vert" },{ GL_FRAGMENT_SHADER, "triangles.frag" },{ GL_NONE, NULL }};// LoadShaders()是我們自定義(這里沒有給出)的一個(gè)函數(shù),// 用于簡化為GPU準(zhǔn)備shaders的過程,后面會(huì)詳細(xì)講述GLuint program = LoadShaders(shaders);glUseProgram(program);// 最后這部分我們成為shader plumbing,// 我們把需要的數(shù)據(jù)和shader程序中的變量關(guān)聯(lián)在一起,后面會(huì)詳細(xì)講述glVertexAttribPointer(vPosition, 2, GL_FLOAT,GL_FALSE, 0, BUFFER_OFFSET(0));glEnableVertexAttribArray(vPosition); }//--------------------------------------------------------------------- // // display // // 這個(gè)函數(shù)是真正進(jìn)行渲染的地方.它調(diào)用OpenGL的函數(shù)來請求數(shù)據(jù)進(jìn)行渲染. // 幾乎所有的display函數(shù)都會(huì)進(jìn)行下面的三個(gè)步驟. //void display(void) {// 1. 調(diào)用glClear()清空窗口glClear(GL_COLOR_BUFFER_BIT);// 2. 發(fā)起OpenGL調(diào)用來請求渲染你的對象glBindVertexArray(VAOs[Triangles]);glDrawArrays(GL_TRIANGLES, 0, NumVertices);// 3. 請求將圖像繪制到窗口glFlush(); }//--------------------------------------------------------------------- // // main // // main()函數(shù)用于創(chuàng)建窗口,調(diào)用init()函數(shù),最后進(jìn)入到事件循環(huán)(event loop). // 這里仍會(huì)看到一些以gl開頭的函數(shù),但和上面的有所不同. // 這些函數(shù)來自第三方庫,以便我們可以在不同的系統(tǒng)中更方便地使用OpenGL. // 這里我們使用的是GLUT和GLEW. //int main(int argc, char** argv) {glutInit(&argc, argv);glutInitDisplayMode(GLUT_RGBA);glutInitWindowSize(512, 512);glutInitContextVersion(4, 3);glutInitContextProfile(GLUT_CORE_PROFILE);glutCreateWindow(argv[0]);if (glewInit()) {cerr << "Unable to initialize GLEW ... exiting" << endl; exit(EXIT_FAILURE);}init();glutDisplayFunc(display);glutMainLoop(); }Vertex Shader如下:
#version 430 core layout(location = 0) in vec4 vPosition; void main(){gl_Position = vPosition; }Fragment Shader如下:
#version 430 core out vec4 fColor; void main() { fColor = vec4(0.0, 0.0, 1.0, 1.0); }OpenGL的語法
?
這里插播一段語法解釋。從上面可以看出,OpenGL里面的函數(shù)長得都有一個(gè)特點(diǎn),都是由“gl”開頭的,然后緊跟一個(gè)或多個(gè)大寫字母(例如,glBindVertexArray())。而且可以告訴,所有的OpenGL函數(shù)都長這樣。在上面的程序里面還有一些函數(shù)是“glut”開頭的,這是來自O(shè)penGL實(shí)用工具(OpenGL Utility Toolkit)——GLUT。這是一個(gè)非常流行的跨平臺(tái)工具,可以用于打開窗口、管理輸入等操作。龍書用的GLUT版本是Freeglut,是原始GLUT的一個(gè)變種。GLUT已經(jīng)不再更新了。。。Sad。。。同樣,還有一個(gè)函數(shù),glewInit(),它來自GLEW庫。GLUT和GLEW就是龍書所用的兩個(gè)庫了。
?
和OpenGL函數(shù)的命名規(guī)范類似,在display()函數(shù)里見到的GL_COLOR_BUFFER_BIT這樣的常量,也是OpenGL定義的。它們由GL_開頭,實(shí)用下劃線來分割字符。它們的定義就是通過OpenGL頭文件(glcorearb.h和glewt.h)里面的#define指令定義的。
?
OpenGL為了跨平臺(tái)還自己定義了一系列數(shù)據(jù)類型,如GLfloat。而且,因?yàn)镺penGL是一個(gè)“C”語言庫,它不使用函數(shù)重載來解決不同類型的數(shù)據(jù)問題,而是使用函數(shù)命名規(guī)范來組織不同的函數(shù)。例如,后面我們會(huì)碰到一個(gè)函數(shù)叫g(shù)lUniform*(),這個(gè)函數(shù)有很多形式,例如,glUniform2f()和glUniform3fv。這些函數(shù)名字后面的后綴——2f和3fv,提供了函數(shù)的參數(shù)信息。例如,2f中的2表示有兩個(gè)數(shù)據(jù)將會(huì)傳遞給函數(shù),f表示這兩個(gè)參數(shù)的類型是GLfloat。而3fv中最后的v,則是vector的簡寫,表明這三個(gè)GLfloat將以vector的形式傳遞給函數(shù),而不是三個(gè)獨(dú)立的參數(shù)。
?
一些例子中沒有使用OpenGL定義的數(shù)據(jù)類型,直接使用了float這樣的變量。這可能會(huì)造成在不同平臺(tái)上不兼容的問題。
?
?
?
在三維的世界里,所有的故事都是從頂點(diǎn)開始的。雖然題目是“詳解第一個(gè)程序”,但目的是為了讓大家理解最基礎(chǔ)的頂點(diǎn)是怎么一步步傳遞到GLSL中的。
?
?
重點(diǎn)內(nèi)容開始!
?
?
傳遞頂點(diǎn)數(shù)據(jù):你會(huì)怎么做
?
?
那么,現(xiàn)在的問題是,如果是你,你會(huì)怎么把頂點(diǎn)和它相關(guān)的信息,例如紋理坐標(biāo)、法線等,傳遞給GLSL呢?一般人都會(huì)想到多維數(shù)組。我們下面把它稱為頂點(diǎn)流(Vertex Stream)。(什么?!你不是這么想的?!沒關(guān)系,OpenGL是這么想的就好。。。)
?
我們負(fù)責(zé)創(chuàng)建這個(gè)頂點(diǎn)流,然后只需要告訴OpenGL怎樣解讀它就可以了。
?
為了渲染一個(gè)對象,我們必須使用一個(gè)shader program。而這個(gè)program會(huì)定義一系列頂點(diǎn)屬性,例如上述Vertex Shader中的vPosition一行。這些屬性決定了我們需要傳遞哪些頂點(diǎn)數(shù)據(jù)。每一個(gè)屬性對應(yīng)了一個(gè)數(shù)組,并且這些數(shù)據(jù)的維度都必須相等,即是一一對應(yīng)的關(guān)系。
?
比如我們想要渲染3個(gè)頂點(diǎn),我們會(huì)定義下面的數(shù)據(jù):
{ {1, 1, 1}, {0, 0, 0}, {0, 0, 1} }?
這些頂點(diǎn)的順序是非常重要的,OpenGL將會(huì)根據(jù)這些順序渲染網(wǎng)格。我們可以直接使用上述這種數(shù)據(jù)來直接渲染,也可以使用索引(indices)來指定順序,這樣可以重復(fù)使用同一個(gè)頂點(diǎn)。
?
例如,我們使用下面的索引列表:
{2, 1, 0, 2, 1, 2}
那么OpenGL將會(huì)渲染6個(gè)頂點(diǎn):
?
?
現(xiàn)在,我們還想傳遞一個(gè)新的頂點(diǎn)屬性,即每個(gè)頂點(diǎn)的紋理坐標(biāo),那么新的紋理數(shù)組可能長這樣:
{ {0, 0}, {0.5, 0}, {0, 1} }
注意,紋理數(shù)據(jù)的維度大小一定要和上面的坐標(biāo)數(shù)組大小一致,而其他頂點(diǎn)屬性數(shù)組的維度也要滿足這個(gè)條件。這是非常容易理解的。
?
那么,合并后的頂點(diǎn)屬性列表就是:
[{0, 0, 1}, {0, 1}], [{0, 0, 0}, {0.5, 0}], [{1, 1, 1}, {0, 0}], [{0, 0, 1}, {0, 1}], [{0, 0, 0}, {0.5, 0}], [{0, 0, 1}, {0, 1}] }OpenGL的做法:VAO和VBO
?
?
OpenGL使用了VAO來實(shí)現(xiàn)上述管理頂點(diǎn)數(shù)據(jù)的數(shù)據(jù)作用,以及VBO來存放真正的頂點(diǎn)屬性數(shù)據(jù)。
?
VAO(Vertex Array Object)
?
?
我們這里遇到了第一種OpenGL對象——VAO(Vertex Array Object)。前面說到OpenGL對象是狀態(tài)的集合,那么VAO就是所有頂點(diǎn)數(shù)據(jù)的狀態(tài)集合。它存儲(chǔ)了頂點(diǎn)數(shù)據(jù)的格式以及頂點(diǎn)數(shù)據(jù)數(shù)據(jù)所需的緩存對象的引用。前面提過,OpenGL對象都有三個(gè)非常重要的函數(shù),而VAO對應(yīng)的就是glGenVertexArrays?、glDeleteVertexArrays和glBindVertexArray?。
?
VAO負(fù)責(zé)管理頂點(diǎn)屬性,而這些頂點(diǎn)屬性從0到GL_MAX_VERTEX_ATTRIBS??- 1被編號(hào)。這些屬性在Vertex Shader里的表現(xiàn)就是類似下面的語句:
layout(location = 0) in vec4 vPosition;上述頂點(diǎn)屬性vPosition被編號(hào)為0。
?
每個(gè)屬性可以被enable或者disable,被disable的屬性是不會(huì)傳遞給shader的,即便在shader里定義了這些屬性,它們讀出的值也會(huì)是一個(gè)常量,而非真正的數(shù)據(jù)。一個(gè)新建的VAO的所有屬性訪問都是disable的。而開啟一個(gè)屬性是通過下面的函數(shù):
void glEnableVertexAttribArray?(GLuint index?);與其對應(yīng)的是glDisableVertexAttribArray??函數(shù)。
?
而為了使用上述函數(shù)來改變VAO的狀態(tài),我們首先需要把VAO綁定到當(dāng)前的context上。
?
?
?
VBO(Vertex Buffer Object)
?
?
VBO是一種Buffer Object,即它也是一個(gè)OpenGl對象。VBO是頂點(diǎn)數(shù)組數(shù)據(jù)真正所在的地方。
?
為了指定一個(gè)屬性數(shù)據(jù)的格式和來源,我們需要告訴OpenGL,編號(hào)為0的屬性使用哪個(gè)VBO,編號(hào)為1的屬性使用哪個(gè)VBO等等。為了實(shí)現(xiàn)它,我們可以這么做。
?
首先,我們要知道,任何VBO都需要先綁定到GL_ARRAY_BUFFER?才可以對它進(jìn)行操作。綁定后,我們可以調(diào)用下面的函數(shù)之一:
void glVertexAttribPointer?( GLuint index?, GLint size?, GLenum type?,GLboolean normalized?, GLsizei stride?, const void *offset?);void glVertexAttribIPointer?( GLuint index?, GLint size?, GLenum type?,GLsizei stride?, const void *offset? );void glVertexAttribLPointer?( GLuint index?, GLint size?, GLenum type?,GLsizei stride?, const void *offset? );它們的作用大同小異,就是告訴OpenGl,編號(hào)為index的屬性使用當(dāng)前綁定在GL_ARRAY_BUFFER?的VBO。為了更好理解,我們舉例:
glBindBuffer(GL_ARRAY_BUFFER, buf1); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0); glBindBuffer(GL_ARRAY_BUFFER, 0);上面第一行代碼將buf1綁定到了GL_ARRAY_BUFFER?上。第二行意味著,編號(hào)為0的屬性將使用buf1的數(shù)據(jù),因?yàn)楫?dāng)前綁定到GL_ARRAY_BUFFER?上的是buf1。第三行將緩存對象0綁定到了GL_ARRAY_BUFFER?上,這不會(huì)對頂點(diǎn)屬性有任何影響,只有g(shù)lVertexAttribPointer?函數(shù)可以影響它們!
?
這個(gè)過程就像一個(gè)中介人的作用,而中介人就是GL_ARRAY_BUFFER?。我們可以這么想,glBindBuffer??設(shè)置了一個(gè)全局變量,然后glVertexAttribPointer讀取了這個(gè)全局變量并把它存儲(chǔ)在VAO中,這個(gè)全局變量就是GL_ARRAY_BUFFER。當(dāng)調(diào)用完glVertexAttribPointer后,頂點(diǎn)屬性已經(jīng)知道了數(shù)據(jù)來源就是buf1,它們之間就會(huì)直接聯(lián)系,而不需要在通過GL_ARRAY_BUFFER。
總結(jié)
以上是生活随笔為你收集整理的【OpenGL】详解第一个OpenGL程序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 延续一代“豌豆射手”风格:苹果AirPo
- 下一篇: cable