【笔记】Cherno Opengl Tutorial note 02

08 How I Deal with Shaders

在之前的着色器编写中,对于每个字符串都要加引号和换行符,这是非常不方便的。

因此我们需要从文件读取shader。

一般来说会把vs和fs分成两个文件。但作者认为两个文件也很不方便,而是把vs和fs合并在一起(shaderlab就是这样做的)。并且在文件中对两部分进行区别。

在项目文件夹中新建resource文件夹,包含shaders文件夹,(以后也可能添加纹理、模型等资源),创建一个后缀为shader的文件

image-20220824221249780

将着色器的代码粘贴到文件中,并分别加上指定vs和fs编译的宏(可以用替换的功能去掉引号和换行符)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#shader vertex
#version 330 core

layout(location = 0) in vec4 position;

void main()
{
gl_Position = position;
};

#shader fragment
#version 330 core

layout(location = 0) out vec4 color;

void main()
{
color = vec4(1.0,0.0,0.0,1.0);
};

然后我们要做的是把这个文件转换成std字符串。读文件采用fstream设置文件流

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
#include <fstream>
#include <string>
#include <sstream>//stringstream

struct ShaderProgramSource{
std::string VertexSource;
std::string FrgmentSource;
};// 为了返回多重数据而制作一个结构体
static ShaderProgramSource ParseShader(const std::string& filepath) {
std::ifstream stream(filepath);

enum class ShaderType {
NONE = -1, VERTEX = 0, FRAGMENT = 1
};

std::string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;

while(getline(stream, line)) {//string头文件中的getline方法
if (line.find("#shader")!=std::string::npos){
//std::string::npos,表示字符串末尾(无效字符串)
if(line.find("vertex")!=std::string::npos){
// set mode to vertex
type = ShaderType::VERTEX;
} else if (line.find("fragment")!=std::string::npos) {
// set mode to fragment
type = ShaderType::FRAGMENT;
}
} else {
//把代码添加到vs或fs的字符串流中
ss[(int)type] <<line<<"\n";
}
}

return {ss[0].str(),ss[1].str()};
}

1
2
ShaderProgramSource source= ParseShader("res/shaders/Basic.shader");
//我们这里使用相对路径,可执行文件的默认工作目录是可执行文件的目录,而visual studio调试器运行,工作目录是可设置的
image-20220824224240477

将解析的着色器代码输出出来,一切正常

image-20220824224450410

那么我们实现了从一个文件里读取vs和fs,这部分修改就完成了

09 Index Buffers

  • Index Buffer是什么

我们已经画好了一个三角形。我们如果想画一个正方形呢?实际上,也是由三角形组成的。

image-20220824230232057
1
2
3
4
5
6
7
8
9
float positions[12] = {
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
// 第二个三角形
0.5f, 0.5f,
-0.5f, 0.5f,
-0.5f, -0.5f
};

相应地,顶点缓冲区大小也增大了,绘制方法也需要改成6个顶点

1
glDrawArrays(GL_TRIANGLES, 0, 6);

我们成功地 绘制出了想要的正方形/长方形

image-20220824230651997

但是实际上顶点数组发生了大量重复,这也浪费了内存,尤其是顶点储存属性很多时。

因此我们使用索引缓冲Index Buffer,使顶点能够重复使用。

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
float positions[12] = {
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f
};//01230
unsigned int indices[] = {
0,1,2,
2,3,0
};//必须使用无符号类型

// 顶点缓冲
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);

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

// 制作索引缓冲区和顶点缓冲区相似
unsigned int ibo;//index buffer object
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

//...
//drawcall的变化
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);//GL_INT就出现了问题,无法渲染出画面,而index buffer是无符号的

也非常顺利

10 Dealing with Errors

这里讨论的是Opengl提供的检查错误的方法,不会用到外部工具

主要有两种方法

glGetError

使用glGetError的工作流程是首先在opengl的每个函数调用前,==在循环中调用==,直到清除所有错误。

glMessageCallBack是opengl4.3添加的功能,允许我们指定一个指向Opengl函数的指针,比glGetError更方便。这一集教程中只关注glGetError。

  • 返回值flag

image-20220825000148080

1
2
3
4
5
6
7
8
9
10
11
12
13
static void GLClearError() {
while(glGetError()!=GL_NO_ERROR);//!glGetError()
}
static void GLCheckError() {
while(GLenum error = glGetError()) {
std::cout<<"[OpenGL Error] ("<< error <<")"<<std::endl;
}
}

......
GLClearError();
glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr);//前面的符号类型错误
GLCheckError();

控制台打印出1280的错误

image-20220825000933480

在glew.h中 ,会发现各种宏的定义都是16进制的

image-20220825001114710

因此我们把1280转换成16进制,如果懒得搜的话,在断点 中定位到变量,右键可以选择16进制显示,这里结果是0x00000500,

image-20220825001334790

在glew.h中搜索,我们可以找到是GL_INVALID_ENUM的错误,也就是说,在我们检查的glDrawElements函数中,传递了一个无效的枚举类型参数。也就是GL_INT

image-20220825001532054

那么,最终的问题是,对于每个函数都做这样的处理太过麻烦,也会污染代码。所以我们最好知道错误发生在哪里 。

在这里我们已经设置了断点,当然我们在堆栈中可以找到具体的出现问题的函数。我们也可以用ASSERT来做这件事,如果条件不成立,通常发送一个消息到控制台,或是停止程序的执行 。这相当于用代码来设置断点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define ASSERT(x) if (!(x)) __debugbreak();//MSVC function
static void GLClearError() {
while(glGetError()!=GL_NO_ERROR);//!glGetError()
}
static bool GLLogCall() {
while(GLenum error = glGetError()) {
std::cout<<"[OpenGL Error] ("<< error <<")"<<std::endl;
return false;
}
return true;
}

...
GLClearError();
glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr);
ASSERT(GLLogCall());

运行后自动触发了断点

image-20220825003024764

我们还可以再设置一宏命令来简化这个过程。

1
2
3
4
5
6
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall())

...
GLCALL(glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr));

非常好用。

还要解决的问题是,我们的错误消息并不能指定实际错误发生在哪个文件或行上,甚至函数名。

1
2
3
4
5
6
7
8
9
10
11
12
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall(#x, __FILE__, __LINE__))//#把x转换成字符串
...
static bool GLLogCall(const char* function, const char* file, int line) {//提供函数名与文件名以及行数
while (GLenum error = glGetError()) {
std::cout << "[OpenGL Error] (" << error << "): " << function <<
" "<< file<<" : "<<line<<std::endl;
return false;
}
return true;
}

更好用了。

image-20220825004247444

接下来我们要做的就是把每个opengl方法用GLCall包装起来。

有一些情况会为GLCall添加if、while等作用域,但是这样就无法使用于赋值的构造函数语句等,因为这会让创建的变量无法被调用,因为超出了作用域(相当于封装在了一个{}scope中)

1
2
GLCall(unsigned int program = glCreaateProgram());
//这是可以使用的,但是添加作用域后不能使用,相当于把它放进了大括号

11 Uniforms

Uniform是一种从CPU获取数据的方式。

在交互时着色器变量有可能需要更新, 因此C++的变量通过Uniform传递到着色器。

我们在每次绘制前设置uniform

1
2
3
4
5
6
7
8
9
10
11
12
//Basic.shader
#shader fragment
#version 330 core

layout(location = 0) out vec4 color;

uniform vec4 u_Color;

void main()
{
color = u_Color;
};
1
2
3
4
5
6
7
8
9
//Application.cpp

glUseProgram(shader);

//创建着色器后,每个uniform都会被分配一个ID,并通过ID检索该变量的位置
int location = glGetUniformLocation(shader, "u_Color");
//如果在着色器中声明了但没有使用uniform变量,那么opengl编译时会舍弃该变量
ASSERT(location != -1);
glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f);

通过Uniform来对变量进行变化控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int location = glGetUniformLocation(shader, "u_Color");
ASSERT(location != -1);
glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f);

float r = 0.0f;
float increment = 0.05f;
while (!glfwWindowShouldClose(window))
{
glUniform4f(location, r, 0.3f, 0.8f, 1.0f);
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));
if (r>1.0f)
increment = -0.05f;
else if (r<0.0f)
increment = 0.05f;
r += increment;
.....
}

(有可能会出现变化太快的问题,可以把双缓冲交换间隔设置为1。虽然我好像没有这个问题)

1
2
3
4
...
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
...

12 Vertex Arrays

  • 绑定vertex buffer
  • 指定vertex layout
  • 绑定index buffer
1
2
3
4
//解除绑定
glUseProgram(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

在每一帧重新绑定,结果是一样的。事实上由于每一帧绘制数据可能发生变化,正需要在这里进行绑定。

1
2
3
4
5
6
7
8
9
glUseProgram(shader);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

glUniform4f(location, r, 0.3f, 0.8f, 1.0f);
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));//前面的符号类型错误

我们需要正确使用顶点数组,为每个几何体设置不同的顶点数组对象,然后只需要在drawcall前进行绑定。这就是VAO。

绘制过程

  • 绑定着色器
  • 绑定顶点缓冲区
  • 设置顶点布局
  • 绑定index缓冲区
  • drawcall

-》使用VAO

  • 绑定着色器
  • 绑定顶点数组
  • 绑定index缓冲
  • drawcall

使用core profile

1
2
3
4
5
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//使用opengl最高版本或最低版本为3(3.3)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用Core Profile
//可兼容性的opengl profile(compatbility Opengl profile)有VAO默认对象是0.但core profile没有默认对象,必须绑定VAO

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

在这些都绑定好后,由于Opengl的状态机属性,我们可以解除绑定。然后在drawcall前再绑定vao(把vertex buffer链接到VAO),就可以正常绘制。

  • 绑定VAO
  • 绑定顶点缓冲
  • 指定顶点属性指针,这时将会把顶点缓冲和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
// 绑定VAO
unsigned int vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

// 顶点缓冲
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);

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

// 制作索引缓冲区和顶点缓冲区相似
unsigned int ibo;//index buffer object
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

.....

// 解除绑定
//glUseProgram(0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);//反正不要在VAO激活时解绑EBO/IBO

while (!glfwWindowShouldClose(window))
{
/* Render here */

glUseProgram(shader);
glBindVertexArray(vao);
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
//同样重要的是要注意,索引缓冲区/ELEMENT_ARRAY_BUFFER也包含在VAO状态中。它不需要按帧重新定义...因此,您也可以删除该行。 GL_ELEMENT_ARRAY_BUFFER必须在绑定顶点数组对象(glBindVertex 数组)之后绑定。GL_ELEMENT_ARRAY_BUFFER对象存储在顶点数组对象状态向量中。 如果顶点数组对象已解绑并再次绑定,则GL_ELEMENT_ARRAY_BUFFER也已知并再次绑定。但是,如果在绑定顶点数组对象时元素数组缓冲区显式解除绑定,则会从状态向量中删除该缓冲区。


glUniform4f(location, r, 0.3f, 0.8f, 1.0f);
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));//前面的符号类型错误
//.....
}

原来的做法:

每次需要绘制时,绑定顶点缓冲区,指定顶点属性指针,绑定顶点索引缓冲,然后渲染。

VAO的方法:

为每个几何对象创建VAO,绑定顶点缓冲区,指定顶点属性指针,绑定顶点索引缓冲,解绑

在渲染该对象前绑定对应的VAO

LearnOpengl对此操作的描述

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

// remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);


在之后我们可以用class来做这件事,会方便很多。

每次绑定vertex buffer和VAO的方式哪种更快呢?

  • 实际上在过去使用一个VAO,每次绑定其他东西更快。
  • NVIDIA提出不建议使用VAO
  • 但其实在不同环境下也可能出现不同的结果
  • 如果一定特别需要压榨性能,可以看情况使用

13 Abstracting Opengl into Class

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#include <GL/glew.h>
#include <GLFW/glfw3.h>


#include <iostream>
#include <fstream>
#include <string>
#include <sstream>//stringstream

#define ASSERT(x) if (!(x)) __debugbreak();//MSVC function
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall(#x, __FILE__, __LINE__))//#把x转换成字符串
static void GLClearError() {
while (glGetError() != GL_NO_ERROR);//!glGetError()
}
static bool GLLogCall(const char* function, const char* file, int line) {//提供函数名与文件名以及行数
while (GLenum error = glGetError()) {
std::cout << "[OpenGL Error] (" << error << "): " << function <<
" " << file << " : " << line << std::endl;
return false;
}
return true;
}


struct ShaderProgramSource {
std::string VertexSource;
std::string FrgmentSource;
};// 为了返回多重数据而制作一个结构体
static ShaderProgramSource ParseShader(const std::string& filepath) {
std::ifstream stream(filepath);

enum class ShaderType {
NONE = -1, VERTEX = 0, FRAGMENT = 1
};

std::string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;

while (getline(stream, line)) {//string头文件中的getline方法
if (line.find("#shader") != std::string::npos) {
//std::string::npos,表示字符串末尾(无效字符串)
if (line.find("vertex") != std::string::npos) {
// set mode to vertex
type = ShaderType::VERTEX;
}
else if (line.find("fragment") != std::string::npos) {
// set mode to fragment
type = ShaderType::FRAGMENT;
}
}
else {
//把代码添加到vs或fs的字符串流中
ss[(int)type] << line << "\n";
}
}

return { ss[0].str(),ss[1].str() };
}



static unsigned int ComplieShader(unsigned int type, const std::string& source) {
unsigned int id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr); //传递
glCompileShader(id); //编译
// TODO: Error handling
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (!result) {
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);//错误信息长度
char* message = (char*)alloca(length * sizeof(char));
//alloca可以在栈上动态分配
glGetShaderInfoLog(id, length, &length, message);
std::cout << "Fail to complie " << (type == GL_VERTEX_SHADER ? "vertex " : "fragment ") << "shader!" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}
return id;
}

static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
unsigned int program = glCreateProgram();
unsigned int vs = ComplieShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = ComplieShader(GL_FRAGMENT_SHADER, fragmentShader);

glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program); //链接
glValidateProgram(program); //验证程序

//释放
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}


int main(void)
{
GLFWwindow* window;

/* Initialize the library */
if (!glfwInit())
return -1;


glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);


/* Create a windowed mode window and its OpenGL context */
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}

/* Make the window's context current */
glfwMakeContextCurrent(window);
glfwSwapInterval(1);


if (glewInit() != GLEW_OK)
std::cout << "GlewInit fail!" << std::endl;

float positions[12] = {
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f
};//01230
unsigned int indices[] = {
0,1,2,
2,3,0
};//必须使用无符号类型



//绑定VAO
unsigned int vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

// 顶点缓冲
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);

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

// 制作索引缓冲区和顶点缓冲区相似
unsigned int ibo;//index buffer object
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
/*
std::string vertexShader =
"#version 330 core\n"
"\n"
"layout(location = 0) in vec4 position;\n"
//和顶点属性的laout一致。
//注意这里是vec4,而我们实际只有vec2,剩余的部分Opengl会默认转换z0w1
"\n"
"void main()"
"{\n"
" gl_Position = position;\n"
"}\n";
std::string fragmentShader =
"#version 330 core\n"
"\n"
"layout(location = 0) out vec4 color;\n"
"\n"
"void main()"
"{\n"
" color = vec4(1.0,0.0,0.0,1.0);\n"
"}\n";
*/
ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");
unsigned int shader = CreateShader(source.VertexSource, source.FrgmentSource);
//glUseProgram(shader);

//创建着色器后,每个uniform都会被分配一个ID
int location = glGetUniformLocation(shader, "u_Color");
//如果在着色器中声明了但没有使用uniform变量,那么opengl编译时会舍弃该变量
ASSERT(location != -1);
//glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f);

float r = 0.0f;
float increment = 0.05f;

//解除绑定
//glUseProgram(0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */

glUseProgram(shader);
glBindVertexArray(vao);
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

glUniform4f(location, r, 0.3f, 0.8f, 1.0f);
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));//前面的符号类型错误


if (r > 1.0f)
increment = -0.05f;
else if (r < 0.0f)
increment = 0.05f;
r += increment;
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
}
//glDeleteProgram(shader);
glfwTerminate();
return 0;
}

到这里先放一放整体的代码,然后我们将要进行大的修改。

Renderer

首先创建Renderer.h 和cpp文件。并且把错误处理的部分挪到头文件中

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
//Renderer.h#################

#pragma once

#include <GL/glew.h>

#define ASSERT(x) if (!(x)) __debugbreak();//MSVC function
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall(#x, __FILE__, __LINE__))//#把x转换成字符串
void GLClearError();
bool GLLogCall(const char* function, const char* file, int line);

//Renderer.cpp###############

#include "Renderer.h"
#include <iostream>
void GLClearError() {
while (glGetError() != GL_NO_ERROR);//!glGetError()
}
bool GLLogCall(const char* function, const char* file, int line) {//提供函数名与文件名以及行数
while (GLenum error = glGetError()) {
std::cout << "[OpenGL Error] (" << error << "): " << function <<
" " << file << " : " << line << std::endl;
return false;
}
return true;
}

VertexBuffer

我们再创建VertexBuffer的头文件和cpp文件,用来定义顶点缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//VertexBuffer.h
#pragma once

class VertexBuffer
{
private:
unsigned int m_RendererID;
// 在其他API中,也表示是一个ID,因此用这种命名来通用地表达

public:
VertexBuffer(const void* data, unsigned int size);
~VertexBuffer();

void Bind() const;
void Unbind() const;

};

我们可以右键类名后选择“快速操作和重构”,然后创建函数定义,这样IDE自动在cpp中为我们创建了各个方法的定义框架(但实际上我操作失败了,提示说所选的文本不包含任何函数签名,我只能全选下面的方法,然后再右键快速操作)

image-20220910170329326

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
//VertexBuffer.cpp
#include "VertexBuffer.h"

#include "Renderer.h"
VertexBuffer::VertexBuffer(const void* data, unsigned int size)
{
glGenBuffers(1, &m_RendererID);
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
}

VertexBuffer::~VertexBuffer()
{
glDeleteBuffers(1, &m_RendererID);
}

void VertexBuffer::Bind() const
{
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
}

void VertexBuffer::Unbind() const
{
glBindBuffer(GL_ARRAY_BUFFER, 0);
}

IndexBuffer

对indexBuffer也可以用同样的做法.

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
//#########IndexBuffer.h##############
#pragma once

class IndexBuffer
{
private:
unsigned int m_RendererID;
unsigned int m_Count;

public:
IndexBuffer(const unsigned int* data, unsigned int count);
~IndexBuffer();

void Bind() const;
void Unbind() const;

inline unsigned int GetCount() { return m_Count; }
};
//##########IndexBuffer.cpp#############
#include "IndexBuffer.h"

#include "Renderer.h"
IndexBuffer::IndexBuffer(const unsigned int* data, unsigned int count)
: m_Count(count)
{
ASSERT(sizeof(unsigned int) == sizeof(GLuint));
//count*sizeof(GLuint)即使可能不会,但仍可能出现平台差异
glGenBuffers(1, &m_RendererID);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), data, GL_STATIC_DRAW);
}

IndexBuffer::~IndexBuffer()
{
glDeleteBuffers(1, &m_RendererID);
}

void IndexBuffer::Bind() const
{
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
}

void IndexBuffer::Unbind() const
{
glBindBuffer(GL_ARRAY_BUFFER, 0);
}

Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    VertexBuffer vb(positions, 4*2*sizeof(float));

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

IndexBuffer ib(indices, 6);

/*
// 顶点缓冲
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);

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

// 制作索引缓冲区和顶点缓冲区相似
unsigned int ibo;//index buffer object
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
*/

14 Buffer Layout Abstraction

我们希望用如下的结构来组织vao

1
2
3
4
5
6
7
8
9
VertexArray va;
VertexBuffer vb(positions, 4*2*sizeof(float));
va.AddBuffer(vb);

BufferLayout layout;
layout.Push<float>(3);
va.AddLayout(layout);
....
va.Bind();

VertexArray

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
//VertexArray.h
#pragma once
#include "VertexBuffer.h"
#include "VertexBufferLayout.h"
class VertexArray {
private:
unsigned int m_RendererID;

public:
VertexArray();
~VertexArray();

void AddBuffer(const VertexBuffer& vb, const VertexBufferLayout& layout);
void Bind() const;
void Unbind() const;
};


//VertexArray.cpp
#include "VertexArray.h"
#include "Renderer.h"
VertexArray::VertexArray()
{
glGenVertexArrays(1, &m_RendererID);

}

VertexArray::~VertexArray()
{
glDeleteVertexArrays(1, &m_RendererID);
}

void VertexArray::AddBuffer(const VertexBuffer& vb, const VertexBufferLayout& layout)
{
// 绑定VAO
Bind();
// 绑定VBO
vb.Bind();

// 顶点属性Layout
const auto& elements = layout.GetElements();

unsigned int offset = 0;
for (unsigned int i = 0; i < elements.size();i++) {
const auto& element = elements[i];
glVertexAttribPointer(i, element.count,element.type,
element.normalized, layout.GetStride(), (const void*) offset);
glEnableVertexAttribArray(i);
offset += element.count * VertexBufferElement::GetSizeOfType(element.type);
//glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0);
}

}

void VertexArray::Bind() const
{
glBindVertexArray(m_RendererID);
}

void VertexArray::Unbind() const
{
glBindVertexArray(0);
}

VertexArrayLayout

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
//VertexArrayLayout.h
#pragma once
#include <vector>
#include "Renderer.h"
struct VertexBufferElement {
unsigned int type;
unsigned int count;
unsigned char normalized;

static unsigned int GetSizeOfType(unsigned int type) {
switch (type) {
case GL_FLOAT: return 4;
case GL_UNSIGNED_INT: return 4;
case GL_UNSIGNED_BYTE: return 1;
}
ASSERT(false);
return 0;
}
};
class VertexBufferLayout
{
private:
std::vector<VertexBufferElement> m_Elements;

unsigned int m_Stride;

public:
VertexBufferLayout()
: m_Stride(0) {};

template<typename T>
void Push(unsigned int count) {
static_assert(false);
}


template<>
void Push<float>(unsigned int count) {
m_Elements.push_back({ GL_FLOAT, count, GL_FALSE});
m_Stride += VertexBufferElement::GetSizeOfType(GL_FLOAT) * count;//4 * 2
}
//用模板来完成不同类型顶点属性的添加,也方便进行其他类型的扩展
template<>
void Push<unsigned int>(unsigned int count) {
m_Elements.push_back({ GL_UNSIGNED_INT, count, GL_FALSE });
m_Stride += VertexBufferElement::GetSizeOfType(GL_UNSIGNED_INT) * count;
}

template<>
void Push<unsigned char>(unsigned int count) {
m_Elements.push_back({ GL_UNSIGNED_BYTE, count, GL_TRUE });
m_Stride += VertexBufferElement::GetSizeOfType(GL_UNSIGNED_BYTE) * count;//GLbyte
}

inline const std::vector<VertexBufferElement> GetElements() const { return m_Elements; }
inline unsigned int GetStride() const { return m_Stride; }
};

作者在Bind和Unbind的地方其实处理得不是很舒服,受之前学长的影响,个人喜欢在添加完顶点属性后直接解绑,也就是AddBuffer函数之后,把va和vb解绑直接做掉。并且elementarray也就是IBO(EBO)Learnopengl中说过是绑定在VAO中的,可以不管。

15 Shader Abstraction

游戏和引擎中,通常有一种自定义着色语言,然后编译成每种api或平台适合的语言。并且是可控和可扩展的(着色器动态创建)。

我们在这里shader abstraction要完成的:

  1. 能够使用文件和字符串编译shader
  2. 绑定和取消绑定
  3. 设置uniform

Shader

shader.h

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
//Shader.h

#pragma once

#include <string>
#include <unordered_map>//hash table

struct ShaderProgramSource {
std::string VertexSource;
std::string FrgmentSource;
};// 为了返回多重数据而制作一个结构体

class Shader
{
private:
std::string m_FilePath;
unsigned int m_RendererID;


std::unordered_map<std::string, int> m_UniformLocationCache;
public:
Shader(const std::string& filepath);
~Shader();

void Bind() const;
void Unbind() const;

//set uniforms
void SetUniform4f(const std::string& name, float v0, float v1, float v2, float v3);
private:
int GetUniformLocation(const std::string& name);
unsigned int CompileShader(unsigned int type, const std::string& source);
unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader);
ShaderProgramSource ParseShader(const std::string& filepath);
};


Shader.cpp

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//Shader.cpp
#include <iostream>
#include <fstream>
#include <sstream>

#include "Renderer.h"
#include "Shader.h"
Shader::Shader(const std::string& filepath)
: m_FilePath(filepath), m_RendererID(0)
{
ShaderProgramSource source = ParseShader(filepath);
m_RendererID = CreateShader(source.VertexSource, source.FrgmentSource);

}

Shader::~Shader()
{
glDeleteProgram(m_RendererID);
}

unsigned int Shader::CompileShader(unsigned int type, const std::string& source) {
unsigned int id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr); //传递
glCompileShader(id); //编译
// TODO: Error handling
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (!result) {
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);//错误信息长度
char* message = (char*)alloca(length * sizeof(char));
//alloca可以在栈上动态分配
glGetShaderInfoLog(id, length, &length, message);
std::cout << "Fail to complie " << (type == GL_VERTEX_SHADER ? "vertex " : "fragment ") << "shader!" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}
return id;
}

ShaderProgramSource Shader::ParseShader(const std::string& filepath) {
std::ifstream stream(filepath);

enum class ShaderType {
NONE = -1, VERTEX = 0, FRAGMENT = 1
};

std::string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;

while (getline(stream, line)) {//string头文件中的getline方法
if (line.find("#shader") != std::string::npos) {
//std::string::npos,表示字符串末尾(无效字符串)
if (line.find("vertex") != std::string::npos) {
// set mode to vertex
type = ShaderType::VERTEX;
}
else if (line.find("fragment") != std::string::npos) {
// set mode to fragment
type = ShaderType::FRAGMENT;
}
}
else {
//把代码添加到vs或fs的字符串流中
ss[(int)type] << line << "\n";
}
}

return { ss[0].str(),ss[1].str() };
}

unsigned int Shader::CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
unsigned int program = glCreateProgram();
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program); //链接
glValidateProgram(program); //验证程序

//释放
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}

void Shader::Bind() const
{

glUseProgram(m_RendererID);
}

void Shader::Unbind() const
{
glUseProgram(0);
}

void Shader::SetUniform4f(const std::string& name, float v0, float v1, float v2, float v3)
{
glUniform4f(GetUniformLocation(name), v0, v1, v2, v3);
}

int Shader::GetUniformLocation(const std::string& name)
{
//优化:我们每次setUniform的时候,都需要重新GetUniformLocation,找到属性在Layout中的位置
//这是多余的消耗,我们可以用hash table 把它储存起来。
if (m_UniformLocationCache.find(name) != m_UniformLocationCache.end()) {
return m_UniformLocationCache[name];
}

int location = glGetUniformLocation(m_RendererID, name.c_str());
if (location == -1)
std::cout << "Warning: uniform '" << name << "' dosen't exist!" << std::endl;
m_UniformLocationCache[name] = location;
return location;

}