OpenGL之渲染管线-VBO-VAO 三角形示例
在OpenGL中,一切都是3D的,但屏幕或窗口是一个2D像素阵列,因此OpenGL的大部分工作是将所有3D坐标转换为适合屏幕的2D像素。这个过程由OpenGL的渲染管线管理。
渲染管线可以分为两大部分:
- 将3D坐标转换为2D坐标
- 将2D坐标转换为实际的彩色像素
顶点着色器处理后,顶点值应该是NDC坐标;NDC坐标使用glViewport提供的数据,通过视口转换变为屏幕坐标。生成的屏幕空间坐标将转换为片段,作为片段着色器的输入。
标准化设备坐标 (Normalized Device Coordinates, NDC)
顶点着色器中处理过后,就应该是标准化设备坐标了,x、y 和 z 的值在-1.0到1.0的一小段空间(立方体)。落在范围外的坐标都会被裁剪。
顶点输入
在GPU上创建内存,储存的顶点数据
- 通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理
- 顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
配置OpenGL如何解释这些内存
- 通过顶点数组对象(Vertex Array Objects, VAO)管理
使用缓冲区对象的优点是,可以一次将大量数据发送到显卡,不必一次发送一个数据。
VAO并不保存实际数据,而是放顶点结构定义 数组里的每一个项都对应一个属性的解析
必须使用VAO,OpenGL才能知道如何处理顶点输入。如果没有绑定VAO,OpenGL会拒绝绘制任何东西。
OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。 (每一个缓冲类型类似于前面说的子集,每个VBO是一个小助理)
下图展示了VBO和VAO:
下面是使用的关键代码:
//创建VBO和VAO对象,并赋予ID unsigned int VBO, VAO; //创建1个VAO对象 glGenVertexArrays(1, &VAO); //创建1个VBO对象 glGenBuffers(1, &VBO); //绑定VBO和VAO对象 glBindVertexArray(VAO); //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); //为当前绑定到target的缓冲区对象创建一个新的数据存储。 //如果data不是NULL,则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //上面我们将数据存到了缓冲区对象中,下面就需要 //告知Shader(着色器)如何解析缓冲里的属性值 //第一个属性是从哪个位置开始解析 //第二个属性是每个顶点属性由几个组合而成 //第三个属性是数组中每个元素的类型 //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用 //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型 //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //开启VAO管理的第一个属性值 glEnableVertexAttribArray(0); //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定, glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);
然后创建一个三角形的示例:
// GLAD的include文件包含所需的OpenGL头文件(如GL/GL.h),因此确保在其他需要OpenGL的头文件(如GLFW)之前包含GLAD。就是#include <glad/glad.h> 放在最前面 #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow* window); int main() { // 初始化GLFW,只有初始化完成之后才能够使用GLFW的函数 glfwInit(); // GLFW配置设置 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 如果是苹果系统的话使用下面代码 #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif // 创建窗口 大小和名称 GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; // 此函数销毁所有剩余的窗口和光标 glfwTerminate(); return -1; } //GLFW将窗口的上下文设置为当前线程的上下文 glfwMakeContextCurrent(window); // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //GLAD // glad: 加载所有OpenGL函数指针 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } //创建VBO和VAO对象,并赋予ID unsigned int VBO, VAO; //创建1个VAO对象 glGenVertexArrays(1, &VAO); //创建1个VBO对象 glGenBuffers(1, &VBO); //绑定VBO和VAO对象 glBindVertexArray(VAO); //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); //为当前绑定到target的缓冲区对象创建一个新的数据存储。 //如果data不是NULL,则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //上面我们将数据存到了缓冲区对象中,下面就需要 //告知Shader(着色器)如何解析缓冲里的属性值 //第一个属性是从哪个位置开始解析 //第二个属性是每个顶点属性由几个组合而成 //第三个属性是数组中每个元素的类型 //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用 //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型 //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //开启VAO管理的第一个属性值 glEnableVertexAttribArray(0); //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // 渲染循环 一个循环就是一帧 只要是窗体不关闭,就会一直循环 while (!glfwWindowShouldClose(window)) { processInput(window); // 在这里,我们将屏幕设置为了类似黑板的深蓝绿色 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置 // 调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。 glClear(GL_COLOR_BUFFER_BIT); //状态使用 #pragma region 绘制三角形 // 需要注意的是前面我们已经解绑了VAO,所以现在是无法解析数据的,所以我们需要重新绑定, // 至于数据我们已经存到缓冲区了 glBindVertexArray(VAO); // 从数据数组中indexwei=0处开始读取,每三个做一个三角形的顶点(这是在VAO中定义的)。第三个参数是说一共绘制三个顶点数据(每个顶点由vertices数组中的3个元素组成) glDrawArrays(GL_TRIANGLES, 0, 3); #pragma endregion // glfw: 交换缓冲区 该函数在指定窗口的前后缓冲区交换 // 前缓冲区:屏幕上显示的图像 // 后缓冲区:正在渲染的图像 // glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲), // 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上 glfwSwapBuffers(window); // 轮询IO事件(按键按下 / 释放、鼠标移动等)通过下面方法就可以是得窗体对鼠标做出的动作做出反应,比如关闭,移动窗体等 glfwPollEvents(); } // glfw: 回收前面分配的GLFW先关资源. 一定要注意,只有关闭窗体之后才会跳出while循环走到这一步!!! glfwTerminate(); return 0; } // glfwGetKey函数:需要一个窗口以及一个按键作为输入;函数将会返回这个按键是否正在被按下 void processInput(GLFWwindow* window) { // 如果按下了ESC键,设置窗体的关闭标志为true,代表窗体可以退出 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } // 当改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // 设置窗口维度 // glViewport(前两参数为窗口左下角位置,3.宽度,4.高度) glViewport(0, 0, width, height); }
结果:
我们还没做着色器,所以这里的三角形颜色是根据自己电脑本身的默认颜色。