在上篇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(); // 初始化GLFW
glfwWindowHint(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);
//设置视口,即窗口中需要渲染的部分,前两个参数是左下角坐标,后两个是宽高
//当用户改变窗口的大小的时候,视口也应该被调整,可以设置一个窗口回调函数,自动调整
//将回调函数注册到GLFW
glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
while (!glfwWindowShouldClose(window)) //渲染主循环
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //使用颜色清除buffer
glClear(GL_COLOR_BUFFER_BIT);
processInput(window); //处理输入事件
rendering(window);
glfwSwapBuffers(window); //交换渲染buffer,将glfw窗口显示到屏幕。双缓冲buffer,之前龚大大视频讲过
glfwPollEvents(); //检查有没有触发什么事件,调用相应的回调函数
}
glfwTerminate(); //结束glfw
return 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); //告诉显卡,用这个VAO
glUseProgram(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; //着色器ID
vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建VERTEX_SHADER类型的shader,返回shader的ID
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //将着色器源代码绑定到shader
glCompileShader(vertexShader); //编译shader
int 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表示索引偏移量
}