课程地址:

https://www.youtube.com/watch?v=W3gAzLwfIP0&list=PLlrATfBNZ98foTJPJ_Ev03o2oq3-GGOS2&index=1

Visual Assist X crack for VS2022:

https://blog.csdn.net/gongzixiaobai8842/article/details/125540217

Setting

glfw:https://www.glfw.org/

测试代码:https://www.glfw.org/documentation.html

glew:https://glew.sourceforge.net/

视频攻略:

https://www.youtube.com/watch?v=H2E3yO0J7TM&list=PLlrATfBNZ98foTJPJ_Ev03o2oq3-GGOS2&index=3

我们需要以下库:

  • GLFW:用于创建窗口、OpenGL上下文提供了一个简单的 API。
  • GLEW:OpenGL扩展库,包括OpenGL 核心和扩展功能

首先,需要通过静态链接的方式添加库。

  • 属性-> c++ ->常规->附加包含目录中,将include文件夹的绝对路径放进去。

  • Linker ->常规中修改附加库目录,添加包含相关的lib文件的路径进去

    属性-> Linker ->输入->附加依赖项中,添加.lib文件的名称。

需要注意的是,glew32s.lib用于静态链接,glew32.lib用于动态链接,前者的内容比较大。

还需要包含以下文件:opengl32.lib

顺带一提,以下是整理后的文件结构,可以使用$(SolutionDir)宏简化路径,这个宏的路径位于Solution所在的位置,即项目的位置。

1
2
3
4
5
6
├─Dependencies
│ └─GLFW
│ ├─include
│ └─lib-vc2022
├─src
├─Solution

使用该指令可以调出目录树:tree /F

然后,需要在属性-> c++ ->预处理器->预处理器定义中加入GLEW_STATIC,并且确保<GL/glew.h>的包含发生在任何OpenGL相关的库之前,例如以下这段示例:

1
2
#include <GL/glew.h>
#include <GLFW/glfw3.h>

接着,可以使用测试代码进行测试,看看是否能够创建一个用于渲染的窗口,测试代码在最上方的注释中。

最后,需要用以下这段代码用于初始化,但是需要放在GL渲染上下文之后。

1
2
if (glewInit() != GLEW_OK)
std::cout << "Error" << std::endl;

以下是GL渲染上下文的代码

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

Basic Triangle in OpenGL

OpenGL像状态机一样运行,每次运行一个指令都有一个唯一标识的序号。

在屏幕上绘制三角形需要以下两个前置条件:

  • Vertex Buffers
  • Shader

Vertex Buffers

在将顶点数据传给Shader之前,需要先配置一个顶点缓存对象(VBO)。

  • glGenBuffers:用于生成缓存对象,第一个参数是需要生成的数目,第二个是对象ID。
  • glBindBuffer:将之后的所有操作绑定到指定的缓冲对象。
  • glBufferData:将数据加载到VBO中。
    • GL_ARRAY_BUFFER:指定要绑定到的缓冲对象类型,这里是顶点缓冲对象。
    • 6 * sizeof(float):指定要传递到缓冲对象的数据的大小,这里是6个浮点数,每个浮点数占4个字节,所以总共是 6 * sizeof(float) 字节。
    • positions:包含要加载到VBO的实际顶点数据的数组。
    • GL_STATIC_DRAW:提示OpenGL如何使用这些数据。GL_STATIC_DRAW 表示数据将不会频繁改变,适用于静态的顶点数据。
  • glEnableVertexAttribArray:启用顶点属性数组,其中的参数很重要,是顶点数组的索引,之后用来标识传入Shader中的顶点数组。
  • glVertexAttribPointer:定义顶点属性指针。这告诉OpenGL如何解释VBO中的数据
    • 0:顶点属性索引,对应于启用的顶点属性数组。
    • 2:每个顶点属性的组件数量。在这里是2,因为每个顶点有两个浮点数,对应于二维坐标。
    • GL_FLOAT:数据的类型,这里是浮点数。
    • GL_FALSE:是否需要归一化。对于浮点数,这里是GL_FALSE
    • sizeof(float) * 2:每个顶点的步长,即每个顶点属性的字节数。这里是每个顶点包含两个浮点数,所以步长是 sizeof(float) * 2 字节。
    • 0:偏移量,即在VBO中数据的起始位置。在这里,数据从VBO的开头开始。
1
2
3
4
5
6
7
8
9
10
11
12
13
float positions[6] = {
-0.5f, -0.5f,
0.0f, 0.5f,
0.5f, -0.5f
};

unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);

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

Shader

首先我们需要使用以下函数来创建、编译链接Shader。

  • 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);
glDeleteShader(fs);

return program;
}
  • CompileShader:用于编译一个着色器程序,并返回着色器的标识符。
    • 创建一个着色器对象,并指定其类型
    • 将着色器源代码附加到着色器对象上
    • 编译着色器源代码
    • 检查编译是否成功,如果失败,输出错误消息和日志,然后删除着色器对象并返回0。如果成功编译,返回着色器的标识符。

Trick:这里的message使用alloca转换为长度为length的数组

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

//检查编译是否成功,如果失败,输出错误消息和日志,然后删除着色器对象并返回0。如果成功编译,返回着色器的标识符。
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE)
{
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" : "fragmemnt") << " shader" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}

return id;
}
  • ShaderProgramSource:用于解析从文件加载的着色器源代码文件,将其分为顶点着色器和片段着色器的部分,并将它们存储在 ShaderProgramSource 结构体中。
    • 打开指定路径的文件,并逐行读取文件内容。
    • 当遇到 #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
30
31
32
33
34
35
36
37
38
39
#include <fstream>
#include <string>
#include <sstream>

struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};

static ShaderProgramSource ParseShader(const std::string& filepath)
{
std::ifstream stream(filepath);
std::string line;
std::stringstream ss[2];

enum class ShaderType
{
None = -1, VERTEX = 0, FRAGMENT = 1
};
ShaderType type = ShaderType::None;

while (getline(stream, line))
{
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != std::string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != std::string::npos)
type = ShaderType::FRAGMENT;
}
else
{
ss[(int)type] << line << '\n';
}
}

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

然后,我们在创建顶点缓存之后使用Shader。

  • 读取Shader程序的代码如下
1
2
3
ShaderProgramSource source = ParseShader("res/shader/Basic.shader");
unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
glUseProgram(shader);
  • Shader相关代码如下:
    • 顶点着色器:读取索引为0的顶点缓存对象
    • 片段着色器:设置对应的像素
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(0.7, 0.0, 0.2, 1.0);
};

Index Buffers

注意:任何索引数组都需要用unsigned int类型。

由于多个顶点存在复用,为了减少这种情况的发生,使用indices数组去索引这些顶点。

  • 首先需要根据positions创建缓存对象
  • 然后使用indices创建索引缓存对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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[] = {
0, 1, 2,
2, 3, 0
};

unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, 6 * 2 * sizeof(float), positions, GL_STATIC_DRAW);

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

glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
  • 调用DrawCall方式如下:
1
glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr)

Dealing with Errors in OpenGL

通过编写一个宏函数:GLCall,每次使用OpenGL语句的之前使用这个宏,能够定位错误的位置。

  • ASSERT:使用MSVC的函数,当找到错误的时候抛出异常
  • glGetError:这个函数能够检查OpenGL函数调用是否产生错误

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define ASSERT(x) if(!(x)) __debugbreak();//MSVC函数
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall(#x, __FILE__, __LINE__))

static void GLClearError()
{
while (glGetError() != GL_NO_ERROR);
}

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
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr));

Uniforms in OpenGL

uniform 是一种特殊类型的变量,它是在着色器程序中声明的全局变量,但是它可以从CPU代码中发送数据到GPU,以在着色器中使用。

  • 在Shader中定义Uniform变量:
1
2
3
4
5
6
7
8
9
10
#shader fragment
#version 330 core

layout(location = 0) out vec4 color;
uniform vec4 u_Color;

void main()
{
color = u_Color;
};
  • 在代码中更改Uniform的值:
1
2
3
4
int location = glGetUniformLocation(shader, "u_Color");
//找不到uniform名就抛出异常
ASSERT(location != -1);
GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

Vertex Arrays

Vertex Array Object(VAO)允许我们将顶点缓冲绑定到顶点规范,简化绘制过程。

  • 在OpenGL的兼容性模式下,使用一个全局的VAO,绑定一个缓冲区,或者在核心模式下,显式创建和绑定每一个变量到VAO。
  • 可以根据实际需要,选择使用一个全局的VAO还是为每个几何图形创建一个独立的VAO。
1
2
3
unsigned int vao;
GLCall(glGenVertexArrays(1, &vao));
GLCall(glBindVertexArray(vao);)

Abstraction

将之前的大部分繁琐的操作抽象出来,可以封装成以下几个类。

  • VertexBuffer
  • VertexArray
  • VertexBufferLayout
  • IndexBuffer
  • Shader

调用用例:

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

Shader shader("res/shader/Basic.shader");

Texture texture("res/textures/1500981_cg_scale.png");
texture.Bind();
//Texture Bind slot = 0, 所以这里也是0
shader.SetUniform1i("u_Texture", 0);

Renderer renderer;

在Window循环中:

1
2
renderer.Clear();
renderer.Draw(va, ib, shader);

VertexBuffer

顶点缓存类,用来存储顶点数据。

  • VertexBuffer:VertexBuffer的构造函数中需要生成顶点缓存绑定顶点缓存以及存入顶点缓存数据
  • ~VertexBuffer:VertexBuffer的析构函数中需要删除顶点数组
  • Bind:绑定顶点缓存
  • UnBind:取消对顶点缓存的绑定

VertexBufferLayout

顶点缓存布局类,描述了以怎样的方式来读取顶点缓存的数据。

  • VertexBufferLayout:设置步长
  • Push:存入VertexBufferElement,并根据计算步长。

VertexArray

顶点数组,包含顶点缓存以及顶点布局数据

  • VertexArray:VertexArray的构造函数中需要生成顶点数组以及绑定顶点数组
  • ~VertexArray:VertexArray的析构函数中需要删除顶点数组
  • AddBuffer:
  • Bind:绑定顶点数组
  • UnBind:取消对顶点数组的绑定

IndexBuffer

为了复用顶点而引申出来的类,和va、shader一起作为draw函数的参数。

  • IndexBuffer:生成索引数组绑定索引数组以及存入索引数组数据
  • ~IndexBuffer:删除索引数据
  • Bind:绑定索引数组
  • UnBind:取消对索引数组的绑定

Shader

着色器类,用来编译shader文件以及设置Uniforms

  • Shader:用来编译shader文件,包括之前提到的ParseShader、CreateShader、CompileShader函数
  • ~Shader:用来删除生成的程序
  • Bind:使用生成的程序
  • UnBind:解除对生成程序的使用
  • SetUniform:与变量交换,更改shader中的值

Renderer

C4430 Error:头文件中包含的头文件不能包含自己,否则会死循环。推荐列一个图表,找出其中冲突的地方,将头文件使用到的库移到cpp文件中。

封装渲染的过程的类,包含宏函数用来判断异常。

  • GLClearError:清除错误日志
  • GLLogCall:定位错误发生的位置
  • Clear:清除颜色缓冲区
  • Draw:使用VertexArray、IndexBuffer、Shader作为参数绘制像素

Texture

用来读取图片成纹理的类,需要stb_image库的支持。

  • Texture:读取图片,将图片上下翻转、使用插值对图片的缩放、平铺和拉伸进行处理。
  • ~Texture:删除读取的纹理
  • Bind:激活纹理缓存,并且绑定到TEXTURE0 + slot的位置上
  • UnBind:取消纹理缓冲区的绑定
  • GetWidth:返回读取纹理的宽的值
  • GetHeight:返回读取纹理的长的值

Blend

在OpenGL中,遇到png文件这样带有透明通道的图片需要进行混合才能正常显示。

有三种方式进行混合:

  • glEnable(GL_BLEND) - glDisable(GL_BLEND)

  • glBlendFunc(src,dest)

    • src = how the src RGBA factor is computed (default is GL_ONE)

    • dest = how the dest RGBA factor is computed (default is GL_ZERO)

  • glBlendEquation(mode)

    • mode = how we combine the src and dest colors
    • Default value is GL_FUNC_ADD

混合的时候实际上发生的事情:

image-20231002163742276

以下是将透明的白色正方形与不透明的洋红色长方形进行混合的示例:

image-20231002164012865

开启混合的代码如下:

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

Math

数学库:https://github.com/g-truc/glm

这个库只有头文件没有cpp文件,不需要编译以及链接,直接包含在src里面即可。

然后将这个src\vendor;加入到属性c++中的附加包含目录中即可

需要包含的头文件:

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

ImGui

imgui:https://github.com/ocornut/imgui

这是一个支持多种图形API的GUI库,对于OpenGL,需要的文件如图:

image-20231002205511611

需要包含的头文件如下:

1
2
3
#include "imgui/imgui.h"
#include "imgui/imgui_impl_glfw.h"
#include "imgui/imgui_impl_opengl3.h"

使用示例

  • 主循环中,需要进行以下初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
Renderer renderer;

// ImGui Setup
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls

ImGui::StyleColorsDark();

ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 440");
  • 渲染的窗口的循环中,需要对每一帧进行渲染:
1
2
3
4
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();

以下是渲染的内容:

1
2
3
4
5
6
7
8
9
10
11
12
// ImGui Content
{
ImGui::Begin("Hello, world!");
ImGui::SliderFloat3("TranslationA", &translationA.x, 0.0f, 960.0f);
ImGui::SliderFloat3("TranslationB", &translationB.x, 0.0f, 960.0f);
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
ImGui::End();
}

// ImGui Rendering
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
  • 在渲染窗口之外,需要删除相应的设置释放内存
1
2
3
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();