一些基本概念
VAO、 VBO 和 EBO
顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO
VBO
存储顶点数据(位置、颜色、纹理坐标、法线等)的缓冲区对象,保存在 GPU 内存中
VAO
存储顶点属性的配置状态,记录如何解释 VBO 中的数据
我们先来看一组数据和一行代码
cpp
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
cpp
/*
void glVertexAttribPointer(
GLuint index, // 顶点属性索引
GLint size, // 每个顶点属性的分量数
GLenum type, // 数据类型
GLboolean normalized,// 是否归一化
GLsizei stride, // 步长(相邻顶点属性的字节间距)
const void* pointer // 偏移量(当前属性在顶点数据中的起始位置)
);
*/
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
vertices 存储的是一个三角形顶点的位置属性,OpenGL 并不知道我们存储的是 (-0.5f, -0.5f), (0.0f, 0.5f), (-0.5f, 0.0f), (0.0f, 0.5f), 0.0f, 还是 (-0.5f, -0.5f, 0.0f), (0.5f, -0.5f, 0.0f), (0.0f, 0.5f, 0.0f) 又或者其它,所以我们需要告诉它,我们存储的数据类型是 每个顶点属性包含3个分量,分量是 32 位浮点数,从当前顶点的该属性到下一个顶点的该属性,需要跳过多少字节(这里因为顶点数据只有 坐标(3 个 float),没有其他属性(如颜色、纹理坐标),所以步长等于单个顶点的字节数) 。
VAO 存储的就是这样的一些信息
EBO
EBO 存储顶点索引数据,在本篇文章中,我们暂时不需要用到它,下一篇文章再详细介绍
着色器

图片源于 https://learnopengl-cn.github.io
这张图展示了 OpenGL 把一组3D坐标转变屏幕上的2D像素输出的过程。其中蓝色的部分(顶点着色器,几何着色器和片段着色器)可以用程序员注入自定义的着色器,且现代OpenGL需要我们至少设置一个顶点和一个片段着色器。
顶点着色器的主要作用就是把一个 3D 坐标转换成另外一种 3D 坐标。这部分大家可以查看我另外一篇文章 OpenGL 坐标映射
输入:一个单独的顶点数据。
这个数据不仅包含顶点的位置坐标(通常是局部空间的3D坐标 (x, y, z, w)),还可能包含其他属性,如法线、颜色、纹理坐标等。这些数据来自应用程序(通过顶点缓冲区传递)。
输出:一个处理后的顶点。
最重要的输出:顶点在裁剪空间 中的位置坐标(gl_Position)。
还可以输出其他需要传递给后续阶段(如片元着色器)的数据,如纹理坐标、颜色等。
顶点着色器阶段的输出可以选择性地传递给几何着色器(Geometry Shader)。几何着色器将一组顶点作为输入,这些顶点形成图元,并且能够通过发出新的顶点来形成新的(或其他)图元来生成其他形状。在这个例子中,它从给定的形状中生成第二个三角形。
图元装配(Primitive Assembly)阶段将顶点着色器(或几何着色器)输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定图元的形状;本节例子中是两个三角形。
图元装配阶段的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
片段着色器的主要目的是计算一个像素的最终颜色,它包含3D场景的数据(比如光照、阴影、光的颜色等等),根据这个数据来计算最终像素的颜色。
开始绘制三角形
着色器程序
现在我们来编写一段顶点着色器代码和一段片段着色器代码,我们暂时忽略它的语法,我们现在只需要知道顶点着色器把我们传入的坐标原封不动地输出出来了,片段着色器返回了一个固定不变的颜色。
顶点着色器
cpp
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
片段着色器
cpp
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
然后,我们创建一个顶点着色器对象,把着色器代码传给着色器对象,然后编译
cpp
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER); // 创建着色器对象
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // 设置着色器代码
glCompileShader(vertexShader); // 编译着色器
片段着色器也是类似的
cpp
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
最后,我们需要创建一个着色器程序,把顶点着色器和片段着色器附加到程序上,然后用glLinkProgram链接。
cpp
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 顶点着色器和片段着色器到这里就可以释放了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
顶点数据
首先创建一个 float 数组,然后把数据写入 VBO
cpp
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
使用 glGenBuffers 生成一个缓冲对象。
使用 glBindBuffer 把新创建的缓冲对象绑定到GL_ARRAY_BUFFER(顶点缓冲类型)上。可能有些读者会疑惑为什么要有个 bind 的动作,这是因为 OpenGL 采用状态机 + 绑定模式而不是直接传递对象。一般情况下,我们想要操作一个对象,会把这个对象传入到对应函数,而 OpenGL 的模式是,先绑定对象,之后的操作都是对这个对象操作,直到切换绑定,这样的好处是在批处理时更高效。
使用用 glBufferData 函数把之前顶点数据复制到缓冲的内存中,GL_STATIC_DRAW 表示数据不会或几乎不会改变 我们还可以设置为 GL_DYNAMIC_DRAW 或者GL_STREAM_DRAW 表示数据内容会被频繁修改,但修改频率低于每帧或者数据内容每帧或几乎每帧都会被修改
然后创建一个 VAO 对象,告诉 OpenGL 如何读取 VBO 的数据
cpp
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
第一个参数指定我们要配置的顶点属性。比如着色器代码可能是这样的
cpp
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
参数设为 0 就表示 VAO 的数据会传入到着色器 location = 0 对应的输入里,即 position。
当然也可以不在着色器显示设置 location ,通过 glGetAttribLocation 获取属性的 location 。
cpp
GLint posLoc = glGetAttribLocation(shaderProgram, "position");
GLint colorLoc = glGetAttribLocation(shaderProgram, "color");
// 然后使用查询到的位置
glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, ...);
glVertexAttribPointer(colorLoc, 3, GL_FLOAT, GL_FALSE, ...);
第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec都是由浮点数值组成的)。
下个参数定义我们是否希望数据被标准化(Normalize)。如果设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。
第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
最后一个参数表示位置数据在缓冲中起始位置的偏移量(Offset),它的类型是void ,是出于对以往版本的兼容考虑的。由于位置数据在数组的开头(我们也可能在数组里存颜色等信息),所以这里是0。
最后我们还需要启用顶点属性,它默认是禁用的。
cpp
glEnableVertexAttribArray(0);
绘制三角形
cpp
while (!glfwWindowShouldClose(window))
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
}
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
在循环里,使用我们定义的着色器程序,绑定 VAO 对象,绘制三角形
这里调用 glBindVertexArray(VAO) 是因为当我们有多个顶点数组对象时,我们需要先绑定其中一个,绘制,再绑定另外一个,再进行绘制,虽然我们这里只有一个,但是为了保持良好的习惯,还是加上 glBindVertexArray(VAO) 这句了。
glDrawArrays函数,第一个参数 GL_TRIANGLES 表示绘制三角形,每个3个顶点构成一个独立的三角形,顶点按顺序分组:(0,1,2)、(3,4,5)。第二个参数表示起始索引------从顶点数组的哪个位置开始绘制,第三个参数表示绘制的顶点数量。
最后给出完整代码,代码源于 https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.1.hello_triangle/hello_triangle.cpp
cpp
#include "glad/glad.h"
#include "GLFW/glfw3.h"
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
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
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glViewport(0, 0, 800, 600);
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
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);
glBindVertexArray(VAO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
while (!glfwWindowShouldClose(window))
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
}
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
}
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
由于笔者水平有限,错误不足之处,烦请各位读者斧正。