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一般被認(rèn)為是一個(gè)API(Application Programming Interface),即應(yīng)用程序編程接口,包含了一系列可以操作圖形、圖像的函數(shù),允許程序員指定著色程序、對(duì)象和涉及到生成高質(zhì)量圖形圖像(特別是三維對(duì)象的彩色圖像)的操作,但其實(shí),OpenGL本身并不是一個(gè)API,它僅僅是一個(gè)規(guī)范,這就像SFS和GDAL的關(guān)系一樣,OpenGL嚴(yán)格規(guī)范了函數(shù)的功能,函數(shù)的執(zhí)行流程以及輸出值,而函數(shù)的內(nèi)部實(shí)現(xiàn)由開發(fā)者自行決定,只要其功能和結(jié)果與規(guī)范相匹配即可。
一 狀態(tài)機(jī)
首先,我們要理解的是OpenGL是一個(gè)巨大的狀態(tài)機(jī):OpenGL內(nèi)部定義了一系列的變量去描述OpenGL運(yùn)行的模式,OpenGL的狀態(tài)通常被稱為OpenGL上下文。因此,當(dāng)我們實(shí)際使用OpenGL的時(shí)候,會(huì)使用狀態(tài)設(shè)置函數(shù)改變上下文,使用狀態(tài)使用函數(shù)根據(jù)當(dāng)前OpenGL的狀態(tài)執(zhí)行一些操作。
二 立即渲染模式
早期的OpenGL常使用立即渲染模式,即固定渲染管線,這種模式繪圖十分方便,但OpenGL的大多數(shù)功能都被隱藏了,開發(fā)者很少可以自由的控制OpenGL,隨著時(shí)間的推移,開發(fā)者迫切希望能有更多的靈活性,規(guī)范越來越靈活,開發(fā)者對(duì)繪圖細(xì)節(jié)有了更多的掌控。從OpenGL3.2開始,規(guī)范文檔開始廢棄立即渲染模式,鼓勵(lì)開發(fā)者在OpenGL的核心模式下進(jìn)行開發(fā)。
下面給出立即將渲染模式的繪制流程:
/*初始化*/ void initializeGL() {// 初始化OpenGL函數(shù)initializeOpenGLFunctions();glClearColor(255, 255, 255, 1); // 狀態(tài)設(shè)置函數(shù) } /*繪制*/ void paintGL() {// 清理顏色和深度緩存(狀態(tài)使用函數(shù))glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 設(shè)置矩陣模式為投影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)設(shè)置點(diǎn)for (int i = 0; i < 10; i++) {glVertex2f(i, 0);glVertex2f(0, -i);glVertex2f(-i, 0);glVertex2f(0, i);}// 結(jié)束繪圖glEnd();// 清除矩陣glPopMatrix();// 刷新glFlush();} /*視口改變*/ void resizeGL(int w, int h) {glViewport(0, 0, w, h); }結(jié)果如圖:
立即渲染模式的優(yōu)點(diǎn)是流程簡(jiǎn)單,面向過程,易于理解,但相對(duì)的,繪制效率很低,主要原因:
- glVertexglVertexglVertex函數(shù)每次調(diào)用只把一個(gè)頂點(diǎn)從客戶端(CPU或內(nèi)存)傳輸?shù)椒?wù)端(GPU),而這個(gè)傳輸?shù)倪^程相對(duì)于GPU處理數(shù)據(jù)的過程是很慢的;
- glVertexglVertexglVertex函數(shù)的調(diào)用次數(shù)過多。
三 核心模式
核心模式完全移除了舊的特性,具有很高的靈活性和效率,但同時(shí)也更難于學(xué)習(xí),要求使用者真正理解OpenGL和圖形編程。
首先給出一個(gè)OpenGL的核心模式繪圖的例子:
unsigned int VAO, VBO,ID; //頂點(diǎn)數(shù)組對(duì)象、頂點(diǎn)緩沖對(duì)象、著色器 /*初始化*/ void initializeGL() {// 初始化OpenGL函數(shù)initializeOpenGLFunctions();glClearColor(255, 255, 255, 1); // 狀態(tài)設(shè)置函數(shù)createShader("G:\\kmj\\實(shí)習(xí)\\teach\\shader.vs", "G:\\kmj\\實(shí)習(xí)\\teach\\shader.fs");float* vertices = new float[40 * 3]; //生成40個(gè)點(diǎn)// 循環(huán)生成點(diǎ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對(duì)象glGenBuffers(1, &VBO);glGenVertexArrays(1, &VAO);// 將VAO與當(dāng)前VBO關(guān)聯(lián)glBindVertexArray(VAO);// 綁定VBO到上下文,頂點(diǎn)緩沖對(duì)象的緩沖類型是GL_ARRAY_BUFFERglBindBuffer(GL_ARRAY_BUFFER, VBO);// 綁定數(shù)據(jù)到緩沖GL_ARRAY_BUFFERglBufferData(GL_ARRAY_BUFFER, 120 * sizeof(float), vertices, GL_STATIC_DRAW);// 設(shè)置頂點(diǎn)屬性指針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); }其中著色器編譯是固定操作,直接復(fù)制即可,結(jié)果為:
可以看出核心模式相較于立即渲染模式更為復(fù)雜,下面我們?cè)敿?xì)講解這個(gè)例子。
3.1 圖形渲染管線
OpenGL的坐標(biāo)系是三維坐標(biāo)系,也就是說每一個(gè)物體都同時(shí)具有x,y,zx,y,zx,y,z三個(gè)坐標(biāo),但屏幕是二維坐標(biāo),這就需要我們使用某種方法把3D轉(zhuǎn)為2D,在OpenGL中,圖形渲染管線(Graphics Pipeline)用于實(shí)現(xiàn)這個(gè)過程。
圖形渲染管線接受一組3D坐標(biāo),然后把它們轉(zhuǎn)變?yōu)槟闫聊簧系挠猩?D像素輸出。圖形渲染管線可以被劃分為幾個(gè)階段,每個(gè)階段將會(huì)把前一個(gè)階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個(gè)特定的函數(shù)),并且很容易并行執(zhí)行。正是由于它們具有并行執(zhí)行的特性,當(dāng)今大多數(shù)顯卡都有成千上萬(wàn)的小處理核心,它們?cè)贕PU上為每一個(gè)(渲染管線)階段運(yùn)行各自的小程序,從而在圖形渲染管線中快速處理你的數(shù)據(jù)。這些小程序叫做著色器(Shader)。
一個(gè)圖形渲染管線的抽象展示為下圖:
其中我們能夠控制的著色器包括頂點(diǎn)著色器、幾何著色器和片段著色器。
OpenGL中定義著色器使用OpenGL著色器語(yǔ)言(OpenGL Shading Language,GLSL)。
3.1.1 頂點(diǎn)著色器
頂點(diǎn)著色器是圖形渲染管線的第一個(gè)部分,它接受一個(gè)頂點(diǎn)數(shù)據(jù),允許我們對(duì)頂點(diǎn)屬性進(jìn)行一些基本的處理,一個(gè)簡(jiǎn)單的頂點(diǎn)著色器如下:
#version 450 core layout (location = 0) in vec3 aPos;void main() {gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }標(biāo)準(zhǔn)化設(shè)備坐標(biāo)
OpenGL在處理我們輸入的頂點(diǎn)數(shù)據(jù)時(shí),并不是把所有的3D坐標(biāo)變換為屏幕的2D像素,僅在3D坐標(biāo)在三個(gè)軸x,y,zx,y,zx,y,z∈\in∈(?1.0,1.0)(-1.0,1.0)(?1.0,1.0)時(shí)才進(jìn)行處理,這就是標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的概念。
在這個(gè)著色器中,#version 450 core表示著色器的版本號(hào),GLSL和OpenGL的版本應(yīng)該是相互匹配的,這句話表示當(dāng)前使用的OpenGL是4.5核心模式。
gl_Position設(shè)置的值是該頂點(diǎn)著色器的輸出,vec4表示定義一個(gè)四維向量。
layout (location = 0) in vec3 aPos;設(shè)定輸入變量的位置,in表示輸入?yún)?shù)。
這是一個(gè)最簡(jiǎn)單的頂點(diǎn)著色器,未對(duì)輸入數(shù)據(jù)進(jìn)行任何處理進(jìn)行輸出,也就是默認(rèn)數(shù)據(jù)均處于標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中,實(shí)際開發(fā)中,我們還在該著色器中進(jìn)行坐標(biāo)的變換,以使坐標(biāo)轉(zhuǎn)換到標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系之中。
3.1.2 圖元裝配
圖元裝配階段接受頂點(diǎn)著色器輸出的所有頂點(diǎn),并把所有的點(diǎn)裝配成指定圖元的形狀,如:點(diǎn)、三角形、線、面等。常用圖元類型:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP、GL_LINE_LOOP。
3.1.3 幾何著色器
幾何著色器用于產(chǎn)生新的其他形狀,圖中產(chǎn)生了另一個(gè)三角形。
3.1.4 光柵化階段
幾何著色器的輸出會(huì)進(jìn)入光柵化階段,會(huì)把圖元映射為最終屏幕上相應(yīng)的像素,生成供片段著色器使用的片段,在片段著色器運(yùn)行之前會(huì)執(zhí)行裁切(Clipping)。裁切會(huì)丟棄超出你的視圖以外的所有像素,用來提升執(zhí)行效率。
3.1.5 片段著色器
用于計(jì)算一個(gè)像素的最終顏色,也是OpenGL高級(jí)效果產(chǎn)生的地方,如光照、陰影、光的顏色等。一個(gè)簡(jiǎn)單的片段著色器如下:
#version 450 core out vec4 FragColor; void main() {FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }片段著色器只需要一個(gè)輸出變量,這個(gè)變量是一個(gè)4分量向量,它表示的是最終的輸出顏色,我們應(yīng)該自己將其計(jì)算出來。我們可以用out關(guān)鍵字聲明輸出變量,這里我們命名為FragColor。
3.1.6 測(cè)試和混合
這個(gè)階段檢測(cè)片段的對(duì)應(yīng)的深度(和模板(Stencil))值(后面會(huì)講),用它們來判斷這個(gè)像素是其它物體的前面還是后面,決定是否應(yīng)該丟棄。這個(gè)階段也會(huì)檢查alpha值(alpha值定義了一個(gè)物體的透明度)并對(duì)物體進(jìn)行混合(Blend)。所以,即使在片段著色器中計(jì)算出來了一個(gè)像素輸出的顏色,在渲染多個(gè)三角形的時(shí)候最后的像素顏色也可能完全不同。
*注意,在使用核心模式開發(fā)時(shí),我們必須至少定義頂點(diǎn)著色器和片段著色器,這也是OpenGL核心模式較困難的其中一個(gè)原因。
3.2 開始實(shí)踐
在大致理解了OpenGL的圖形渲染流程之后,我們可以開始實(shí)踐了,我們從最簡(jiǎn)單的例子出發(fā),對(duì)如下頂點(diǎn)數(shù)據(jù)進(jìn)行可視化:
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f };根據(jù)圖形渲染流程,我們首先要把數(shù)據(jù)發(fā)送給頂點(diǎn)著色器進(jìn)行處理,這需要用到VBO對(duì)象,它可以在GPU內(nèi)存中儲(chǔ)存頂點(diǎn)數(shù)據(jù),使用VBO的好處是我們可以一次性的發(fā)送一大批數(shù)據(jù)到顯卡上,而不需要每個(gè)頂點(diǎn)發(fā)送一次(立即渲染模式就是這樣做的),從CPU把數(shù)據(jù)發(fā)送到顯卡相對(duì)較慢,所以我們要盡可能嘗試一次性發(fā)送盡可能多的數(shù)據(jù),當(dāng)數(shù)據(jù)發(fā)送至顯卡的內(nèi)存中后,頂點(diǎn)著色器幾乎能立即訪問頂點(diǎn)
glGenBuffers函數(shù)用于生成VBO對(duì)象,同時(shí)我們還需要定義一個(gè)ID來唯一標(biāo)識(shí)這個(gè)緩沖對(duì)象。
unsigned int VBO; glGenBuffers(1,&VBO);接著使用glBindBuffer函數(shù)把VBO對(duì)象綁定到頂點(diǎn)緩沖對(duì)象的緩沖類型GL_ARRAY_BUFFER上。
glBindBuffer(GL_ARRAY_BUFFER,VBO);綁定完畢之后,就相當(dāng)于把VBO對(duì)象綁定到了OpenGL狀態(tài)機(jī)的上下文中,那么接下來我們?cè)贕L_ARRAY_BUFFER上的所有操作都會(huì)改變VBO對(duì)象,然后我們可以調(diào)用glBufferData函數(shù)設(shè)置緩沖對(duì)象的數(shù)據(jù)。
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);- 第一個(gè)參數(shù)是數(shù)據(jù)綁定的緩沖類型;
- 第二個(gè)參數(shù)是數(shù)據(jù)的大小,以字節(jié)數(shù)為單位;
- 第三個(gè)參數(shù)是數(shù)據(jù);
- 第四個(gè)參數(shù)是顯卡管理給定數(shù)據(jù)的方式。
為緩沖綁定好數(shù)據(jù)之后,數(shù)據(jù)將會(huì)進(jìn)入頂點(diǎn)著色器:
#version 450 core layout (location = 0) in vec3 aPos;void main() {gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }apos是我們要輸入到頂點(diǎn)著色器中的數(shù)據(jù)。
頂點(diǎn)著色器允許我們指定任何以頂點(diǎn)屬性為形式的輸入。這使其具有很強(qiáng)的靈活性的同時(shí),它還的確意味著我們必須手動(dòng)指定輸入數(shù)據(jù)的哪一個(gè)部分對(duì)應(yīng)頂點(diǎn)著色器的哪一個(gè)頂點(diǎn)屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點(diǎn)數(shù)據(jù)。
對(duì)于本例,我們的頂點(diǎn)緩沖對(duì)象會(huì)被解析成:
所以我們需要調(diào)用glVertexAttribPointer函數(shù)告訴OpenGL如何去解析我們的輸入數(shù)據(jù)。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);- 第一個(gè)參數(shù)表示我們把數(shù)據(jù)傳到哪一個(gè)位置,即location的值;
- 第二個(gè)參數(shù)表示頂點(diǎn)屬性的大小,vec3大小為3;
- 第三個(gè)參數(shù)表示頂點(diǎn)屬性的類型,GLSL中vec是GL_FLOAT類型的;
- 第四個(gè)參數(shù)表示是否希望數(shù)據(jù)標(biāo)準(zhǔn)化;
- 第五個(gè)參數(shù)表示步長(zhǎng),也就是相鄰頂點(diǎn)屬性的間隔,以字節(jié)數(shù)為單位;
- 第六個(gè)參數(shù)是該數(shù)據(jù)在緩沖中起始位置的偏移量,這一參數(shù)往往在我們既儲(chǔ)存位置又儲(chǔ)存顏色數(shù)據(jù)的時(shí)候設(shè)置。
在片段著色器中,我們定義顏色,此時(shí)我們定義一個(gè)很簡(jiǎn)單的片段著色器:
#version 450 core out vec4 FragColor; void main() {FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }定義完成著色器后,我們還需要進(jìn)行編譯并將這兩個(gè)著色器鏈接為著色器程序;
unsigned int vertex, fragment,ID; // 編譯頂點(diǎn)著色器 vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); // 編譯片段著色器 fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); // 鏈接兩個(gè)著色器 ID = glCreateProgram(); glAttachShader(ID, vertex); glAttachShader(ID, fragment); glLinkProgram(ID); // 鏈接完回收 glDeleteShader(vertex); glDeleteShader(fragment);著色器編譯的完整實(shí)現(xiàn)在上例中已經(jīng)定義,可以當(dāng)成工具函數(shù)使用,不贅述。
到這里為止,我們已經(jīng)可以繪制出圖像了,結(jié)果為:
這種繪制方式已經(jīng)是對(duì)傳統(tǒng)的立即繪制的很大改良了,但我們還是可以發(fā)現(xiàn),每繪制一個(gè)物體,我們都要重復(fù)下面過程:
glGenBuffers(1, &VBO); // 0. 復(fù)制頂點(diǎn)數(shù)組到緩沖中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, count * sizeof(float), vertices, GL_STATIC_DRAW); // 1. 設(shè)置頂點(diǎn)屬性指針 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);這其實(shí)也挺麻煩的,所以VAO出現(xiàn)了,下面我們采用VAO對(duì)上例進(jìn)行改進(jìn)。
VAO是頂點(diǎn)數(shù)組對(duì)象,它可以像頂點(diǎn)緩沖對(duì)象那樣進(jìn)行綁定,任何隨后的頂點(diǎn)屬性調(diào)用都會(huì)存儲(chǔ)在這個(gè)VAO中。
這樣的好處就是,當(dāng)配置頂點(diǎn)屬性指針時(shí),你只需要將那些調(diào)用執(zhí)行一次,之后再繪制物體的時(shí)候只需要綁定相應(yīng)的VAO就行了。這使在不同頂點(diǎn)數(shù)據(jù)和屬性配置之間切換變得非常簡(jiǎn)單,只需要綁定不同的VAO就行了。剛剛設(shè)置的所有狀態(tài)都將存儲(chǔ)在VAO中。
VAO會(huì)存儲(chǔ):
- glEnableVertexAttribArray和glDisableVertexAttribArray的調(diào)用。
- 通過glVertexAttribPointer設(shè)置的頂點(diǎn)屬性配置。
- 通過glVertexAttribPointer調(diào)用與頂點(diǎn)屬性關(guān)聯(lián)的頂點(diǎn)緩沖對(duì)象。
創(chuàng)建VAO:
unsigned int VBO; glGenBuffers(1,&VBO);綁定VAO:
glBindVertexArray(VAO);現(xiàn)在這段代碼應(yīng)該是這個(gè)樣子:
// 生成VAO、VBO對(duì)象 glGenBuffers(1, &VBO); glGenVertexArrays(1, &VAO); // 將VAO與當(dāng)前VBO關(guān)聯(lián) glBindVertexArray(VAO); // 綁定VBO到上下文,頂點(diǎn)緩沖對(duì)象的緩沖類型是GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); // 綁定數(shù)據(jù)到緩沖GL_ARRAY_BUFFER glBufferData(GL_ARRAY_BUFFER, 120 * sizeof(float), vertices, GL_STATIC_DRAW); // 設(shè)置頂點(diǎn)屬性指針 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 解綁VAO glEnableVertexAttribArray(0); // 解綁VBO glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);這段代碼只需要運(yùn)行一次,當(dāng)我們繪制物體的時(shí)候就拿出相應(yīng)的VAO進(jìn)行綁定就可以了。
繪制結(jié)果當(dāng)然就和之前的一樣啦。
接著我還想討論下EBO即索引緩沖對(duì)象,首先為什么需要索引緩沖對(duì)象?我們來看一組頂點(diǎn)數(shù)據(jù):
float vertices[] = {// 第一個(gè)三角形0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, 0.5f, 0.0f, // 左上角// 第二個(gè)三角形0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角 };很明顯兩個(gè)三角形之間有重復(fù)頂點(diǎn),在圖形變多的時(shí)候,這一問題愈發(fā)突出,我們就需要EBO對(duì)象。EBO的思想是存儲(chǔ)所有不重復(fù)的頂點(diǎn)和這些頂點(diǎn)的繪制方式,對(duì)于該數(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, // 第一個(gè)三角形1, 2, 3 // 第二個(gè)三角形 };然后我們創(chuàng)建EBO,和VAO、VBO類似:
unsigned int EBO; glGenBuffers(1,&EBO);綁定EBO到緩沖,緩沖類型為GL_ELEMENT_ARRAY_BUFFER:
// 綁定EBO到上下文,頂點(diǎn)緩沖對(duì)象的緩沖類型是GL_ELEMENT_ARRAY_BUFFER glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);綁定數(shù)據(jù):
習(xí)\teach\test2.png)glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);若不使用VAO對(duì)象,我們的繪制方式應(yīng)該是:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);事實(shí)上,VAO也會(huì)儲(chǔ)存EBO的值,所以在使用VAO的情況下,我們的繪制方式為:
glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);最后的數(shù)據(jù)讀取及繪制代碼大概如下:
/*數(shù)據(jù)錄入,僅需要執(zhí)行一次*/ // 生成VAO、VBO、EBO對(duì)象 glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 將VAO與當(dāng)前VBO關(guān)聯(lián) glBindVertexArray(VAO); // 綁定VBO到上下文,頂點(diǎn)緩沖對(duì)象的緩沖類型是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); // 設(shè)置頂點(diǎn)屬性指針 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);運(yùn)行結(jié)果為:
到這里為止,對(duì)于均處于標(biāo)準(zhǔn)設(shè)備坐標(biāo)的數(shù)據(jù)我們可以很好的繪制,你可以試試自己的數(shù)據(jù)了,但是對(duì)于超出這個(gè)范圍的數(shù)據(jù)我們還無能為力,所以我們需要進(jìn)行坐標(biāo)變換,不用擔(dān)心,如果你已經(jīng)看到這里,我相信你是可以掌握接下來的東西的。
3.2 OpenGL坐標(biāo)系統(tǒng)
前文說到,OpenGL希望在頂點(diǎn)著色器運(yùn)行完成后,所有的頂點(diǎn)應(yīng)該轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo),我們通常會(huì)自己設(shè)定一個(gè)坐標(biāo)的范圍,之后再在頂點(diǎn)著色器中將這些坐標(biāo)變換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)。然后將這些標(biāo)準(zhǔn)化設(shè)備坐標(biāo)傳入光柵器(Rasterizer),將它們變換為屏幕上的二維坐標(biāo)或像素。
輸入坐標(biāo)轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo),接著轉(zhuǎn)化為屏幕坐標(biāo)的過程是分步進(jìn)行的,會(huì)經(jīng)歷多個(gè)坐標(biāo)系統(tǒng),是一個(gè)流水線的作業(yè),首先我們應(yīng)該知道下面的坐標(biāo)系統(tǒng):
- Local Space–局部空間
- World Space–世界空間
- View Space–觀察空間
- Clip Space–裁剪空間
- Screen Space–屏幕空間
這是一個(gè)頂點(diǎn)在最終被轉(zhuǎn)化為片段之前經(jīng)歷的所有狀態(tài)。
為了將一個(gè)坐標(biāo)系轉(zhuǎn)化為另一個(gè)坐標(biāo)系,我們需要使用到變換矩陣,其中模型矩陣將局部空間轉(zhuǎn)換為世界坐標(biāo),觀察矩陣將世界空間轉(zhuǎn)換為觀察空間,投影矩陣將觀察空間轉(zhuǎn)換為裁剪空間,最后,使用視口變換將位于(?1.0,1.0)(-1.0,1.0)(?1.0,1.0)范圍的坐標(biāo)轉(zhuǎn)換到由glViewPort函數(shù)所指定的視口范圍內(nèi),最后變換出來的坐標(biāo)會(huì)進(jìn)入光柵器,轉(zhuǎn)換為片段,繼續(xù)進(jìn)行渲染管線的其它步驟。
關(guān)于每個(gè)空間,LearnOpenGL中已經(jīng)講的很清楚了。
我想詳細(xì)討論下其中的ViEWSPACE->CLIPSPACE階段。
到了VIEWSPACE之時(shí),坐標(biāo)已經(jīng)變成二維平面坐標(biāo)了,我們需要知道的是頂點(diǎn)著色器運(yùn)行的最后,OpenGL期望所有要展現(xiàn)在屏幕上的坐標(biāo)應(yīng)該在一個(gè)特定的范圍內(nèi),所有不在這個(gè)范圍內(nèi)的坐標(biāo)都不會(huì)展示,而這個(gè)范圍的坐標(biāo)將會(huì)轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(?1.0,1.0)(-1.0,1.0)(?1.0,1.0)。
這一過程的實(shí)現(xiàn)需要借助投影這一概念,投影包括正射投影和透視投影,對(duì)于二維圖像,正射投影即可,而三維圖像需要使用到透視投影,因?yàn)橛羞h(yuǎn)近之分。
正射投影示意圖:
通過定義我們期望繪制的窗口的寬度width和高度height,可以將其投影到標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的范圍內(nèi)。
透視投影示意圖:
經(jīng)過投影之后,VIEWSPACE便轉(zhuǎn)換到了CLIPSPACE,接著最終的操作透視除法將會(huì)執(zhí)行,透視除法主要針對(duì)的是三維物體,因?yàn)橛羞h(yuǎn)近之分,透視除法的結(jié)果就是讓我們看出這種遠(yuǎn)近之分。具體原理可參考原網(wǎng)站,不作介紹。
3.2.1 一個(gè)二維的例子
下面我們可以玩一些更復(fù)雜的數(shù)據(jù)了,我用一個(gè)簡(jiǎn)單的程序生成了40個(gè)橫縱坐標(biāo)處于(?10,10)(-10,10)(?10,10)范圍內(nèi)的的點(diǎn)。
float vertices = new float[40 * 3]; //生成40個(gè)點(diǎn) // 循環(huán)生成點(diǎ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; }下面我將對(duì)這個(gè)數(shù)據(jù)進(jìn)行可視化。
頂點(diǎn)著色器要改一改:
#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); }接著我們定義一個(gè)三個(gè)矩陣,本例使用QT環(huán)境,所以定義如下:
QMatrix4x4 ortho; QMatrix4x4 view; QMatrix4x4 model;事實(shí)上,在對(duì)二維圖形展示的時(shí)候,我們并不需要定義三個(gè),只需要投影即可,但為了完整,我們還是定義一下。
對(duì)于三者的操作為:
// 表示從z軸這個(gè)方向來看 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ù)傳到頂點(diǎn)著色器中:
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());注意:坐標(biāo)變換定義應(yīng)該在繪制之前進(jìn)行;
結(jié)果圖為:
大功告成~
3.2.2 一個(gè)三維的例子
有了這些基礎(chǔ)知識(shí),我們已經(jīng)可以搞定三維了,下面讓我們感受一下OpenGL三維的魅力~
數(shù)據(jù)準(zhǔn)備:
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軸旋轉(zhuǎn)45.0f° model.rotate(45.0f,1.0f, 0.0f, 0.0f); // 觀察矩陣 view.translate(QVector3D(0.0f, 0.0f, -3.0f)); // 透視投影,第一個(gè)參數(shù)是視口(FOV)為45.0f°,第二個(gè)參數(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());結(jié)果為:
注意:標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系中OpenGL實(shí)際上使用的是左手坐標(biāo)系:
到此為止,我想是真正的入門了OpenGL…至于那些酷炫的效果,有時(shí)間就再研究下好了。
總結(jié)
以上是生活随笔為你收集整理的OpenGL核心模式详细讲解[结合LearnOpenGL]的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux Shell编程学习笔记
- 下一篇: D-MNSV7-X16搬运机器人磁导航传