在上篇blog中已经画了一个三角形了,这篇讲解一下一个三角形的渲染过程。
上篇blog中的glbegin搭配glend的流程,在OpenGL3.2中已经被弃用了,3.3以后推荐使用VBO+EBO+VAO的流程。
图形渲染管线
作用:将三维坐标经过一系列变换,生成一个二维坐标,这个二维坐标的值就是渲染的结果。
渲染管线的过程就如上图所示,不同的图形API可能每个阶段的任务和名称有所不同,但是整体流程都大同小异。
顶点数据中包含了很多数据,包括:顶点坐标,顶点颜色,顶点法线等。
- 顶点着色器:处理顶点
- 曲面细分着色器:4.0以后才有的功能,自动将一个曲面细分为很多三角形,在复杂地形构建中很实用
- 几何着色器:处理一个图元,可以同时访问图元中所有的三角形的所有顶点
- 光栅化:将3D连续的三角形进行格栅化,对应输出像素。连续的三角形变成离散的像素点数据
- 片段着色器:片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
数据的流动
我们使用显卡进行渲染,需要将数据由内存放到显存里面,OpenGL中使用 顶点缓冲对象(Vertex Buffer Objects, VBO) 管理这部分用于存放顶点数据的显存。但是VBO只是存储数据,OpenGL读取的数据的时候,需要知道数据的格式,数据存储的位置,每个顶点数据的大小等等。这步我们可以使用 顶点数组对象(Vertex Array Object, VAO) 来记录,后续当我们想使用这个VBO的数据时,先绑定一下对应的VAO,就可以知道数据的具体信息了。
相邻三角形的两个顶点是共用的,如果按照上面的存储方式三角形的共同顶点会被存储三次,这样会浪费很多空间,所以常见的存储方式是,将顶点单独存储起来,三角形中存储顶点索引,渲染的时候根据索引在顶点数据序列中找顶点数据。在OpenGL中,使用 元素缓冲对象(Element Buffer Object,EBO) 来实现这个功能,
其中蓝色部分,是要使用着色器语言编程的,OpenGL使用GLSL着色器语言。
渲染管线是软硬相耦合的,具体发展历史可以看这两篇blog:
GPU硬件发展
GPU软件发展
代码
使用VAO和VBO画三角形的代码
OpenGL代码:
#include <iostream>
#include "glad/glad.h" //管理OpenGL的函数指针的
#include "GLFW/glfw3.h"
#include "gl/GL.h"void framebufferSizeCallback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
void rendering(GLFWwindow* window);
unsigned int shaderProgramInit();int main()
{glfwInit(); // 初始化GLFWglfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);// 配置GLFW,OpenGL版本,Core核心模式GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); //创建窗口if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return -1;}glfwMakeContextCurrent(window); //将上下文设置为该窗口// 给GLAD传入用来加载系统相关的OpenGL函数指针地址的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed initialize GLAD" << std::endl;return -1;}glViewport(0, 0, 800, 600); //设置视口,即窗口中需要渲染的部分,前两个参数是左下角坐标,后两个是宽高//当用户改变窗口的大小的时候,视口也应该被调整,可以设置一个窗口回调函数,自动调整//将回调函数注册到GLFWglfwSetFramebufferSizeCallback(window, framebufferSizeCallback);while (!glfwWindowShouldClose(window)) //渲染主循环{glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //使用颜色清除bufferglClear(GL_COLOR_BUFFER_BIT);processInput(window); //处理输入事件rendering(window);glfwSwapBuffers(window); //交换渲染buffer,将glfw窗口显示到屏幕。双缓冲buffer,之前龚大大视频讲过glfwPollEvents(); //检查有没有触发什么事件,调用相应的回调函数}glfwTerminate(); //结束glfwreturn 0;}void framebufferSizeCallback(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);
}void rendering(GLFWwindow* window)
{//OpenGL的坐标系是-1到1之间,中心点在屏幕中心,与OpenCV和DX的不同。float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f};unsigned int shaderProgram = shader_program_init();unsigned int VBO;glGenBuffers(1, &VBO); //创建顶点缓冲buffer,在显存上分配一段空间,存储顶点数据unsigned int VAO;glGenVertexArrays(1, &VAO); //创建VAO,存储应该使用的VBO位置,以及该VBO中顶点数据的组成和分布glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO); //将这段内存绑定到顶点缓存上,GL_ARRAY_BUFFER表示缓存buffer的类型glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 传输数据,类似于cudaMemcpy作用,// 最后的GL_STATIC_DRAW是向显卡表示如何管理该段数据,// GL_STATIC_DRAW表示该段数据几乎不变化,GL_DYNAMIC_DRAW表示该段数据经常变换,GL_STREAM_DRAW表示该段数据每次渲染都会变换// 显卡会根据数据的特性选择如何管理数据,以达到最快速度//渲染程序已经搞定了,接下来要告诉程序如何从缓冲区获取并解析顶点数据glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);// arg1表示顶点坐标信息在顶点数据中的哪个位置,arg2表示数据格式,arg3决定是数据否需要归一化,arg4表示每个顶点数据的大小,作为step来判断取下一个顶点数据// arg5表示顶点数据在缓冲区的那个位置,因为在缓冲区开头位置,所以直接为0//一般不需要主动解绑VAO和VBO,当我们再次绑定其他VAO,VBO时,会自动把旧的解绑换新的。//当我们要渲染一个物体的时候glEnableVertexAttribArray(0);// 开启顶点渲染功能glBindVertexArray(VAO); //告诉显卡,用这个VAOglUseProgram(shaderProgram); //告诉显卡,使用这个着色器glDrawArrays(GL_TRIANGLES, 0, 3);}unsigned int shaderProgramInit()
{const char* vertexShaderSource = "#version 330 core\n""layout (location = 0) in vec3 aPos;\n" //告诉顶点坐标信息在哪里,position=0,也就是一开始就是坐标信息"void main()\n""{\n"" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n""}\0"; // GLSL代码,先存储到一个字符串中,后续多了可以存到文件里unsigned int vertexShader; //着色器IDvertexShader = glCreateShader(GL_VERTEX_SHADER); //创建VERTEX_SHADER类型的shader,返回shader的IDglShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //将着色器源代码绑定到shaderglCompileShader(vertexShader); //编译shaderint success;char infoLog[512];glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); //收集着色器语言编译的结果状态if (!success){glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}const char* fragmentShaderSource = "#version 330 core \n""out vec4 FragColor;\n" //声明输出变量FragColor,类型为vec4"void main()\n""{FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}\0";unsigned int fragmentShader;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 << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;}unsigned int shaderProgram;shaderProgram = glCreateProgram(); //创建着色程序,可以理解为前面都是在编译目标文件,这一步是将所有目标文件链接成一个可执行文件glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader); //添加shader,着色器程序会自动把上一个shader的输出作为下一个shader的输入,如果输出输入格式不匹配,就会报错glLinkProgram(shaderProgram);glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);if (!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl;}glDeleteShader(vertexShader);glDeleteShader(fragmentShader); //清理掉之前的shader,已经不需要了return shaderProgram;
}
使用VAO+VBO+EBO画矩形的代码:
void rectangleRendering()
{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,2,3)就是顶点数组vertices的下标,// 这样可以由下标代表顶点组合成矩形0, 1, 3, // 第一个三角形1, 2, 3 // 第二个三角形};unsigned int shaderProgram = shaderProgramInit();unsigned int VBO;glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);unsigned int VAO;glGenVertexArrays(1, &VAO); //创建VAO,存储应该使用的VBO位置,以及该VBO中顶点数据的组成和分布glBindVertexArray(VAO);unsigned int EBO;glGenBuffers(1, &EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);glUseProgram(shaderProgram);glBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);// 跟glDrawArrays基本一样,arg1表示模式,arg2表示绘画点数,arg3表示数据类型,arg4表示索引偏移量
}