OpenGL教程一
引自:https://blog.csdn.net/u013654125/article/details/73613644
GLEW, GLFW和GLM介紹
現在你有了工程,就讓我們開始介紹下工程所用到的開源庫和為啥需要這些。
The OpenGL Extension Wrangler (GLEW)是用來訪問OpenGL 3.2 API函數的。不幸的是你不能簡單的使用#include <GL/gl.h>來訪問OpenGL接口,除非你想用舊版本的OpenGL。在現代OpenGL中,API函數是在運行時(run time)確定的,而非編譯期(compile time)。GLEW可以在運行時加載OpenGL API。
GLFW允許我們跨平臺創建窗口,接受鼠標鍵盤消息。OpenGL不處理這些窗口創建和輸入,所以就需要我們自己動手。我選擇GLFW是因為它很小,并且容易理解。
OpenGL Mathematics (GLM)是一個數學庫,用來處理矢量和矩陣等幾乎其它所有東西。舊版本OpenGL提供了類似glRotate,?glTranslate和glScale等函數,在現代OpenGL中,這些函數已經不存在了,我們需要自己處理所有的數學運算。GLM能在后續教程里提供很多矢量和矩陣運算上幫助。
在這系列的所有教程中,我們還編寫了一個小型庫tdogl用來重用C++代碼。這篇教程會包含tdogl::Shader和tdogl::Program用來加載,編譯和鏈接shaders。
什么是Shaders?
Shaders在現代OpenGL中是個很重要的概念。應用程序離不開它,除非你理解了,否則這些代碼也沒有任何意義。
Shaders是一段GLSL小程序,運行在GPU上而非CPU。它們使用OpenGL Shading Language (GLSL)語言編寫,看上去像C或C++,但卻是另外一種不同的語言。使用shader就像你寫個普通程序一樣:寫代碼,編譯,最后鏈接在一起才生成最終的程序。
Shaders并不是個很好的名字,因為它不僅僅只做著色。只要記得它們是個用不同的語言寫的,運行在顯卡上的小程序就行。
在舊版本的OpenGL中,shaders是可選的。在現代OpenGL中,為了能在屏幕上顯示出物體,shaders是必須的。
為可能近距離了解shaders和圖形渲染管線,我推薦Durian Software的相關文章The Graphics Pipeline chapter。
| 語言 | C++ | GLSL |
| 主函數 | int main(int, char**); | void main(); |
| 運行于 | CPU | GPU |
| 需要編譯? | 是 | 是 |
| 需要鏈接? | 是 | 是 |
那shaders實際上干了啥?這取決于是哪種shader。
Vertex Shaders
Vertex shader主要用來將點(x,y,z坐標)變換成不同的點。頂點只是幾何形狀中的一個點,一個點叫vectex,多個點叫vertices(發音為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。
第三行定義函數main,這是shader運行入口。這看上去像C,但GLSL中main不需要帶任何參數,并且不用返回void。
第四行gl_Position = vec4(vert, 1);將輸入的頂點直接輸出,變量gl_Position是OpenGL定義的全局變量,用來存儲vertex shader的輸出。所有vertex shaders都需要對gl_Position進行賦值。
gl_Position是4D坐標(vec4),但vert是3D坐標(vec3),所以我們需要將vert轉換為4D坐標vec4(vert, 1)。第二個的參數1是賦值給第四維坐標。我們會在后續教程中學到更多關于4D坐標的東西。但現在,我們只要知道第四維坐標是1即可,i可以忽略它就把它當做3D坐標來對待。
Vertex Shader在本文中沒有做任何事,后續我們會修改它來處理動畫,攝像機和其它東西。
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)是創建一個RGBA顏色,并且紅綠藍和alpha都設為最大值,即白色。
現在,就能用shader在OpenGL中繪制出了純白色。在之后的文章中,我們還會加入不同顏色和貼圖。貼圖就是你3D模型上的圖像。
編譯和鏈接Shaders
在C++中,你需要對你的.cpp文件進行編譯,然后鏈接到一起組成最終的程序。OpenGL的shaders也是這么回事。
在這篇文章中用到了兩個可復用的類,是用來處理shaders的編譯和鏈接:tdogl::Shader和tdogl::Program。這兩個類代碼不多,并且有詳細的注釋,我建議你閱讀源碼并且去鏈接OpenGL是如何工作的。
什么是VBO和VAO?
當shaders運行在GPU,其它代碼運行在CPU時,你需要有種方式將數據從CPU傳給GPU。在本文中,我們傳送了一個三角的三個頂點數據,但在更大的工程中3D模型會有成千上萬個頂點,顏色,貼圖坐標和其它東西。
這就是我們為什么需要Vertex Buffer Objects (VBOs)和Vertex Array Objects (VAOs)。VBO和VAO用來將C++程序的數據傳給shaders來渲染。
在舊版本的OpenGL中,是通過glVertex,glTexCoord和glNormal函數把每幀數據發送給GPU的。在現代OpenGL中,所有數據必須通過VBO在渲染之前發送給顯卡。當你需要渲染某些數據時,通過設置VAO來描述該獲取哪些VBO數據推送給shader變量。
Vertex Buffer Objects (VBOs)
第一步我們需要從內存里上傳三角形的三個頂點到顯存中。這就是VBO該干的事。VBO其實就是顯存的“緩沖區(buffers)” - 一串包含各種二進制數據的字節區域。你能上傳3D坐標,顏色,甚至是你喜歡的音樂和詩歌。VBO不關心這些數據是啥,因為它只是對內存進行復制。
Vertex Array Objects (VAOs)
第二步我們要用VBO的數據在shaders中渲染三角形。請記住VBO只是一塊數據,它不清楚這些數據的類型。而告訴OpenGL這緩沖區里是啥類型數據,這事就歸VAO管。
VAO對VBO和shader變量進行了連接。它描述了VBO所包含的數據類型,還有該傳遞數據給哪個shader變量。在OpenGL所有不準確的技術名詞中,“Vertex Array Object”是最爛的一個,因為它根本沒有解釋VAO該干的事。
你回頭看下本文的vertex shader(在文章的前面),你就能發現我們只有一個輸入變量vert。在本文中,我們用VAO來說明“hi,OpenGL,這里的VBO有3D頂點,我想要你在vertex shader時,發三個頂點數據給vert變量?!?/p>
在后續的文章中,我們會用VAO來說“hi,OpenGL,這里的VBO有3D頂點,顏色,貼圖坐標,我想要你在shader時,發頂點數據給vert變量,發顏色數據給vertColor變量,發貼圖坐標給vertTexCoord變量?!?/p>
?
代碼解釋
打開main.cpp,我們從main()函數開始。
首先,我們初始化GLFW:
| 1 2 3 | glfwSetErrorCallback(OnError); if(!glfwInit()) throw std::runtime_error("glfwInit failed"); |
glfwSetErrorCallback(OnError)這一行告訴GLFW當錯誤發生時調用OnError函數。OnError函數會拋一個包含錯誤信息的異常,我們能從中發現哪里出錯了。
然后我們用GLFW創建一個窗口。
| 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內核上下文。假如glfwCreateWindow失敗了,你應該降低OpenGL版本。
創建窗口最后一步,我們應該設置一個“當前”OpenGL上下文給剛創建的窗口:
| 1 | glfwMakeContextCurrent(gWindow); |
無論我們調用哪個OpenGL函數,都會影響到“當前上下文”。我們只會用到一個上下文,所以設置完后,就別管它了。理論上來說,我們可以有多個窗口,且每個窗口都可以有自己的上下文。
現在我們窗口有了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內核有點小問題,設置glewExperimental就可以修復,但希望再未來永遠不要發生。
我們也可以用GLEW再次確認3.2版本是否存在:
| 1 2 | if(!GLEW_VERSION_3_2) throw std::runtime_error("OpenGL 3.2 API is not available."); |
在LoadShaders函數中,我們使用本教程提供的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函數中,我們創建了一個VAO和VBO。這是第一步,創建和綁定新的VAO:
| 1 2 | glGenVertexArrays(1, &gVAO); glBindVertexArray(gVAO); |
然后我們創建和綁定新的VBO:
| 1 2 | glGenBuffers(1, &gVBO); glBindBuffer(GL_ARRAY_BUFFER, gVBO); |
接著,我們上傳一些數據到VBO中。這些數據就是三個頂點,每個頂點包含三個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); |
現在緩沖區包含了三角形的三個頂點,是時候開始設置VAO了。首先,我們應該啟用shader程序中的vert變量。這些變量能被開啟或關閉,默認情況下是關閉的,所以我們需要開啟它。vert變量是一個“屬性變量(attribute variable)”,這也是為何OpenGL函數名稱中有帶“Attrib”。我們可以在后續的文章中看到更多類型。
| 1 | glEnableVertexAttribArray(gProgram->attrib("vert")); |
VAO設置最復雜的部分就是下個函數:glVertexAttribPointer。讓我們先調用該函數,等會解釋。
| 1 | glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL); |
第一個參數,gProgram->attrib("vert"),這就是那個需要上傳數據的shder變量。在這個例子中,我們需要發數據給vertshader變量。
第二個參數,3表明每個頂點需要三個數字。
第三個參數,GL_FLOAT說明三個數字是GLfloat類型。這非常重要,因為GLdouble類型的數據大小跟它是不同的。
第四個參數,GL_FALSE說明我們不需要對浮點數進行“歸一化”,假如我們使用了歸一化,那這個值會被限定為最小0,最大1。我們不需要對我們的頂點進行限制,所以這個參數為false。
第五個參數,0,該參數可以在頂點之間有間隔時使用,設置參數為0,表示數據之間沒有間隔。
第六個參數,NULL,假如我們的數據不是從緩沖區頭部開始的話,可以設置這個參數來指定。設置該參數為NULL,表示我們的數據從VBO的第一個字節開始。
現在VBO和VAO都設置完成,我們需要對它們進行解綁定,防止一不小心被哪里給更改了。
| 1 2 | glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); |
到此,shader,VBO和VAO都準備好了。我們可以開始在Render函數里繪制了。
首先,我們先清空下屏幕,讓它變成純黑色:
| 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); |
調用glDrawArrays函數說明我們需要繪制三角形,從第0個頂點開始,有3個頂點被發送到shader。OpenGL會在當前VAO范圍內確定該從哪里獲取頂點。
頂點將會從VBO中取出并發送到vertex shader。然后三角形內的每個像素會發送給fragment shader。接著fragment shader將每個像素變成白色。歡呼!
現在繪制結束了,為了安全起見,我們需要將shader和VAO進行解綁定:
| 1 2 | glBindVertexArray(0); glUseProgram(0); |
最后一件事,在我們看到三角形之前需要切換幀緩沖:
| 1 | glfwSwapBuffers(gWindow); |
在幀緩沖被交換前,我們會繪制到一個不可見的離屏(off-screen)幀緩沖區。當我們調用glfwSwapBuffers時,離屏緩沖會變成屏幕緩沖,所以我們就能在窗口上看見內容了。
第一個OpenGL程序解讀
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這個對象。
這些對象都有三個相關的重要函數:
1、負責生成一個對象的name。而name就是這個對象的引用。
2、負責銷毀一個對象
3、將對象綁定到context上。
- 渲染(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 // /// //-------------------------------------------------------------------- // // 在程序一開頭,我們包含了所需的頭文件, // 聲明了一些全局變量(但通常是不用全局變量在做的,這里只是為了說明一些基本問題) // 以及其他一些有用的程序結構 // #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()函數用于設置我們后面會用到的一些數據.例如頂點信息,紋理等 // 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()是我們自定義(這里沒有給出)的一個函數,// 用于簡化為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(); }?
轉載于:https://www.cnblogs.com/Anita9002/p/9145079.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: 树链剖分 完美的想法
- 下一篇: 德哥的PostgreSQL私房菜 - 史