OpenGL教程

视频链接:OpenGL - YouTube

B站搬运链接:最好的OpenGL教程之一_哔哩哔哩_bilibili

Github仓库链接:N0w13re/OpenGL (github.com)

前三节没做笔记,可以参考:

openGl新手入门学习笔记(一)什么是openGl,使用glfw库和环境搭建_opengl glfw_埃卡洛斯的博客-CSDN博客

openGl新手入门学习笔记(二)下载glew,配置glew的环境与glew的初始化_埃卡洛斯的博客-CSDN博客

04 Vertex Buffers and Drawing a Triangle

在调整visual studio自动补全,遇到个问题是输入unsigned,当我按下nu会被自动补全为uint16,搞半天才发现是成员列表提交字符那里我输入了enter…于是每次遇到这些字符都会触发补全

正确做法是删掉这些字符,回车键是默认的

opengl是状态机,为生成的所有东西分配了唯一标识,是实际对象(顶点缓冲、顶点数组、纹理、着色器)的id,需要用这个对象时就调用这个id

1
2
unsigned int buffer;    
glGenBuffers(1, &buffer);

上述buffer就是id

1
glBindBuffer(GL_ARRAY_BUFFER, buffer)

该代码说明刚生成的buffer的用途

接下来既可以提供数据,也可以之后再提供。以下演示直接提供三角形数据:

1
2
3
4
5
6
float positions[6] = {
-0.5f, -0.5f,
0.0f, 0.5f,
0.5f, -0.5f
};
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);

这里教了在docs.gl中查阅函数参数,比如glBufferData(),通过该文档得知第二个参数以字节为单位,也得知参数usage中的static以及dynamic的区别

接下来是绘制。由于还没学到着色器(shader),没有index buffer,因此借助glDrawArrays()

1
glDrawArrays(GL_TRIANGLE, 0, 3);

image-20230826114915802

但这是不完整的,因此是黑屏输出

另一种绘制的函数是glDrawElements(),一般跟index buffer一起用,之后讲。

上述代码能绘制的原因在于之前有glBindBuffer()绑定了,若绑错了就会绘制到其他“图层”上去


05 Vertex Attributes and Layouts

首先要区分vertex和position是不同的,vertex是点,坐标只是它的一个attribute

指定attribute时需调用函数glVertexAttribPointer(),告诉OpenGL如何解释和处理顶点数据。下面介绍其参数:

  • index: 所需修改的attribute是第几个,比如可以规定坐标是第0个,颜色是第1个,法线(normal)是第2个……
  • size: 每个属性的组件数量,比如之前的每个坐标就有2个分量
  • type: 略
  • normalized: 将0-255规约至0-1
  • stride: buffer中一个vertex所有属性的字节大小和
  • position: 略,注意此position已经在顶点中了

例如,上述第一个vertex的第一个属性:

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

如果要第二个属性可以将position参数改为(const int *)4

此外还需在其前加上:

1
glEnableVertexAttribArray(0);

通过这两行代码,就告诉了opengl我们的buffer的layout是什么样的


06 How Shaders Work

着色器用于处理传入的数据,最流行的两种shader:vertex shader, fracture shader, 后者又称为pixel shader

rendering pipeline is how do we go from having data to actually having results on the screen

当我们发出draw call时,vertex shader, fracture shader先后被调用

vertex shader告诉opengl想在哪里绘制这三个点,这样opengl才能在屏幕上找到对应坐标。每个vertex调用一次。

fracture shader作用是告诉opengl三角形中每个像素点的颜色,用于之后rasterization stage中的rasterize(光栅化,也就是实际绘制该像素点)。每个像素点调用一次。


07 Writing a Shader

首先是创建shader的函数接口:

1
2
3
4
5
static int CreateShader(const std::string& vertexShader, const std::string& fractureShader)
{
unsigned int program = glCreateProgram();
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
}

接下来完成CompileShader():

1
2
3
4
5
6
7
8
9
10
11
static unsigned int CompileShader(unsigned int type, const std::string& source)
{
unsigned int id = glCreateShader(type);
const char* src = source.c_str(); //亦可写作&source[0],总之指向数据区首字节地址
glShaderSource(id, 1, &src, nullptr); //Replaces the source code in a shader object
glCompileShader(id);

//TODO: ERROR handling

return id;
}

此后继续完成CreateShader():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static unsigned int 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); //由于已经link到program上了,中间产物(intermediates)可以删掉
glDeleteShader(fs);

return program;
}

接下来完善CompileShader()中的TODO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static unsigned int 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);

int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result); //https://docs.gl/gl4/glCompileShader中有提及用该函数验证
if (!result) //可以是result == GL_FALSE, 不过GL_FALSE等于0
{
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
char *message = (char *)alloca(length * sizeof(char));
glGetShaderInfoLog(id, length, &length, message);
std::cout << "Failed to Compile " <<
(type == GL_VERTEX_SHADER ? "vertex" : "fragment") << "shader!" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}

return id;
}

其中alloc()函数是在栈上分配空间,详情参考malloc、calloc、realloc、new以及alloca函数区别_malloc new alloca_勇敢无畏的活着的博客-CSDN博客

此后在main()函数中调用CreateShader()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
std::string vertexShader =
"#version 330 core\n" //version 330:没用更新的版本是因为暂时不需要那么复杂
//core表示不使用deprecated的函数
"\n"
"layout(location = 0) in vec4 position;\n" //0是index of attribute
//至于为什么是vec4而不是vec2?因为之后gl_Position是vec4,就算这里用了vec2之后也得用vec4(position.xy,0,0)去扩展
"void main()\n"
"{\n"
"gl_Position = position;\n"
"}\n";

std::string fragmentShader =
"#version 330 core\n"
"\n"
"layout(location = 0) out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0, 0.0, 0.0, 1.0);\n" //rgba
"}\n";

unsigned int shader = CreateShader(vertexShader, fragmentShader);
glUseProgram(shader);

layout(location=0) in vec4 vPosition_layout(location = 0) in vec4 position;_Luckie stone的博客-CSDN博客

  1. vPosition指变量名称,它所保存的是顶点的位置信息

  2. vec4是GLSL中的一种数据类型,在这里表示GLSL的四维浮点数向量,默认值为(0,0,0,1),表示(x,y,z,w)。当有字段缺失时,会填充对应的默认值。

  3. in字段的话,表示设置这个变量,即vPosition为着色器阶段的输入变量,指定了数据进入着色器的流向,在这里代表数据从外部流入。

  4. layout(location=0),这一字段非常重要,它将vPosition的位置属性location设置为0,为它提供了元数据。

openGL之API学习(四十七)layout作用详解_glsl layout-CSDN博客

1
layout(location = attribute index) in vec3 position;

可以指定顶点着色器输入变量使用的顶点属性索引值,一般在glVertexAttribPointer中指定属性索引值。如果同时使用了glBindAttribLocation,那么这个layout优先。

©position 是这个顶点着色器中定义的一个输入变量,通常用于表示顶点的位置信息。下面是对 position 变量的详细解释:

  1. 变量类型position 被定义为 vec4 类型的变量。vec4 是GLSL中的一种矢量类型,表示一个包含四个分量的向量。通常,顶点的位置可以由一个三维向量 (x, y, z) 表示,但在这里使用了一个四维向量,通常情况下第四个分量(通常是w)被用于进行坐标变换和投影。

  2. 输入变量position 是一个输入变量,因为它以 in 关键字定义。这意味着 position 的值将由渲染管道的前阶段(通常是应用程序代码)提供给顶点着色器。这通常是从顶点缓冲对象(VBO)中读取的顶点数据。

  3. 位置属性:通常情况下,position 变量用于表示顶点在模型坐标系中的位置。这是指顶点相对于模型的局部坐标系的位置,还没有经过任何变换。在顶点着色器中,您可以对这些位置进行变换,以将它们转换到世界坐标系、相机坐标系和裁剪空间,从而最终在屏幕上进行渲染。

  4. 用途position 变量通常用于将顶点的位置信息传递给图形渲染管道的后续阶段,特别是裁剪阶段和视口变换阶段。在这些阶段,它将经过一系列的变换和处理,最终确定每个顶点在屏幕上的位置。

总之,position 是顶点着色器中用于表示顶点位置的输入变量,它是一个四维向量,通常表示顶点在模型坐标系中的位置,而在顶点着色器中,您可以对这些位置进行各种变换,以便进行后续的渲染操作。

该段GLSL与之前glVertexAtttibPointer()的联系:OpenGL glVertexAttribPointer()函数解析_soft_logic的博客-CSDN博客

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

第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0

总结下,layout那句是GLSL的声明,声明position这个变量,并使其获得属性位置0处的数据;而glVertexAttribPointer是定义从GL_ARRAY_BUFFER传数据进入shader的方式,比如给属性位置0处传入代表坐标的positions数组

编译生成得到红色三角形。如果去掉color后的分号,拿去编译,会提示

1
2
Failed to Compile fragmentshader!
ERROR: 0:4: 'void' : syntax error syntax error

表示我们的错误处理代码正确

此外,最后应加上清除代码

1
glDeleteProgram(shader);

08 How Do I Deal with Shaders

该节讲述了如何将上述shader代码独立成文件

新建res\shaders\Basic.shader,写入:

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

一开始没弄对,新建的是筛选器,筛选器只是表面上的分类,实际并未新建文件夹。需要点击“显示所有文件”,之后才能新建文件夹

VisualStudio如何在解决方案里添加真实的文件夹而不是虚的解决方案文件夹 - Kit_L - 博客园 (cnblogs.com)

此后在主文件中添加:

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
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};

static ShaderProgramSource ParseShader(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))
{
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != -1)
type = ShaderType::VERTEX;
else if (line.find("fragment") != -1)
type = ShaderType::FRAGMENT;
}
else
{
ss[(int)type] << line << "\n";
}
}

return { ss[0].str(), ss[1].str() }; //str()将stringstream转为string
}

其中stringstream参考C++编程语言中stringstream类介绍_liitdar的博客-CSDN博客

此后在main()中调用ParseShader()并测试(需删去后面跟shader有关的函数)

1
2
3
ShaderProgramSource source = "/res/shaders/Basic.shader";
std::cout << source.VertexSource << std::endl;
Std::cout << source.FragmentSource << std::endl;

报错:

image-20230902215629770

ParseShader()中参数前加上const即可

最后版本:

1
2
3
4
ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");

unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
glUseProgram(shader);

09 Index Buffers

如何画个正方形?将其看做两个三角形,因此需要修改如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
float positions[] = {
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,

0.5f, 0.5f,
-0.5f, 0.5f,
-0.5f, -0.5f
};
//...
glBufferData(GL_ARRAY_BUFFER, 6 * 2 * sizeof(float), positions, GL_STATIC_DRAW);
//...
glDrawArrays(GL_TRIANGLES, 0, 6);

缺点是position数组中有重复点,因此需要index buffer来复用

删去position数组中重复点,添加index buffer如下:

1
2
3
4
5
6
7
8
9
10
11
float positions[] = {
-0.5f, -0.5f, //0
0.5f, -0.5f, //1
0.5f, 0.5f, //2
-0.5f, 0.5f //3
};

unsigned int indices[] = { //必须unsigned
0, 1, 2,
2, 3, 0
};

仿照之前的buffer,另外创建绑定index buffer:

1
2
3
4
unsigned int ibo;
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); //第一个参数不同
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);

将while循环内的glDrawArrays()改为glDrawElements()

1
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);	//6是indices中元素个数;最后的参数是pointer to index buffer,由于之前已经将index buffer绑定在GL_ELEMENT_ARRAY_BUFFER,因此不需要放指针

10 Dealing with Errors

两种函数常用语处理错误,glGetError()glDebugMessageCallback(),后者功能更强但仅支持4.3版本及之后。本节内容主要关注前者。根据glGetError - OpenGL 4 - docs.gl:

To allow for distributed implementations, there may be several error flags. If any single error flag has recorded an error, the value of that flag is returned and that flag is reset to GL_NO_ERROR when glGetError is called. If more than one flag has recorded an error, glGetError returns and clears an arbitrary error flag value. Thus, glGetError should always be called in a loop, until it returns GL_NO_ERROR, if all error flags are to be reset.

必须在循环中调用。

在开头添加函数

1
2
3
4
5
6
7
8
9
10
11
12
static void GLClearError()
{
while (glGetError() != GL_NO_ERROR);
}

static void GLCheckError()
{
while (GLenum error = glGetError())
{
std::cout << "[OpenGL ERROR] (" << error << ")" << std::endl;
}
}

之后将原来glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);中的GL_UNSIGNED_INT改为GL_INT,同时在前后添加上述两个函数

1
2
3
GLClearError();
glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr);
GLCheckError();

运行可以得到1280的错误信息

将其通过打断点的方式转为十六进制0x500,在glew.h中搜索0x0500,可得#define GL_INVALID_ENUM 0x0500

然而这样的方式是在已知错误语句的情况下完成的,显然不符需要。因此采用断言

首先将GLCheckError()改为返回bool类型

1
2
3
4
5
6
7
8
9
static bool GLLogCall()
{
while (GLenum error = glGetError())
{
std::cout << "[OpenGL ERROR] (" << error << ")" << std::endl;
return false;
}
return true;
}

之后在开头添加宏#define ASSERT(x) if (!(x)) __debugbreak();

GLCheckError()改为ASSERT(GLLogCall());,运行后循环一次就会停下

__debugbreak()仅在MSVC环境下支持,clang等不支持

继续添加宏定义

1
2
3
#define GLCall(x) GLClearError();\		//注意\表示该行未结束,后面不能有空格
x;\
ASSERT(GLLogCall())

即可将之前的三句指令简化为GLCall(glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr));

多文件下想要快速定位出问题的地方,可以在GLLogCall()添加参数

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

将宏改为

1
2
3
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall(#x, __FILE__, __LINE__)) //#将后面的宏参数进行字符串

再次运行代码,就能定位出具体哪里出的bug

此后要做的就是,每个调用OpenGL函数的地方都加上GLCall()

有提到上面这种宏会有flaw(缺陷),当x是在单行if语句时,在两侧加上GLCall(),则仅有第一行代码会在if语句中。解决方案是采用scope或者do while循环

并不是很明白为什么那样会出问题,搜了下发现c++17后多了个init-statement,但不知道有何联系(?)

C++17新特性(2) – if/switch初始化(Init statement for if/switch)_c++17 新特性 if 定义_yangsenUCAS的博客-CSDN博客

但用scope的方法(就是加花括号)又会有别的问题,比如unsigned int id = glCreateShader(type);,若我们在其两侧加了花括号,将导致之后的id均无法使用(因为id在scope内)


11 Uniforms

uniform是从cpu获取数据到shader的一种方式,可以被当作变量使用。uniform每次绘制都需要设置。

将fragment shader部分改为:

1
2
3
4
5
6
uniform vec4 u_Color;

void main()
{
color = u_Color;
};

在main函数glUseProgram()后添加:

1
2
3
GLCall(int location = glGetUniformLocation(shader, "u_Color"));
ASSERT(location != -1);
GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

(C)这段代码看起来是使用OpenGL进行着色器参数设置的代码片段。让我们逐行进行分析:

  1. GLCall(int location = glGetUniformLocation(shader, "u_Color"));

    这一行代码调用了一个名为 GLCall 的函数,它的作用是用来检查OpenGL函数调用是否出错,并返回函数调用的结果。这里调用了 glGetUniformLocation 函数,该函数用于获取着色器程序中 uniform 变量的位置(location)。它的参数 shader 是着色器程序的句柄,而 "u_Color" 是要获取位置的 uniform 变量的名称。如果找不到该名称的 uniform 变量,glGetUniformLocation 将返回 -1。

  2. ASSERT(location != -1);

    这一行代码使用了一个断言,用于检查 glGetUniformLocation 返回的 location 是否不等于 -1。如果 location 等于 -1,这意味着无法找到名称为 “u_Color” 的 uniform 变量,可能是由于名称拼写错误或者该变量在着色器程序中不存在。断言用于在这种情况下终止程序的执行,以便及早发现问题。

  3. GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

    这一行代码使用 glUniform4f 函数来设置名为 “u_Color” 的 uniform 变量的值。它接受 location(在前一行获取的)作为第一个参数,然后将一个四维向量 (0.2f, 0.3f, 0.8f, 1.0f) 分配给该 uniform 变量。这将改变该 uniform 变量在着色器程序中的值,影响着色器的渲染结果。

总的来说,这段代码的作用是设置着色器程序中的一个 uniform 变量 “u_Color” 的值为 (0.2f, 0.3f, 0.8f, 1.0f),并且在找不到该 uniform 变量或者其他OpenGL调用错误时进行错误检查和处理。注意,可能存在与 GLCallASSERT 相关的宏定义或函数实现,以执行错误检查和处理的具体逻辑,但这里的代码片段并未包含这些定义或实现。

再做出如下改动:

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
GLCall(int location = glGetUniformLocation(shader, "u_Color"));
ASSERT(location != -1);
GLCall(glUniform4f(location, 0.8f, 0.3f, 0.8f, 1.0f));

float r = 0.0f;
float increment = 0.05f;
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
GLCall(glClear(GL_COLOR_BUFFER_BIT));

GLCall(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();
}

效果是在蓝和粉之间跳跃。因为红通道在0和1之间跳跃。

glfwMakeContext();后加上glfwSwapInterval(1);,用于开启图形渲染的垂直同步

glfwSwapInterval 是 GLFW 库中的函数,用于控制垂直同步(Vertical Synchronization,简称V-Sync)的行为。V-Sync 是一种图形渲染技术,用于防止图像撕裂(tearing),同时将帧速率与显示器的刷新率同步。

以下是有关 glfwSwapInterval 函数的详细信息:

  1. 函数签名

    1
    void glfwSwapInterval(int interval);
  2. 功能

    • glfwSwapInterval 用于控制图形渲染的垂直同步。
    • interval 参数指定了要设置的垂直同步模式。通常情况下,它可以是以下之一:
      • 0:禁用垂直同步,允许无限制地渲染帧,可能会导致撕裂。
      • 1:启用垂直同步,将渲染帧与显示器的刷新率同步,防止撕裂。

垂直同步对于控制帧速率以及提高图形渲染的平滑度非常有用,特别是在游戏和图形应用程序中。

Q: 如何更改使得长方形沿对角,分别显示两种不同的颜色?

C: 要使长方形沿对角线分别显示两种不同的颜色,你可以在顶点数据中添加颜色信息,并在着色器中根据顶点的位置来插值颜色。以下是如何修改你的代码来实现这个效果的步骤:

  1. 修改顶点数据:

    首先,你需要修改顶点数据以便每个顶点都有一个颜色属性。为了使矩形沿对角线分别显示两种不同的颜色,你可以为每个顶点分配不同的颜色值。这里,我们假设你要使用红色和绿色两种颜色,可以修改你的 positions 数组和顶点属性的设置如下:

    1
    2
    3
    4
    5
    6
    7
    float vertices[] = {
    // 位置 // 颜色
    -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, // 左下角,红色
    0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // 右下角,绿色
    0.5f, 0.5f, 0.0f, 0.0f, 1.0f, // 右上角,蓝色
    -0.5f, 0.5f, 1.0f, 1.0f, 0.0f // 左上角,黄色
    };

    这里我们在每个顶点后面添加了颜色属性,每个顶点有五个值:x坐标、y坐标、红色分量、绿色分量和蓝色分量。

  2. 修改顶点属性指针:

    在你的代码中,你需要修改顶点属性指针来正确处理颜色数据。在 GLCall(glVertexAttribPointer(...)) 调用中,修改如下:

    1
    2
    3
    4
    5
    6
    7
    // 位置属性
    GLCall(glEnableVertexAttribArray(0));
    GLCall(glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 0));

    // 颜色属性
    GLCall(glEnableVertexAttribArray(1));
    GLCall(glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float))));

    这里,我们启用了第一个属性(位置属性)和第二个属性(颜色属性),并通过 glVertexAttribPointer 指定了它们的属性大小和偏移量。位置属性有两个浮点数(x和y坐标),所以属性大小是2,颜色属性有三个浮点数(红色、绿色和蓝色分量),所以属性大小是3。颜色属性的偏移量为 2 * sizeof(float),因为位置属性占据了前两个浮点数的空间。

  3. 修改着色器:

    在着色器中,你需要接收颜色属性并在片段着色器中插值它们。以下是修改着色器的示例:

    顶点着色器(Basic.shader 中的内容):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #version 330 core

    layout(location = 0) in vec2 a_Position;
    layout(location = 1) in vec3 a_Color;

    out vec3 v_Color;

    void main()
    {
    gl_Position = vec4(a_Position, 0.0, 1.0);
    v_Color = a_Color;
    }

    片段着色器(Basic.shader 中的内容):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #version 330 core

    in vec3 v_Color;

    out vec4 FragColor;

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

    这里,我们在顶点着色器中接收颜色属性并将它传递到片段着色器中,然后在片段着色器中使用颜色属性设置片段颜色。

完成这些步骤后,你的长方形应该沿对角线分别显示红色和绿色,因为你在每个顶点上指定了不同的颜色值。希望这能帮助你实现你的目标!


12 Vertex Arrays

glVertexAttribPointer()并不是跟缓冲区绑定在一起的(并非存储在buffer中),而是跟vertex array object绑定在一起的。

为做测试,先在while循环前解绑所有:

1
2
3
GLCall(glUseProgram(0));
GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

将while循环改为:

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
while (!glfwWindowShouldClose(window))
{
/* Render here */
GLCall(glClear(GL_COLOR_BUFFER_BIT));

GLCall(glUseProgram(shader));
GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

GLCall(glBindBuffer(GL_ARRAY_BUFFER, buffer));
GLCall(glEnableVertexAttribArray(0));
GLCall(glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0));

GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));

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();
}

可得到相同结果。问题是,每次都需要glEnableVertexAttribArray(0)以及glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0)吗?答案是是的,因为可能下次画的时候layout就不同了。而vertex array object恰好包含这个信息,如果我们为每个要绘制的图形创建一个顶点数组对象,则对于每次绘制,我们仅需要绑定顶点数组对象。因为VAO包含vertex buffer与实际的vertex specification或者说vertex layout间的绑定;换句话说glVertexAttribPointer()会绑定vertex buffer和array buffer(GL_ARRAY_BUFFER)。OpenGL compatability profile会默认为我们创建VAO,但core profile不会,因此需要我们手动创建。

Vertex_Array_Object

The compatibility OpenGL profile makes VAO object 0 a default object. The core OpenGL profile makes VAO object 0 not an object at all. So if VAO 0 is bound in the core profile, you should not call any function that modifies VAO state. This includes binding the GL_ELEMENT_ARRAY_BUFFER with glBindBuffer.

©在OpenGL中,VAO(Vertex Array Object)是一种用于管理顶点数据和渲染状态的对象。OpenGL有两种不同的配置文件(profile):兼容性(compatibility)配置文件和核心(core)配置文件。这两个配置文件的行为在某些方面有所不同。

  1. 兼容性OpenGL配置文件:在兼容性配置文件中,VAO对象0被视为默认对象。这意味着如果没有显式绑定VAO,OpenGL将使用默认的VAO对象0。在此配置文件中,可以调用一些函数来修改VAO对象0的状态,包括绑定GL_ELEMENT_ARRAY_BUFFER

  2. 核心OpenGL配置文件:在核心配置文件中,VAO对象0不被视为对象。这意味着VAO对象0在核心配置文件中不可用。如果尝试在核心配置文件中绑定VAO对象0,应该避免调用任何会修改VAO状态的函数,包括绑定GL_ELEMENT_ARRAY_BUFFER

简而言之,兼容性OpenGL配置文件允许VAO对象0的使用,并允许在其上进行状态更改操作,而核心OpenGL配置文件禁用了VAO对象0,因此在核心配置文件中不应该对其进行任何操作。这是OpenGL版本之间的一个重要差异,需要根据所使用的配置文件来谨慎管理VAO对象的状态。

glfwCreateWindow()前添加:

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

此时会将版本号调整为3.3,并用core profile,运行,就会在glEnableVertexAttribArray()报错,1282(=0x00000502),在glew.h中搜索0x0502可知这是GL_INVALID_OPERATION,再从glEnableVertexAttribArray - OpenGL 4 - docs.gl得知,这是因为没有VAO被绑定。(确切的说,根据Vertex_Array_Object,core profile根本就没有VAO 0)

glBindBuffer()前添加:

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

再度运行,成功。

更进一步,将while循环改成:(对比起见,在解绑的三条指令前添加GLCall(glBindVertexArray(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
while (!glfwWindowShouldClose(window))
{
/* Render here */
GLCall(glClear(GL_COLOR_BUFFER_BIT));

GLCall(glUseProgram(shader));
GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

GLCall(glBindVertexArray(vao));
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));

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();
}

此时在while循环中删去了一些指令,只剩下绑定vao以及index buffer,但效果与之前相同。

解释:glBindVertexArray(vao)以及glBindBuffer(GL_ARRAY_BUFFER, buffer)并没有将二者连接起来,但glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0) makes index 0 of this vertex array be bound to GL_ARRAY_BUFFER.

不知道啥意思,总之就是这条指令连接了VAO与VBO(vertex buffer object, 也即buffer变量对应的对象)

如果将glVertexAttribPointer()的第一个参数改为1, 那么vao的index 1会指向其他buffer.

实际上也可以将index buffer绑定在vao上

大型项目的两种做法:一种是就只要一个vao,每次绑定不同的buffer;另一种是每个geometry都设置一个vao. 各有优劣,依实际选择。


13 Abstracting into Classes

将错误处理部分单独提出来作为Renderer.cpp以及Renderer.h

将生成GL_ARRAY_BUFFER的部分提出来作为VertexBuffer.cpp以及.h

将生成GL_ELEMENT_ARRAY_BUFFER的部分作为IndexBuffer.cpp以及.h

在生成index buffer的时候,将参数从vertex buffer的const void* 改为const unsigned int* size改为count,因为size一般是作为字节数,而这里已经声明了是unsigned int,则应该传入其个数count

修改Application.cpp后运行,仍能弹出矩形,但关闭窗口后,命令台并不会退出。点击暂停调试,查看堆栈:

image-20230923114142560

这是因为程序退出时要运行析构函数,但在return 0前就已经有glfwTerminate(),摧毁了OpenGL context. 此时由于失去context,GLGetError()就会一直报错

我们可以通过new与delete手动调用解构,不过这里用scope的方式,加花括号。


14 Buffer Layout Abstraction

VAO的作用是将vertex buffer以及其layout整合在一起

本节将vertex buffer, index buffer, vertex buffer layout以及最主要的vertex array分别抽象出来为单独的文件


15 Shader Abstraction

将Shader抽象出Shader.cpp及.h


16 Writing a Basic Renderering

在Renderer.h中添加Renderer类并加入成员函数Draw()

不过在编译时出现如下报错:

1
2
严重性	代码	说明	项目	文件	行	禁止显示状态
错误 C3861 “ASSERT”: 找不到标识符 OpenGL D:\Projects\OpenGL\OpenGL\src\VertexBufferLayout.h 20

但打开VertexBufferLayout.h会看到我们包含了Renderer.h文件,而Renderer.h中有ASSERT的宏定义

image-20230926142756700

究其原因,此时我们在LayoutBuffer.h中#include "Renderer.h",又在Renderer.h中#include "VertexArray.h"(包含这个是因为Draw函数需要使用),而VertexArray.h中有#include "VertexBufferLayout.h",如此导致循环包含,这种情况会导致编译器陷入无限循环,因为它无法确定要包含的头文件的顺序。

此处采用的方法是将VertexArray.h添加class VertexBufferLayout;,并在VertexBuffer.cpp中加入#include "VertexBufferLayout.h"

©在VertexArray.h这段代码中,class VertexBufferLayout; 是一个前置声明(forward declaration)。前置声明是一种告诉编译器某个类或类型将在稍后的代码中定义的方式。它告诉编译器有关类 VertexBufferLayout 的基本信息,但不提供关于类的详细定义。这种声明通常用于解决循环依赖或仅需要知道类型的名称而不需要知道其内部结构的情况。

在这里,VertexArray 类中使用了 VertexBufferLayout 类的对象作为参数,但它并没有包含 VertexBufferLayout.h 的头文件。相反,它只是在类定义之前声明了 VertexBufferLayout 类。这允许编译器在编译此文件时知道 VertexBufferLayout 类的名称和基本信息,而不需要了解其详细定义。然后,在其他地方的代码中,你需要包含 VertexBufferLayout.h 头文件,以获得关于 VertexBufferLayout 类的实际定义。

这种前置声明有助于减少编译时间,特别是在大型代码库中,因为它可以减少不必要的头文件包含。但请确保在使用 VertexBufferLayout 类的任何实际功能之前,你必须包含相关的头文件以获取完整的定义。

弹幕说这种方式对于大型项目是危险的,正确处理方式是将宏定义单独写在一个头文件中

完成后,此时每次渲染前仍需绑定shader以及指定uniform,这很麻烦,因此我们需要学习material. material就是renderer+shaders.


17 Textures

新建Texture.h,输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once

#include "Renderer.h"

class Texture
{
private:
unsigned int m_RendererID;
std::string m_FilePath;
unsigned char* m_LocalBuffer;
int m_Width, m_Height, m_BPP;
public:
Texture(const std::string& path);
~Texture();

void Bind(unsigned int slot = 0) const;
void Unbind();

inline int GetWidth() const { return m_Width; }
inline int GetHeight() const { return m_Height; }
};

其中Bind()需要加参数slot是因为可同时绑定多个texture,在windows上一般是32个,而手机上一般就只有8个(取决于GPU)

Texture类的构造函数中添加stbi_set_flip_vertically_on_load(1);,因为OpenGL希望texture从左下角开始,左下角是(0,0). 而PNG是从上到下的,所以应当flip. 此后的m_LoadBuffer = stbi_load(path.c_str(), &m_Width, &m_Height, &m_BPP);函数创建了加载texture的buffer.

在构造函数中创建Texture,并规定参数:

GLCall(glGenTextures(1, &m_RendererID));
GLCall(glBindTexture(GL_TEXTURE_2D, m_RendererID));

GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));

©这里的代码执行了一系列OpenGL函数调用,用于设置纹理对象的参数,具体如下:

  1. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR):设置2D纹理的缩小过滤器(minification filter)为线性过滤(GL_LINEAR)。这意味着当纹理被缩小时,OpenGL会使用线性插值来计算像素颜色。
  2. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR):设置2D纹理的放大过滤器(magnification filter)为线性过滤(GL_LINEAR)。这意味着当纹理被放大时,OpenGL会使用线性插值来计算像素颜色。
  3. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP):设置2D纹理的水平包裹模式(wrap mode)为GL_CLAMP,这意味着当纹理坐标超出[0,1]的范围时,OpenGL会使用边缘的纹理像素颜色。
  4. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP):设置2D纹理的垂直包裹模式(wrap mode)为GL_CLAMP,与上一行类似,这意味着当纹理坐标超出[0,1]的范围时,OpenGL会使用边缘的纹理像素颜色。

接下来将buffer中数据传给texture:

1
GLCall(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Width, m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_LocalBuffer));

第三个参数Internal_format是数据在内存中的数据格式,倒数第二个参数format则是传给OpenGL的数据格式

  • GL_TEXTURE_2D:指定了目标纹理类型,这里是2D纹理。
  • 0:指定了纹理的层级(level)。通常,0表示基本的纹理级别。
  • GL_RGBA8:指定了纹理的内部格式(internal format)。在这里,它表示纹理内部存储的像素数据使用RGBA通道,每个通道占8位(一个字节)。
  • m_Width:纹理的宽度(以像素为单位)。
  • m_Height:纹理的高度(以像素为单位)。
  • 0:指定了边框的宽度。通常情况下,这是0。
  • GL_RGBA:指定了源图像数据的格式,这里是RGBA格式。
  • GL_UNSIGNED_BYTE:指定了源图像数据的数据类型,这里是无符号字节数据类型。
  • m_LocalBuffer:包含纹理像素数据的指针,这是你要加载到纹理中的图像数据。

这行代码的目的是将 m_LocalBuffer 中的图像数据加载到2D纹理对象中,并指定了有关纹理的各种参数,包括纹理的大小、格式等。这样,在渲染时,你可以将这个纹理绑定到一个纹理单元并使用它来渲染物体。

Texture::Bind()函数的定义中,glActiveTexture(GL_TEXTURE0 + slot)用于激活纹理单元(纹理槽),一旦激活纹理单元,将会影响其上绑定的纹理对象。+ slot是因为GL_TEXTURE1 = GLTEXTURE0 + 1, …,可以此激活不同的纹理对象。

此后在Application.cpp中建立Texture对象,并在texture.Bind()后跟上shader.SetUniform1i("u_Texture", 0);,因为前者在我们的函数中默认绑定了slot 0.

我们还需要建立texture坐标,在positions数组中添加:

1
2
3
4
5
6
float positions[] = {
-0.5f, -0.5f, 0.0f, 0.0f, //0
0.5f, -0.5f, 1.0f, 0.0f, //1
0.5f, 0.5f, 1.0f, 1.0f, //2
-0.5f, 0.5f, 0.0f, 1.0f //3
};

相应的更改VertexBufferLayout, Shader

修改后的Basic.shader:

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
#shader vertex
#version 330 core

layout(location = 0) in vec4 position;
layout(location = 1) in vec2 texCoord;

out vec2 v_TexCoord;

void main()
{
gl_Position = position;
v_TexCoord = texCoord;
};

#shader fragment
#version 330 core

layout(location = 0) out vec4 color;

in vec2 v_TexCoord;

uniform vec4 u_Color;
uniform sampler2D u_Texture;

void main()
{
vec4 texColor = texture(u_Texture, v_TexCoord); //使用 texture 函数从纹理 u_texture 中采样纹理颜色,并将结果存储在 textColor 中
color = texColor;
};

此后得到的结果仍有些怪异,因为未启用Blending,因此透明区域未被渲染。在Application.cpp中添加:

1
2
GLCall(glEnable(GL_BLEND));
GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); //定义如何正确blend alpha pixel

当想要在原有颜色上重新绘制其他颜色,设置1α1-\alpha的透明度就可以看出差异。

©glBlendFunc 函数用于设置混合函数,它决定了如何混合源颜色(新绘制的颜色)和目标颜色(已存在的颜色)。

  • GL_SRC_ALPHA 表示源颜色使用 alpha 值作为权重。
  • GL_ONE_MINUS_SRC_ALPHA 表示目标颜色使用 1 减去源颜色的 alpha 值作为权重。
  • 这个混合函数通常用于标准的透明度混合,其中源颜色的 alpha 值用于控制新绘制的颜色在最终混合中的权重,而目标颜色的 alpha 值则用于控制已存在颜色在混合中的权重。

18 Blending

image-20230927001644674 image-20230927001823159

上图中,glBlendFunc()用于定义src和dest的混合权重,glBlendEquation()用于定义如何计算混合值。

image-20230927002003981 image-20230927002056148

19 Maths

引入glm库,使用glm库创建正交投影矩阵

1
glm::mat4 proj = glm::ortho(-2.0f, 2.0f, -1.5f, 1.5f, -1.0f, 1.0f);

glm::mat4 表示创建一个4x4的矩阵,这是通常用于进行图形变换的矩阵类型。

  • glm::ortho 是glm库中的一个函数,用于创建正交投影矩阵。它接受6个参数,分别是左、右、底、顶、近裁剪面和远裁剪面的坐标值。
    • -2.0f2.0f 分别表示左和右裁剪面的x坐标范围,即从-2到2。
    • -1.5f1.5f 分别表示底和顶裁剪面的y坐标范围,即从-1.5到1.5。
    • -1.0f1.0f 分别表示近裁剪面和远裁剪面的z坐标范围,即从-1到1。

在Basic.shader中vertex shader部分添加uniform mat4 u_MVP;(model view projection),并在输出坐标乘上该投影。

在Application.cpp中添加shader.SetUniformMat4f("u_MVP", proj);

在shader.h及cpp中完善该函数,需调用glUniformMatrix4fv(GetUniformLocation(name), 1, GL_FALSE, &matrix[0][0]),其中GL_FALSE表示不需要转置该矩阵


20 Projection Matrices

从3D到2D,需要投影矩阵。

image-20230927094206912

在之前的glm::mat4 proj = glm::ortho(-2.0f, 2.0f, -1.5f, 1.5f, -1.0f, 1.0f);中,规定了x轴视图只有-2到2,超出该范围的点将不在屏幕上显示

该正交矩阵的作用在于将输入坐标投影至-1到1的区间上(屏幕上)

比如将position数组及正交矩阵改为:

1
2
3
4
5
6
7
8
float positions[] = {
100.0f, 100.0f, 0.0f, 0.0f, //0
200.0f, 100.0f, 1.0f, 0.0f, //1
200.0f, 200.0f, 1.0f, 1.0f, //2
100.0f, 200.0f, 0.0f, 1.0f //3
};
//...
glm::mat4 proj = glm::ortho(0.0f, 640.0f, 0.0f, 480.0f, -1.0f, 1.0f);

则图标会出现在屏幕左下角位置

注:vp以及result只是调试时测试投影矩阵功能的


21 Model View Projection

View matrix, or eye matrix, is the view of camera.

Model matrix is the way we simulate model. TRS.

view matrix是相机position and orientation,model matrix是object transform.

我们要做的是将这三种矩阵乘起来。

模型(Model)、观察(View)和投影(Projection)详解_model projection-CSDN博客

计算机图形学 5:齐次坐标与 MVP 矩阵变换 - 知乎 (zhihu.com)

上节中建立了projection matrix,下面建立view matrix.

view matrix是相机,当我们把相机左移100时相当于将物体右移100

1
2
3
4
5
6
7
8
9
glm::mat4 proj = glm::ortho(0.0f, 640.0f, 0.0f, 480.0f, -1.0f, 1.0f);
glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(-100, 0, 0));

glm::mat4 mvp = proj * view;

Shader shader("res/shaders/Basic.shader");
shader.Bind();
shader.SetUniform4f("u_Color", 0.8f, 0.3f, 0.8f, 1.0f);
shader.SetUniformMat4f("u_MVP", mvp);

接下来是model matrix,我们将之左移200并上移200

1
2
3
4
5
glm::mat4 proj = glm::ortho(0.0f, 640.0f, 0.0f, 480.0f, -1.0f, 1.0f);
glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(-100, 0, 0));
glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(200, 200, 0));

glm::mat4 mvp = proj * view * model;

22 ImGui

通过一个已有的ui便捷地控制参数,地址:1.60 · Releases · ocornut/imgui (github.com)

复制部分文件到工程目录下,其中main.cpp不能包含在工程中,仅做用例

通过对main.cpp的模仿,改编Application.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
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
#include <GL/glew.h>
#include <GLFW/glfw3.h>

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

#include "Renderer.h"

#include "VertexBuffer.h"
#include "VertexBufferLayout.h"
#include "IndexBuffer.h"
#include "VertexArray.h"
#include "Shader.h"
#include "Texture.h"

#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"

#include "imgui/imgui.h"
#include "imgui/imgui_impl_glfw_gl3.h"

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 << "Error!" << std::endl;

std::cout << glGetString(GL_VERSION) << std::endl;

{
float positions[] = {
100.0f, 100.0f, 0.0f, 0.0f, //0
200.0f, 100.0f, 1.0f, 0.0f, //1
200.0f, 200.0f, 1.0f, 1.0f, //2
100.0f, 200.0f, 0.0f, 1.0f //3
};

unsigned int indices[] = {
0, 1, 2,
2, 3, 0
};

GLCall(glEnable(GL_BLEND));
GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

VertexArray va;
VertexBuffer vb(positions, 4 * 4 * sizeof(float));

VertexBufferLayout layout;
layout.Push<float>(2);
layout.Push<float>(2);
va.AddBuffer(vb, layout);

IndexBuffer ib(indices, 6);

glm::mat4 proj = glm::ortho(0.0f, 640.0f, 0.0f, 480.0f, -1.0f, 1.0f);
glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(-100, 0, 0));
glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(200, 200, 0));

glm::mat4 mvp = proj * view * model;

Shader shader("res/shaders/Basic.shader");
shader.Bind();
shader.SetUniform4f("u_Color", 0.8f, 0.3f, 0.8f, 1.0f);
shader.SetUniformMat4f("u_MVP", mvp);

Texture texture("res/textures/ChernoLogo.png");
texture.Bind();
shader.SetUniform1i("u_Texture", 0);

va.Unbind();
shader.Unbind();
vb.Unbind();
ib.Unbind();

Renderer renderer;

ImGui::CreateContext();
ImGui_ImplGlfwGL3_Init(window, true);
ImGui::StyleColorsDark();

bool show_demo_window = true;
bool show_another_window = false;
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
float r = 0.0f;
float increment = 0.05f;
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
renderer.Clear();

ImGui_ImplGlfwGL3_NewFrame();

shader.Bind();
shader.SetUniform4f("u_Color", r, 0.3f, 0.8f, 1.0f);

renderer.Draw(va, ib, shader);

if (r > 1.0f)
increment = -0.05f;
else if (r < 0.0f)
increment = 0.05f;

r += increment;

{
static float f = 0.0f;
static int counter = 0;
ImGui::Text("Hello, world!"); // Display some text (you can use a format string too)
ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f
ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color

ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our windows open/close state
ImGui::Checkbox("Another Window", &show_another_window);

if (ImGui::Button("Button")) // Buttons return true when clicked (NB: most widgets return true when edited/activated)
counter++;
ImGui::SameLine();
ImGui::Text("counter = %d", counter);

ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
}

ImGui::Render();
ImGui_ImplGlfwGL3_RenderDrawData(ImGui::GetDrawData());

/* Swap front and back buffers */
glfwSwapBuffers(window);

/* Poll for and process events */
glfwPollEvents();
}
}

ImGui_ImplGlfwGL3_Shutdown();
ImGui::DestroyContext();
glfwTerminate();
return 0;
}

此后将model matrix以及mvp移入while循环中,这样可以通过滑块更新每次打印时的model matrix.

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
while (!glfwWindowShouldClose(window))
{
/* Render here */
renderer.Clear();

ImGui_ImplGlfwGL3_NewFrame();

glm::mat4 model = glm::translate(glm::mat4(1.0f), translation); //每次循环都可以改变model
//mat4(1.0f)是indentity matrix
glm::mat4 mvp = proj * view * model;

shader.Bind();
shader.SetUniform4f("u_Color", r, 0.3f, 0.8f, 1.0f);
shader.SetUniformMat4f("u_MVP", mvp);

renderer.Draw(va, ib, shader);

if (r > 1.0f)
increment = -0.05f;
else if (r < 0.0f)
increment = 0.05f;

r += increment;

{
ImGui::SliderFloat3("Translation", &translation.x, 0.0f, 960.0f); //关联滑块,0到960是之前设置的范围
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
}

ImGui::Render();
ImGui_ImplGlfwGL3_RenderDrawData(ImGui::GetDrawData());

/* Swap front and back buffers */
glfwSwapBuffers(window);

/* Poll for and process events */
glfwPollEvents();
}

23 Rendering Multiple Objects

1
2
3
4
5
6
7
8
{
glm::mat4 model = glm::translate(glm::mat4(1.0f), translationA);
glm::mat4 mvp = proj * view * model;
shader.Bind();
shader.SetUniformMat4f("u_MVP", mvp);

renderer.Draw(va, ib, shader);
}

这个块多次复制即可渲染出多个物体。

然而这样的效率是低下的,因此之后要学习batch rendering.


24 Setting up a Test Framework

建立测试,方法是在src/test下建立Test.h,之后建立继承类(本节介绍了TestClearColor)

之后更改Application.cpp以进行测试。但这样很破坏主程序,因此之后将采用菜单的方式测试。


25 Creating Tests

先在之前的Test.h中添加TestMenu

1
2
3
4
5
6
7
8
9
10
11
12
13
class TestMenu : public Test
{
public:
TestMenu(Test*& currentTestPointer);
~TestMenu();

void OnUpdate(float deltatime) override;
void OnRender() override;
void OnImGuiRender() override;
private:
Test*& m_CurrentTest;
std::vector<std::pair<std::string, std::function<Test*()>>> m_Tests;
};

为什么不在Test类中用纯虚函数?因为之后的派生类中可以直接不写void OnUpdate(float deltatime) override;以及其他两个;而纯虚函数则不可能这样

m_Tests 是一个私有成员变量,它是一个 std::vector,这是C++标准库提供的一种动态数组容器。在这个上下文中,std::vector 是一个能够动态增长和缩小的数组,存储类型为 std::pair 的元素。

std::pair 是C++标准库提供的一个模板类,它可以存储两个不同类型的值。在这里,std::pair 包含了一个 std::function 对象,该对象可以持有可调用的实体(函数指针、函数对象、Lambda表达式等),以及这个可调用实体返回的 Test* 类型的指针。

所以,m_Tests 是一个动态数组,每个元素是一个 std::pair,其中包含了一个可以返回 Test* 指针的可调用实体。这样的设计通常用于实现一种工厂模式,即根据不同的可调用实体创建不同类型的对象。在这个特定的代码片段中,m_Tests 的目的可能是用于创建不同类型的 Test 对象。

之后还需要在TestMenu中添加模板函数RegisterTest()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestMenu : public Test
{
public:
TestMenu(Test*& currentTestPointer);

void OnImGuiRender() override;

template<typename T>
void RegisterTest(const std::string& name)
{
std::cout << "Register test " << name << std::endl;
m_Tests.push_back(std::make_pair(name, []() { return new T(); }));
}
private:
Test*& m_CurrentTest;
std::vector<std::pair<std::string, std::function<Test*()>>> m_Tests;
};

Lambda 表达式中包含了一个无参数的函数,该函数返回一个新创建的测试类的指针(new T())。

这是lambda表达式的基本语法:

1
2
3
cppCopy code[捕获列表](参数列表) -> 返回类型 {
// 函数体
}

在这个代码中的lambda表达式是这样的:

1
2
cppCopy code
[]() { return new T(); }
  • [] 是一个空的捕获列表,表示lambda表达式没有捕获任何外部变量。
  • () 是参数列表,这里是空的,表示lambda函数不接受任何参数。
  • { return new T(); } 是lambda函数的函数体。它包含一个单一的语句,即使用 new 操作符创建了一个类型为 T 的新对象,并将其指针返回。

在这个上下文中,lambda表达式充当了一个无参数函数,每当它被调用时,它会动态地创建一个新的 T 类型的对象,并将对象的指针返回。在这种情况下,lambda表达式被用作一个工厂函数,用于创建不同类型的测试对象。

总结起来,lambda表达式允许你在这个代码中以一种简洁的方式定义了一个匿名的函数,该函数的行为在每次被调用时都是动态的,它用于创建不同类型的测试对象。

此后修改Application.cpp以完成相应任务


26 Creating a Texture Test

构造函数:

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
test::TestTexture2D::TestTexture2D()
: m_Proj(glm::ortho(0.0f, 640.0f, 0.0f, 480.0f, -1.0f, 1.0f)),
m_View(glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0))),
m_TranslationA(200, 200, 0), m_TranslationB(400, 200, 0)
{
float positions[] = {
-50.0f, -50.0f, 0.0f, 0.0f, //0
50.0f, -50.0f, 1.0f, 0.0f, //1
50.0f, 50.0f, 1.0f, 1.0f, //2
-50.0f, 50.0f, 0.0f, 1.0f //3
};

unsigned int indices[] = {
0, 1, 2,
2, 3, 0
};

GLCall(glEnable(GL_BLEND));
GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

m_VAO = std::make_unique<VertexArray>();
VertexBuffer vb(positions, 4 * 4 * sizeof(float));

VertexBufferLayout layout;
layout.Push<float>(2);
layout.Push<float>(2);
m_VAO->AddBuffer(vb, layout);

m_IndexBuffer = std::make_unique<IndexBuffer>(indices, 6);

m_Shader = std::make_unique<Shader>("res/shaders/Basic.shader");
m_Shader->Bind();
m_Shader->SetUniform4f("u_Color", 0.8f, 0.3f, 0.8f, 1.0f);

m_Texture = std::make_unique<Texture>("res/textures/ChernoLogo.png");
m_Shader->SetUniform1i("u_Texture", 0);
}

此时按F5生成整个项目并运行,点击2D Texture会出错。在出错时将线程回到main

image-20231007155213213

根据下面的输出

image-20231007155246373

(此处不知为何跟视频不一样)发现问题发生在TestTexture2D::OnRender()renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);

最后发现是上述代码中VertexBuffer vb(positions, 4 * 4 * sizeof(float));有误,因为该vb在上述TestTexture2D的构造函数结束时被析构(也即,调用了~VertexBuffer()


27 How to make your Uniforms Faster

之前做过了,就是在Shader.cpp中添加了一个unordered_map

不过这里进一步将GetUniformLocation()改成const函数,并在unordered_map前添加mutable关键词以示可以修改

mutable 用来解决常函数中不能修改对象的数据成员的问题。


28 Batch Rendering - An Introduction

image-20231007171841698

如上图所示,若按原本的方式渲染两个正方形,则需要调用两次draw call. 但若我们将两个vertex buffer以及index buffer合并,则可以在一次draw call完成两个正方形的绘制。与之而来的是,vertex buffer应为动态的,这样在每次绘制前都能传输数据(不是很懂?)


29 Batch Rendering - Colors

只需要在上节基础上加上颜色即可


30 Batch Rendering - Textures

加入另一张图片,用另一个纹理进行采样。构造函数:

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
test::TestBatchRender::TestBatchRender()
: m_Proj(glm::ortho(0.0f, 640.0f, 0.0f, 480.0f, -1.0f, 1.0f)),
m_View(glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0))),
m_TranslationA(200, 200, 0), m_TranslationB(400, 200, 0)
{
float positions[] = {
100.0f, 100.0f, 0.0f, 1.0f, 0.18f, 0.6f, 0.96f, 1.0f, 0.0f, 0.0f, 0.0f,
200.0f, 100.0f, 0.0f, 1.0f, 0.18f, 0.6f, 0.96f, 1.0f, 1.0f, 0.0f, 0.0f,
200.0f, 200.0f, 0.0f, 1.0f, 0.18f, 0.6f, 0.96f, 1.0f, 1.0f, 1.0f, 0.0f,
100.0f, 200.0f, 0.0f, 1.0f, 0.18f, 0.6f, 0.96f, 1.0f, 0.0f, 1.0f, 0.0f,

300.0f, 100.0f, 0.0f, 1.0f, 1.0f, 0.93f, 0.24f, 1.0f, 0.0f, 0.0f, 1.0f,
400.0f, 100.0f, 0.0f, 1.0f, 1.0f, 0.93f, 0.24f, 1.0f, 1.0f, 0.0f, 1.0f,
400.0f, 200.0f, 0.0f, 1.0f, 1.0f, 0.93f, 0.24f, 1.0f, 1.0f, 1.0f, 1.0f,
300.0f, 200.0f, 0.0f, 1.0f, 1.0f, 0.93f, 0.24f, 1.0f, 0.0f, 1.0f, 1.0f
};

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

GLCall(glEnable(GL_BLEND));
GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

m_VAO = std::make_unique<VertexArray>();

m_VertexBuffer = std::make_unique<VertexBuffer>(positions, 8 * 11 * sizeof(float));
VertexBufferLayout layout;
layout.Push<float>(4); //postion
layout.Push<float>(4); //color
layout.Push<float>(2); //texture coordinate
layout.Push<float>(1); //texture slot
m_VAO->AddBuffer(*m_VertexBuffer, layout);

m_IndexBuffer = std::make_unique<IndexBuffer>(indices, 12);

m_Shader = std::make_unique<Shader>("res/shaders/Batch.shader");
m_Shader->Bind();

m_Texture[0] = std::make_unique<Texture>("res/textures/ChernoLogo.png");
m_Texture[1] = std::make_unique<Texture>("res/textures/HazelLogo.png");
for (size_t i = 0; i < 2; i++)
{
m_Texture[i]->Bind();
}
int samplers[2] = { 0, 1 };
m_Shader->SetUniform1iv("u_Textures", 2, samplers);
}

OnRender()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void test::TestBatchRender::OnRender()
{
GLCall(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
GLCall(glClear(GL_COLOR_BUFFER_BIT));

Renderer renderer;
{
m_Texture[0]->Bind();
glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationA);
glm::mat4 mvp = m_Proj * m_View * model;
m_Shader->SetUniformMat4f("u_MVP", mvp);
renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
}
{
m_Texture[1]->Bind();
glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationB);
glm::mat4 mvp = m_Proj * m_View * model;
m_Shader->SetUniformMat4f("u_MVP", mvp);
renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
}
}

主要是留意在构造函数中对两个texture调用Bind(),以及在调用Draw前也要分别Bind

实话实说,我不知道这么写对不对,虽然结果是对的,但调用了两次Draw,与Batch Rendering理念不符

一种做法或许是设置两个不同的u_MVP?之后做

更新:采用另一种函数glBindTextureUnit()完成了想要的效果,但是此时只能由一个滑块来控制(因为两张图片会一起动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test::TestBatchRender::OnRender()
{
GLCall(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
GLCall(glClear(GL_COLOR_BUFFER_BIT));

Renderer renderer;

glBindTextureUnit(0, m_Texture[0]->GetID());
glBindTextureUnit(1, m_Texture[1]->GetID());

glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationA);
glm::mat4 mvp = m_Proj * m_View * model;
m_Shader->SetUniformMat4f("u_MVP", mvp);
renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
}

glBindTextureglBindTextureUnit 是OpenGL中的两个不同的函数,它们用于绑定纹理到纹理单元,但在用法和功能上有一些区别。

  1. glBindTexture:

    glBindTexture 函数用于将一个纹理绑定到当前OpenGL上下文的特定纹理目标(例如GL_TEXTURE_2DGL_TEXTURE_CUBE_MAP)。它的原型为:

    1
    void glBindTexture(GLenum target, GLuint texture);

    这个函数将指定的纹理对象(由参数texture指定)绑定到目标target上。绑定后,所有对该目标的纹理操作都会影响到被绑定的纹理对象。这意味着,之后的纹理操作会影响到当前绑定的纹理对象。

  2. glBindTextureUnit:

    glBindTextureUnit 函数是OpenGL 4.5版本引入的。它允许将一个纹理直接绑定到一个纹理单元(texture unit),而不是特定的纹理目标。这样,一个纹理可以在多个不同的纹理目标之间共享,或者在同一个着色器中绑定到多个不同的纹理目标上。它的原型为:

    1
    void glBindTextureUnit(GLuint unit, GLuint texture);

    这个函数将纹理对象(由参数texture指定)绑定到指定的纹理单元unit上。绑定后,该纹理单元会包含被绑定的纹理对象,而不是特定的纹理目标。这种方法更加灵活,允许在不同的上下文中共享纹理对象,也更适合现代OpenGL编程的需求。

    不像之前的OpenGL版本中(如OpenGL 3.x),在OpenGL 4.5及以上版本中,你不再需要在绑定纹理时指定纹理的类型(例如GL_TEXTURE_2D、GL_TEXTURE_CUBE_MAP等)。取而代之的是,OpenGL会根据纹理对象的类型自动选择正确的目标。

总的来说,glBindTexture 是OpenGL早期版本中使用的函数,它将纹理对象绑定到特定的纹理目标上,而 glBindTextureUnit 则是OpenGL 4.5引入的新函数,它将纹理对象绑定到纹理单元上,提供了更灵活的纹理绑定方式。

  1. 纹理目标(Texture Target):纹理目标是指纹理被绑定到的图形渲染管线的阶段。在OpenGL和DirectX等图形API中,纹理可以绑定到不同的目标上,例如2D纹理、立方体贴图、3D纹理等。不同的纹理目标决定了纹理将如何被使用和渲染。
  2. 纹理对象(Texture Object):纹理对象是指在图形渲染中用来存储和管理纹理数据的对象。当程序需要使用纹理时,通常会创建一个纹理对象,并将纹理数据加载到该对象中。纹理对象包含了纹理的各种属性和数据。
  3. 纹理单元(Texture Unit):纹理单元是图形渲染管线中的一个部分,它负责处理纹理采样和纹理操作。在图形渲染中,可以同时使用多个纹理,这些纹理可以在不同的纹理单元上进行绑定。每个纹理单元可以有不同的纹理目标和纹理对象,允许程序在渲染过程中使用多个纹理。

纹理目标(Texture Target)、纹理对象(Texture Object)和纹理单元(Texture Unit)之间的关系如下:

  1. 纹理目标和纹理对象的关系
    • 纹理目标指定了纹理被用于渲染管线的哪个阶段,例如2D纹理、立方体贴图等。一个纹理对象可以被绑定到一个特定的纹理目标上。同一个纹理对象可以在不同的渲染阶段被绑定到不同的纹理目标上,以满足渲染需求。
  2. 纹理对象和纹理单元的关系
    • 纹理对象是存储和管理纹理数据的数据结构。在图形编程中,您通常会创建一个纹理对象,将图像数据加载到这个对象中。一个纹理对象可以被绑定到一个或多个纹理单元上。
    • 纹理单元是图形渲染管线中的一个部分,负责处理纹理采样和纹理操作。每个纹理单元可以有一个纹理对象绑定到上面。当渲染时,着色器程序可以通过纹理单元来访问与之相关联的纹理对象,从而进行纹理采样等操作。

综上所述,纹理目标定义了纹理在渲染管线中的用途,纹理对象存储了纹理的数据和属性,而纹理单元则负责管理纹理对象的使用,允许多个纹理对象同时存在,并在渲染过程中被正确地绑定和采样。这三者共同协作,使得在图形渲染中能够有效地使用和处理纹理数据。

openGL之API学习(一九九)纹理单元和纹理对象的关系-CSDN博客

glGenTextures产生的是纹理对象(简称纹理),纹理单元数量在GPU上确定的,不需要创建,glBindTexture将纹理对象绑定到当前纹理单元的的目标类型上(一个纹理单元可以有多个类型1D、2D(注:即纹理目标)等,一个纹理对象能够绑定到多个目标类型上),一个纹理对象可以绑定到多个纹理单元上,一个纹理单元上只能有一个同种纹理类型(如果有多个采样会无所适从)

img

更新:翻了各种博客,愣是没看出glBindTexture()glBindTextureUnit()有什么本质上的区别……最后发现是因为没在调Texture::Bind()时加参数……

今天看到收获较大的有关纹理的博客有(可以知道batch rendering-textures的具体代码):
纹理单元、纹理对象、纹理类型、取样器对象_纹理单元和纹理-CSDN博客
[纹理 - LearnOpenGL-CN](https://learnopengl-cn.readthedocs.io/zh/latest/01 Getting started/06 Textures/)
OpenGL学习笔记:多个纹理_shader 多个纹理坐标-CSDN博客

总结下就是,每个显卡有个最大的纹理单元数,每个纹理单元上可以有多个纹理目标用于绑定纹理对象,纹理对象可以绑定多个纹理目标。
使用纹理必须要:ActivateTexture->BindTexture->shader中uniform的关联(一个uniform对应一个纹理单元)
不过现在也是一个滑块控制两个图片,二者同时被移动,不过这也正常,毕竟是一起通过一个draw call绘制的,相对坐标不会变


##31 Batch Rendering - Dynamic Geometry

原来的position数组是静态的,现在将之改为可变的

我们首先要修改glBufferData,将之用做分配内存。具体改法是为每个vertex建立struct用于存储各项属性,之后调用glBufferData(GL_ARRAY_BUFFER, 1000 * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW). 这里nullptr表明只是用作分配内存,而GL_DYNAMIC_DRAW则表明会动态变化。

此后在BindBuffer()后需要用某种方式将数据发送到vertex buffer. 一种方式是调用glMapBuffer(),这里使用更低版本支持的glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);

glMapBuffer - OpenGL 4 - docs.gl
glBufferSubData - OpenGL 4 - docs.gl

之后新建函数CreateQuad(),接受三个参数x, y以及textureID,传回两个图片各自所需的std::array<Vertex, 4>并将其memcpy()vertices中。最后通过ImGui提供的滑块来控制x, y. 最终实现效果与[30 Batch Rendering - Textures](#30 Batch Rendering - Textures)效果类似,只是方式不同,上面是通过调整MVP矩阵实现移动,这里则是改变顶点坐标。

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
struct Vertex
{
float Position[4];
float Color[3];
float TextCoord[2];
float TextID;
};

static std::array<Vertex, 4> CreateQuad(float x, float y, int TextID)
{
float size = 1.0f;

Vertex v0;
v0.Position = { x, y, 0.0f, 1.0f };
v0.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v0.TextCoord = { 1.0f, 0.0f };
v0.TextID = TextID;

Vertex v0;
v0.Position = { x + size, y, 0.0f, 1.0f };
v0.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v0.TextCoord = { 1.0f, 0.0f };
v0.TextID = TextID;

Vertex v0;
v0.Position = { x, y, 0.0f, 1.0f };
v0.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v0.TextCoord = { 1.0f, 1.0f };
v0.TextID = TextID;

Vertex v0;
v0.Position = { x, y, 0.0f, 1.0f };
v0.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v0.TextCoord = { 0.0f, 1.0f };
v0.TextID = TextID;

return { v0, v1, v2, v3 };
}

像上面这样写会疯狂报错,因为数组不能像上面那样赋值,因此添加几个struct用于赋值。

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
static std::array<Vertex, 4> CreateQuad(float x, float y, float TextID)
{
float size = 100.0f;

Vertex v0;
v0.Position = { x, y, 0.0f, 1.0f };
v0.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v0.TextCoord = { 1.0f, 0.0f };
v0.TextID = TextID;

Vertex v1;
v1.Position = { x + size, y, 0.0f, 1.0f };
v1.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v1.TextCoord = { 1.0f, 0.0f };
v1.TextID = TextID;

Vertex v2;
v2.Position = { x + size, y + size, 0.0f, 1.0f };
v2.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v2.TextCoord = { 1.0f, 1.0f };
v2.TextID = TextID;

Vertex v3;
v3.Position = { x, y + size, 0.0f, 1.0f };
v3.Color = { 0.18f, 0.6f, 0.96f, 1.0f };
v3.TextCoord = { 0.0f, 1.0f };
v3.TextID = TextID;

return { v0, v1, v2, v3 };
}

上面这个看似没问题,实际上v0.TextCoord出问题了,导致结果如下:

image-20231008235827097

以我目前的水平不足以分析为什么会长成上面这样,不过改对了就行了。(?)


完结撒花!前后用时挺久的,大概八月下旬开始做,断断续续到10/8,花了一个半月,不过收获还不错。