OpenGL核心模式详细讲解[结合LearnOpenGL]
OpenGL立即渲染模式&核心模式
OpenGL (for“Open Graphics Library”) is an API (Application Programming Interface) to graphics hardware. The API consists of a set of several hundred procedures and functions that allow a programmer to specify the shader programs, objects, and operations involved in producing high-quality graphical images, specifically color images of three-dimensional objects.
OpenGL一般被認為是一個API(Application Programming Interface),即應用程序編程接口,包含了一系列可以操作圖形、圖像的函數(shù),允許程序員指定著色程序、對象和涉及到生成高質量圖形圖像(特別是三維對象的彩色圖像)的操作,但其實,OpenGL本身并不是一個API,它僅僅是一個規(guī)范,這就像SFS和GDAL的關系一樣,OpenGL嚴格規(guī)范了函數(shù)的功能,函數(shù)的執(zhí)行流程以及輸出值,而函數(shù)的內部實現(xiàn)由開發(fā)者自行決定,只要其功能和結果與規(guī)范相匹配即可。
一 狀態(tài)機
首先,我們要理解的是OpenGL是一個巨大的狀態(tài)機:OpenGL內部定義了一系列的變量去描述OpenGL運行的模式,OpenGL的狀態(tài)通常被稱為OpenGL上下文。因此,當我們實際使用OpenGL的時候,會使用狀態(tài)設置函數(shù)改變上下文,使用狀態(tài)使用函數(shù)根據(jù)當前OpenGL的狀態(tài)執(zhí)行一些操作。
二 立即渲染模式
早期的OpenGL常使用立即渲染模式,即固定渲染管線,這種模式繪圖十分方便,但OpenGL的大多數(shù)功能都被隱藏了,開發(fā)者很少可以自由的控制OpenGL,隨著時間的推移,開發(fā)者迫切希望能有更多的靈活性,規(guī)范越來越靈活,開發(fā)者對繪圖細節(jié)有了更多的掌控。從OpenGL3.2開始,規(guī)范文檔開始廢棄立即渲染模式,鼓勵開發(fā)者在OpenGL的核心模式下進行開發(fā)。
下面給出立即將渲染模式的繪制流程:
/*初始化*/ void initializeGL() {// 初始化OpenGL函數(shù)initializeOpenGLFunctions();glClearColor(255, 255, 255, 1); // 狀態(tài)設置函數(shù) } /*繪制*/ void paintGL() {// 清理顏色和深度緩存(狀態(tài)使用函數(shù))glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 設置矩陣模式為投影glMatrixMode(GL_PROJECTION);// 加載單位矩陣glLoadIdentity();// 定義投影glOrtho(-10.0, 10.0, -10.0, 10.0, -1, 1);// 開始繪圖glBegin(GL_LINE_LOOP);// 定義顏色glColor3f(0, 1, 0);// 循環(huán)設置點for (int i = 0; i < 10; i++) {glVertex2f(i, 0);glVertex2f(0, -i);glVertex2f(-i, 0);glVertex2f(0, i);}// 結束繪圖glEnd();// 清除矩陣glPopMatrix();// 刷新glFlush();} /*視口改變*/ void resizeGL(int w, int h) {glViewport(0, 0, w, h); }結果如圖:
立即渲染模式的優(yōu)點是流程簡單,面向過程,易于理解,但相對的,繪制效率很低,主要原因:
- glVertexglVertexglVertex函數(shù)每次調用只把一個頂點從客戶端(CPU或內存)傳輸?shù)椒斩?#xff08;GPU),而這個傳輸?shù)倪^程相對于GPU處理數(shù)據(jù)的過程是很慢的;
- glVertexglVertexglVertex函數(shù)的調用次數(shù)過多。
三 核心模式
核心模式完全移除了舊的特性,具有很高的靈活性和效率,但同時也更難于學習,要求使用者真正理解OpenGL和圖形編程。
首先給出一個OpenGL的核心模式繪圖的例子:
unsigned int VAO, VBO,ID; //頂點數(shù)組對象、頂點緩沖對象、著色器 /*初始化*/ void initializeGL() {// 初始化OpenGL函數(shù)initializeOpenGLFunctions();glClearColor(255, 255, 255, 1); // 狀態(tài)設置函數(shù)createShader("G:\\kmj\\實習\\teach\\shader.vs", "G:\\kmj\\實習\\teach\\shader.fs");float* vertices = new float[40 * 3]; //生成40個點// 循環(huán)生成點for (int i = 0; i < 10; i++) {vertices[i * 12] = i;vertices[i * 12 + 1] = 0;vertices[i * 12 + 2] = 1.0;vertices[i * 12 + 3] = 0;vertices[i * 12 + 4] = -i;vertices[i * 12 + 5] = 1.0;vertices[i * 12 + 6] = -i;vertices[i * 12 + 7] = 0;vertices[i * 12 + 8] = 1.0;vertices[i * 12 + 9] = 0;vertices[i * 12 + 10] = i;vertices[i * 12 + 11] = 0;}// 生成VAO、VBO對象glGenBuffers(1, &VBO);glGenVertexArrays(1, &VAO);// 將VAO與當前VBO關聯(lián)glBindVertexArray(VAO);// 綁定VBO到上下文,頂點緩沖對象的緩沖類型是GL_ARRAY_BUFFERglBindBuffer(GL_ARRAY_BUFFER, VBO);// 綁定數(shù)據(jù)到緩沖GL_ARRAY_BUFFERglBufferData(GL_ARRAY_BUFFER, 120 * sizeof(float), vertices, GL_STATIC_DRAW);// 設置頂點屬性指針glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);// 解綁VAOglEnableVertexAttribArray(0);// 解綁VBOglBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);delete[] vertices; } /*繪圖*/ void paintGL() {// 清理顏色和深度緩存glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 定義投影glOrtho(-10.0, 10.0, -10.0, 10.0, -1, 1);QMatrix4x4 ortho;QRectF rect(QPointF(-10,10),QPointF(10,-10));ortho.ortho(rect);GLuint projLoc = glGetUniformLocation(ID, "ortho");glUniformMatrix4fv(projLoc, 1, GL_FALSE, ortho.data());// 開始繪圖glUseProgram(ID);glBindVertexArray(VAO);glDrawArrays(GL_LINE_LOOP, 0, 40);} /*視口*/ void resizeGL(int w, int h) {glViewport(0, 0, w, h); }/*著色器編譯*/ void checkCompileErrors(unsigned int shader, std::string type) {int success;char infoLog[1024];if (type != "PROGRAM"){glGetShaderiv(shader, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(shader, 1024, NULL, infoLog);std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;}}else{glGetProgramiv(shader, GL_LINK_STATUS, &success);if (!success){glGetProgramInfoLog(shader, 1024, NULL, infoLog);std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;}} } /*創(chuàng)建著色器*/ void createShader(const char* vertexPath, const char* fragmentPath) {// 1. retrieve the vertex/fragment source code from filePathstd::string vertexCode;std::string fragmentCode;std::ifstream vShaderFile;std::ifstream fShaderFile;// ensure ifstream objects can throw exceptions:vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);try{// open filesvShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// read file's buffer contents into streamsvShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf();// close file handlersvShaderFile.close();fShaderFile.close();// convert stream into stringvertexCode = vShaderStream.str();fragmentCode = fShaderStream.str();}catch (std::ifstream::failure e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;}const char* vShaderCode = vertexCode.c_str();const char* fShaderCode = fragmentCode.c_str();// 2. compile shadersunsigned int vertex, fragment;// vertex shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);checkCompileErrors(vertex, "VERTEX");// fragment Shaderfragment = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragment, 1, &fShaderCode, NULL);glCompileShader(fragment);checkCompileErrors(fragment, "FRAGMENT");// shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);checkCompileErrors(ID, "PROGRAM");// delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment); }shader.vs:
#version 450 core layout (location = 0) in vec3 aPos;uniform mat4 ortho; void main() {gl_Position = ortho * vec4(aPos.x, aPos.y, aPos.z, 1.0); }shader.fs:
#version 450 core out vec4 FragColor; void main() {FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }其中著色器編譯是固定操作,直接復制即可,結果為:
可以看出核心模式相較于立即渲染模式更為復雜,下面我們詳細講解這個例子。
3.1 圖形渲染管線
OpenGL的坐標系是三維坐標系,也就是說每一個物體都同時具有x,y,zx,y,zx,y,z三個坐標,但屏幕是二維坐標,這就需要我們使用某種方法把3D轉為2D,在OpenGL中,圖形渲染管線(Graphics Pipeline)用于實現(xiàn)這個過程。
圖形渲染管線接受一組3D坐標,然后把它們轉變?yōu)槟闫聊簧系挠猩?D像素輸出。圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個特定的函數(shù)),并且很容易并行執(zhí)行。正是由于它們具有并行執(zhí)行的特性,當今大多數(shù)顯卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數(shù)據(jù)。這些小程序叫做著色器(Shader)。
一個圖形渲染管線的抽象展示為下圖:
其中我們能夠控制的著色器包括頂點著色器、幾何著色器和片段著色器。
OpenGL中定義著色器使用OpenGL著色器語言(OpenGL Shading Language,GLSL)。
3.1.1 頂點著色器
頂點著色器是圖形渲染管線的第一個部分,它接受一個頂點數(shù)據(jù),允許我們對頂點屬性進行一些基本的處理,一個簡單的頂點著色器如下:
#version 450 core layout (location = 0) in vec3 aPos;void main() {gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }標準化設備坐標
OpenGL在處理我們輸入的頂點數(shù)據(jù)時,并不是把所有的3D坐標變換為屏幕的2D像素,僅在3D坐標在三個軸x,y,zx,y,zx,y,z∈\in∈(?1.0,1.0)(-1.0,1.0)(?1.0,1.0)時才進行處理,這就是標準化設備坐標的概念。
在這個著色器中,#version 450 core表示著色器的版本號,GLSL和OpenGL的版本應該是相互匹配的,這句話表示當前使用的OpenGL是4.5核心模式。
gl_Position設置的值是該頂點著色器的輸出,vec4表示定義一個四維向量。
layout (location = 0) in vec3 aPos;設定輸入變量的位置,in表示輸入?yún)?shù)。
這是一個最簡單的頂點著色器,未對輸入數(shù)據(jù)進行任何處理進行輸出,也就是默認數(shù)據(jù)均處于標準化設備坐標中,實際開發(fā)中,我們還在該著色器中進行坐標的變換,以使坐標轉換到標準化設備坐標系之中。
3.1.2 圖元裝配
圖元裝配階段接受頂點著色器輸出的所有頂點,并把所有的點裝配成指定圖元的形狀,如:點、三角形、線、面等。常用圖元類型:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP、GL_LINE_LOOP。
3.1.3 幾何著色器
幾何著色器用于產(chǎn)生新的其他形狀,圖中產(chǎn)生了另一個三角形。
3.1.4 光柵化階段
幾何著色器的輸出會進入光柵化階段,會把圖元映射為最終屏幕上相應的像素,生成供片段著色器使用的片段,在片段著色器運行之前會執(zhí)行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執(zhí)行效率。
3.1.5 片段著色器
用于計算一個像素的最終顏色,也是OpenGL高級效果產(chǎn)生的地方,如光照、陰影、光的顏色等。一個簡單的片段著色器如下:
#version 450 core out vec4 FragColor; void main() {FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }片段著色器只需要一個輸出變量,這個變量是一個4分量向量,它表示的是最終的輸出顏色,我們應該自己將其計算出來。我們可以用out關鍵字聲明輸出變量,這里我們命名為FragColor。
3.1.6 測試和混合
這個階段檢測片段的對應的深度(和模板(Stencil))值(后面會講),用它們來判斷這個像素是其它物體的前面還是后面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)并對物體進行混合(Blend)。所以,即使在片段著色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同。
*注意,在使用核心模式開發(fā)時,我們必須至少定義頂點著色器和片段著色器,這也是OpenGL核心模式較困難的其中一個原因。
3.2 開始實踐
在大致理解了OpenGL的圖形渲染流程之后,我們可以開始實踐了,我們從最簡單的例子出發(fā),對如下頂點數(shù)據(jù)進行可視化:
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f };根據(jù)圖形渲染流程,我們首先要把數(shù)據(jù)發(fā)送給頂點著色器進行處理,這需要用到VBO對象,它可以在GPU內存中儲存頂點數(shù)據(jù),使用VBO的好處是我們可以一次性的發(fā)送一大批數(shù)據(jù)到顯卡上,而不需要每個頂點發(fā)送一次(立即渲染模式就是這樣做的),從CPU把數(shù)據(jù)發(fā)送到顯卡相對較慢,所以我們要盡可能嘗試一次性發(fā)送盡可能多的數(shù)據(jù),當數(shù)據(jù)發(fā)送至顯卡的內存中后,頂點著色器幾乎能立即訪問頂點
glGenBuffers函數(shù)用于生成VBO對象,同時我們還需要定義一個ID來唯一標識這個緩沖對象。
unsigned int VBO; glGenBuffers(1,&VBO);接著使用glBindBuffer函數(shù)把VBO對象綁定到頂點緩沖對象的緩沖類型GL_ARRAY_BUFFER上。
glBindBuffer(GL_ARRAY_BUFFER,VBO);綁定完畢之后,就相當于把VBO對象綁定到了OpenGL狀態(tài)機的上下文中,那么接下來我們在GL_ARRAY_BUFFER上的所有操作都會改變VBO對象,然后我們可以調用glBufferData函數(shù)設置緩沖對象的數(shù)據(jù)。
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);- 第一個參數(shù)是數(shù)據(jù)綁定的緩沖類型;
- 第二個參數(shù)是數(shù)據(jù)的大小,以字節(jié)數(shù)為單位;
- 第三個參數(shù)是數(shù)據(jù);
- 第四個參數(shù)是顯卡管理給定數(shù)據(jù)的方式。
為緩沖綁定好數(shù)據(jù)之后,數(shù)據(jù)將會進入頂點著色器:
#version 450 core layout (location = 0) in vec3 aPos;void main() {gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }apos是我們要輸入到頂點著色器中的數(shù)據(jù)。
頂點著色器允許我們指定任何以頂點屬性為形式的輸入。這使其具有很強的靈活性的同時,它還的確意味著我們必須手動指定輸入數(shù)據(jù)的哪一個部分對應頂點著色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點數(shù)據(jù)。
對于本例,我們的頂點緩沖對象會被解析成:
所以我們需要調用glVertexAttribPointer函數(shù)告訴OpenGL如何去解析我們的輸入數(shù)據(jù)。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);- 第一個參數(shù)表示我們把數(shù)據(jù)傳到哪一個位置,即location的值;
- 第二個參數(shù)表示頂點屬性的大小,vec3大小為3;
- 第三個參數(shù)表示頂點屬性的類型,GLSL中vec是GL_FLOAT類型的;
- 第四個參數(shù)表示是否希望數(shù)據(jù)標準化;
- 第五個參數(shù)表示步長,也就是相鄰頂點屬性的間隔,以字節(jié)數(shù)為單位;
- 第六個參數(shù)是該數(shù)據(jù)在緩沖中起始位置的偏移量,這一參數(shù)往往在我們既儲存位置又儲存顏色數(shù)據(jù)的時候設置。
在片段著色器中,我們定義顏色,此時我們定義一個很簡單的片段著色器:
#version 450 core out vec4 FragColor; void main() {FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }定義完成著色器后,我們還需要進行編譯并將這兩個著色器鏈接為著色器程序;
unsigned int vertex, fragment,ID; // 編譯頂點著色器 vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); // 編譯片段著色器 fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); // 鏈接兩個著色器 ID = glCreateProgram(); glAttachShader(ID, vertex); glAttachShader(ID, fragment); glLinkProgram(ID); // 鏈接完回收 glDeleteShader(vertex); glDeleteShader(fragment);著色器編譯的完整實現(xiàn)在上例中已經(jīng)定義,可以當成工具函數(shù)使用,不贅述。
到這里為止,我們已經(jīng)可以繪制出圖像了,結果為:
這種繪制方式已經(jīng)是對傳統(tǒng)的立即繪制的很大改良了,但我們還是可以發(fā)現(xiàn),每繪制一個物體,我們都要重復下面過程:
glGenBuffers(1, &VBO); // 0. 復制頂點數(shù)組到緩沖中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, count * sizeof(float), vertices, GL_STATIC_DRAW); // 1. 設置頂點屬性指針 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //delete[] vertices; glEnableVertexAttribArray(0); // 開始繪圖 glUseProgram(ID); //glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3);這其實也挺麻煩的,所以VAO出現(xiàn)了,下面我們采用VAO對上例進行改進。
VAO是頂點數(shù)組對象,它可以像頂點緩沖對象那樣進行綁定,任何隨后的頂點屬性調用都會存儲在這個VAO中。
這樣的好處就是,當配置頂點屬性指針時,你只需要將那些調用執(zhí)行一次,之后再繪制物體的時候只需要綁定相應的VAO就行了。這使在不同頂點數(shù)據(jù)和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO就行了。剛剛設置的所有狀態(tài)都將存儲在VAO中。
VAO會存儲:
- glEnableVertexAttribArray和glDisableVertexAttribArray的調用。
- 通過glVertexAttribPointer設置的頂點屬性配置。
- 通過glVertexAttribPointer調用與頂點屬性關聯(lián)的頂點緩沖對象。
創(chuàng)建VAO:
unsigned int VBO; glGenBuffers(1,&VBO);綁定VAO:
glBindVertexArray(VAO);現(xiàn)在這段代碼應該是這個樣子:
// 生成VAO、VBO對象 glGenBuffers(1, &VBO); glGenVertexArrays(1, &VAO); // 將VAO與當前VBO關聯(lián) glBindVertexArray(VAO); // 綁定VBO到上下文,頂點緩沖對象的緩沖類型是GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); // 綁定數(shù)據(jù)到緩沖GL_ARRAY_BUFFER glBufferData(GL_ARRAY_BUFFER, 120 * sizeof(float), vertices, GL_STATIC_DRAW); // 設置頂點屬性指針 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 解綁VAO glEnableVertexAttribArray(0); // 解綁VBO glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);這段代碼只需要運行一次,當我們繪制物體的時候就拿出相應的VAO進行綁定就可以了。
繪制結果當然就和之前的一樣啦。
接著我還想討論下EBO即索引緩沖對象,首先為什么需要索引緩沖對象?我們來看一組頂點數(shù)據(jù):
float vertices[] = {// 第一個三角形0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, 0.5f, 0.0f, // 左上角// 第二個三角形0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角 };很明顯兩個三角形之間有重復頂點,在圖形變多的時候,這一問題愈發(fā)突出,我們就需要EBO對象。EBO的思想是存儲所有不重復的頂點和這些頂點的繪制方式,對于該數(shù)據(jù),我們定義:
float vertices[] = {0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角 };unsigned int indices[] = { // 注意索引從0開始! 0, 1, 3, // 第一個三角形1, 2, 3 // 第二個三角形 };然后我們創(chuàng)建EBO,和VAO、VBO類似:
unsigned int EBO; glGenBuffers(1,&EBO);綁定EBO到緩沖,緩沖類型為GL_ELEMENT_ARRAY_BUFFER:
// 綁定EBO到上下文,頂點緩沖對象的緩沖類型是GL_ELEMENT_ARRAY_BUFFER glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);綁定數(shù)據(jù):
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);若不使用VAO對象,我們的繪制方式應該是:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);事實上,VAO也會儲存EBO的值,所以在使用VAO的情況下,我們的繪制方式為:
glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);最后的數(shù)據(jù)讀取及繪制代碼大概如下:
/*數(shù)據(jù)錄入,僅需要執(zhí)行一次*/ // 生成VAO、VBO、EBO對象 glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 將VAO與當前VBO關聯(lián) glBindVertexArray(VAO); // 綁定VBO到上下文,頂點緩沖對象的緩沖類型是GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 綁定數(shù)據(jù)到緩沖GL_ARRAY_BUFFER glBufferData(GL_ARRAY_BUFFER, 12 * sizeof(float), vertices3, GL_STATIC_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 設置頂點屬性指針 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 解綁VAO glEnableVertexAttribArray(0); // 解綁VBO glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);/*數(shù)據(jù)繪制*/ glUseProgram(ID); glBindVertexArray(VAO); //glDrawArrays(GL_LINE_STRIP, 0, 40); glDrawElements(GL_LINE_LOOP, 6, GL_UNSIGNED_INT, 0); glBindVertexArray(0);運行結果為:
到這里為止,對于均處于標準設備坐標的數(shù)據(jù)我們可以很好的繪制,你可以試試自己的數(shù)據(jù)了,但是對于超出這個范圍的數(shù)據(jù)我們還無能為力,所以我們需要進行坐標變換,不用擔心,如果你已經(jīng)看到這里,我相信你是可以掌握接下來的東西的。
3.2 OpenGL坐標系統(tǒng)
前文說到,OpenGL希望在頂點著色器運行完成后,所有的頂點應該轉換為標準化設備坐標,我們通常會自己設定一個坐標的范圍,之后再在頂點著色器中將這些坐標變換為標準化設備坐標。然后將這些標準化設備坐標傳入光柵器(Rasterizer),將它們變換為屏幕上的二維坐標或像素。
輸入坐標轉換為標準化設備坐標,接著轉化為屏幕坐標的過程是分步進行的,會經(jīng)歷多個坐標系統(tǒng),是一個流水線的作業(yè),首先我們應該知道下面的坐標系統(tǒng):
- Local Space–局部空間
- World Space–世界空間
- View Space–觀察空間
- Clip Space–裁剪空間
- Screen Space–屏幕空間
這是一個頂點在最終被轉化為片段之前經(jīng)歷的所有狀態(tài)。
為了將一個坐標系轉化為另一個坐標系,我們需要使用到變換矩陣,其中模型矩陣將局部空間轉換為世界坐標,觀察矩陣將世界空間轉換為觀察空間,投影矩陣將觀察空間轉換為裁剪空間,最后,使用視口變換將位于(?1.0,1.0)(-1.0,1.0)(?1.0,1.0)范圍的坐標轉換到由glViewPort函數(shù)所指定的視口范圍內,最后變換出來的坐標會進入光柵器,轉換為片段,繼續(xù)進行渲染管線的其它步驟。
關于每個空間,LearnOpenGL中已經(jīng)講的很清楚了。
我想詳細討論下其中的ViEWSPACE->CLIPSPACE階段。
到了VIEWSPACE之時,坐標已經(jīng)變成二維平面坐標了,我們需要知道的是頂點著色器運行的最后,OpenGL期望所有要展現(xiàn)在屏幕上的坐標應該在一個特定的范圍內,所有不在這個范圍內的坐標都不會展示,而這個范圍的坐標將會轉換為標準化設備坐標(?1.0,1.0)(-1.0,1.0)(?1.0,1.0)。
這一過程的實現(xiàn)需要借助投影這一概念,投影包括正射投影和透視投影,對于二維圖像,正射投影即可,而三維圖像需要使用到透視投影,因為有遠近之分。
正射投影示意圖:
通過定義我們期望繪制的窗口的寬度width和高度height,可以將其投影到標準化設備坐標的范圍內。
透視投影示意圖:
經(jīng)過投影之后,VIEWSPACE便轉換到了CLIPSPACE,接著最終的操作透視除法將會執(zhí)行,透視除法主要針對的是三維物體,因為有遠近之分,透視除法的結果就是讓我們看出這種遠近之分。具體原理可參考原網(wǎng)站,不作介紹。
3.2.1 一個二維的例子
下面我們可以玩一些更復雜的數(shù)據(jù)了,我用一個簡單的程序生成了40個橫縱坐標處于(?10,10)(-10,10)(?10,10)范圍內的的點。
float vertices = new float[40 * 3]; //生成40個點 // 循環(huán)生成點 for (int i = 0; i < 10; i++) {vertices[i * 12] = i;vertices[i * 12 + 1] = 0;vertices[i * 12 + 2] = 1.0;vertices[i * 12 + 3] = 0;vertices[i * 12 + 4] = -i;vertices[i * 12 + 5] = 1.0;vertices[i * 12 + 6] = -i;vertices[i * 12 + 7] = 0;vertices[i * 12 + 8] = 1.0;vertices[i * 12 + 9] = 0;vertices[i * 12 + 10] = i;vertices[i * 12 + 11] = 0; }下面我將對這個數(shù)據(jù)進行可視化。
頂點著色器要改一改:
#version 330 core layout (location = 0) in vec3 aPos;uniform mat4 ortho; // 投影矩陣 uniform mat4 view; // 觀察矩陣 uniform mat4 model; // 模型矩陣 void main() {gl_Position = ortho* view *model* vec4(aPos.x, aPos.y, aPos.z, 1.0); }接著我們定義一個三個矩陣,本例使用QT環(huán)境,所以定義如下:
QMatrix4x4 ortho; QMatrix4x4 view; QMatrix4x4 model;事實上,在對二維圖形展示的時候,我們并不需要定義三個,只需要投影即可,但為了完整,我們還是定義一下。
對于三者的操作為:
// 表示從z軸這個方向來看 view.lookAt(QVector3D(0, 0, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 0)); QRectF rect(QPointF(-10,10),QPointF(10,-10)); ortho.ortho(rect);將數(shù)據(jù)傳到頂點著色器中:
GLuint projLoc = glGetUniformLocation(ID, "ortho"); GLuint viewLoc = glGetUniformLocation(ID, "view"); GLuint modelLoc = glGetUniformLocation(ID, "model"); glUniformMatrix4fv(projLoc, 1, GL_FALSE, ortho.data()); glUniformMatrix4fv(viewLoc, 1, GL_FALSE, view.data()); glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data());注意:坐標變換定義應該在繪制之前進行;
結果圖為:
大功告成~
3.2.2 一個三維的例子
有了這些基礎知識,我們已經(jīng)可以搞定三維了,下面讓我們感受一下OpenGL三維的魅力~
數(shù)據(jù)準備:
float vertices[] = {-0.5f, -0.5f, -0.5f,0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f,-0.5f, 0.5f, -0.5f,-0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f,-0.5f, 0.5f, 0.5f,-0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f,-0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f,0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f,0.5f, -0.5f, 0.5f,-0.5f, -0.5f, 0.5f,-0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f,0.5f, 0.5f, -0.5f,0.5f, 0.5f, 0.5f,0.5f, 0.5f, 0.5f,-0.5f, 0.5f, 0.5f,-0.5f, 0.5f, -0.5f, };模型矩陣、觀察矩陣、投影矩陣定義分別為:
QMatrix4x4 ortho; QMatrix4x4 view; QMatrix4x4 model; // 繞x軸旋轉45.0f° model.rotate(45.0f,1.0f, 0.0f, 0.0f); // 觀察矩陣 view.translate(QVector3D(0.0f, 0.0f, -3.0f)); // 透視投影,第一個參數(shù)是視口(FOV)為45.0f°,第二個參數(shù)是投影屏幕比,第三第四定義深度 ortho.perspective(45.0f, width() / height(), 0.1f, 100.0f); GLuint projLoc = glGetUniformLocation(ID, "ortho"); GLuint viewLoc = glGetUniformLocation(ID, "view"); GLuint modelLoc = glGetUniformLocation(ID, "model"); glUniformMatrix4fv(projLoc, 1, GL_FALSE, ortho.data()); glUniformMatrix4fv(viewLoc, 1, GL_FALSE, view.data()); glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data());結果為:
注意:標準化設備坐標系中OpenGL實際上使用的是左手坐標系:
到此為止,我想是真正的入門了OpenGL…至于那些酷炫的效果,有時間就再研究下好了。
總結
以上是生活随笔為你收集整理的OpenGL核心模式详细讲解[结合LearnOpenGL]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux Shell编程学习笔记
- 下一篇: Redsi通过geo计算距离