3D Computer Grapihcs Using OpenGL - 19 Vertex Array Object(顶点数组对象)
大部分OpenGL教程都会在一开始就讲解VAO,但是该教程的作者认为这是很不合理的,因为要理解它的作用需要建立在我们此前学过的知识基础上。因此直到教程已经进行了一大半,作者才引入VAO这个概念。在我看来这也是非常合理和自然的。
先预览一下最终的代码逻辑:
准备工作
为了讲解后面的内容,我们对代码进行了更改(算是回退吧,改回到以前不使用Instancing的版本):
- 去掉了sendDataToOpenGL()函数中关于实例化的部分代码
- 把VertexShader中的MVP矩阵改回Uniform
- 在paintGL()函数中直接提供MVP矩阵的信息,并改回使用glDrawElements函数绘制
更改以后的VetexShader代码:
1 #version 430 2 3 in layout(location=0) vec3 position; 4 in layout(location=1) vec3 color; 5 6 uniform mat4 fullTransformMatrix; 7 out vec3 passingColor; 8 9 void main() 10 { 11 gl_Position = fullTransformMatrix * vec4(position,1); 12 passingColor= color; 13 }
MyGlWindow.cpp文件的更改请参考在应用实例化绘制之前的代码,这里就不再重复了。
另外我们在ShapeGenerator中添加了一种新的图形,四面体。
代码如下:
1 ShapeData ShapeGenerator::makeTetrahedron() 2 { 3 ShapeData ret; 4 Vertex stackVerts[] = 5 { 6 glm::vec3(-0.289f, -0.408f, -0.500f), //0 7 glm::vec3(+1.0f, 0.0f, 0.0f), //Color 8 glm::vec3(-0.289f, -0.408f, 0.500f), //1 9 glm::vec3(0.0f, +1.0f, 0.0f), //Color 10 glm::vec3(0.577f, -0.408f, 0.000f), //2 11 glm::vec3(0.0f, 0.0f, +1.0f), //Color 12 glm::vec3(0.000f, 0.408f, 0.000f), //3 13 glm::vec3(+0.0f, +1.0f, +1.0f), //Color 14 }; 15 16 ret.numVertices = NUM_ARRAY_ELEMENTS(stackVerts); 17 ret.vertices = new Vertex[ret.numVertices]; 18 memcpy(ret.vertices, stackVerts, sizeof(stackVerts)); 19 20 unsigned short stackIndices[] = 21 { 22 0,1,2, 23 0,1,3, 24 1,2,3, 25 2,0,3, 26 }; 27 28 ret.numIndices = NUM_ARRAY_ELEMENTS(stackIndices); 29 ret.indices = new GLushort[ret.numIndices]; 30 memcpy(ret.indices, stackIndices, sizeof(stackIndices)); 31 return ret; 32 }
问题的提出
需求:我们需要同时绘制两个立方体,以及两个四面体
目前的解决方法
我们利用已有的知识可以这样去完成:
- 在sendDataToOpenGL()中生成立方体形状
- 在sendDataToOpenGL()中创建,绑定,设置立方体的VertexBuffer
- 在sendDataToOpenGL()中开启通道0,1
- 在sendDataToOpenGL()中设置通道0,1如何获取数据(glVertexAttribPointer)
- 在sendDataToOpenGL()中创建,绑定,设置立方体的IndexBuffer
- 在sendDataToOpenGL()中生成四面体形状
- 在sendDataToOpenGL()中创建,绑定,设置四面体的VertexBuffer
- 在sendDataToOpenGL()中开启通道0,1
- 在sendDataToOpenGL()中设置通道0,1如何获取数据(glVertexAttribPointer)
- 在sendDataToOpenGL()中创建,绑定,设置四面体的IndexBuffer
- 在paintGL()中再次绑定立方体的VertexBuffer
- 在paintGL()中再次设置0,1如何获取数据(glVertexAttribPointer)
- 在paintGL()中再次绑定立方体的IndexBuffer
- 在paintGL()中绘制立方体
- 在paintGL()中再次绑定四面体的VertexBuffer
- 在paintGL()中再次设置0,1如何获取数据(glVertexAttribPointer)
- 在paintGL()中再次绑定四面体的IndexBuffer
- 在paintGL()中绘制四面体
我们可以看到,在每次绘制中都要进行一起“切换”:
- 先切换到立方体的状态,绑定VertexBuffer,设置数据格式,绑定IndexBuffer,绘制立方体
- 再切换到四面体的状态,绑定VertexBuffer,设置数据格式,绑定IndexBuffer,绘制四面体
这个过程是必须的,如果不进行“切换”,会绘制错误的数据。
引入VAO的解决方案
VAO是顶点数组对象的简称,可以理解为一种“容器”,包含了绘制某种图形所需要的所有状态。
我们先给MyGlWindow类增加一个成员函数setupVertexArrays(),在initializeGL()函数中的sendDataToOpenGL()函数后面调用它。
1 void MyGlWindow::initializeGL() 2 { 3 glewInit(); 4 glEnable(GL_DEPTH_TEST); 5 sendDataToOpenGL(); 6 setupVertexArrays(); 7 installShaders(); 8 }
在setupVertexArrays()函数中,我们分别为立方体和四面体创建了两个VAO,并分别把相关的设置都在VAO后准备好。
然后在paintGL()绘制时,只需要先绑定立方体的VAO,然后进行绘制,就会绘制立方体,再绑定四面体的VAO,然后进行绘制,就会绘制四面体。
看看完整代码:
MyGlWindow.h:
1 #pragma once 2 #include <QtOpenGL\qgl.h> 3 #include <string> 4 #include "Camera.h" 5 6 class MyGlWindow :public QGLWidget 7 { 8 protected: 9 void sendDataToOpenGL(); 10 void installShaders(); 11 void initializeGL(); 12 void paintGL(); 13 GLuint transformMatrixBufferID; 14 Camera camera; 15 std::string ReadShaderCode(const char* fileName); 16 void mouseMoveEvent(QMouseEvent*); 17 void keyPressEvent(QKeyEvent*); 18 void setupVertexArrays(); 19 };
MyGlWindow.cpp:
1 #include <gl\glew.h> 2 #include "MyGlWindow.h" 3 #include <iostream> 4 #include <fstream> 5 #include <glm\gtc\matrix_transform.hpp> 6 #include <glm\gtx\transform.hpp> 7 #include <ShapeGenerator.h> 8 #include <Qt3DInput\qmouseevent.h> 9 #include <Qt3DInput\qkeyevent.h> 10 11 12 GLuint programID; 13 14 //立方体的索引数组长度 15 GLuint cubeNumIndices; 16 //立方体的VAO ID 17 GLuint cubeVertexArrayObjectID; 18 //立方体的VertexBufferID 19 GLuint cubeVertexBufferID; 20 //立方体的IndexBuffer的ID 21 GLuint cubeIndexBufferID; 22 23 24 25 //四面体的索引数组长度 26 GLuint tetraNumIndices; 27 //四面体的VAO ID 28 GLuint tetraVertexArrayObjectID; 29 //四面体的BufferID 30 GLuint tetraVertexBufferID; 31 //四面体的IndexBufferID 32 GLuint tetraIndexBufferID; 33 34 35 36 GLuint fullTransformUniformLocation; 37 38 void MyGlWindow::sendDataToOpenGL() 39 { 40 //创建Cube 41 ShapeData cube = ShapeGenerator::makeCube(); 42 43 //创建和设置VertexBuffer 44 glGenBuffers(1, &cubeVertexBufferID); 45 glBindBuffer(GL_ARRAY_BUFFER, cubeVertexBufferID); 46 glBufferData(GL_ARRAY_BUFFER, cube.vertexBufferSize(), cube.vertices, GL_STATIC_DRAW); 47 48 //创建和设置IndexBuffer 49 glGenBuffers(1, &cubeIndexBufferID); 50 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cubeIndexBufferID); 51 glBufferData(GL_ELEMENT_ARRAY_BUFFER, cube.indexBufferSize(), cube.indices, GL_STATIC_DRAW); 52 53 cubeNumIndices = cube.numIndices; 54 cube.cleanUp(); 55 56 //创建四面体 57 ShapeData tetra = ShapeGenerator::makeTetrahedron(); 58 59 //创建和设置VertexBuffer 60 glGenBuffers(1, &tetraVertexBufferID); 61 glBindBuffer(GL_ARRAY_BUFFER, tetraVertexBufferID); 62 glBufferData(GL_ARRAY_BUFFER, tetra.vertexBufferSize(), tetra.vertices, GL_STATIC_DRAW); 63 64 //创建和设置IndexBuffer 65 glGenBuffers(1, &tetraIndexBufferID); 66 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tetraIndexBufferID); 67 glBufferData(GL_ELEMENT_ARRAY_BUFFER, tetra.indexBufferSize(), tetra.indices, GL_STATIC_DRAW); 68 69 tetraNumIndices = tetra.numIndices; 70 tetra.cleanUp(); 71 72 } 73 74 void MyGlWindow::setupVertexArrays() 75 { 76 //设置绘制Cube的VAO 77 //生成VAO 78 glGenVertexArrays(1, &cubeVertexArrayObjectID); 79 //绑定VAO,后续的一系列状态和设置都会存储在这个VAO里。 80 glBindVertexArray(cubeVertexArrayObjectID); 81 82 //开启通道1(位置) 83 glEnableVertexAttribArray(0); 84 //开启通道2(颜色) 85 glEnableVertexAttribArray(1); 86 87 //绑定顶点数据ID到绑定点 88 glBindBuffer(GL_ARRAY_BUFFER, cubeVertexBufferID); 89 //设置通道1如何获取数据 90 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, 0); 91 //设置通道2如何获取数据 92 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (char*)(sizeof(float) * 3)); 93 94 //绑定索引数据ID到绑定点 95 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cubeIndexBufferID); 96 97 98 //设置绘制四面体的VAO 99 glGenVertexArrays(1, &tetraVertexArrayObjectID); 100 glBindVertexArray(tetraVertexArrayObjectID); 101 102 //开启通道1(位置) 103 glEnableVertexAttribArray(0); 104 //开启通道2(颜色) 105 glEnableVertexAttribArray(1); 106 107 //绑定顶点数据ID到绑定点 108 glBindBuffer(GL_ARRAY_BUFFER, tetraVertexBufferID); 109 //设置通道1如何获取数据 110 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, 0); 111 //设置通道2如何获取数据 112 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (char*)(sizeof(float) * 3)); 113 114 //绑定索引数据ID到绑定点 115 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tetraIndexBufferID); 116 117 118 } 119 120 void MyGlWindow::installShaders() 121 { 122 GLuint vertexShaderID = glCreateShader(GL_VERTEX_SHADER); 123 GLuint fragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER); 124 125 std::string tmp = ReadShaderCode("VertexShaderCode.glsl"); 126 const char* vertexShaderCode = tmp.c_str(); 127 glShaderSource(vertexShaderID, 1, &vertexShaderCode, 0); 128 129 tmp = ReadShaderCode("FragmentShaderCode.glsl"); 130 const char* fragmentShaderCode = tmp.c_str(); 131 glShaderSource(fragmentShaderID, 1, &fragmentShaderCode, 0); 132 133 glCompileShader(vertexShaderID); 134 glCompileShader(fragmentShaderID); 135 136 programID = glCreateProgram(); 137 glAttachShader(programID, vertexShaderID); 138 glAttachShader(programID, fragmentShaderID); 139 140 glLinkProgram(programID); 141 142 glUseProgram(programID); 143 } 144 145 void MyGlWindow::initializeGL() 146 { 147 glewInit(); 148 glEnable(GL_DEPTH_TEST); 149 sendDataToOpenGL(); 150 setupVertexArrays(); 151 installShaders(); 152 } 153 154 void MyGlWindow::paintGL() 155 { 156 glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); 157 glViewport(0, 0, width(), height()); 158 159 //绑定cube的VAO,下面绘制的都是立方体-------------------------------------- 160 glBindVertexArray(cubeVertexArrayObjectID); 161 162 glm::mat4 fullTransformMatrix; 163 glm::mat4 viewToProjectionMatrix = glm::perspective(30.0f, ((float)width()) / height(), 0.1f, 10.0f); 164 glm::mat4 worldToViewMatrix = camera.getWorldToViewMatrix(); 165 glm::mat4 worldToProjectionMatrix = viewToProjectionMatrix * worldToViewMatrix; 166 167 //绘制Cube1 168 glm::mat4 cube1ModelToWorldMatrix = 169 glm::translate(glm::vec3(-1.0f, 0.0f, -3.0f))* 170 glm::rotate(36.0f, glm::vec3(1.0f, 0.0f, 0.0f)); 171 172 fullTransformMatrix = worldToProjectionMatrix * cube1ModelToWorldMatrix; 173 glUniformMatrix4fv(fullTransformUniformLocation, 1, GL_FALSE, &fullTransformMatrix[0][0]); 174 glDrawElements(GL_TRIANGLES, cubeNumIndices, GL_UNSIGNED_SHORT, 0); 175 176 //绘制Cube2 177 glm::mat4 cube2ModelToWorldMatrix = 178 glm::translate(glm::vec3(1.0f, 0.0f, -3.75f))* 179 glm::rotate(36.0f, glm::vec3(0.0f, 1.0f, 0.0f)); 180 fullTransformMatrix = worldToProjectionMatrix * cube2ModelToWorldMatrix; 181 glUniformMatrix4fv(fullTransformUniformLocation, 1, GL_FALSE, &fullTransformMatrix[0][0]); 182 glDrawElements(GL_TRIANGLES, cubeNumIndices, GL_UNSIGNED_SHORT, 0); 183 184 //绑定Tetra的VAO,下面绘制的都是四面体-------------------------------------- 185 glBindVertexArray(tetraVertexArrayObjectID); 186 187 //绘制Tetra1 188 glm::mat4 tetra1ModelToWorldMatrix = 189 glm::translate(glm::vec3(1.0f, -2.0f, -3.75f))* 190 glm::rotate(36.0f, glm::vec3(0.0f, 1.0f, 0.0f)); 191 fullTransformMatrix = worldToProjectionMatrix * tetra1ModelToWorldMatrix; 192 glUniformMatrix4fv(fullTransformUniformLocation, 1, GL_FALSE, &fullTransformMatrix[0][0]); 193 glDrawElements(GL_TRIANGLES, tetraNumIndices, GL_UNSIGNED_SHORT, 0); 194 195 glm::mat4 tetra2ModelToWorldMatrix = 196 glm::translate(glm::vec3(-3.0f, -2.0f, -3.75f))* 197 glm::rotate(36.0f, glm::vec3(1.0f, 1.0f, 0.0f)); 198 fullTransformMatrix = worldToProjectionMatrix * tetra2ModelToWorldMatrix; 199 glUniformMatrix4fv(fullTransformUniformLocation, 1, GL_FALSE, &fullTransformMatrix[0][0]); 200 glDrawElements(GL_TRIANGLES, tetraNumIndices, GL_UNSIGNED_SHORT, 0); 201 } 202 203 204 std::string MyGlWindow::ReadShaderCode(const char* fileName) 205 { 206 std::ifstream myInput(fileName); 207 if (!myInput.good()) 208 { 209 std::cout << "File failed to load..." << fileName; 210 exit(1); 211 } 212 return std::string( 213 std::istreambuf_iterator<char>(myInput), 214 std::istreambuf_iterator<char>()); 215 } 216 217 void MyGlWindow::mouseMoveEvent(QMouseEvent * e) 218 { 219 camera.mouseUpdate(glm::vec2(e->x(), e->y())); 220 repaint(); 221 } 222 223 void MyGlWindow::keyPressEvent(QKeyEvent * e) 224 { 225 switch (e->key()) 226 { 227 case Qt::Key::Key_W: 228 camera.moveForward(); 229 break; 230 case Qt::Key::Key_S: 231 camera.moveBackward(); 232 break; 233 case Qt::Key::Key_A: 234 camera.strafeLeft(); 235 break; 236 case Qt::Key::Key_D: 237 camera.strafeRight(); 238 break; 239 case Qt::Key::Key_Q: 240 camera.moveUp(); 241 break; 242 case Qt::Key::Key_E: 243 camera.moveDown(); 244 break; 245 246 default: 247 break; 248 } 249 repaint(); 250 }
总结一下加入VAO以后的绘制流程
1. 初始化数据sendDataToOpenGL():
- 创建/绑定/设置内容 立方体的VertexBuffer
- 创建/绑定/设置内容 立方体的IndexBuffer
- 为四面体重复上述步骤
2. 设置VAO, setupVertexArrays():
- 创建/绑定立方体的VAO
- 开启通道0,1
- 绑定VertexBuffer
- 设置通道0,1的获取数据格式
- 绑定IndexBuffer
- 为四面体重复上述步骤
3. 绘制,paintGL()
- 绑定立方体的VAO
- 绘制两个立方体
- 绑定四面体的VAO
- 绘制两个四面体
从代码中我们可以发现OpenGL中“绑定”这一操作的模式:“绑定"以后相当于进入了一种环境,之后我们可以对其进行设置,再次“绑定”相当于又重新进入了之前设置好的环境。这一种模式对VAO, VBO都是适用的。
最终效果:
代码下载: https://mrart.coding.net/p/3DGraphics/d/3DGraphics/git/releases, 找到VAO这个Release