GUI and Draw Simple Graphics

画一个简单的三角形

在初始化GLFW和GLAD以及创建窗口完毕后,首先要写顶点着色器和片段着色器并编译、链接为着色器程序。

顶点着色器

1
2
3
4
5
6
7
8
9
10
# version 330 core
// 设置当前使用GLSL版本名称

// 布局限定符,把缓冲区索引的数据绑定输入输出,并声明三个浮点数的输入变量
layout (location = 0) in vec3 aPos;

void main() {
// 内置变量,保存顶点位置的齐次坐标,第四个分量为透明度
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

片段着色器

1
2
3
4
5
6
7
#version 330 core
out vec4 FragColor;

void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

生成并编译shader

首先根据文件类型创建shader对象。

glCreateShader: 创建空的shader对象并返回其非零引用值。用来维护定义shader的源代码字符串。

读取文件内容,并将创建的shader对象与定义它的源代码文件内容串联起来。

glShaderSource: 将shader对象的源代码字符串复制为文件中读取的代码字符串。

使用glCompileShader编译源代码,并检查编译错误。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void compileShader(unsigned int & shader, const char * filename, const int & shaderType) {
shader = glCreateShader(shaderType);
string shaderSource;
if (readFile(filename, shaderSource)) {
const GLchar* p[1];
p[0] = shaderSource.c_str();
GLint Lengths[1];
Lengths[0] = strlen(shaderSource.c_str());
glShaderSource(shader, 1, p, Lengths);
glCompileShader(shader);
checkCompile(shader, 1);
}
else {
cout << "Fail to read shader file" << endl;
}
}
检查编译错误

如果出错success会变为0并将错误信息存入info,可以输出错误信息。

1
2
3
4
5
6
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
cout << "COMPILE ERROR: " << endl;
glGetShaderInfoLog(shader, 512, NULL, info);
cout << info << endl;
}

链接程序

和编译相似,首先需要使用glCreateProgram创建程序,获得其引用。之后将编译好的顶点着色器和片段着色器都通过glAttachShader添加到程序中,并使用glLinkProgram链接。

最后需要激活程序对象

1
glUseProgram(shaderProgram);

完成了上述步骤后才可以开始正常绘制三角形。这里由于不涉及到颜色的问题,线将三角形的三点坐标确定。视口中心为坐标(0, 0)点,且x, y轴范围为[-1, 1]。由于本次作业要求绘制的均为平面图形,这里设所有所有顶点(包括后面其他图元的绘制) z = 0.0f.

顶点缓冲对象VBO

创建

声明一个变量VBO并通过函数glGenBuffers生成缓冲对象名。

void glGenBuffers(GLsizei n, GLuint * buffers);

其中n为要多少个VBO,而buffers存储生成的缓冲对象名。

绑定

上一步结束的时候,虽然生成了缓冲对象名,但其实没有真正被绑定到缓冲上。因此需要glBindBuffer这个函数将上一步存储在变量VBO中的名字绑定:

glBindBuffer : 将缓冲对象名VBO绑定到目标缓冲上。

另外,当缓冲对象绑定到了一个目标的时候,其之前的绑定会被覆盖

数据存储

这里为目的为顶点属性,因此对应的Buffer Binding Target为GL_ARRAY_BUFFER。参见这里:https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBindBuffer.xhtml

之后就将具体的数值存储到前面绑定好的目标即可:

1
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

参数为目标名、数据大小、数据,最后一个为数据存储的使用模式,这里时绘制,而且不怎么会改变,因而选用GL_STATIC_DRAW.

解析数据

由于数据是以一维数组的方式存储的,并没有显示说明每个数字的含义。但绘制图像时是需要了解如何解析数组的。这里主要是和传入的数组相匹配:

比如三角形顶点这样指定

1
float vertices[] = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f };

另外着色器文件中location = 0为输入的点坐标,因此第一个参数为0. 之后,每个点有3个维度组成,数据类型为FLOAT,不进行标准化,并且每次读取下一个坐标点数组需要经过3个float长度,以及由于前面没有存储其他信息,直接从0开始读取就是坐标。

1
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

顶点数组对象VAO

VAO和VBO生成和绑定的步骤相似。主要将VBO的顶点变为顶点数组方便使用。

glBindVertexArray 将顶点数组对象绑定到顶点数组对象名字VAO

渲染循环

通过在while循环内不断绘制图形使得图片显示。激活程序、绑定VAO并调用glDrawArrays传入三角形图元即可绘制出一个简单三角形。

同时在while中使用glfwSwapBuffers(window);达到交换缓冲的目的。

结果截图

对三角形的三个顶点分别改为红绿蓝,并解释为什么会出现这样的结果

着色器之间的参数传递

修改三角形颜色涉及到着色器之间的信息交换,一个相同的变量名作为一个文件的输入的同时作为另一个文件的输出即可达成信息传递的目的。比如这里需要向.vs文件即顶点着色器中同时输入顶点和颜色,并将其中的颜色部分交由片段着色器处理。

顶点着色器会从main中接受两个glVertexAttribPointer函数的解析,分别对应两个输入:坐标和颜色。

1
2
3
4
// vs
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 color;
out vec3 ourColor;

同时vs需要将ourColor传给fs:

1
2
// fs
in vec3 ourColor;

采用同样的变量名就可以收到。

解析输入数组为顶点坐标和rgb值

上文提到了两个location都要匹配到对应的数组中的数据内容,需要通过两个glVertexAttribPointer完成。

三角形信息数组这样定义:

1
2
3
4
GLfloat vertices[] = {
0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f };

其中左边三列为顶点的xyz坐标,右边为对应顶点的rgb值。

处理位置属性的时候,需要从头(0)开始,每次读取三个元素,但同时要跳过6个而不再是3个元素(坐标3+颜色3)。这里第一个0,对应vs文件中location = 0.

1
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), 0);

处理颜色的时候,也是每次读取三个元素、跳过6个元素,并且不是从头读取,而是3个坐标元素之后。此外,这里第一个参数1,对应vs文件中location = 1.

1
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (void*)(3 * sizeof(GLfloat)));

这样三角形的三个顶点都通过片段着色器赋予了不同的颜色。

结果截图

原因说明

在定义了三角形三个顶点的颜色后,中间产生混合效果的主要原因是OpenGL的混合方程决定的。(参见:https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/03%20Blending/)

Blending的步骤中默认的混合函数公式:

这里F为影响因子,设置对源颜色的alpha值影响。

比如到三角形中心位置的时候,由于三个顶点的颜色对其影响因子相等,所以显示为红、绿、蓝混合得到的灰色。同样的道理,三个边中间的颜色为黄色、青色和紫色。

给上述工作添加GUI,使得可以选择并改变三角形颜色

初始化

首先初始化imgui的环境:

1
2
3
4
5
6
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
设置窗口样式

在循环渲染的过程中,新建GUI窗口,并通过组件设置窗口的细节:

1
2
3
ImGui::Begin("Color Setting");
ImGui::ColorEdit3("Triangle color", (float*)&triangleColor);
ImGui::End();

这个窗口比较简单,只有一个颜色编辑组件,修改triangleColor变量(对应三角形颜色)。

渲染

渲染过程需要设置当前上下文,并且根据窗口设置视口的尺寸:

1
2
3
4
5
6
ImGui::Render();
int display_w, display_h;
glfwMakeContextCurrent(window);
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

glfwGetFramebufferSize: 根据窗口的缓冲大小获取尺寸。

实验思路

初始化和上一问相同,渲染出彩色的三角形。这里初始化颜色为负数,用于判断三角形的颜色是否被修改。(修改结果一定为正)

判断状态

由于生成彩色三角形的数组和修改颜色后的数组在颜色值上不同。需要一个bool类型变量dirty判断三角形是在初始化的状态false还是单一颜色的状态true

在未修改的状态中判断triangleColor的元素是否为正数,如果是,则将dirty改为true并且之后不再修改dirty.

如果当前已经被修改过,则使用数组:

1
2
3
4
GLfloat vertices[] = {
0.0f, 1.0f, 0.0f, triangleColor.x, triangleColor.y, triangleColor.z,
-1.0f, -1.0f, 0.0f, triangleColor.x, triangleColor.y, triangleColor.z,
-1.0f, -1.0f, 0.0f, triangleColor.x, triangleColor.y, triangleColor.z };

否则使用初始RGB(三个顶点颜色不同):

1
2
3
4
GLfloat triangleVertices[] = {
0, 0.5f, 0, 1, 0, 0,
-0.5f, -0.5f, 0, 0, 1, 0,
0.5f, -0.5f, 0, 0, 0, 1 };
获取颜色

在GUI中通过

1
ImGui::ColorEdit3("Triangle color", (float*)&triangleColor);

这一语句在用户修改了颜色的时候,用triangleColor接收新的颜色值。由于在状态判断中已经将这一步设置为了dirty = true,因此下一轮循环的时候使用新的颜色渲染三角形的三个顶点,从而改变其颜色。

结果截图

绘制其他图元

实现思路

坐标位置以及颜色:

1
2
3
float pointVertex[] = {
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f
};

生成、绑定和解析顶点缓冲/数组对象的方法与三角形相似。不同之处为:

  1. 需要设置点大小
  2. 图元参数为GL_POINTS
1
2
glPointSize(5.0f);
glDrawArrays(GL_POINTS, 0, 1);
线

坐标位置以及颜色:

1
2
3
4
float lineVertices[] = {
-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f
};

绘制函数参数为GL_LINE_STRIP

结果截图

使用EBO绘制多个三角形

实现思路

(一)绘制矩形

坐标位置以及颜色:

1
2
3
4
5
6
float recVertices[] = {
0.5f, 0.5f, 0.0f, 1, 1, 1, // 右上角
0.5f, -0.5f, 0.0f, 0, 1, 1, // 右下角
-0.5f, -0.5f, 0.0f, 1, 0, 1, // 左下角
-0.5f, 0.5f, 0.0f, 1, 1, 0 // 左上角
};

这里需要用到Indices数组辅助,把它拆分成两个三角形绘制。

1
2
3
4
unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

indices数组里的元素对应recVertices的一组元素(6个)的下标,即这种书写方式的行下标。

EBO的生成、绑定方法和VBO和VAO类似,但是绑定和存储数据的时候target需要改变:

1
2
3
// 这里的 缓冲类型为GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

最后绘制的时候使用的函数如下:

1
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

这里虽然是矩形,但是由于要绘制两个三角形,因而还是需要绘制6个顶点。

(二)绘制没有公共顶点多个三角形

这一步和绘制矩形的思路相似,在indices数组中直接按顺序写出每个三角形的三个顶点坐标和颜色在vertices数组中的位置即可:

1
2
3
4
5
6
unsigned int indices[] = { 
0, 1, 2, // 第一个三角形
3, 4, 5, // 第二个三角形
6, 7, 8, // 第三个三角形
9, 10, 11 // 第四个三角形
};

这里绘制12个顶点:

1
glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_INT, 0);

结果截图

源代码:https://github.com/Yuandi-Sherry/CG_GUI-and-Draw-simple-graphics