计算机图形学实验笔记
  计算机视觉学累了就看看计算机图形学…反向消化…
  理论看累了就敲敲代码
  说不弃坑,就不弃坑|( ´・∧・`)

实验:搭建OpenGL环境

  OpenGL版本:3.3

GLFW

  GLFW是一个OpenGL的C语言库,是对之前固定管线常用的GLUT的一种改进。
  官网下载源代码包(推荐),或者下载32位的预编译的二进制版本。

编译glfw3.lib

  我下载的最新3.3版,下载后解压。并新建一个build文件夹。
  下载CMake,安装Win32版本。

  找到bin文件夹下cmake-gui.exe,
  填入源代码路径和build文件夹路径:
  Where is the source code: E:/glfw-3.3
  Where to build the binaries: E:/glfw-3.3/build

  点击Configure按钮,选择合适的生成器(我选择的是VS2015)。点Finish。
  再次点击Configure按钮保存设置。
  点击Generate按钮,生成工程文件。

  在build文件夹打开GLFW.sln文件,并且生成解决方案。
  在E:\glfw-3.3\build\src\Debug中所需要的glfw3.lib就有了。

配置

  方便起见,我专门建立了一个文件夹OpenGLFiles用来存放相关的头文件和库文件。
  在文件夹下新建include文件夹和lib文件夹。

  将glfw-3.3\include下的文件复制到新建的include文件夹中,将glfw3.lib复制到新建的lib文件夹中。

  新建项目所需工程文件,创建空项目。
  打开工程属性页,选择VC++目录-包含目录中加上:
  E:\OpenGLFiles\include
  库目录加上:
  E:\OpenGLFiles\lib

  链接器-输入,附加依赖项加上:
  glfw3.lib

GLAD

  开发者需要在OpenGL运行时获取函数地址并保存在一个函数指针中,取地址方法因平台而异,而GLAD库能简化该过程。

  通过GLAD在线服务,language选择C/C++,gl选择3.3,Profile选择Core,Options选中Generate a loader。
  点击Generate,下载压缩包。解压后,将include文件夹下的文件复制到我新建的include文件夹里,并且将src下的glad.c添加到工程中。

  环境搭好了!


实验:绘制一个窗口

包含头文件

1
2
#include <glad/glad.h>
#include <GLFW/glfw3.h>

初始化GLFW

1
2
3
4
5
6
glfwInit();                                                     // 初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // OpenGL版本为3.3,主次版本号均设为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); // 不可改变窗口大小

查看GLFW版本

1
2
3
int Major, Minor, Rev;
glfwGetVersion(&Major, &Minor, &Rev);
printf("GLFW %d.%d.%d initialized\n", Major, Minor, Rev);

创建窗口

1
2
3
4
5
6
7
8
GLFWwindow* window = glfwCreateWindow(screen_width, screen_height, "HelloWorld", nullptr, nullptr);
if (window == nullptr)
{ // 如果窗口创建失败
std::cout << "Failed to Create OpenGL Context" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); // 将窗口的上下文设置为当前线程的主上下文

初始化GLAD

加载OpenGL函数指针地址的函数

1
2
3
4
5
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}

指定视口

1
glViewport(0, 0, screen_width, screen_height);

glViewport函数前两个参数控制窗口左下角的位置,第三、第四个参数
控制渲染窗口的宽度和高度(像素)。
实际上也可以将视口的维度设置为比GLFW的维度小,这样所
有的OpenGL渲染将会在一个更小的窗口中显示,这样我们可以将一
些其它元素显示在OpenGL视口之外。

渲染

1
2
3
4
5
while (!glfwWindowShouldClose(window)) 
{
glfwSwapBuffers(window);// 交换缓冲
glfwPollEvents();//检查是否有触发事件(比如键盘输入、鼠标移动等)
}

双缓冲:
单缓冲使图像闪烁(图像是从左到右,从上到下逐像素绘制),不够真实。使用双缓冲规避该问题,前缓冲保存着最终输出的图像,显示在屏幕上。所有渲染指令在后缓冲上绘制,指令执行完毕后交换(swap)前缓冲和后缓冲,图像会立即呈现。

渲染结束,释放资源:

1
glfwTerminate();

运行,此时会看到一个黑色的窗口。

修改窗口颜色

在渲染循环中加入:

1
2
glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

当调用 glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为
glClearColor里所设置的颜色。

运行效果

1
GLFW 3.3.0 initialized
绘制窗口
绘制窗口

完整代码

https://github.com/hubojing/ComputerGraphics/blob/master/CreateWindow.cpp

总结:

  • 初始化:GLFW窗口,GLAD。
  • 渲染:清空缓冲,交换缓冲区检查触发事件后释放资源。

实验:绘制三角形

初始化

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
//初始化GLFW
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
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
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}

//指定当前视口尺寸
glViewport(0, 0, screen_width, screen_height);

顶点输入

  三角形顶点数据是标准化的设备坐标,即x,y,z轴坐标映射到[-1,1]之间。

1
2
3
4
5
6
const float triangle[]=
{
-0.5f,-0.5f,0.0f;//左下
0.5f, -0.5f, 0.0f;//右下
0.0f, 0.5f, 0.0f;//正上
};

数据处理

VBO

  将顶点数据发送到GPU处理。生成一个顶点缓冲对象VBO,将其绑定到顶点缓冲对象上。
  作用:不用将顶点数据逐个发送至显卡,可借助VBO一次性发送过去。
  再使用glBufferData将顶点数据绑定到当前默认的缓冲上。

1
2
3
4
5
6
//生成并绑定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中,不用再调用VBO。

1
2
3
GLuint vertex_array_object;
glGenVertexArrays(1, &vertex_array_object);
glBindVertexArray(vertex_array_object);

顶点属性

  告诉OpenGL如何解释顶点数据。

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

  glVertexAttribPointer函数参数含义:
  1-顶点着色器位置值
  2-顶点属性是一个三分量的向量
  3-顶点类型
  4-数据是否被标准化(映射到0-1之间)
  5-步长(这里表示下组数据在3个float之后)
  6-数据偏移量(此处位置属性在数组开头,因此为0)

  glEnableVertexAttribArray表示开启0这个通道,默认状态是关闭的。

  此时需要解绑VAO和VBO。
  原因:
  1. 防止继续绑定VAO时影响当前VAO。
  2. 使代码更具灵活性,在渲染需要时会再次绑定VAO。

1
2
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);

顶点着色器和片段着色器

顶点着色器

  GLSL语言,类似C语言。
  源码:

1
2
3
4
5
6
7
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";

  第一行:使用OpenGL3.3核心模式
  第二行:上面提到的位置值 in表示输入变量
  main函数中将顶点数据直接输出到GLSL定义好的内建变量gl_Position中,这是顶点着色器的输出。
  (即在顶点着色器这儿上面都没做,只是将顶点位置作为顶点着色器的输出。)

片段着色器

  源码:

1
2
3
4
5
6
7
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";

  前两行类似上面,out表示输出变量。四分量是RGBA。

生成和编译

  目的是为了得到着色器程序,所以首先生成和编译着色器,再链接到着色器程序中。
  生成并编译顶点着色器

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

  生成并编译片段着色器

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

  链接顶点和片段着色器至一个着色器程序,并删除着色器

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

渲染

  渲染时只需使用链接好的着色器程序就行,不再需要使用顶点和片段着色器。
  窗口未关闭就一直进行渲染。

1
2
3
4
while (!glfwWindowShouldClose(window))
{
//渲染操作
}

  这里使用蓝色背景色清空屏幕颜色缓冲。

1
2
glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

  接下来使用链接好的着色器和VAO来绘制三角形。

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

  其实绘制本身只是一个glDrawArrays函数,参数1表示三角形,参数2表示顶点数组起始索引值,参数3表示要绘制的顶点数量。  绘制结束后解除绑定。
  双缓冲技术:

1
2
3
4
//交换缓冲
glfwSwapBuffers(window);
//检查是否有触发事件(键盘输入、鼠标移动等)
glfwPollEvents();

善后工作

1
2
3
4
5
6
//删除VAO和VBO
glDeleteVertexArrays(1, &vertex_array_object);
glDeleteBuffers(1, &vertex_buffer_object);

//退出
glfwTerminate();

实验效果

三角形
三角形

完整代码

https://github.com/hubojing/ComputerGraphics/blob/master/Triangle

总结:

  • 初始化。
  • 数据处理:给定顶点数据,生成并绑定VAO和VBO,准备在GPU处理,设置顶点属性指针(告诉OpenGL怎么处理数据)。
  • 着色器:生成并编译顶点和片段着色器,链接为着色器程序。
  • 渲染绘制三角形。

下一个实验准备中…