使用GLFW绘制三角形
实现结果:
STEP1 绘制三角形
具体的实现步骤参考自OpenGL官方教程,注意OpenGL版本在3.3以上。
创建窗口
首先初始化GLFW,然后我们可以通过使用glfwWindowHint函数来对GLFW进行配置:glfwSetErrorCallback(glfw_error_callback); if (!glfwInit()) return 1; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint函数的第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;第二个参数接受一个整形,用来设置这个选项的值。
接着使用glfwCreateWindow函数来创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据:GLFWwindow* window = glfwCreateWindow(800, 600, "ImGui GLFW+OpenGL3 example", NULL, NULL);
glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数,第三个参数表示这个窗口的名称(标题)。
窗口创建完成后我们就可以通知GLFW将我们窗口的上下文设置为当前线程的上下文了:
glfwMakeContextCurrent(window);
当需要调整窗口大小的时候,视口也应该被调整,可以采用一个回调函数,在每次窗口大小被调整的时候被调用:
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
这个帧缓冲大小函数需要一个GLFWwindow作为它的第一个参数,以及两个整数表示窗口的新维度。每当窗口改变大小,GLFW会调用这个函数并填充相应的参数供你处理。
- 绘制三角形
具体步骤参考你好,三角形
在OpenGL中,任何事物都在3D空间中,但是对于计算机而言,屏幕和窗口都是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变成适应屏幕的2D像素。这与前面我们所学到的计算机图形学的基本工作是相照应的。
3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分成为两个主要部分::第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
下面是具体的绘制步骤:
- 给OpenGL输入一些顶点数据,即三角形三个顶点的位置信息:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
这里由于是在2D空间绘制简单三角形,所以对应顶点的z坐标(通常表示深度)设置为0。
在对顶点数据进一步处理之前,我们需要将其存储到顶点缓冲对象VBO中。首先生成一个VBO缓冲对象:
unsigned int VBO; // 缓冲ID
glGenBuffers(1, &VBO); //生成缓冲对象
OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER
目标上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
最后我们可以调用glBufferData函数,将之前定义的顶点数据复制到缓冲的内存中去:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
后续对VBO进行处理即可。
2.顶点着色器
将顶点信息作为输入发送给图形渲染管道的第一个处理阶段。
首先我们要使用着色器语言(GLSL)编写一个顶点着色器的源码:
const char *vertexShaderSource = "#version 330 core\n" // GLSL版本号与OpenGL版本号相匹配,且使用核心模式
"layout (location = 0) in vec3 aPos;\n" // 使用关键字in,声明输入顶点的属性,aPos表示位置属性
"void main()\n" // 将gl_Position(vec4类型)作为输出,故在aPos后加w分量1.0f
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
"}\0";
然后编译着色器。仍旧需要创建一个对象——着色器对象,这里使用glCreateShader创建着色器对象:
unsigned int vertexShader; // ID设置为vertexShader,注意着色器对象使用ID来引用的
vertexShader = glCreateShader(GL_VERTEX_SHADER);
将着色器的源码附加到着色器对象上,然后编译:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
3.片段着色器
片段着色器是第二个用于渲染三角形的着色器,它的作用是计算像素最后的颜色输出。片段着色器的设置过程与顶点着色器类似,首先编写源码:
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n" // 注意使用关键字out,这个变量是一个4维的向量,表示最终的输出颜色
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
接下来对着色器的编译与顶点着色器类似,只需要注意将GL_FRAGMENT_SHADER
常量作为着色器类型。
4.着色器对象
着色器程序对象是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
首先依旧是创建程序对象:
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
然后将之前编译的着色器附加到程序对象上,用glLinkProgram链接它们:
glAttachShader(shaderProgram, vertexShader); // 顶点着色器附加
glAttachShader(shaderProgram, fragmentShader); // 片段着色器附加
glLinkProgram(shaderProgram); // 链接成一个着色器程序
激活shaderProgram程序对象:
glUseProgram(shaderProgram);
在将着色器对象链接到程序对象以后,就可以将着色器对象删除了。
5.链接顶点属性
在第一步中我们设置了顶点的信息,第二到四步中我们将顶点数据发送给了GPU,并指示了GPU如何在顶点着色器和片段着色器中处理它。
但是OpenGL并不知道如何解释内存中的顶点数据,以及它该如何将顶点数据连接到顶点着色器的属性上,而这一步的目的就是在渲染开始前指定OpenGL如何解释顶点数据:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
最后一个参数的类型是void*。
6.顶点数组对象
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。使用VAO的目的是在配置顶点属性指针的时候,只需要将调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使得在不同顶点数据和属性配置之间切换时只需要绑定不同的VAO就行了。
要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上即可:
glBindVertexArray(VAO);
[......]
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
绘制结束后,再解绑VAO。
最后一步即进行渲染,在渲染循环中加入绘制三角形的代码即可:
// 渲染循环
while (!glfwWindowShouldClose(window)) {
// 使用一个自定义的颜色清空屏幕
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 画三角形
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 检查有没有触发什么事件
glfwPollEvents();
//交换颜色缓冲
glfwSwapBuffers(window);
}
结果如下图:
STEP2 更改三角形三个顶点的颜色
STEP1中设定的三角形是橙色的,STEP2在1的基础上更改每个顶点vertex的颜色,显然需要更改的地方有以下几点:
- GLSL源代码部分
顶点着色器和片段着色器对应的着色源代码:
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" // 添加输出ourColor。分别对应RGB三色,使用vec3
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
" ourColor = aColor;\n" // 输出颜色值
"}\0";
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n" // 上一阶段(顶点着色器)的输出作为本阶段的输入
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0f);\n" // 片段着色器的输出值与ourColor密切相关
"}\n\0";
- vertices矩阵
在每一行为每个顶点添加颜色属性对应的信息:
//设置顶点,注意顶点坐标范围以及颜色
float vertices[] = {
// 顶点坐标 // 顶点颜色
0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下顶点
-0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下顶点
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 上顶点
}
- 顶点属性的解析
即OpenGL对于顶点数组的解析方法。在STEP1的基础上添加对颜色属性的解析:
// 位置属性以及颜色属性的解析
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 注意最后一个参数表示位置数据在缓冲中起始位置的偏移量,颜色数据在数组的3个float之后
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
结果如下图所示:
注意到片段着色器已经对三角形除定点外的其他部分进行了片段插值:当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的,如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;也就是30%蓝 + 70%绿。所以当我们有3个顶点,和相应的3个颜色,片段着色器为这些像素进行插值颜色。片段插值会被应用到片段着色器的所有输入属性上。
STEP3给上述工作添加一个GUI,使得可以选择并改变三角形的颜色
这一步的实现较为简单,重点在于理解ImGui的具体使用方法。参考实例:ImGui opengl3
代码在STEP2的基础上只需要添加部分代码即可:
render loop外
- 创建ImGui的上下文,风格
// Setup ImGui binding
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
ImGui_ImplGlfwGL3_Init(window, true);
// Setup style
ImGui::StyleColorsDark();
- 创建一些必须的变量
// 分别更改顶点颜色所需要的三个ImVec4变量,颜色可随意设置
ImVec4 clear_color1 = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
ImVec4 clear_color2 = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
ImVec4 clear_color3 = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
// 菜单选项
bool change_color = false;
bool show_original_triangle = true; // 默认显示step2中的三角形
render loop中
建立ImGui新的帧,并对帧添加对象、处理:
ImGui_ImplGlfwGL3_NewFrame();
{
ImGui::Text("change the color of the triangle");
ImGui::Checkbox("show_original_triangle", &show_original_triangle);
ImGui::Checkbox("change_color", &change_color);
glClear(GL_COLOR_BUFFER_BIT); // 缺少该语句会导致拖拽窗口时产生残影。。。
...
}
接着分别对菜单选项进行操作。show_original_triangle是显示原有三角形,与STEP2剩余步骤相同。
change_color选项是分别更改每个顶点的颜色,这里需要使用ColorEdit3函数,先对三个顶点的颜色进行了重置:
if (change_color) {
ImGui::ColorEdit3("x1", (float*)&clear_color1);
ImGui::ColorEdit3("x2", (float*)&clear_color2);
ImGui::ColorEdit3("x3", (float*)&clear_color3);
...
}
接着如果要自由选择三角形每个顶点的颜色,需要引入新的组件。
首先使用glGetUniformLocation函数获取光标在颜色表上所处的位置:
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
然后使用glUniform4f函数get当前位置的颜色对应ImVec4数值,分别存入到clear_color1,clear_color2,clear_color3中去,在将新获取的数值赋值给vertices数组即改变了顶点的原有颜色:
// 获取颜色属性值
glUniform4f(vertexColorLocation, clear_color1.x, clear_color1.y, clear_color1.z, clear_color1.w);
glUniform4f(vertexColorLocation, clear_color2.x, clear_color2.y, clear_color2.z, clear_color2.w);
glUniform4f(vertexColorLocation, clear_color3.x, clear_color3.y, clear_color3.z, clear_color3.w);
//修改顶点数组
vertices[3] = clear_color1.x;
vertices[4] = clear_color1.y;
vertices[5] = clear_color1.z;
vertices[9] = clear_color2.x;
vertices[10] = clear_color2.y;
vertices[11] = clear_color2.z;
vertices[15] = clear_color3.x;
vertices[16] = clear_color3.y;
vertices[17] = clear_color3.z;
之后再进行渲染绘制即可:
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glDrawArrays(GL_TRIANGLES, 0, 3);
结果如下
STEP4使用EBO绘制多个图形
EBO(element buffer object)索引缓冲对象是为了避免相同顶点的重复存储而产生的。和顶点缓冲对象一样,EBO也是一个缓冲,它专门存储索引,OpenGL调用这些顶点的索引来决定该绘制那个顶点。在前面顶点矩阵的基础上,我们需要新建一个绘制所需的索引矩阵(indices):
float vertices[] = {
0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下顶点
-0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下顶点
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 上顶点
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
};
unsigned int indices[] = {
0, 2, 1,
3, 4, 2
};
接着创建索引缓冲对象:
unsigned int EBO;
glGenBuffers(1, &EBO);
下面与VBO类似,我们先绑定EBO,然后用glBufferData把索引复制到缓冲里(这些函数是放在绑定和解绑函数之间的,需要注意缓冲类型定义为GL_ELEMENT_ARRAY_BUFFER):
// 绑定EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
// 索引复制到缓冲里
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
最后需要做的是用glDrawElements来替换glDrawArrays函数,来指明我们从索引缓冲渲染。使用glDrawElements时,使用的是当前绑定的索引缓冲对象中的索引进行绘制:
// GUI下 render loop中
if (show_two_triangles) {
if (show_two_triangles) {
glUseProgram(shaderProgram);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式绘制两个三角形
glBindVertexArray(VAO);
//glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 转换回填充模式
}
}
对于点和线的绘制也是类似的做法。
main函数的具体实现