Xungerrrr's Blog

GUI and Draw Simple Graphics

Compute Graphics - Homework 2

Word count: 2kReading time: 9 min
2019/03/11 Share

使用OpenGL(3.3及以上)+GLFW画一个简单的三角形

在画三角形之前,首先要初始化GLFW和GLAD,以及创建窗口。这里不详细说明。

在OpenGL中,3D坐标转换成2D坐标是由图形渲染管线完成的,可以被分成几个阶段,每一个阶段都有专门的运行程序,称为着色器。OpenGL中的着色器是用GLSL语言编写的。在现代OpenGL中,必须定义至少一个顶点着色器和一个片段着色器,因此要编写、编译和链接着色器。

着色器

顶点着色器

画一个简单的三角形,只需要顶点的位置属性。在关键字in后声明一个三维的位置属性,可以实现这个简单的功能。在main函数中将位置转换成四维变量,输出到gl_Position中,就实现了顶点着色器的位置输出。

1
2
3
4
5
6
#version 330 core
layout (location = 0) in vec3 aPos;

void main() {
gl_Position = vec4(aPos, 1.0);
}

片段着色器

在片段着色器中,使用关键字out输出一个颜色变量,颜色为(1.0, 0.5, 0.2, 1.0)。

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

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

着色器的编译和链接

为了实现这一功能,编写一个类,用于着色器的编译、链接和使用。

1
2
3
4
5
6
7
8
9
class Shader {
public:
// 程序ID
unsigned int shaderProgram;
// 编译和链接
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
// 使用
void use();
};

在构造函数中,读取着色器文件,并使用glCompileShader编译。以顶点着色器为例,编译过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int vertexShader;
int success;
char infoLog[512];

vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 检查编译状态
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

编译完成后,将两个着色器链接成程序:

1
2
3
4
5
6
7
8
9
10
11
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);

着色器程序的使用

调用glUseProgram函数以激活着色器程序:

1
2
3
void Shader::use() {
glUseProgram(shaderProgram);
}

顶点数据的输入、绑定和解析

定义一个数组来存储三角形三个点的坐标数据:

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

通过顶点缓冲对象(VBO)管理数据。这里的数据是不变的,因此使用GL_STATIC_DRAW。

1
2
3
4
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

此外,还需要调用glVertexAttribPointer,告诉OpenGL怎么解析数组中的数据。解析的时候,每次解析3个元素,跳过3个元素。

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

顶点数组对象

通过绑定VAO,可以将多个VBO集成到顶点数组中,方便多次绘制重复的图形。

1
2
3
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

渲染

使用While循环进行多帧渲染。先激活程序、绑定VAO,然后输入三角形图元,用glDrawArrays生成图形。

1
2
3
shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

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

修改着色器

顶点着色器

为了使三个顶点输出不同的颜色,要在顶点着色器中增加颜色属性。aColor为输入颜色,ourColor为输出颜色。

1
2
3
4
5
6
7
8
9
10
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main() {
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
}

片段着色器

片段着色器接收来自顶点着色器的输入,转成vec4后输出。

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

in vec3 ourColor;

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

修改数组和解析方式

在数组的每一行添加三个颜色分量数据,表示顶点的颜色:

1
2
3
4
5
float vertices[] = {
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // 右下
0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f // 上
};

解析位置的时候,每次解析3个元素,跳过6个元素;解析颜色的时候,每次解析3个元素,跳过6个元素,开始时跳过3个元素:

1
2
3
4
5
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

渲染结果

原因

在光栅化的阶段,会产生比指定顶点更多的片段。对于没有定义颜色的片段,会根据片段所处的位置,进行颜色的插值。简单来讲,就是根据位置,对已知顶点的颜色进行线性组合。

给上述工作添加一个GUI,里面有一个菜单栏,使得可以选择并改变三角形的颜色。

初始化ImGUI

1
2
3
4
5
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO(); (void)io;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330");

定义一个向量,存储三个顶点的颜色值

1
2
3
4
std::vector<ImVec4> colors;
colors.push_back(ImVec4(0.0f, 1.0f, 0.0f, 1.0f));
colors.push_back(ImVec4(0.0f, 0.0f, 1.0f, 1.0f));
colors.push_back(ImVec4(1.0f, 0.0f, 0.0f, 1.0f));

在渲染循环里,设置ImGUI样式,生成三个颜色选择器

1
2
3
4
5
6
7
8
9
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();

ImGui::Begin("Color Setting");
ImGui::ColorEdit3("Left corner color", (float*)&colors[0]);
ImGui::ColorEdit3("Right corner color", (float*)&colors[1]);
ImGui::ColorEdit3("Top corner color", (float*)&colors[2]);
ImGui::End();

根据选择的颜色,改变数组元素的值,达到修改颜色的目的。修改后要重新绑定数据。

1
2
3
4
5
for (int i = 0; i < 3; i++) {
vertices[6 * i + 3] = colors[i].x;
vertices[6 * i + 4] = colors[i].y;
vertices[6 * i + 5] = colors[i].z;
}

渲染

1
2
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

使用EBO(Element Buffer Object)绘制多个三角形

添加三角形和对应的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
float vertices[] = {
// 第一个三角形
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
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, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,

// 第三个三角形
1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f
};

unsigned int indices[] = {
0, 1, 2,
3, 4, 5,
6, 7, 8
};

EBO的创建和绑定

缓冲类型是GL_ELEMENT_ARRAY_BUFFER

1
2
3
4
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

渲染

渲染时,将glDrawArrays改为glDrawElements,使用EBO中的索引进行渲染。参数分别是图元、顶点数量、索引数据类型和索引偏移量。

1
2
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

绘制其他的图元,除了三角形,还有点、线等

扩充数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
float vertices[] = {
// 第一个三角形
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
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, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,

// 第三个三角形
1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,

// 第一条线
-1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,

// 第二条线
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,

// 点
-0.75f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.75f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f
};

渲染

使用glDrawArrays进行渲染,传入图元GL_LINES绘制直线,传入图元GL_POINTS绘制点。

1
2
glDrawArrays(GL_LINES, 9, 4);
glDrawArrays(GL_POINTS, 13, 2);

如下图,新增两条白线和两个白点:

GitHub

https://github.com/Xungerrrr/SYSU-CG/tree/master/GUI%20and%20Draw%20Simple%20Graphics

CATALOG
  1. 1. 使用OpenGL(3.3及以上)+GLFW画一个简单的三角形
    1. 1.1. 着色器
      1. 1.1.1. 顶点着色器
      2. 1.1.2. 片段着色器
    2. 1.2. 着色器的编译和链接
    3. 1.3. 着色器程序的使用
    4. 1.4. 顶点数据的输入、绑定和解析
    5. 1.5. 顶点数组对象
    6. 1.6. 渲染
  2. 2. 对三角形的三个顶点分别改为红绿蓝。并解释为什么会出现这样的结果
    1. 2.1. 修改着色器
      1. 2.1.1. 顶点着色器
      2. 2.1.2. 片段着色器
    2. 2.2. 修改数组和解析方式
    3. 2.3. 渲染结果
    4. 2.4. 原因
  3. 3. 给上述工作添加一个GUI,里面有一个菜单栏,使得可以选择并改变三角形的颜色。
    1. 3.1. 初始化ImGUI
    2. 3.2. 定义一个向量,存储三个顶点的颜色值
    3. 3.3. 在渲染循环里,设置ImGUI样式,生成三个颜色选择器
    4. 3.4. 根据选择的颜色,改变数组元素的值,达到修改颜色的目的。修改后要重新绑定数据。
    5. 3.5. 渲染
  4. 4. 使用EBO(Element Buffer Object)绘制多个三角形
    1. 4.1. 添加三角形和对应的索引
    2. 4.2. EBO的创建和绑定
    3. 4.3. 渲染
  5. 5. 绘制其他的图元,除了三角形,还有点、线等
    1. 5.1. 扩充数组
    2. 5.2. 渲染
  6. 6. GitHub