OpenGL學習視頻下載:https://pan.baidu.com/s/1muWuuuo1_89AijQRNOcJmg
提取碼:xcwn
原文出處:http://blog.csdn.net/candycat1992/article/details/39676669
支持大家去看原文,真是一片好的博文。關於OpenGL寫點很詳細也很透徹,
OpenGL能做的事情太多了!很多程序也看起來很復雜。很多人感覺OpenGL晦澀難懂,原因大多是被OpenGL里面各種語句搞得頭大,一會gen一下,一會bind一下,一會又active一下。搞到最后都不知道自己在干嘛,更有可能因為某一步的順序錯誤導致最后渲染出錯,又或者覺得記下這些操作的順序是非常煩人的一件事。那么,OpenGL為什么會長成這個樣子呢?這篇文章旨在通過一個最簡單的OpenGL程序開始,讓我們能夠“看懂”它,“記住”這些操作順序。
我們先來解釋一下OpenGL為什么會涉及這么多操作順序。這是因為,和我們現在使用的C++、C#這種面向對象的語言不同,OpenGL中的大多數函數使用了一種基於狀態的方法,大多數OpenGL對象都需要在使用前把該對象綁定到context上。這里有兩個新名詞——OpenGL對象和Context。
Context
Context是一個非常抽象的概念,我們姑且把它理解成一個包含了所有OpenGL狀態的對象。如果我們把一個Context銷毀了,那么OpenGL也不復存在。
OpenGL對象
我們可以把OpenGL對象理解成一個狀態的集合,它負責管理它下屬的所有狀態。當然,除了狀態,OpenGL對象還會存儲其他數據。注意。這些狀態和上述context中的狀態並不重合,只有在把一個OpenGL對象綁定到context上時,OpenGL對象的各種狀態才會映射到context的狀態。因此,這時如果我們改變了context的狀態,那么也會影響這個對象,而相反地,依賴這些context狀態的函數也會使用存儲在這個對象上的數據。
因此,OpenGL對象的綁定既可能是為了修改該對象的狀態(大多數對象需要綁定到context上才可以改變它的狀態),也可能是為了讓context渲染時使用它的狀態。
畫了一個圖,僅供理解。圖中灰色的方塊代表各種狀態,箭頭表示當把一個OpenGL對象綁定到context上后,對應狀態的映射。
前面提到過,OpenGL就是一個“狀態機”。那些各種各樣的API調用會改變這些狀態,或者根據這些狀態進行操作。但我們要注意的是,這只是說明了OpenGL是怎樣被定義的,但硬件是否是按狀態機實現的就是另一回事了。不過,這不是我們需要擔心的地方。
OpenGL對象包含了下面一些類型:Buffer Objects,Vertex Array Objects,Textures,Framebuffer Objects等等。我們下面會講到Vertex Array Objects這個對象。
這些對象都有三個相關的重要函數:
void glGen*(GLsizei n, GLuint *objects);
負責生成一個對象的name。而name就是這個對象的引用。
void glDelete*(GLsizei n, const GLuint *objects);
負責銷毀一個對象。
void glBind*(GLenum target, GLuint object);
將對象綁定到context上。
關於OpenGL對象還有很多內容,這里就不講了。可以參見官方wiki。
在開始第一個程序之前,我們還要了解一些圖形名詞。
- 渲染(Rendering):計算機從模型到創建一張圖像的過程。OpenGL僅僅是其中一個渲染系統。它是一個基於光柵化的系統,其他的系統還有光線追蹤(但有時也會用到OpenGL)等。
- 模型(Models)或者對象(Objects):這里兩者的含義是一樣的。指從幾何圖元——點、線、三角形中創建的東西,由頂點指定。
- Shaders:這是一類特殊的函數,是在圖形硬件上執行的。我們可以理解成,Shader是一些為圖形處理單元(GPU)編譯的小程序。OpenGL包含了編譯工具來把我們編寫的Shader源代碼編譯成可以在GPU上運行的代碼。在OpenGL中,我們可以使用四種shader階段。最常見的就是vertex shaders——它們可以處理頂點數據;以及fragment shaders,它們處理光柵化后生成的fragments。vertex shaders和fragment shaders是每個OpenGL程序必不可少的部分。
- 像素(pixel):像素是我們顯示器上的最小可見元素。我們系統中的像素被存儲在一個幀緩存(framebuffer)中。幀緩存是一塊由圖形硬件管理的內存空間,用於供給給我們的顯示設備。
驚鴻一瞥
我們的第一個程序(不完整)的運行結果如下:
代碼如下(提示:這里可以粗略地看下中文注釋,后面會更詳細講述的):
-
///
-
//
-
// triangles.cpp
-
//
-
///
-
-
-
 //--------------------------------------------------------------------
-
//
-
// 在程序一開頭,我們包含了所需的頭文件,
-
// 聲明了一些全局變量(但通常是不用全局變量在做的,這里只是為了說明一些基本問題)
-
// 以及其他一些有用的程序結構
-
//
-
-
-
using namespace std;
-
-
-
-
-
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()函數用於設置我們后面會用到的一些數據.例如頂點信息,紋理等
-
//
-
-
void init(void) {
-
glGenVertexArrays(NumVAOs, VAOs);
-
glBindVertexArray(VAOs[Triangles]);
-
-
// 我們首先指定了要渲染的兩個三角形的位置信息.
-
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 shaders
-
ShaderInfo shaders[] = {
-
{ GL_VERTEX_SHADER, "triangles.vert" },
-
{ GL_FRAGMENT_SHADER, "triangles.frag" },
-
{ GL_NONE, NULL }
-
};
-
-
// LoadShaders()是我們自定義(這里沒有給出)的一個函數,
-
// 用於簡化為GPU准備shaders的過程,后面會詳細講述
-
GLuint program = LoadShaders(shaders);
-
glUseProgram(program);
-
// 最后這部分我們成為shader plumbing,
-
// 我們把需要的數據和shader程序中的變量關聯在一起,后面會詳細講述
-
glVertexAttribPointer(vPosition, 2, GL_FLOAT,
-
GL_FALSE, 0, BUFFER_OFFSET(0));
-
glEnableVertexAttribArray(vPosition);
-
}
-
-
//---------------------------------------------------------------------
-
//
-
// display
-
//
-
// 這個函數是真正進行渲染的地方.它調用OpenGL的函數來請求數據進行渲染.
-
// 幾乎所有的display函數都會進行下面的三個步驟.
-
//
-
-
void display(void) {
-
// 1. 調用glClear()清空窗口
-
glClear(GL_COLOR_BUFFER_BIT);
-
-
// 2. 發起OpenGL調用來請求渲染你的對象
-
glBindVertexArray(VAOs[Triangles]);
-
glDrawArrays(GL_TRIANGLES, 0, NumVertices);
-
-
// 3. 請求將圖像繪制到窗口
-
glFlush();
-
}
-
-
//---------------------------------------------------------------------
-
//
-
// main
-
//
-
// main()函數用於創建窗口,調用init()函數,最后進入到事件循環(event loop).
-
// 這里仍會看到一些以gl開頭的函數,但和上面的有所不同.
-
// 這些函數來自第三方庫,以便我們可以在不同的系統中更方便地使用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如下:
-
-
layout(location = 0) in vec4 vPosition;
-
void
-
main()
-
{
-
gl_Position = vPosition;
-
}
Fragment Shader如下:
-
-
out vec4 fColor;
-
void
-
main()
-
{
-
fColor = vec4( 0.0, 0.0, 1.0, 1.0);
-
}
OpenGL的語法
這里插播一段語法解釋。從上面可以看出,OpenGL里面的函數長得都有一個特點,都是由“gl”開頭的,然后緊跟一個或多個大寫字母(例如,glBindVertexArray())。而且可以告訴,所有的OpenGL函數都長這樣。在上面的程序里面還有一些函數是“glut”開頭的,這是來自OpenGL實用工具(OpenGL Utility Toolkit)——GLUT。這是一個非常流行的跨平台工具,可以用於打開窗口、管理輸入等操作。龍書用的GLUT版本是Freeglut,是原始GLUT的一個變種。GLUT已經不再更新了。。。Sad。。。同樣,還有一個函數,glewInit(),它來自GLEW庫。GLUT和GLEW就是龍書所用的兩個庫了。
和OpenGL函數的命名規范類似,在display()函數里見到的GL_COLOR_BUFFER_BIT這樣的常量,也是OpenGL定義的。它們由GL_開頭,實用下划線來分割字符。它們的定義就是通過OpenGL頭文件(glcorearb.h和glewt.h)里面的#define指令定義的。
OpenGL為了跨平台還自己定義了一系列數據類型,如GLfloat。而且,因為OpenGL是一個“C”語言庫,它不使用函數重載來解決不同類型的數據問題,而是使用函數命名規范來組織不同的函數。例如,后面我們會碰到一個函數叫glUniform*(),這個函數有很多形式,例如,glUniform2f()和glUniform3fv。這些函數名字后面的后綴——2f和3fv,提供了函數的參數信息。例如,2f中的2表示有兩個數據將會傳遞給函數,f表示這兩個參數的類型是GLfloat。而3fv中最后的v,則是vector的簡寫,表明這三個GLfloat將以vector的形式傳遞給函數,而不是三個獨立的參數。
一些例子中沒有使用OpenGL定義的數據類型,直接使用了float這樣的變量。這可能會造成在不同平台上不兼容的問題。
在三維的世界里,所有的故事都是從頂點開始的。雖然題目是“詳解第一個程序”,但目的是為了讓大家理解最基礎的頂點是怎么一步步傳遞到GLSL中的。
重點內容開始!
傳遞頂點數據:你會怎么做
那么,現在的問題是,如果是你,你會怎么把頂點和它相關的信息,例如紋理坐標、法線等,傳遞給GLSL呢?一般人都會想到多維數組。我們下面把它稱為頂點流(Vertex Stream)。(什么?!你不是這么想的?!沒關系,OpenGL是這么想的就好。。。)
我們負責創建這個頂點流,然后只需要告訴OpenGL怎樣解讀它就可以了。
為了渲染一個對象,我們必須使用一個shader program。而這個program會定義一系列頂點屬性,例如上述Vertex Shader中的vPosition一行。這些屬性決定了我們需要傳遞哪些頂點數據。每一個屬性對應了一個數組,並且這些數據的維度都必須相等,即是一一對應的關系。
比如我們想要渲染3個頂點,我們會定義下面的數據:
{ {1, 1, 1}, {0, 0, 0}, {0, 0, 1} }
這些頂點的順序是非常重要的,OpenGL將會根據這些順序渲染網格。我們可以直接使用上述這種數據來直接渲染,也可以使用索引(indices)來指定順序,這樣可以重復使用同一個頂點。
例如,我們使用下面的索引列表:
{2, 1, 0, 2, 1, 2}
那么OpenGL將會渲染6個頂點:
{ {0, 0, 1}, {0, 0, 0}, {1, 1, 1}, {0, 0, 1}, {0, 0, 0}, {0, 0, 1} }
現在,我們還想傳遞一個新的頂點屬性,即每個頂點的紋理坐標,那么新的紋理數組可能長這樣:
[plain] view plain copy print?
- { {0, 0}, {0.5, 0}, {0, 1} }
注意,紋理數據的維度大小一定要和上面的坐標數組大小一致,而其他頂點屬性數組的維度也要滿足這個條件。這是非常容易理解的。
那么,合並后的頂點屬性列表就是:
[{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來實現上述管理頂點數據的數據作用,以及VBO來存放真正的頂點屬性數據。
VAO(Vertex Array Object)
我們這里遇到了第一種OpenGL對象——VAO(Vertex Array Object)。前面說到OpenGL對象是狀態的集合,那么VAO就是所有頂點數據的狀態集合。它存儲了頂點數據的格式以及頂點數據數據所需的緩存對象的引用。前面提過,OpenGL對象都有三個非常重要的函數,而VAO對應的就是glGenVertexArrays、glDeleteVertexArrays和glBindVertexArray。
VAO負責管理頂點屬性,而這些頂點屬性從0到GL_MAX_VERTEX_ATTRIBS - 1被編號。這些屬性在Vertex Shader里的表現就是類似下面的語句:
layout(location = 0) in vec4 vPosition;
上述頂點屬性vPosition被編號為0。
每個屬性可以被enable或者disable,被disable的屬性是不會傳遞給shader的,即便在shader里定義了這些屬性,它們讀出的值也會是一個常量,而非真正的數據。一個新建的VAO的所有屬性訪問都是disable的。而開啟一個屬性是通過下面的函數:
void glEnableVertexAttribArray(GLuint index);
與其對應的是glDisableVertexAttribArray 函數。
而為了使用上述函數來改變VAO的狀態,我們首先需要把VAO綁定到當前的context上。
VBO(Vertex Buffer Object)
VBO是一種Buffer Object,即它也是一個OpenGl對象。VBO是頂點數組數據真正所在的地方。
為了指定一個屬性數據的格式和來源,我們需要告訴OpenGL,編號為0的屬性使用哪個VBO,編號為1的屬性使用哪個VBO等等。為了實現它,我們可以這么做。
首先,我們要知道,任何VBO都需要先綁定到GL_ARRAY_BUFFER才可以對它進行操作。綁定后,我們可以調用下面的函數之一:
-
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,編號為index的屬性使用當前綁定在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上。第二行意味着,編號為0的屬性將使用buf1的數據,因為當前綁定到GL_ARRAY_BUFFER上的是buf1。第三行將緩存對象0綁定到了GL_ARRAY_BUFFER上,這不會對頂點屬性有任何影響,只有glVertexAttribPointer函數可以影響它們!
這個過程就像一個中介人的作用,而中介人就是GL_ARRAY_BUFFER。我們可以這么想,glBindBuffer 設置了一個全局變量,然后glVertexAttribPointer讀取了這個全局變量並把它存儲在VAO中,這個全局變量就是GL_ARRAY_BUFFER。當調用完glVertexAttribPointer后,頂點屬性已經知道了數據來源就是buf1,它們之間就會直接聯系,而不需要在通過GL_ARRAY_BUFFER。