计算机图形学 | 实验二:绘制一个三角形

计算机图形学 | 实验二:绘制一个三角形

  • 计算机图形学 | 实验二:绘制一个三角形
    • 初始化
    • 顶点输入
    • 数据处理
      • VAO、VBO
      • 顶点属性
    • 顶点着色器和片段着色器
    • 渲染

华中科技大学《计算机图形学》课程

MOOC地址:计算机图形学(HUST)

计算机图形学 | 实验二:绘制一个三角形

在正式搭建环境之前,我们先来介绍一下读完下面的部分你会了解些什么。

  • 基础绘制的过程,包括初始化,顶点输入,数据处理,着色器计算以及渲染
  • 用VAO/VBO的方式绘制三角形

接下来,我们来介绍一下绘制三角形。绘制效果如下:

计算机图形学 | 实验二:绘制一个三角形_第1张图片

初始化

在代码的开始部分,我们依然对OpenGL进行一个初始化,其中就包括初始化GLFW,创建窗口,初始化GLAD,创建视口这四个部分。

// 初始化GLFW glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_RESIZABLE, FALSE);
// 创建窗口(宽、高、窗口名称)
auto window = glfwCreateWindow(screen_width, screen_height, "Triangle", nullptr, nullptr);
if (window == nullptr)
{
	std::cout << "Failed to Create OpenGL Context" << std::endl;
	glfwTerminate();
	return -1;
}
glfwMakeContextCurrent(window);
// 初始化GLAD,加载OpenGL函数指针地址的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
	std::cout << "Failed to initialize GLAD" << std::endl;
	return -1;
}
// 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高)
glViewport(0, 0, screen_width, screen_height);

顶点输入

初始化之后,我们需要给出我们三角形的顶点数据,并对数据做出一些处理,包括生成绑定VAO、VBO和属性设置,最后将其解绑。

// 三角形的顶点数据
const float triangle[] =
{
	// ---- 位置 ----
	-0.5f, -0.5f, 0.0f, // 左下
	0.5f, -0.5f, 0.0f, // 右下
	0.0f, 0.5f, 0.0f // 正上
};

由于我们绘制的是一个三角形,因此我们的顶点数据由三个顶点 组成。这里的顶点数据是标准化的设备坐标,也就是x,y,z轴坐标均映射到[-1, 1]之间。

数据处理

VAO、VBO

我们有了顶点数据,接下来就是要将这些顶点数据发送到GPU中去处理,这里我们生成了一个顶点缓冲对象VBO,并且将其绑定到顶点缓冲对象上,使用这个顶点缓冲对象的好处是我们不用将顶点数据一个一个的发送到显卡,而是可以借助VBO一次性的发送一大批数据过去,然后使用glBufferData将顶点数据绑定到当前默认的缓冲上,这里的GL_STATIC_DRAW表示我们的三角形位置数据不会被改变。

// 生成并绑定VBO
GLuint vertex_buffer_object;
glGenBuffers(1, &vertex_buffer_object);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object);
// 将顶点数据绑定至当前默认的缓冲中
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);

这里我们还生成了一个顶点数组对象VAO,使用VAO的原因是首先我们使用的核心模式要求我们需要使用VAO,其次使用VAO的好处在于我们在渲染的时候只需要调用一次VAO就可以了,之前的数据都对应存储在了VAO中,不用再调用VBO。那么VAO的生成过程也跟VBO一样,需要先生成再绑定,等到这些操作都进行完,我们可以解绑我们的VAO,VBO。

// 生成并绑定VAO
GLuint vertex_array_object;
glGenVertexArrays(1, &vertex_array_object);
glBindVertexArray(vertex_array_object);

发送到GPU之后我们还需要告诉OpenGL我们如何解释这些顶点数据。因此我们用glVertexAttribPointer这个函数告诉OpenGL我们如何解释这些顶点数据。

顶点属性

// 设置顶点属性指针 
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); 
glEnableVertexAttribArray(0);

这个函数第一个参数是我们后面会用到的顶点着色器的位置值,3表示的是顶点属性是一个三分量的向量,第三个参数表示的是我们顶点的类型,第四个是我们是否希望数据被标准化,就是映射到0-1之间,第五个参数叫做步长,它表示连续顶点属性之间的间隔,因为我们这里只有顶点的位置,所以我们将步长设置为这个,表示下组数据在3个float之后。最后一个是数据的偏移量,这里我们的位置属性是在数组的开头,因此这里是0,并且由于参数类型的限制,我们需要将其进行强制类型转换。

而下面Enable的函数则是表明我们开启了0的这个通道,默认状态下是关闭的,因此我们在这里要开启一下。
等到设置属性指针完成之后,我们这里需要解绑VAO和VBO。那么,我们可以思考一下,为什么我们在这里要解绑VAO和VBO呢?

一个原因是因为在防止之后再继续绑定VAO的时候会影响当前的VAO,另一个原因是为了使代码更加灵活规范,在渲染需要的时候我们会再绑定VAO。

我们已经通过VAO、VBO将顶点数据储存在显卡的GPU上了,接下来我们会创建顶点和片段着色器真正处理这些数据。这里我们会给出着色器的源码,然后生成并编译着色器,最后将顶点和片段着色器链接到一个着色器程序,在之后的渲染流程中我们会使用这个着色器程序,最后将之前的着色器删除。

顶点着色器和片段着色器

这里我们给出的这两段分别是顶点着色器的源码和片段着色器的源码,这个是用GLSL语言来编写的,而且这个GLSL语言看起来与C语言的风格类似,很容易懂。我们先看顶点着色器,第一行表示我们使用的是OpenGL3.3的核心模式,第二行就是我们之前说到的位置值。Main函数中的部分就是将我们之前的顶点数据直接输出到GLSL已经定义好的一个内建变量gl_Position中,这个就是我们顶点着色器的输出,也就是说我们在顶点着色器这里什么都没做,就只是将顶点位置作为顶点着色器的输出。

// 顶点着色器源码
const char *vertex_shader_source = "#version 330 core\n"
	"layout (location = 0) in vec3 aPos;\n"
	"void main()\n"
	"{\n" " gl_Position = vec4(aPos, 1.0);\n" "}\n\0";

接下来是片段着色器,前面两行类似,这里的out表示输出变量,就像之前的in表示输入变量。然后我们这里的四分量向量就是我们之前看到的三角形是红色的来源,是一个四分量的RGBA,那么我们也可以将其更改一下,我们输出的三角形颜色就会发生变化。

// 片段着色器源码
const char *fragment_shader_source =
	"#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";

有了顶点和片段着色器的源码,我们还需要生成和编译着色器,那么这里我们先生成了顶点着色器,并且附加上了之前的源码,将其进行编译,然后我们这里对其进行检测,是否成功编译,如果编译不成功就打印错误信息。

同样的片段着色器也是一样的。

最后我们将顶点和片段着色器链接到一个着色器程序中,这样我们在渲染时只需要调用一个着色器程序就可以了,同样这里我们检测了一下链接是否成功。

最后删除掉顶点和片段着色器。因为在后面渲染的时候我们只需要用那个我们之前链接好的着色器程序就可以了,不需要再使用顶点和片段着色器了。

渲染

接下来我们进入我们的渲染阶段,当窗口没有关闭的时候我们就一直进行渲染。首先我们先清空颜色缓冲,我们这里用的是黑色的背景色来清空屏幕颜色缓冲,当然这里我们可以更换颜色。接下来我们使用我们之前已经链接好的着色器程序,和VAO,来绘制三角形,绘制三角形其实只要一句话,就是这个glDrawArrays。这里的第一个参数表示我们是要绘制三角形,第二个参数表示我们顶点数组的起始索引值,第三个参数表示我们要绘制的顶点数量,这里绘制三角形我们要绘制三个顶点。绘制结束后解除绑定。最后我们会交换一下缓冲,这里我们使用的是一个双缓冲的做法,前缓冲保存着输出的图像,而渲染指令都在后缓冲中进行,当指令执行完毕后我们交换前后缓冲,最后我们还会检测是否有触发一些回调函数。

// 使用着色器程序
glUseProgram(shader_program); 
// 绘制三角形 
glBindVertexArray(vertex_array_object);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);

当我们的窗口关闭之后,我们还会进行一些善后工作,这里包括删除我们之前所创建的VAO、VBO,以及调用GLFW的函数来清理所有的资源并退出程序。

整个绘制三角形的程序就到此为止。

我们回头再来梳理一遍绘制三角形流程,刚开始我们先初始化OpenGL,接下来对我们的数据进行处理,通过VAO、VBO将其发送至GPU中,并设置属性指针告诉GPU我们会如何解释这些数据,然后在着色器中通过顶点和片段着色器对数据进行处理,最后进行渲染,渲染之后做好我们的善后工作,一个三角形就绘制成功了。效果如下:

计算机图形学 | 实验二:绘制一个三角形_第2张图片

完整代码:

/***
* 例程  绘制三角形 (MAKE后运行时可删除ALL_BUILD,也可以将Task-triangle设为默认启动工程)
* 步骤:
* 1-初始化:   GLFW窗口,GLAD。
* 2-数据处理: 给定顶点数据,生成并绑定VAO&VBO(准备在GPU中进行处理),设置顶点属性指针(本质上就是告诉OpenGL如何处理数据)。
* 3-着色器:   给出顶点和片段着色器,然后链接为着色器程序,渲染时使用着色器程序。
* 4-渲染:     清空缓冲,绑定纹理,使用着色器程序,绘制三角形,交换缓冲区检查触发事件后释放资源
*/

#include 
#include "glad/glad.h"
#include "GLFW/glfw3.h"

// 三角形的顶点数据
const float triangle[] =
{
	//---- 位置 ---- 
	-0.5f, -0.5f, 0.0f,   // 左下
	0.5f, -0.5f, 0.0f,   // 右下
	0.0f,  0.5f, 0.0f    // 正上
};

// 屏幕宽,高
int screen_width = 1280;
int screen_height = 720;

int main()
{
	glfwInit();// 初始化GLFW
	// OpenGL版本为3.3,主次版本号均设为3
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// 使用核心模式(无需向后兼容性)
	//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);// 如果使用的是Mac OS X系统,需加上这行
	glfwWindowHint(GLFW_RESIZABLE, false);// 不可改变窗口大小
	// 创建窗口(宽、高、窗口名称)
	auto window = glfwCreateWindow(screen_width, screen_height, "Triangle", nullptr, nullptr);
	if (window == nullptr)
	{
		// 如果窗口创建失败,输出Failed to Create OpenGL Context
		std::cout << "Failed to Create OpenGL Context" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);// 将窗口的上下文设置为当前线程的主上下文
	// 初始化GLAD,加载OpenGL函数指针地址的函数
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}
	// 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高)
	glViewport(0, 0, screen_width, screen_height);
	// 生成并绑定VAO和VBO
	GLuint vertex_array_object; // == VAO
	glGenVertexArrays(1, &vertex_array_object);
	glBindVertexArray(vertex_array_object);
	GLuint vertex_buffer_object; // == VBO
	glGenBuffers(1, &vertex_buffer_object);
	glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object);
	// 将顶点数据绑定至当前默认的缓冲中
	glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);
	// 设置顶点属性指针
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// 解绑VAO和VBO
	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	// 顶点着色器和片段着色器源码
	const char *vertex_shader_source =
		"#version 330 core\n"
		"layout (location = 0) in vec3 aPos;\n"           // 位置变量的属性位置值为0
		"void main()\n"
		"{\n"
		"    gl_Position = vec4(aPos, 1.0);\n"
		"}\n\0";
	const char *fragment_shader_source =
		"#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";
	// 生成并编译着色器
	// 顶点着色器
	int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
	glCompileShader(vertex_shader);
	int success;
	char info_log[512];
	// 检查着色器是否成功编译,如果编译失败,打印错误信息
	glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << info_log << std::endl;
	}
	// 片段着色器
	int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
	glCompileShader(fragment_shader);
	// 检查着色器是否成功编译,如果编译失败,打印错误信息
	glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(fragment_shader, 512, NULL, info_log);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << info_log << std::endl;
	}
	// 链接顶点和片段着色器至一个着色器程序
	int shader_program = glCreateProgram();
	glAttachShader(shader_program, vertex_shader);
	glAttachShader(shader_program, fragment_shader);
	glLinkProgram(shader_program);
	// 检查着色器是否成功链接,如果链接失败,打印错误信息
	glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
	if (!success)
	{
		glGetProgramInfoLog(shader_program, 512, NULL, info_log);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << info_log << std::endl;
	}
	// 删除着色器
	glDeleteShader(vertex_shader);
	glDeleteShader(fragment_shader);
	// 线框模式
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	// 渲染循环
	while (!glfwWindowShouldClose(window))
	{
		// 清空颜色缓冲
		glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
		// 使用着色器程序
		glUseProgram(shader_program);
		// 绘制三角形
		glBindVertexArray(vertex_array_object);// 绑定VAO
		glDrawArrays(GL_TRIANGLES, 0, 3);// 绘制三角形
		glBindVertexArray(0);// 解除绑定
		// 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等)
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	// 删除VAO和VBO
	glDeleteVertexArrays(1, &vertex_array_object);
	glDeleteBuffers(1, &vertex_buffer_object);
	// 清理所有的资源并正确退出程序
	glfwTerminate();

	return 0;
}

你可能感兴趣的:(计算机图形学,OpenGL,计算机图形学)