引言
1、本专栏为LearnOpenGL的学习笔记,用来记录基础知识、渲染流程和遇到的问题。如果文中理解有误,还请指正。
2、GLFW API手册
3、OpenGL Specification
一、为什么需要VAO、VBO、EBO?它们各自的用处是什么?
- 先说结论:个人理解,这三个数据结构在渲染pipeline中最基本的是VBO,其在显存上开辟一块内存,用来存放批量顶点和纹理等数据。在其基础上EBO是为了顶点复用、节约内存,VAO是为了顶点属性解析复用,避免每组顶点传过去之后都要重新设置顶点属性。综上,VBO是为了传数据,EBO和VAO都是通过复用来提升效率的一种“(索引)绑定结构”。------->但是在渲染pipeline中,VAO和VBO是必须有的,EBO可以在顶点数据需要复用时再用。为什么不能只用VBO?其实我也不知道可不可以,但是没看到VBO有相关API告诉GPU如何解析数据,解析数据的函数glVertexAttribPointer()是设置VAO每个属性点(属性点对应每个不同类型数据的VBO)的,所以我感觉基本的结构是VAO+VBO,顶点多时,加入EBO。
- VBO: 为了实现内存数据到显存的批量传输,从而加快渲染速度。因为CPU从内存中将数据发送到显卡显存速度的较慢,而顶点着色器几乎能立即访问到显存中的数据,所以需要一次发送尽可能多的顶点和纹理数据,以平衡IO速度制约渲染速度的问题。
- VAO:在渲染Pipeline中,我们通过VBO将一大堆不同类型的数据(一般来说一种数据类型存一个VBO,算一个顶点属性,也可以把坐标、纹理等不同类型的数据存入一个VBO,然后解析顶点属性时通过偏移量来区分,但是大数据量时显然一个VBO存一个类型的数据更清晰)存入显存,但此时GPU并不知道如何解析显存中的数据,因此每当数据通过glBufferData()传输到显存后还需要调用glVertexAttribPointer()告诉GPU如何解析。而VAO的作用就是绑定一组VBO并分别设置好归属其的每个VBO的解析规则,使其在循环渲染时不需要重复解析。打个比方,VAO是一个鸭妈妈,VBO是它身后跟着的一群各不相同鸭宝宝,首先将每个鸭宝宝绑定到鸭妈妈不同的属性位置0、1、2…,并设置其解析规则。然后在循环渲染的过程中就可以通过绑定鸭妈妈就可以直接知道它身后所有鸭宝宝的数据解析规则,而不需要再通过循环去绑定每个VBO并设置解析规则,可以大幅减少篇幅。----->【Question】个人感觉代码量并未减少,而是像在渲染前将一组数据打包,然后在渲染过程中就只需要通过VAO来调用,使渲染过程更简洁
- EBO: 一个复杂的渲染画面是由成千上万个顶点和纹理数据组成的,而显存是有限的资源,因此在多个三角形共用同一个顶点时,可以只存储该顶点一次而非多次,通过EBO使多个三角形来复用这个顶点,从而达到节约显存的作用。
二、绘制三角形的渲染Pipeline
- 初始化glfw并创建窗口、初始化GLAD
- glfwWindowHint(): 初始化glfw的版本、核心模式
- glfwInit(): 如果初始化失败返回0,所以作为异常处理的条件时要取反
- glfwTerminate()
- glfwMakeContextCurrent(): 创建完窗口一定要将其设置为当前进程的上下文
- glfwSetFramebufferSizeCallback()
- gladLoadGLLoader(): 初始化glad
- 创建着色器程序对象
- glCreateShader(): 创建指定类型的着色器
- glShaderSource(): 为着色器对象绑定着色器程序源码
- glCompileShader(): 编译源码
- glGetShaderiv(): 查看编译是否成功
- glGetShaderInfoLog(): 返回Shader对象的日志信息
- glCreatreProgram(): 创建着色器程序对象(就是用来拼接这个着色器对象的程序,用来形成一段完整的着色器程序,像把一段段的水管接起来一样)
- glAttachShader(): 附加一个着色器对象到着色器程序对象中
- glLinkProgram(): 将附加到着色器程序对象中的着色器对象拼接起来
- glGetProgramiv()
- glGetProgramInfoLog()
- glDeleteShader():删除指定的shader对象,释放内存
- 导入顶点数据到显存并配置顶点属性
- glGenVertexArrays():创建顶点数组对象—>注意!!对象ID是指向该内存的引用
- glGenBuffers(): 创建缓冲对象(顶点缓冲/元素缓冲)
- glBindVertexArray(): 绑定(激活)VAO,在该调用后紧接着绑定的VBO都属于该VAO。---->注意!!即第一节所说一个VAO可以有多个VBO,并且他们的顶点解析方法是相同的(顶点解析方法由VAO相关的函数决定)
- glBindBuffer(): 将顶点数据(在内存中)拷贝到显存中(VBO指向该块显存空间)
- glVertexAttribPointer(): 指定该VAO的所有VBO数据的解析方法
- glEnableVertexAttribArray(): 使能数据 ----> 注意!!即上一个函数是告诉GPU如何解析显存中的数据,这个函数是让GPU能看到显存中的数据
- 循环渲染
- processInput(): 处理输入
- glClearColor: 设置清屏RGB
- glClear(): 执行清屏
- glUseProgram(): 激活着色器程序对象
- glDrawElements() / glDrawArrays():都是从VBO中提取数据来渲染基本图元的作用。但是前者需要索引参数,适用于顶点复用多的情况。
- glfwSwapBuffers(): 因为渲染画面是逐帧渲染的,所以如果只有一个缓冲,那渲染的时间就等于绘制+显示,所以就会造成闪屏(因为只有一个Buffer,拿到数据先绘制,绘制完再显示,绘制时屏就是黑的),所以利用双缓冲技术,用两个缓冲区来交替渲染,前缓冲区用于显示屏幕上的内容,后缓冲区用于绘制,每一帧开始时将两个缓冲区交换,即可达到前缓冲区一直显示,后缓冲区一直绘制的作用。
- glfwPollEvent():通过窗体的关闭标志来控制循环会导致主线程(渲染进程)占用资源,从而导致窗体无法处理消息,所以需要在主线程中调用窗体处理消息函数来处理消息队列中的消息。
三、本节实验及其注意事项
3.1 作业要求及源码
- 实验要求及源码实现:通过3个顶点数据绘制一个三角形
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> // window parameter const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const char* SCR_NAME = "Kkaiw"; //framebuffer_size_callback funtion void framebuffer_size_callback(GLFWwindow* window,int width,int height); //input catch funciton void processInput(GLFWwindow* window); //shader source const char* vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "out vec3 ourColor;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos,1.0f);\n" " ourColor = aPos;\n" "}\0"; const char* fragmentShaderSource = "#version 330 core\n" "in vec3 ourColor;\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(ourColor,1.0f);\n" "}\n\0"; int main(){ //1. Init //1.1 Init GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3); glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE); if(!glfwInit()){ std::cout << "Fail to init glfw\n" << std::endl; return -1; } #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,GL_TRUE); #endif //1.2 Create Window GLFWwindow* window = glfwCreateWindow(SCR_WIDTH,SCR_HEIGHT,SCR_NAME,NULL,NULL); if(window == NULL){ std::cout << "Fail to create window\n" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window,framebuffer_size_callback); //1,3 Init GLAD if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){ std::cout << "Fail to init glad" << std::endl; return -1; } //2. Shader int success; char InfoLog[512]; //2.1 vertex shader unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader,1,&vertexShaderSource,NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(vertexShader,512,NULL,InfoLog); std::cout << "Fail to compile vertexShader\n " << InfoLog << std::endl; } //2.2 fragment shader unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(fragmentShader,512,NULL,InfoLog); std::cout << "Fail to compile fragmentShader\n" << InfoLog << std::endl; } //2.3 shader program unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram,vertexShader); glAttachShader(shaderProgram,fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram,GL_COMPILE_STATUS,&success); if(!success){ glGetProgramInfoLog(shaderProgram,512,NULL,InfoLog); std::cout << "Fail to compile shaderProgram" << std::endl; } //2.4 delete shader to free memory glDeleteShader(vertexShader); glDeleteShader(fragmentShader); //3. vertex data and config float vertices[]={ -0.5f, -0.5f, 0.0f, // left 0.5f, -0.5f, 0.0f, // right 0.0f, 0.5f, 0.0f // top }; unsigned int VAO,VBO; glGenVertexArrays(1,&VAO); glGenBuffers(1,&VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER,VBO);//【NOTE】为什么需要为缓冲区绑定名称?----->因为可以实现对不同渲染管线的快速切换!【Question】假设有两组顶点数据存在两个VBO中,可以通过将其bind到同一个缓冲区对象绑定类型上,完成切换?。 glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0); //4. loop render while(!glfwWindowShouldClose(window)){ processInput(window); glClearColor(0.52f,0.93f,0.78f,1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES,0,3); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1,&VAO); glDeleteBuffers(1,&VBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; } //function void framebuffer_size_callback(GLFWwindow* window,int width,int height){ glViewport(0,0,width,height); } void processInput(GLFWwindow* window){ if(glfwGetKey(window,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(window,true); } ```
- 渲染结果
3.2 关于本实验的思考和疑问及相应测试源码
3.2.1 一个VAO分别对应1个或多个VBO的实验方式----->VAO和VBO应该如何搭配使用?以及各种情况下是如何使用的?
-
将两个三角形的顶点坐标和颜色放在一个数组中,通过一个VAO和一个VBO绘制
- 修改代码的第3部分和顶点着色器源码
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> // window parameter const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const char* SCR_NAME = "Kkaiw"; //framebuffer_size_callback funtion void framebuffer_size_callback(GLFWwindow* window,int width,int height); //input catch funciton void processInput(GLFWwindow* window); //shader source const char* vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "layout (location = 1) in vec3 aColor;\n" "out vec3 ourColor;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos,1.0f);\n" " ourColor = aColor;\n" "}\0"; const char* fragmentShaderSource = "#version 330 core\n" "in vec3 ourColor;\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(ourColor,1.0f);\n" "}\n\0"; int main(){ //1. Init //1.1 Init GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3); glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE); if(!glfwInit()){ std::cout << "Fail to init glfw\n" << std::endl; return -1; } #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,GL_TRUE); #endif //1.2 Create Window GLFWwindow* window = glfwCreateWindow(SCR_WIDTH,SCR_HEIGHT,SCR_NAME,NULL,NULL); if(window == NULL){ std::cout << "Fail to create window\n" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window,framebuffer_size_callback); //1,3 Init GLAD if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){ std::cout << "Fail to init glad" << std::endl; return -1; } //2. Shader int success; char InfoLog[512]; //2.1 vertex shader unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader,1,&vertexShaderSource,NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(vertexShader,512,NULL,InfoLog); std::cout << "Fail to compile vertexShader\n " << InfoLog << std::endl; } //2.2 fragment shader unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(fragmentShader,512,NULL,InfoLog); std::cout << "Fail to compile fragmentShader\n" << InfoLog << std::endl; } //2.3 shader program unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram,vertexShader); glAttachShader(shaderProgram,fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram,GL_COMPILE_STATUS,&success); if(!success){ glGetProgramInfoLog(shaderProgram,512,NULL,InfoLog); std::cout << "Fail to compile shaderProgram" << std::endl; } //2.4 delete shader to free memory glDeleteShader(vertexShader); glDeleteShader(fragmentShader); //3. vertex data and config float vertices[] = { //first triangle color -0.9f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, -0.0f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, -0.45f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, //second triangle 0.0f, -0.5f, 0.0f, 0.5f, 0.5f, 0.0f, 0.9f, -0.5f, 0.0f, 0.5f, 0.0f, 0.5f, 0.45f, 0.5f, 0.0f, 0.0f, 0.5f, 0.5f }; unsigned int VAO,VBO; glGenVertexArrays(1,&VAO); glGenBuffers(1,&VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER,VBO); glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0); //parse position glEnableVertexAttribArray(0); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); //4. loop render while(!glfwWindowShouldClose(window)){ processInput(window); glClearColor(0.52f,0.93f,0.78f,1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES,0,6); //[NOTE]如果数组中一个定点的熟悉长度是3 就是3 如果是6 就是6 glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1,&VAO); glDeleteBuffers(1,&VBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; } //function void framebuffer_size_callback(GLFWwindow* window,int width,int height){ glViewport(0,0,width,height); } void processInput(GLFWwindow* window){ if(glfwGetKey(window,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(window,true); }
- 渲染结果
- 修改代码的第3部分和顶点着色器源码
-
将两个三角形的顶点坐标和颜色放在两个数组中,分别通过一个VAO和一个VBO绘制
- 可以发现与上一种方式有相同的渲染结果,但如果两个图形是不同的渲染方式,那本方式可以更好地实现渲染pipeline的切换----->所以本方式是在渲染pipeline上对上个实验上流程的优化,即可以完成不同渲染管线的快速切换,这在两个渲染目标不同时应该更具效率和清晰。但本方式的缺点在于,一个图形对象不同类型的顶点属性放在同一个数组里,这并没有很好地执行面对对象的思想。所以在下个实验中,我们取一个三角形为例,对其不同顶点属性进行优化设计,即一个图形由一个VAO和多个VBO组成。
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> // window parameter const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const char* SCR_NAME = "Kkaiw"; //framebuffer_size_callback funtion void framebuffer_size_callback(GLFWwindow* window,int width,int height); //input catch funciton void processInput(GLFWwindow* window); //shader source const char* vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "layout (location = 1) in vec3 aColor;\n" "out vec3 ourColor;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos,1.0f);\n" " ourColor = aColor;\n" "}\0"; const char* fragmentShaderSource = "#version 330 core\n" "in vec3 ourColor;\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(ourColor,1.0f);\n" "}\n\0"; int main(){ //1. Init //1.1 Init GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3); glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE); if(!glfwInit()){ std::cout << "Fail to init glfw\n" << std::endl; return -1; } #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,GL_TRUE); #endif //1.2 Create Window GLFWwindow* window = glfwCreateWindow(SCR_WIDTH,SCR_HEIGHT,SCR_NAME,NULL,NULL); if(window == NULL){ std::cout << "Fail to create window\n" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window,framebuffer_size_callback); //1,3 Init GLAD if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){ std::cout << "Fail to init glad" << std::endl; return -1; } //2. Shader int success; char InfoLog[512]; //2.1 vertex shader unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader,1,&vertexShaderSource,NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(vertexShader,512,NULL,InfoLog); std::cout << "Fail to compile vertexShader\n " << InfoLog << std::endl; } //2.2 fragment shader unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(fragmentShader,512,NULL,InfoLog); std::cout << "Fail to compile fragmentShader\n" << InfoLog << std::endl; } //2.3 shader program unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram,vertexShader); glAttachShader(shaderProgram,fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram,GL_COMPILE_STATUS,&success); if(!success){ glGetProgramInfoLog(shaderProgram,512,NULL,InfoLog); std::cout << "Fail to compile shaderProgram" << std::endl; } //2.4 delete shader to free memory glDeleteShader(vertexShader); glDeleteShader(fragmentShader); //3. vertex data and config float first_vertices[] = { //first triangle color -0.9f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, -0.0f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, -0.45f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, }; float second_vertices[] = { //second triangle 0.0f, -0.5f, 0.0f, 0.5f, 0.5f, 0.0f, 0.9f, -0.5f, 0.0f, 0.5f, 0.0f, 0.5f, 0.45f, 0.5f, 0.0f, 0.0f, 0.5f, 0.5f }; unsigned int VAO[2],VBO[2]; glGenVertexArrays(2,VAO); glGenBuffers(2,VBO); glBindVertexArray(VAO[0]); glBindBuffer(GL_ARRAY_BUFFER,VBO[0]); glBufferData(GL_ARRAY_BUFFER,sizeof(first_vertices),first_vertices,GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0); //parse position glEnableVertexAttribArray(0); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); glBindVertexArray(VAO[1]); glBindBuffer(GL_ARRAY_BUFFER,VBO[1]); glBufferData(GL_ARRAY_BUFFER,sizeof(second_vertices),second_vertices,GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); //4. loop render while(!glfwWindowShouldClose(window)){ processInput(window); glClearColor(0.52f,0.93f,0.78f,1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); //the first render pipeline glBindVertexArray(VAO[0]); glDrawArrays(GL_TRIANGLES,0,6); //[NOTE]如果数组中一个定点的熟悉长度是3 就是3 如果是6 就是6 //fast swap to the second render pipeline glBindVertexArray(VAO[1]); glDrawArrays(GL_TRIANGLES,0,6); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(2,VAO); glDeleteBuffers(2,VBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; } //function void framebuffer_size_callback(GLFWwindow* window,int width,int height){ glViewport(0,0,width,height); } void processInput(GLFWwindow* window){ if(glfwGetKey(window,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(window,true); }
- 渲染结果
- 可以发现与上一种方式有相同的渲染结果,但如果两个图形是不同的渲染方式,那本方式可以更好地实现渲染pipeline的切换----->所以本方式是在渲染pipeline上对上个实验上流程的优化,即可以完成不同渲染管线的快速切换,这在两个渲染目标不同时应该更具效率和清晰。但本方式的缺点在于,一个图形对象不同类型的顶点属性放在同一个数组里,这并没有很好地执行面对对象的思想。所以在下个实验中,我们取一个三角形为例,对其不同顶点属性进行优化设计,即一个图形由一个VAO和多个VBO组成。
-
将一个三角形的顶点坐标和颜色放在两个数组中,通过一个VAO和两个VBO绘制
- 就是把不同的VBO绑到VAO不同的属性点上
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> // window parameter const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const char* SCR_NAME = "Kkaiw"; //framebuffer_size_callback funtion void framebuffer_size_callback(GLFWwindow* window,int width,int height); //input catch funciton void processInput(GLFWwindow* window); //shader source const char* vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "layout (location = 1) in vec3 aColor;\n" "out vec3 ourColor;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos,1.0f);\n" " ourColor = aColor;\n" "}\0"; const char* fragmentShaderSource = "#version 330 core\n" "in vec3 ourColor;\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(ourColor,1.0f);\n" "}\n\0"; int main(){ //1. Init //1.1 Init GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3); glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE); if(!glfwInit()){ std::cout << "Fail to init glfw\n" << std::endl; return -1; } #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,GL_TRUE); #endif //1.2 Create Window GLFWwindow* window = glfwCreateWindow(SCR_WIDTH,SCR_HEIGHT,SCR_NAME,NULL,NULL); if(window == NULL){ std::cout << "Fail to create window\n" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window,framebuffer_size_callback); //1,3 Init GLAD if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){ std::cout << "Fail to init glad" << std::endl; return -1; } //2. Shader int success; char InfoLog[512]; //2.1 vertex shader unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader,1,&vertexShaderSource,NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(vertexShader,512,NULL,InfoLog); std::cout << "Fail to compile vertexShader\n " << InfoLog << std::endl; } //2.2 fragment shader unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(fragmentShader,512,NULL,InfoLog); std::cout << "Fail to compile fragmentShader\n" << InfoLog << std::endl; } //2.3 shader program unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram,vertexShader); glAttachShader(shaderProgram,fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram,GL_COMPILE_STATUS,&success); if(!success){ glGetProgramInfoLog(shaderProgram,512,NULL,InfoLog); std::cout << "Fail to compile shaderProgram" << std::endl; } //2.4 delete shader to free memory glDeleteShader(vertexShader); glDeleteShader(fragmentShader); //3. vertex data and config float vertices[] = { //first triangle -0.9f, -0.5f, 0.0f, -0.0f, -0.5f, 0.0f, -0.45f, 0.5f, 0.0f, }; float colors[] = { //color 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }; unsigned int VAO,VBO[2]; glGenVertexArrays(1,&VAO); glGenBuffers(2,VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER,VBO[0]); glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0); //parse position glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER,VBO[1]); glBufferData(GL_ARRAY_BUFFER,sizeof(colors),colors,GL_STATIC_DRAW); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0); glEnableVertexAttribArray(1); glBindVertexArray(0); //4. loop render while(!glfwWindowShouldClose(window)){ processInput(window); glClearColor(0.52f,0.93f,0.78f,1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); //render glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES,0,6); //[NOTE]如果数组中一个定点的熟悉长度是3 就是3 如果是6 就是6 glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1,&VAO); glDeleteBuffers(2,VBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; } //function void framebuffer_size_callback(GLFWwindow* window,int width,int height){ glViewport(0,0,width,height); } void processInput(GLFWwindow* window){ if(glfwGetKey(window,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(window,true); }
- 渲染结果
- 就是把不同的VBO绑到VAO不同的属性点上
3.2.2 用一个数组存两个三角形的顶点坐标,另一个数组存两个三角形的颜色数据,----->通过引入EBO,实现共同顶点数据的复用
-
【Question】写到这里,让我产生了疑惑。究竟时1VAO+1VBO+1EBO的模式好,还是1VAO+xVBO+1EBO的模式好?前者代码更清晰,写起来方便。后者不同的顶点属性可以分为不同的数组,让数据更干净。望大佬告知。
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> // window parameter const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const char* SCR_NAME = "Kkaiw"; //framebuffer_size_callback funtion void framebuffer_size_callback(GLFWwindow* window,int width,int height); //input catch funciton void processInput(GLFWwindow* window); //shader source const char* vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "layout (location = 1) in vec3 aColor;\n" "out vec3 ourColor;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos,1.0f);\n" " ourColor = aColor;\n" "}\0"; const char* fragmentShaderSource = "#version 330 core\n" "in vec3 ourColor;\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(ourColor,1.0f);\n" "}\n\0"; int main(){ //1. Init //1.1 Init GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3); glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE); if(!glfwInit()){ std::cout << "Fail to init glfw\n" << std::endl; return -1; } #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,GL_TRUE); #endif //1.2 Create Window GLFWwindow* window = glfwCreateWindow(SCR_WIDTH,SCR_HEIGHT,SCR_NAME,NULL,NULL); if(window == NULL){ std::cout << "Fail to create window\n" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window,framebuffer_size_callback); //1,3 Init GLAD if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){ std::cout << "Fail to init glad" << std::endl; return -1; } //2. Shader int success; char InfoLog[512]; //2.1 vertex shader unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader,1,&vertexShaderSource,NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(vertexShader,512,NULL,InfoLog); std::cout << "Fail to compile vertexShader\n " << InfoLog << std::endl; } //2.2 fragment shader unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader,GL_COMPILE_STATUS,&success); if(!success){ glGetShaderInfoLog(fragmentShader,512,NULL,InfoLog); std::cout << "Fail to compile fragmentShader\n" << InfoLog << std::endl; } //2.3 shader program unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram,vertexShader); glAttachShader(shaderProgram,fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram,GL_COMPILE_STATUS,&success); if(!success){ glGetProgramInfoLog(shaderProgram,512,NULL,InfoLog); std::cout << "Fail to compile shaderProgram" << std::endl; } //2.4 delete shader to free memory glDeleteShader(vertexShader); glDeleteShader(fragmentShader); //3. vertex data and config float vertices[] = { //first triangle -0.9f, -0.5f, 0.0f, -0.0f, -0.5f, 0.0f, -0.45f, 0.5f, 0.0f, 0.9f, -0.5f, 0.0f, 0.45f, 0.5f, 0.0f }; float colors[] = { //color 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, 0.0f, 0.5f, 0.0f, 0.5f, 0.5f }; unsigned int indices[] = { 0, 1, 2, 2, 3, 4 }; unsigned int VAO,VBO[2],EBO; glGenVertexArrays(1,&VAO); glGenBuffers(2,VBO); glGenBuffers(1,&EBO); glBindVertexArray(VAO); //NOTE!! EBO的数据要在设置解析规则前送入显存! 所以要写在前面。 因为我们是 1*VAO + X*VBO的格式,所以没法把EBO写在VBO的bufferdata和设置解析格式中间! glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER,VBO[0]); glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0); //parse position glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER,VBO[1]); glBufferData(GL_ARRAY_BUFFER,sizeof(colors),colors,GL_STATIC_DRAW); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0); glEnableVertexAttribArray(1); glBindVertexArray(0); //4. loop render while(!glfwWindowShouldClose(window)){ processInput(window); glClearColor(0.52f,0.93f,0.78f,1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); //render glBindVertexArray(VAO); // glDrawArrays(GL_TRIANGLES,0,6); //[NOTE]如果数组中一个定点的熟悉长度是3 就是3 如果是6 就是6 glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1,&VAO); glDeleteBuffers(2,VBO); glDeleteBuffers(1,&EBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; } //function void framebuffer_size_callback(GLFWwindow* window,int width,int height){ glViewport(0,0,width,height); } void processInput(GLFWwindow* window){ if(glfwGetKey(window,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(window,true); }
-
渲染效果
3.2.3 【Question】如何通过绑定(解绑)缓冲对象实现渲染管线的快速切换?---->可能因为搜索方式不对,未找到清晰的解答???
- Maybe,3.2的第二个实验就是答案。
- 在绑定时可以发现,通过关键字GL_ARRAY_BUFFER绑定到不同的关键字,使顶点数据可以发送到不同的VBO---------->这个关键字就像高铁站,VBO就像多条铁路,通过绑定高铁站,数据可以传输到不同的VBO中。
- 在渲染时可以发现,通过绑定不同的VAO,快速切换了两个图形的渲染,应该就是实现渲染管线的快速切换?
3.3 Tips
- 着色器程序的layout和in out
- layout (postion = 0,1…15) 用于告诉着色器源码,每个输入变量对应VAO的哪个顶点属性数据------>对应于glVertexAttribPointer中的0,1…15
- in和out用于定义着色器程序中的输入和输出变量,顶点着色器必有输入变量(因为要从VBO中按顶点属性拿数据),片段着色器必有输出变量,因为要把颜色传出去。
- 顶点坐标的范围,颜色的范围
- [-1,1],[-1,1],[-1,1]的坐标系,(x,y,z)=(0,0,0)在视域的中心点
- 颜色范围将[0,255]归一化到[0,1],即黑到白。------>所以可以知道为什么第一个渲染图中左下角是黑色的,因为坐标是负值,剪裁成0了。
- 在3.2的实验1中,我们将两个三角形的顶点坐标存入同一个VBO,OPENGL通过GL_TRIANGLES如何分别谁是谁的顶点?
- 以glDrawArrays()的第三个参数决定每个顶点的数据长度,然后依次1-3,4-6的遍历取,如果此时只有任意5行数据而不是代码中的6行,则会自动将那行补0.
四、本节课后作业及其注意事项
- 作业1:EBO复用两个相同的顶点就行,即顶点数组中只需要4组坐标
- 作业2:画多个三角形的VAO和VBO使用方法
- 可以 xVAO + xVBO,即一个VAO对应一个VBO,即LearnOpenGL中画的那个VAO,VBO,EBO的关系图(也即本文3.2节的测试2)
- 也可以 1VAO + xVBO, 即本文3.2节的测试3
- 也可以 1VAO + 1VBO, 即本文3.2节的测试1 - 作业3:即为两个三角形分别生成一个着色器程序
五、BUG记录
- glCreateShader()函数显示未定义,但库都是有效的。-----> 把glad.h的#include语句放到glfw3.h的#include语句前,错误消失。WHY?
- 着色器程序显示以下报错—>着色器程序中变量名写错了或者缺少#等字符型拼接错误
- 屏幕显示一瞬间就消失,但未报错------》窗口关闭状态的返回值问题
- 本文3.3节,1VAO + XVBO + 1*EBO的设计模式,如何摆放EBO BufferData的位置及Draw函数?
- 首先,需要在解析顶点属性前完成EBO的传输,因为有EBO时解析需要用EBO的数据。其次,因为该测试代码是1个VAO拖多个VBO,所以不能像一个VAO拖一个VBO一样卸载VBO的bufferdata和解析顶点属性中间,因为多个VBO切换绑定GL_ARRAY_BUFFER这个关键字时,前面的又关闭了(看前面说的火车站的例子)。
- 用了EBO,绘制时需要用glDrawElements,否则会忽略索引,自动补原点。