现代OpenGL的基本使用

前言

OpenGL不过作为一个标准不断更新与改进的底层图形库,其新旧版本的使用区别很大,前面我们使用python接口对OpenGL进行简单的使用用的是其老式的方法,现在我们要使用新的特性,因此本文在实际代码实现中结合新版OpenGL的特性进行说明。另外为避免因版本不同而导致的问题,使用其原生的C++的接口通常是一个更好的选择。

在这里我使用环境如下:

  • 系统:Windows 10
  • 编译器:VS 2015

会使用到的概念

1. 标准化设备坐标(Normalized Device Coordinates, NDC)

标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。

OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上,一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了。

2. 顶点缓冲对象(Vertex Buffer Objects, VBO)

我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。

顶点缓冲对象是我们在接触现代OpenGL各种对象中最基本的一类。它与其他OpenGL中的对象一样,有一个独一无二的ID,通过glGenBuffers函数生成;同时使用glBindBuffer函数设置缓冲类型;我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中。

3. 着色器

如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。

顶点着色器代码如下,功能只是坐标转化,将输入的坐标转化为OpenGL内部能够处理的形式,相当于最简单的着色器了:

1
2
3
4
5
6
7
#version 330 core
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);
}

4. 编译和链接着色器

上面的着色器代码,我们必须在运行时动态编译它的源码,才能够让OpenGL使用。首先要做的是创建一个着色器对象,注意还是用ID来引用的。首先用glCreateShader创建这个着色器,然后我们把这个着色器源码附加到着色器对象上,然后再编译它。

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。

编译两个第三方库

  • GLAD 负责绘图函数的获取
  • GLFW 负责创建绘图上下文和窗口系统管理

1. GLFW库

GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口,所以它非常的小。它允许用户创建OpenGL上下文,定义窗口参数以及处理用户输入。

为了使用GLFW,同时避免一些兼容性的问题,我们最好自己下载源码编译,GLFW的下载地址为:http://www.glfw.org/download.html,其编译过程在参考链接1中有详细的讲解。编译成成功后会在src/Debug或者src/Release中生成库文件:glfw3.lib

2. 配置GLAD

GLAD是一个开源的库,作用是为了简化OpenGL中的函数调用。在参考链接2中使用的是GLEW库,其作用是一样的。

GLAD使用了一个在线服务,地址为:https://glad.dav1d.de/,选择相应的配置选项,网站就能自动生成库文件。在这里我们只要选择语言为:c/c++、Profile:core、版本选择3.3或者以上就可以了。

GLAD现在应该提供给你了一个zip压缩文件,下载zip文件,包含两个头文件目录,和一个glad.c文件。将两个头文件目录(glad和KHR)复制到你的Include文件夹中(或者增加一个额外的项目指向这些目录),并添加glad.c文件到你的工程中。

新建VS工程

新建vs空项目,将OpenGL、GLFW、GLAD的目录与库文件添加项目属性中,具体说明见参考链接1。对于Windows平台,opengl32.lib已经包含在Microsoft SDK里了,它在Visual Studio安装的时候就默认安装了,但是需要将GLFW、GLAD库加入。

基本窗口代码

下面代码,主要用创建窗口, setVertixData()、deleteObj()、compileShader()、render()函数暂时为空。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
void setVertixData(unsigned int &VBO, unsigned int &VAO);
void deleteObj(unsigned int &VBO, unsigned int &VAO);
void compileShader(int& shaderProgram);
void render(int& shaderProgram, unsigned int &VAO);

int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);

// GLAD init
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}

// 编译shader
int shaderProgram;
compileShader(shaderProgram);

// 设置定点对象
unsigned int VBO, VAO;
setVertixData(VBO, VAO);

// 设置视窗大小
glViewport(0, 0, 800, 600);

// 窗口大小改变时的回调:随窗口改变调整绘制
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

// 在窗口关闭前一直绘制
while (!glfwWindowShouldClose(window))
{
// 接受窗口输入
processInput(window);

// 渲染指令
render(shaderProgram, VAO);

// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}

// 删除创建的对象
deleteObj(VBO, VAO);

glfwTerminate();
return 0;
}

基本绘制代码

以下实现setVertixData()、deleteObj()、compileShader()、render()四个函数,功能是绘制一个三角形,shader代码也直接集成到c++源码中,用字符串数组记录:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

const char *fragmentShaderSource = "#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";

void setVertixData(unsigned int &VBO, unsigned int &VAO)
{
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);

// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices,
GL_STATIC_DRAW);

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

// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}

void deleteObj(unsigned int &VBO, unsigned int &VAO)
{
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
}

void compileShader(int& shaderProgram)
{
// vertex shader
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

// fragment shader
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}

// link shaders
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

// check for linking errors
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);
}

void render(int &shaderProgram, unsigned int &VAO)
{
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle
glUseProgram(shaderProgram);
// seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}

使用索引缓冲对象改进绘制

绘制复杂的图像,一般需要使用索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。OpenGL调用这些顶点的索引来决定该绘制哪个顶点,如矩形由两个三角形组成,绘制时只要指定四个端点、设置两个三角形分别指定的端点。

对setVertixData()、deleteObj()、render()三个函数做部分修改:

  • 修改函数申明:
1
2
3
void setVertixData(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO);
void deleteObj(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO);
void render(int& shaderProgram, unsigned int &VAO);
  • 函数体修改:
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void setVertixData(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO)
{
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};

unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};

glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glGenVertexArrays(1, &VAO);

// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

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

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}

void deleteObj(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO)
{
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);

}

void render(int &shaderProgram, unsigned int &VAO)
{
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}

参考

  1. learnOpenGL CN - 创建窗口
  2. OpenGL学习脚印:环境搭建
  3. learnOpenGL CN - 你好,三角形
Compartir