一、引言
本教程使用GLEW和GLFW库。
通过本教程,你能轻松的、深入的理解OpenGL如何绘制一个三角形。
如果你不了解OpenGL是什么,可以阅读OpenGL深入理解。
二、基本函数和语句介绍
通过阅读以下的函数,你的大脑里能留下关于OpenGL基本函数的一个大致印象。(不需深入理解,可忽略此节)
- glfwInit ()
初始化glfw,为一些空指针赋值 。 - glfwWindowHint(name, value)
设置glfw库的一些属性,name为属性字段,value为设置的值。
GLFW_CONTEXT_VERSION_MAJOR指OpenGL主版本号。
GLFW_CONTEXT_VERSION_MINOR指OpenGL次版本号。
GLFW_OPENGL_PROFILE指渲染模式, GLFW_OPENGL_CORE_PROFILE为核心模式。
GLFW_RESIZABLE表示窗口可调性, GL_FALSE代表不可调。 - glfwCreateWindow(width, height, title, NULL, NULL)
创建一个glfw窗口,参数为:宽、高、窗口标题。返GLFWwindow* 类型。 - glfwMakeContextCurrent(window)
通知GLFW将窗口window的上下文设置为当前线程的主上下文。 - glfwSetKeyCallback(window, callback)
通过GLFW注册函数至合适的回调,callback为函数名。 - glewExperimental = GL_TRUE
让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术,如果把它设置为GL_FALSE的话可能会在使用OpenGL的核心模式时出现一些问题。 - glewInit()
初始化glew。 - glfwGetFramebufferSize(window, &width, &height);
获取窗口window的大小width和height。 - glViewport(x, y, width, height)
设置视口位置,x、y表示视口左下角在窗口中的位置,width和height表示视口大小。 - glCreateShader(shader)
创建一个shader着色器,返回shader的引用。
shader值有GL_VERTEX_SHADER和GL_FRAGMENT_SHADER。 - glShaderSource(shader, n, &shaderSource, NULL)
绑定shader着色器的源码,n为源代码中字符串数量,shaderSource为源代码字符串。 - glCompileShader(shader)
编译shader着色器。 - glGetShaderiv(shader, GL_COMPILE_STATUS, &success)
检查着色器shader编译是否成功编译,success为GLint类型。 - glGetShaderInfoLog(shader, length, NULL, infoLog)
获取错误信息并存储与infoLog字符数组中,length为数组长度。 - glCreateProgram()
创建一个着色器程序,返回其引用。 - glAttachShader(shaderProgram, shader)
将着色器shader添加到着色器程序shaderProgram中。 - glLinkProgram(shaderProgram)
链接着色器程序。 - glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success)
检测链接着色器程序是否成功。 - glGetProgramInfoLog(shaderProgram, length, NULL, infoLog)
获取链接着色器程序 - glUseProgram(shaderProgram)
激活着色器程序,激活后每个着色器调用和渲染调用都会使用这个程序对象。 - glDeleteShader(shader)
删除着色器对象。当把着色器对象链接到程序对象后,要删除着色器对象。 - glGenVertexArrays(n, &VAO)
返回一个顶点数组对象(Vertex Array Object, VAO)的引用。同样当n≥2时,返回的是一个引用的数组。 - glGenBuffers(1, &VBO)
返回一个顶点缓冲对象(Vertex Buffer Objects, VBO)的引用。同样当n≥2时,返回的是一个引用的数组。 - glBindVertexArray(VAO)
绑定VAO。 - glBindBuffer(GL_ARRAY_BUFFER, VBO)
绑定VBO。 - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW)
专门用来把用户定义的数据复制到当前绑定缓冲的函数。
第一个参数是目标缓冲的类型,第二个参数指定传输数据的大小(以字节为单位),第三个参数是我们希望发送的实际数据,第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
GL_STATIC_DRAW : 数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW : 数据每次绘制时都会改变。 - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0)
进行顶点缓冲对象与顶点属性的链接。 - glEnableVertexAttribArray(0)
以顶点属性位置值作为参数,启用顶点属性(顶点属性默认是禁用的)。 - glfwWindowShouldClose(window)
检查GLFW是否被要求退出。 - glfwPollEvents()
检查事件 - glClearColor(0.2f, 0.3f, 0.3f, 1.0f)
设置清空屏幕所用的颜色。 - glClear(GL_COLOR_BUFFER_BIT)
清空屏幕的颜色缓冲。 - glfwSwapBuffers(window)
交换窗口的缓冲区。 - glDeleteVertexArrays(1, &VAO)
删除对应的VAO对象。 - glDeleteBuffers(1, &VBO)
删除对于的VBO对象。 - glfwTerminate()
释放GLFW分配的内存
三、重要语句模块
OpenGL渲染程序的大体框架是固定的,在本节你将会学习到一个个完整且独立的模块。
当我们学习完这些基本模块后,我们把模块拼接起来即可构成一个完整的OpenGL的程序。
如果你对于文章整体脉络感到困惑,可以参考文章目录。
如果你对片段化的模块代码感到头疼,可以往下阅读,在每个阶段我会展现完整的代码。
如果在你读到模块代码时感到看困惑,请不要停止阅读,因为模块代码后会有小节专门解释。
1 GLFW和GLEW头文件的引用
宏定义表示glew是静态链接的。
// GLEW的导入
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW的导入
#include <GLFW/glfw3.h>
OpenGL中的基元类型
使用OpenGL时,建议使用OpenGL定义的基元类型。比如使用float时我们加上前缀GL(因此写作GLfloat)。int、uint、char、bool等等也类似。OpenGL定义的这些GL基元类型的内存布局是与平台无关的,而int等基元类型在不同操作系统上可能有不同的内存布局。使用GL基元类型可以保证你的程序在不同的平台上工作一致。
2 GLFW的初始化和配置
2.1 模块代码
// 初始化 GLFW
glfwInit();
// 设置GLFW的使用 OpenGL的版本 为3.3
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 设置GLFW使用OpenGL的 核心模型
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 设置GLFW窗口大小的可调整性为:否
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// 创建一个宽为WIDTH、高为HEIGHT、标题为"LearnOpenGL"的窗口
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
// 将窗口window的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
2.2 运行结果和完整代码
当你正确导入GLEW和GLFW,并对GLFW进行初始化和配置后,运行程序你将会看到如下图的一个纯白色GLFW窗口。
到此处的完整代码为:
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
const GLuint WIDTH = 800, HEIGHT = 600;
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE,GL_FALSE);
GLFWwindow* window = glfwCreateWindow(WIDTH,HEIGHT,"LearnOpenGL",NULL,NULL);
if (window == NULL)
{
// 当window为NULL时说明GLFW窗口创建失败
std::cout << "Failed to create GLFW window" << std::endl;
// 窗口创建失败则释放GLFW分配的内存,结束程序返回-1表示错误。
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 循环避免程序结束、窗口关闭
while (1);
}
3 GLEW的初始化和配置
// 设置GLEW为现代化的
glewExperimental = GL_TRUE;
// 初始化GLEW
glewInit();
4 视口配置
4.1 模块代码
// 视口位置和大小的设置
glViewport(x, y, width, height);
到此处时如果你运行程序,就会发现只能看到和上文中一样的白窗口,你或许疑惑你的视口在哪?视口是展示渲染后画面的地方,之所以没有画面是因为你没有调用过OpenGL的渲染函数,渲染函数是一种状态应用函数,会基于OpenGL的状态做出对应的操作。
4.2 渲染函数介绍
由于你实在是太想有一些画面了,所有现在"超纲"的介绍一个渲染函数:
glClear(GL_COLOR_BUFFER_BIT)
其作用是使用特定颜色清空屏幕的颜色缓冲,其参数表示缓冲类型暂时不用深究,这是一个状态应用函数,会基于OpenGL的状态做相应的渲染操作。你可能觉得情况屏幕变成全白就行了,为什么还要指定特定颜色?
因为不是每一个程序窗口的背景都是白色,有些可能是暗黑色、蓝绿色,所以需要指定特定的背景颜色去"刷屏"。你可能又有疑问,你并没有指定特定的颜色呀?这是因为OpenGL的函数分为状态应用函数和状态设置函数,所谓特定颜色其实是指清空屏幕颜色缓存所使用的颜色,这种颜色是OpenGL的一个状态,所以我们应该使用状态设置函数去指定那种特定颜色。状态设置函数如下:
glClearColor(0.2f, 0.3f, 0.3f, 1.0f)
其作用是设置清空屏幕所用的特定颜色,即改变OpenGL的状态,四个参数是RGBA。
需要了解的是:现代屏幕画面的呈现使用双缓冲技术。如果当屏幕需要显示内容时才一个个像素的在屏幕上绘制,绘制速度不同步会导致屏幕画面的撕裂。解决方法是我们提前将需要呈现的完整画面生成出来,把即将要展现的一整幅画面存储于显卡的缓冲区中,当需要交换屏幕内容时,将缓冲区中完整的一幅画面展现在屏幕上,这就是双缓冲技术。在OpenGL中交换缓冲区这个步骤需要我们使用函数明确指定:
glfwSwapBuffers(window)
这是一个状态应用函数,它将window窗口的两个缓冲区进行交换。这两个缓冲区是指双缓冲技术中的前缓冲区和后缓冲区:前缓冲区用于展示屏幕上的内容,而后缓冲区就用来绘制,当每一帧开始的时候,将两个缓冲区交换,这样后缓冲区又可以画新的内容。
4.3 运行结果和完整代码
通过以上代码,可以得到下图的窗口内容:
上图对应代码如下:
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
#include <iostream>
const GLuint WIDTH = 800, HEIGHT = 600;
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE,GL_FALSE);
GLFWwindow* window = glfwCreateWindow(WIDTH,HEIGHT,"LearnOpenGL",NULL,NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glewExperimental = true;
glewInit();
GLint width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, 100, 100);
while (1)
{
// 指定清空屏幕颜色缓冲的颜色(改变状态后其会保存于OpenGL中)
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
// 清空屏幕颜色缓冲,这里生成的屏幕画面还存储于后缓冲区中
glClear(GL_COLOR_BUFFER_BIT);
// 交换前缓冲区和后缓冲区
glfwSwapBuffers(window);
}
glfwTerminate();
return 0;
}
4.4 拓展
1.glClear()是针对整个窗口而非视口的,如上代码中所示glViewport(0, 0, 100, 100)设置的视口大小非常小,但是清空屏幕颜色缓存的蓝绿色还是布满了整个窗口。
2.如注释所言,glClearColor会设置OpenGL的状态,状态保存于OpenGL中,或许你感觉设置一次就行了,但这是一个初学OpenGL者常见但错误的想法。当以后你的渲染代码越来越庞大,渲染情况越来越多,你可能会多次改变清空屏幕颜色缓冲所用的颜色,这时你应当在每一次循环(每一帧)中判断并使用所需颜色。所以每一帧都需要设置渲染所需的各式各样的状态,这是必须要做的。
3.将如上代码中的"glClearColor(0.2f, 0.3f, 0.3f, 1.0f);"和"glClear(GL_COLOR_BUFFER_BIT);"删除,你会得到一个纯黑色的窗口。这很容易理解,因为没有配置过的后缓冲区中是一幅的纯黑画面,所以不断交换缓冲区使得前缓冲和后缓冲都是一幅纯黑的画面了。
4.将如上代码的整个循环替换为如下代码,你会得到一个闪烁的窗口.
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
while (1)
{
// 交换前缓冲区和后缓冲区
glfwSwapBuffers(window);
}
这很容易理解。你通过"glClearColor(0.2f, 0.3f, 0.3f, 1.0f);"和"glClear(GL_COLOR_BUFFER_BIT);"将蓝绿色背景的一幅画面存储到后缓冲区中,而前缓存区中最初是没有内容的(无渲染指令所以呈现全白),当你第一次交换时,后缓冲区中的蓝绿色背景交换到前缓冲区中,而此时的后缓区中的内容没有配置(没有执行过清空屏幕颜色,没有写入后缓冲区)为默认的黑色。因此后续每一帧缓冲区交换,蓝绿色和黑色会不断在屏幕上交替,导致屏幕的闪烁。
5.将如上循环部分代码改为如下代码,你会发现窗口停止闪烁。
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
glClear(GL_COLOR_BUFFER_BIT);
while (1)
{
// 交换前缓冲区和后缓冲区
glfwSwapBuffers(window);
}
这是因为前缓冲区在第一次交换后被替换为蓝绿色背景的,而第二次glClear将后缓冲区也替换为蓝绿色背景。而后在循环中交换前后缓冲区都是同一个颜色,所以不会出现闪烁。(这个故事告诉我们默认缓冲区中背景是黑色的,当没有执行渲染函数时,前后缓冲区中的内容不会改变)
5 Shader着色器的创建和编译
OpenGL中我们可以编写的着色器有三个:
顶点着色器 VetrtexShader
片段着色器 FragmentShader
几何着色器 GeometryShader
顶点着色器和片段着色器是我们必须要编写的,因为OpenGL中没有提供默认的这两类着色器,而几何着色器有默认的着色器提供。
5.1 模块代码
// 顶点着色器源代码
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
// 片段着色器源代码
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
// 顶点着色器的创建和编译
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 顶点着色器编译状态检查
GLint success;
GLchar 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;
}
// 片段着色器的创建和编译
GLuint 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;
}
5.2 VertexShader着色器源代码
#version 330 core
layout(location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
一个最简单的顶点着色器代码如上所示,首先要声明着色器使用的OpenGL版本,330即对应OpenGL3.3版本。
vec3 position代表定义一个有xyz分量的对象,in 关键字代表这个对象是输入到顶点着色器的,layout(location = 0)表示这个输入对象的位置标识。
gl_Position指的是需要渲染的顶点的位置坐标,在顶点着色器中我们需要告诉它有哪些位置的顶点需要渲染,即给gl_Position赋值。
顶点着色器的输入比较特殊,它从顶点数据中直接接收输入。
5.3 FragmentShader着色器源代码
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
一个最简单的片段着色器代码如上所示,片段着色器中同样要声明OpenGL版本。
片段着色器中定义了一个vec4类型的color对象,并用out关键字将它声明为输出变量。需要注意的是,片段着色器需要输出光栅化后每一个像素上的颜色值。也就是说我们需要在片段着色器中定义一个RGBA四分量类型即vec4类型的对象,并使用out关键字将它声明为输出变量,并且在main函数中给它赋值。这里将输出的颜色值固定为一种颜色。
5.4 着色器的创建和编译
有了着色器源代码还不够,我们需要在程序中创建着色器对象,需要绑定着色器对象对应的源代码并编译。具体过程如下:
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
首先使用"glCreateShader"返回对应着色器对象的引用,这里传入的参数为"GL_VERTEX_SHADER",因此返回的是顶点着色器引用。如果要创建片段着色器,传入"GL_FRGEMENT_SHADER"即可。
其次使用"glShaderSource"函数将着色器源代码vertexShaderSource绑定到着色器对象vertexShader上,其中参数1表示源代码仅包含一个字符串。
最后使用"glCompileShader"函数编译着色器对象包含的着色器源代码。
5.5 着色器源码编译检测
着色器的源代码并不是每次都能成功编译,原因是它可能出现各种各样的问题。由于着色器在GPU中编译,即使出错也不会显示在Vis Stdio中,我们需要手动获取着色器的编译状态。具体的过程如下:
GLint success;
GLchar 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;
}
使用success记录编译是否成功,使用infoLog记录错误信息。"glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success)"会将着色器vertexShader编译成功与否存储在变量success中。如果编译错误,"glGetShaderInfoLog(vertexShader, 512, NULL, infoLog)"会将着色器vertexShader的编译错误信息存储于infoLog中。
6 着色器程序的创建
模块代码
// 创建着色器程序
GLuint shaderProgram = glCreateProgram();
// 添加顶点着色器和片段着色器到程序中
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
// 链接着色器程序中的着色器
glLinkProgram(shaderProgram);
// 检查程序链接状态
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
// 如果链接失败则获取错误信息存储于infoLog中
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 着色器程序中的着色器链接成功后就可以删除了,节省显存
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
渲染流程包括一整套着色器,仅有几个我们自定义的着色器还不够。我们需要创建一个着色器程序,这个着色器程序创建后就包含许多我们不能自定义的着色器,我们将我们自定义的着色器加入这个着色器程序,然后链接着色器程序中的各个着色器,这样我们就得到了一个完整的着色器程序。
得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象。
glUseProgram(shaderProgram)
在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个着色器程序对象。
7 顶点的输入
在OpenGL中着色器是有顺序的执行的,每一个着色器把上一个着色器的输出当作输入。但在5.2中我们提到顶点着色器的输入比较特殊,它直接从顶点数据中读取输入。
那什么是顶点数据呢?顶点数据就是存储顶点各种各样信息的数据,它最初存在于内存之中,由于渲染时GPU中的顶点着色器需要顶点数据,所以我们应该把内存中的顶点数据传输到显存中。
我们要做的事情是:将顶点数据发送给显卡、配置OpenGL解析顶点数据。
顶点着色器已经在GPU上创建内存用于储存我们的顶点数据,我们需要通过顶点缓冲对象(Vertex Buffer Objects, VBO)来管理这个内存。
模块代码
// 定义顶点数据(三个顶点,每个顶点包含x y z属性)
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
// 创建VBO为缓冲类型的对象
GLuint VBO;
glGenBuffers(1, &VBO);
// 绑定VBO为GL_ARRAY_BUFFER缓冲类型
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将我们定义的数据vertices复制到GL_ARRAY_BUFFER缓冲类型当前绑定的缓冲对象VBO上
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
首先我们定义顶点数据,它可以是任何自定义形式,我们在例中定义顶点数据为:包含9个GLfloat类型变量的数组。我们认为它应该被理解为:三个顶点,每个顶点三个坐标xyz。
顶点着色器已经在显存中创建了缓存区域,这个缓存区域是专门用来存储顶点数据的,顶点数据对应的缓冲类型为GL_ARRAY_BUFFER。
我们创建一个缓冲类型的VBO对象,将对象VBO的缓冲类型绑定为GL_ARRAY_BUFFER。
绑定GL_ARRAY_BUFFER的表示将该缓冲区对象VBO用于顶点属性数据的,当顶点着色器需要顶点数据时将会从VBO中读取。
glBufferData会将数据复制到 绑定对应缓冲类型 的缓冲对象中。
使用glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW)函数将顶点数据vertices复制到 绑定了GL_ARRAY_BUFFER缓冲类型 的缓冲对象VBO中。
你是否感觉上文是否有些别扭,为什么不将顶点数据直接复制到VBO中呢?因为我们是基于OpenGL这个状态机去操作,你无法直接将顶点数据复制到显存VBO,而OpenGL模型中并不知道哪个VBO(我们可以定义多个VBO)对应的显存应该存储顶点数据,所以这个函数将以绑定了GL_ARRAY_BUFFER缓冲类型的缓冲对象为目标进行复制数据的操作。
经过以上操作,我们的顶点着色器就知道在定义的VBO中读取顶点数据了。
8 链接顶点属性
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何读取顶点数据。
模块代码
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
以上的模块中使用glVertexAttribPointer函数通知OpenGL解析顶点数据的方式。
我们传入VBO的是一个float类型的数组,包含9个float类型的变量,我们知道每三个变量代表一个顶点的属性,现在我们要做的就是告诉OpenGL这件事。
OpenGL规定,顶点位置属性VertexPosition的位置值layout为0,顶点颜色属性VertexColor的位置值layout为1。顶点位置属性的索引值为0,颜色的index为1,这是规定的属性。
因此上文中我们的顶点着色器中有定义:layout(location = 0),location = 0表示这个输入属性是顶点位置属性,而在此处第一个参数为0表示这个函数解析出的数据是顶点位置属性,因此解析出的顶点位置属性后将会输入到顶点着色器中location=0的属性。
glVertexAttribPointer函数的第一个参数表示我们要配置的顶点属性,我们现在要配置的是顶点的位置属性,所以传入0。第二个参数指定顶点属性的大小,我们传入的每个顶点的属性由3个 GLfloat 值构成,所以每个顶点属性的大小为3,代表由三个值组成。第三个参数指定数据的类型,即每个值是什么类型呢?传入对应类型GLfloat。第四个参数定义我们是否希望数据被标准化,如果设置为GL_TRUE,则所有的数据会被映射到【-1,1】之间。第五个参数叫步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于每个不同的顶点属性相差三个GLfloat的"距离",所以我们传入3* sizeof(GLfloat)。
你可能觉得这个参数一点用都没有,毕竟每个顶点包含三个GLfloat,那不是一定相差三个GLfloat吗?其实这只是对于这个例子而言,如果我们对每个顶点添加三个GLfloat值代表其颜色RGB值呢?这时我们配置顶点位置的读入时,每个顶点包含6个GLfloat值,前三个变量依旧是位置,2但是步长就变为6个GLfloat了,毕竟这个顶点属性开头到下个顶点属性开头相差了6个GLfloat值,所以现在你还觉得它没有用吗?
最后一个参数的类型是GLvoid*,所以我们需要进行这个奇怪的强制类型转换,它表示读入的数据在缓冲中其实位置的偏移量,你可能又觉得没用,但是同样是对于刚才提到的添加3个颜色后每个顶点包含6个GLfloat属性值的情况,如果要读取颜色必须要从每个顶点属性的第三个值开始,这怎么指定呢?就使用这个参数指定偏移即可,具体值为:(GLvoid*)(3* sizeof(GLfloat))。
glEnableVertexAttribArray(0)的作用是启用顶点在location=0处的属性值,因为顶点属性默认是禁用的(为了节省性能)。
9 VAO的使用
在往后的应用中,我们渲染场景中可能存在很多物体,在渲染一个物体前我们需要重新使用glVertexAttribPointer函数指定顶点数据的解析方式,毕竟在它之前我们可能渲染了其他解析方式不同的物体。当以后每个物体不仅要设置位置解析方式,还要设置颜色、纹理解析方式时,这毫无疑问是非常麻烦的。有没有一种办法可以创建一个变量,将我们对物体的解析方式(包含位置、颜色、纹理解析)存储起来,当我们需要配置解析方式时,直接使用它即可?这当然就是VAO了。
模块代码
// 创建VAO并绑定
GLuint VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// 设置解析方式(会保存到VAO中)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 解绑VAO
glBindVertexArray(0);
上文代码使用glBindVertexArray(VAO)将我们定义的VAO对象绑定到顶点数组对象(Vertex Array Object, VAO)中。然后我们使用"glVertexAttribPointer"函数设置解析方式,"glVertexAttribPointer"函数会自动将设置的解析方式 绑定到 目前绑定 顶点数组对象Vertex Array的VAO对象中。如此我们便使用VAO将解析方式存储了起来。当解析方式设置完毕,我们暂时就不需要指定VAO的的解析方式了,因此我们将它解绑,这样"glVertexAttribPointer"函数就影响不到我们已经设置好的解析方式了,注意解绑后解析方式任然存在于VAO中。
当需要使用VAO这种解析方式时,我们需要将它绑定,这样OpenGL才会使用VAO中的解析方式。
需要注意的是,无论是对VAO还是VBO,我们都是通过将它们绑定到对应的类型上,然后设置对应的类型,以达到设置VAO和VBO的目的。当VAO和VBO设置好后,为了避免我们后来对其的错误设置,应该解绑VAO和VBO。
10 渲染模块
模块代码
// 只要窗口不应关闭就继续循环
while (!glfwWindowShouldClose(window))
{
// 检查窗口事件
glfwPollEvents();
// 清空屏幕缓存
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 使用着色器程序进行渲染
glUseProgram(shaderProgram);
// 绑定VAO指定解析顶点属性的方式
glBindVertexArray(VAO);
// 绘制图形(基于VBO、VAO调用函数)
glDrawArrays(GL_TRIANGLES, 0, 3);
// 使用完VAO后应该解绑VAO
glBindVertexArray(0);
// 交换前后缓冲区
glfwSwapBuffers(window);
}
// 渲染程序结束,释放GPU中VAO和VBO的内存
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// 释放GLFW申请的内存
glfwTerminate();
return 0;
四、运行结果和完整代码
渲染结果:三角形
将以上模块代码按顺序拼接,运行程序可以得到下图结果:
完整代码
#include <iostream>
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// 定义窗口大小
const GLuint WIDTH = 800, HEIGHT = 600;
// 定义字符串:着色器源代码(这种形式会自动拼接,分行只是好看)
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
int main()
{
// GLFW的初始化
glfwInit();
// GLFW的配置
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// 创建glfw窗口并设置窗口的状态为当前线程的主状态
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
glfwMakeContextCurrent(window);
// 设置glew更加现代化
glewExperimental = GL_TRUE;
// 初始化glew
glewInit();
// 定义视口左下角在窗口的位置 和 视口的大小
glViewport(0, 0, 800, 600);
// 顶点着色器的创建、绑定源代码、编译
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 检查顶点着色器编译是否成功
GLint success;
GLchar 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;
}
// 片段着色器的创建、绑定源代码、编译
GLuint 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;
}
// 创建着色器程序、添加着色器后链接着色器
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 检查链接是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
// 链接失败则获取原因并打印
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 链接着色器后就删除着色器,节省显存
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// 定义顶点属性数据(格式任意自定义)
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f // Top
};
// 创建VAO、VBO
GLuint VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 绑定VAO
glBindVertexArray(VAO);
// 绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将顶点数据vertices复制到GL_ARRAY_BUFFER绑定的对象VBO中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 设置解析顶点数据的方式到绑定VertexArray的VAO中
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
// 启用顶点属性0号位置
glEnableVertexAttribArray(0);
// 解绑VBO(因为暂时不需要再向这个VBO传入数据了)
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解绑VAO(因为暂时不需要再配置这个VAO的解析方式了)
// 要用的时候再绑定需要的VAO
glBindVertexArray(0);
// 渲染循环
while (!glfwWindowShouldClose(window))
{
// 检查窗口事件
glfwPollEvents();
// 清空屏幕缓存
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 使用着色器程序
glUseProgram(shaderProgram);
// 需要使用VAO解析顶点数据
glBindVertexArray(VAO);
// 根据传入VBO显存中的顶点数据和VAO解析方式渲染
glDrawArrays(GL_TRIANGLES, 0, 3);
// 使用完解除VAO绑定
glBindVertexArray(0);
// 交换缓冲区
glfwSwapBuffers(window);
}
// 删除VAO、VBO释放显存
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// 释放glfw申请的内存
glfwTerminate();
return 0;
}