OpenGL教程一
引自:https://blog.csdn.net/u013654125/article/details/73613644
GLEW, GLFW和GLM介紹
現(xiàn)在你有了工程,就讓我們開始介紹下工程所用到的開源庫和為啥需要這些。
The OpenGL Extension Wrangler (GLEW)是用來訪問OpenGL 3.2 API函數(shù)的。不幸的是你不能簡單的使用#include <GL/gl.h>來訪問OpenGL接口,除非你想用舊版本的OpenGL。在現(xiàn)代OpenGL中,API函數(shù)是在運行時(run time)確定的,而非編譯期(compile time)。GLEW可以在運行時加載OpenGL API。
GLFW允許我們跨平臺創(chuàng)建窗口,接受鼠標鍵盤消息。OpenGL不處理這些窗口創(chuàng)建和輸入,所以就需要我們自己動手。我選擇GLFW是因為它很小,并且容易理解。
OpenGL Mathematics (GLM)是一個數(shù)學庫,用來處理矢量和矩陣等幾乎其它所有東西。舊版本OpenGL提供了類似glRotate,?glTranslate和glScale等函數(shù),在現(xiàn)代OpenGL中,這些函數(shù)已經(jīng)不存在了,我們需要自己處理所有的數(shù)學運算。GLM能在后續(xù)教程里提供很多矢量和矩陣運算上幫助。
在這系列的所有教程中,我們還編寫了一個小型庫tdogl用來重用C++代碼。這篇教程會包含tdogl::Shader和tdogl::Program用來加載,編譯和鏈接shaders。
什么是Shaders?
Shaders在現(xiàn)代OpenGL中是個很重要的概念。應用程序離不開它,除非你理解了,否則這些代碼也沒有任何意義。
Shaders是一段GLSL小程序,運行在GPU上而非CPU。它們使用OpenGL Shading Language (GLSL)語言編寫,看上去像C或C++,但卻是另外一種不同的語言。使用shader就像你寫個普通程序一樣:寫代碼,編譯,最后鏈接在一起才生成最終的程序。
Shaders并不是個很好的名字,因為它不僅僅只做著色。只要記得它們是個用不同的語言寫的,運行在顯卡上的小程序就行。
在舊版本的OpenGL中,shaders是可選的。在現(xiàn)代OpenGL中,為了能在屏幕上顯示出物體,shaders是必須的。
為可能近距離了解shaders和圖形渲染管線,我推薦Durian Software的相關(guān)文章The Graphics Pipeline chapter。
| 語言 | C++ | GLSL |
| 主函數(shù) | int main(int, char**); | void main(); |
| 運行于 | CPU | GPU |
| 需要編譯? | 是 | 是 |
| 需要鏈接? | 是 | 是 |
那shaders實際上干了啥?這取決于是哪種shader。
Vertex Shaders
Vertex shader主要用來將點(x,y,z坐標)變換成不同的點。頂點只是幾何形狀中的一個點,一個點叫vectex,多個點叫vertices(發(fā)音為ver-tuh-seez)。在本教程中,我們的三角形需要三個頂點(vertices)組成。
Vertex Shader的GLSL代碼如下:
| 1 2 3 4 5 6 7 8 | #version 150 in vec3 vert; void main() { // does not alter the vertices at all gl_Position = vec4(vert, 1); } |
第一行#version 150告訴OpenGL這個shader使用GLSL版本1.50.
第二行in vec3 vert;告訴shader需要那一個頂點作為輸入,放入變量vert。
第三行定義函數(shù)main,這是shader運行入口。這看上去像C,但GLSL中main不需要帶任何參數(shù),并且不用返回void。
第四行g(shù)l_Position = vec4(vert, 1);將輸入的頂點直接輸出,變量gl_Position是OpenGL定義的全局變量,用來存儲vertex shader的輸出。所有vertex shaders都需要對gl_Position進行賦值。
gl_Position是4D坐標(vec4),但vert是3D坐標(vec3),所以我們需要將vert轉(zhuǎn)換為4D坐標vec4(vert, 1)。第二個的參數(shù)1是賦值給第四維坐標。我們會在后續(xù)教程中學到更多關(guān)于4D坐標的東西。但現(xiàn)在,我們只要知道第四維坐標是1即可,i可以忽略它就把它當做3D坐標來對待。
Vertex Shader在本文中沒有做任何事,后續(xù)我們會修改它來處理動畫,攝像機和其它東西。
Fragment Shaders
Fragment shader的主要功能是計算每個需要繪制的像素點的顏色。
一個”fragment”基本上就是一個像素,所以你可以認為片段著色器(fragment shader)就是像素著色器(pixel shader)。在本文中每個片段都是一像素,但這并不總是這樣的。你可以更改某個OpenGL設置,以便得到比像素更小的片段,之后的文章我們會講到這個。
本文所使用的fragment shader代碼如下:
| 1 2 3 4 5 6 7 8 | #version 150 out vec4 finalColor; void main() { //set every drawn pixel to white finalColor = vec4(1.0, 1.0, 1.0, 1.0); } |
再次,第一行#version 150告訴OpenGL這個shader使用的是GLSL 1.50。
第二行finalColor = vec4(1.0, 1.0, 1.0, 1.0);將輸出變量設為白色。vec4(1.0, 1.0, 1.0, 1.0)是創(chuàng)建一個RGBA顏色,并且紅綠藍和alpha都設為最大值,即白色。
現(xiàn)在,就能用shader在OpenGL中繪制出了純白色。在之后的文章中,我們還會加入不同顏色和貼圖。貼圖就是你3D模型上的圖像。
編譯和鏈接Shaders
在C++中,你需要對你的.cpp文件進行編譯,然后鏈接到一起組成最終的程序。OpenGL的shaders也是這么回事。
在這篇文章中用到了兩個可復用的類,是用來處理shaders的編譯和鏈接:tdogl::Shader和tdogl::Program。這兩個類代碼不多,并且有詳細的注釋,我建議你閱讀源碼并且去鏈接OpenGL是如何工作的。
什么是VBO和VAO?
當shaders運行在GPU,其它代碼運行在CPU時,你需要有種方式將數(shù)據(jù)從CPU傳給GPU。在本文中,我們傳送了一個三角的三個頂點數(shù)據(jù),但在更大的工程中3D模型會有成千上萬個頂點,顏色,貼圖坐標和其它東西。
這就是我們?yōu)槭裁葱枰猇ertex Buffer Objects (VBOs)和Vertex Array Objects (VAOs)。VBO和VAO用來將C++程序的數(shù)據(jù)傳給shaders來渲染。
在舊版本的OpenGL中,是通過glVertex,glTexCoord和glNormal函數(shù)把每幀數(shù)據(jù)發(fā)送給GPU的。在現(xiàn)代OpenGL中,所有數(shù)據(jù)必須通過VBO在渲染之前發(fā)送給顯卡。當你需要渲染某些數(shù)據(jù)時,通過設置VAO來描述該獲取哪些VBO數(shù)據(jù)推送給shader變量。
Vertex Buffer Objects (VBOs)
第一步我們需要從內(nèi)存里上傳三角形的三個頂點到顯存中。這就是VBO該干的事。VBO其實就是顯存的“緩沖區(qū)(buffers)” - 一串包含各種二進制數(shù)據(jù)的字節(jié)區(qū)域。你能上傳3D坐標,顏色,甚至是你喜歡的音樂和詩歌。VBO不關(guān)心這些數(shù)據(jù)是啥,因為它只是對內(nèi)存進行復制。
Vertex Array Objects (VAOs)
第二步我們要用VBO的數(shù)據(jù)在shaders中渲染三角形。請記住VBO只是一塊數(shù)據(jù),它不清楚這些數(shù)據(jù)的類型。而告訴OpenGL這緩沖區(qū)里是啥類型數(shù)據(jù),這事就歸VAO管。
VAO對VBO和shader變量進行了連接。它描述了VBO所包含的數(shù)據(jù)類型,還有該傳遞數(shù)據(jù)給哪個shader變量。在OpenGL所有不準確的技術(shù)名詞中,“Vertex Array Object”是最爛的一個,因為它根本沒有解釋VAO該干的事。
你回頭看下本文的vertex shader(在文章的前面),你就能發(fā)現(xiàn)我們只有一個輸入變量vert。在本文中,我們用VAO來說明“hi,OpenGL,這里的VBO有3D頂點,我想要你在vertex shader時,發(fā)三個頂點數(shù)據(jù)給vert變量。”
在后續(xù)的文章中,我們會用VAO來說“hi,OpenGL,這里的VBO有3D頂點,顏色,貼圖坐標,我想要你在shader時,發(fā)頂點數(shù)據(jù)給vert變量,發(fā)顏色數(shù)據(jù)給vertColor變量,發(fā)貼圖坐標給vertTexCoord變量。”
?
代碼解釋
打開main.cpp,我們從main()函數(shù)開始。
首先,我們初始化GLFW:
| 1 2 3 | glfwSetErrorCallback(OnError); if(!glfwInit()) throw std::runtime_error("glfwInit failed"); |
glfwSetErrorCallback(OnError)這一行告訴GLFW當錯誤發(fā)生時調(diào)用OnError函數(shù)。OnError函數(shù)會拋一個包含錯誤信息的異常,我們能從中發(fā)現(xiàn)哪里出錯了。
然后我們用GLFW創(chuàng)建一個窗口。
| 1 2 3 4 5 6 7 8 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); gWindow = glfwCreateWindow((int)SCREEN_SIZE.x, (int)SCREEN_SIZE.y, "OpenGL Tutorial", NULL, NULL); if(!gWindow) throw std::runtime_error("glfwCreateWindow failed. Can your hardware handle OpenGL 3.2?"); |
該窗口包含一個向前兼容的OpenGL 3.2內(nèi)核上下文。假如glfwCreateWindow失敗了,你應該降低OpenGL版本。
創(chuàng)建窗口最后一步,我們應該設置一個“當前”O(jiān)penGL上下文給剛創(chuàng)建的窗口:
| 1 | glfwMakeContextCurrent(gWindow); |
無論我們調(diào)用哪個OpenGL函數(shù),都會影響到“當前上下文”。我們只會用到一個上下文,所以設置完后,就別管它了。理論上來說,我們可以有多個窗口,且每個窗口都可以有自己的上下文。
現(xiàn)在我們窗口有了OpenGL上下文變量,我們需要初始化GLEW以便訪問OpenGL接口。
| 1 2 3 | glewExperimental = GL_TRUE; //stops glew crashing on OSX :-/ if(glewInit() != GLEW_OK) throw std::runtime_error("glewInit failed"); |
這里的GLEW與OpenGL內(nèi)核有點小問題,設置glewExperimental就可以修復,但希望再未來永遠不要發(fā)生。
我們也可以用GLEW再次確認3.2版本是否存在:
| 1 2 | if(!GLEW_VERSION_3_2) throw std::runtime_error("OpenGL 3.2 API is not available."); |
在LoadShaders函數(shù)中,我們使用本教程提供的tdogl::Shader和tdogl::Program兩個類編譯和鏈接了vertex shader和fragment shader。
| 1 2 3 4 | std::vector<tdogl::Shader> shaders; shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("vertex-shader.txt"), GL_VERTEX_SHADER)); shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("fragment-shader.txt"), GL_FRAGMENT_SHADER)); gProgram = new tdogl::Program(shaders); |
在LoadTriangle函數(shù)中,我們創(chuàng)建了一個VAO和VBO。這是第一步,創(chuàng)建和綁定新的VAO:
| 1 2 | glGenVertexArrays(1, &gVAO); glBindVertexArray(gVAO); |
然后我們創(chuàng)建和綁定新的VBO:
| 1 2 | glGenBuffers(1, &gVBO); glBindBuffer(GL_ARRAY_BUFFER, gVBO); |
接著,我們上傳一些數(shù)據(jù)到VBO中。這些數(shù)據(jù)就是三個頂點,每個頂點包含三個GLfloat。
| 1 2 3 4 5 6 7 | GLfloat vertexData[] = { // X Y Z 0.0f, 0.8f, 0.0f, -0.8f,-0.8f, 0.0f, 0.8f,-0.8f, 0.0f, }; glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW); |
現(xiàn)在緩沖區(qū)包含了三角形的三個頂點,是時候開始設置VAO了。首先,我們應該啟用shader程序中的vert變量。這些變量能被開啟或關(guān)閉,默認情況下是關(guān)閉的,所以我們需要開啟它。vert變量是一個“屬性變量(attribute variable)”,這也是為何OpenGL函數(shù)名稱中有帶“Attrib”。我們可以在后續(xù)的文章中看到更多類型。
| 1 | glEnableVertexAttribArray(gProgram->attrib("vert")); |
VAO設置最復雜的部分就是下個函數(shù):glVertexAttribPointer。讓我們先調(diào)用該函數(shù),等會解釋。
| 1 | glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL); |
第一個參數(shù),gProgram->attrib("vert"),這就是那個需要上傳數(shù)據(jù)的shder變量。在這個例子中,我們需要發(fā)數(shù)據(jù)給vertshader變量。
第二個參數(shù),3表明每個頂點需要三個數(shù)字。
第三個參數(shù),GL_FLOAT說明三個數(shù)字是GLfloat類型。這非常重要,因為GLdouble類型的數(shù)據(jù)大小跟它是不同的。
第四個參數(shù),GL_FALSE說明我們不需要對浮點數(shù)進行“歸一化”,假如我們使用了歸一化,那這個值會被限定為最小0,最大1。我們不需要對我們的頂點進行限制,所以這個參數(shù)為false。
第五個參數(shù),0,該參數(shù)可以在頂點之間有間隔時使用,設置參數(shù)為0,表示數(shù)據(jù)之間沒有間隔。
第六個參數(shù),NULL,假如我們的數(shù)據(jù)不是從緩沖區(qū)頭部開始的話,可以設置這個參數(shù)來指定。設置該參數(shù)為NULL,表示我們的數(shù)據(jù)從VBO的第一個字節(jié)開始。
現(xiàn)在VBO和VAO都設置完成,我們需要對它們進行解綁定,防止一不小心被哪里給更改了。
| 1 2 | glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); |
到此,shader,VBO和VAO都準備好了。我們可以開始在Render函數(shù)里繪制了。
首先,我們先清空下屏幕,讓它變成純黑色:
| 1 2 | glClearColor(0, 0, 0, 1); // black glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
然后告訴OpenGL我們要開始使用VAO和shader了:
| 1 2 | glUseProgram(gProgram->object()); glBindVertexArray(gVAO); |
最后,我們繪制出三角形:
| 1 | glDrawArrays(GL_TRIANGLES, 0, 3); |
調(diào)用glDrawArrays函數(shù)說明我們需要繪制三角形,從第0個頂點開始,有3個頂點被發(fā)送到shader。OpenGL會在當前VAO范圍內(nèi)確定該從哪里獲取頂點。
頂點將會從VBO中取出并發(fā)送到vertex shader。然后三角形內(nèi)的每個像素會發(fā)送給fragment shader。接著fragment shader將每個像素變成白色。歡呼!
現(xiàn)在繪制結(jié)束了,為了安全起見,我們需要將shader和VAO進行解綁定:
| 1 2 | glBindVertexArray(0); glUseProgram(0); |
最后一件事,在我們看到三角形之前需要切換幀緩沖:
| 1 | glfwSwapBuffers(gWindow); |
在幀緩沖被交換前,我們會繪制到一個不可見的離屏(off-screen)幀緩沖區(qū)。當我們調(diào)用glfwSwapBuffers時,離屏緩沖會變成屏幕緩沖,所以我們就能在窗口上看見內(nèi)容了。
第一個OpenGL程序解讀
OpenGL中的大多數(shù)函數(shù)使用了一種基于狀態(tài)的方法,大多數(shù)OpenGL對象都需要在使用前把該對象綁定到context上。這里有兩個新名詞——OpenGL對象和Context。
Context
Context是一個非常抽象的概念,我們姑且把它理解成一個包含了所有OpenGL狀態(tài)的對象。如果我們把一個Context銷毀了,那么OpenGL也不復存在。
OpenGL對象
我們可以把OpenGL對象理解成一個狀態(tài)的集合,它負責管理它下屬的所有狀態(tài)。當然,除了狀態(tài),OpenGL對象還會存儲其他數(shù)據(jù)。注意。這些狀態(tài)和上述context中的狀態(tài)并不重合,只有在把一個OpenGL對象綁定到context上時,OpenGL對象的各種狀態(tài)才會映射到context的狀態(tài)。因此,這時如果我們改變了context的狀態(tài),那么也會影響這個對象,而相反地,依賴這些context狀態(tài)的函數(shù)也會使用存儲在這個對象上的數(shù)據(jù)。
因此,OpenGL對象的綁定既可能是為了修改該對象的狀態(tài)(大多數(shù)對象需要綁定到context上才可以改變它的狀態(tài)),也可能是為了讓context渲染時使用它的狀態(tài)。
畫了一個圖,僅供理解。圖中灰色的方塊代表各種狀態(tài),箭頭表示當把一個OpenGL對象綁定到context上后,對應狀態(tài)的映射。
?
前面提到過,OpenGL就是一個“狀態(tài)機”。那些各種各樣的API調(diào)用會改變這些狀態(tài),或者根據(jù)這些狀態(tài)進行操作。但我們要注意的是,這只是說明了OpenGL是怎樣被定義的,但硬件是否是按狀態(tài)機實現(xiàn)的就是另一回事了。不過,這不是我們需要擔心的地方。
OpenGL對象包含了下面一些類型:Buffer Objects,Vertex Array Objects,Textures,Framebuffer Objects等等。我們下面會講到Vertex Array Objects這個對象。
這些對象都有三個相關(guān)的重要函數(shù):
1、負責生成一個對象的name。而name就是這個對象的引用。
2、負責銷毀一個對象
3、將對象綁定到context上。
- 渲染(Rendering):計算機從模型到創(chuàng)建一張圖像的過程。OpenGL僅僅是其中一個渲染系統(tǒng)。它是一個基于光柵化的系統(tǒng),其他的系統(tǒng)還有光線追蹤(但有時也會用到OpenGL)等。
- 模型(Models)或者對象(Objects):這里兩者的含義是一樣的。指從幾何圖元——點、線、三角形中創(chuàng)建的東西,由頂點指定。
- Shaders:這是一類特殊的函數(shù),是在圖形硬件上執(zhí)行的。我們可以理解成,Shader是一些為圖形處理單元(GPU)編譯的小程序。OpenGL包含了編譯工具來把我們編寫的Shader源代碼編譯成可以在GPU上運行的代碼。在OpenGL中,我們可以使用四種shader階段。最常見的就是vertex shaders——它們可以處理頂點數(shù)據(jù);以及fragment shaders,它們處理光柵化后生成的fragments。vertex shaders和fragment shaders是每個OpenGL程序必不可少的部分。
- 像素(pixel):像素是我們顯示器上的最小可見元素。我們系統(tǒng)中的像素被存儲在一個幀緩存(framebuffer)中。幀緩存是一塊由圖形硬件管理的內(nèi)存空間,用于供給給我們的顯示設備。
驚鴻一瞥
我們的第一個程序(不完整)的運行結(jié)果如下: 代碼如下(提示:這里可以粗略地看下中文注釋,后面會更詳細講述的):?
/// // // 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ù)據(jù).例如頂點信息,紋理等 // 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 shadersShaderInfo shaders[] = {{ GL_VERTEX_SHADER, "triangles.vert" },{ GL_FRAGMENT_SHADER, "triangles.frag" },{ GL_NONE, NULL }};// LoadShaders()是我們自定義(這里沒有給出)的一個函數(shù),// 用于簡化為GPU準備shaders的過程,后面會詳細講述GLuint program = LoadShaders(shaders);glUseProgram(program);// 最后這部分我們成為shader plumbing,// 我們把需要的數(shù)據(jù)和shader程序中的變量關(guān)聯(lián)在一起,后面會詳細講述glVertexAttribPointer(vPosition, 2, GL_FLOAT,GL_FALSE, 0, BUFFER_OFFSET(0));glEnableVertexAttribArray(vPosition); }//--------------------------------------------------------------------- // // display // // 這個函數(shù)是真正進行渲染的地方.它調(diào)用OpenGL的函數(shù)來請求數(shù)據(jù)進行渲染. // 幾乎所有的display函數(shù)都會進行下面的三個步驟. // 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ù),最后進入到事件循環(huán)(event loop). // 這里仍會看到一些以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(); }?
轉(zhuǎn)載于:https://www.cnblogs.com/Anita9002/p/9145079.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
- 上一篇: 树链剖分 完美的想法
- 下一篇: git fatal:HttpReques