【笔记】Cherno Opengl Tutorial note 04

24 Setting up a Test Framework

这次我们要对代码进行一些整理,为之后做好铺垫。

因为如果我们想让系统变得更复杂的话,像这样做是不行的。

首先清理下代码的结构

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


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

Renderer renderer;

// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();

// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init();

while (!glfwWindowShouldClose(window))
{
renderer.Clear();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();

ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

glfwSwapBuffers(window);
glfwPollEvents();
}
//glDeleteProgram(shader);//有析构函数
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();

glfwTerminate();
return 0;
}

然后添加test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
test::TestClearColor test;

while (!glfwWindowShouldClose(window))
{
renderer.Clear();

test.OnUpdate(0.0f);
test.OnRender();

ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();

test.OnImGuiRender();

ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());


glfwSwapBuffers(window);
glfwPollEvents();
}

就可以看到如下结果

image-20220917153535589

test的部分结构如下

image-20220917153643711

Test.h

作为Test的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once

namespace test {

class Test {
public:
Test() {}
virtual ~Test() {}

virtual void OnUpdate(float deltaTime) {}
virtual void OnRender() {}
virtual void OnImGuiRender() {}
};
}
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
//TestClearColor.h
#pragma once

#include "Test.h"

namespace test {


class TestClearColor : public Test {
public:
TestClearColor();
~TestClearColor();

void OnUpdate(float deltaTime) override;
void OnRender() override;
void OnImGuiRender() override;

private:
float m_ClearColor[4];

};
}
//TestClearColor.cpp
#include "TestClearColor.h"
#include "Renderer.h"
#include "imgui/imgui.h"
namespace test {
TestClearColor::TestClearColor()
: m_ClearColor {0.2f,0.3f,0.8f,1.0f} {
}

TestClearColor::~TestClearColor() {
}

void TestClearColor::OnUpdate(float deltaTime) {
}

void TestClearColor::OnRender() {
GLCall(glClearColor(m_ClearColor[0],
m_ClearColor[1],m_ClearColor[2],m_ClearColor[3]));
GLCall(glClear(GL_COLOR_BUFFER_BIT));
}

void TestClearColor::OnImGuiRender() {
ImGui::ColorEdit4("Clear Color", m_ClearColor);
}
}

这样就建立好了test的架构。

其实基于这个架构,我们可以做很多事了,例如在imgui上用菜单控制各种变量、贴图。。。

接下来我们要建立一个test的菜单,而不是直接打开test

25 Creating Tests

Test.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
38
39
40
41
42
43
44
45
46
#pragma once
#include <vector>
#include <functional>
#include <string>
#include <iostream>
namespace test {

class Test {
public:
Test() {};
virtual ~Test() {};

virtual void OnUpdate(float deltaTime) {};
virtual void OnRender() {};
virtual void OnImGuiRender() {};
};

class TestMenu : public Test {
public:
TestMenu(Test*& currentTestPointer);

//void OnUpdate(float deltaTime) override;
//void OnRender() override;
void OnImGuiRender() override;


template<typename T>
// 构造一个pari传入m_Tests中,分别是我们获得的名称和lambda,lambda的类型来自模板类型
// lambda这里C++还没看过,以及function的标准函数也不太了解,到时候要去补一下C++这部分
void RegisterTest(const std::string& name) {
std::cout << "Registering test " << name << std::endl;
m_Tests.push_back(std::make_pair(name, []() {return new T(); }));
}
private:
Test*& m_CurrentTest;//我们的按钮会控制当前的Test, 把当前Test的引用传递到这里的成员指针


// 我们并不想直接要一个test的指针,这样就代表这个实例已经被创建了
// 而我们希望的是按下按钮,才创建和构造一个test的实例
// 因此我们提供一个lambda的function,来构造一个test
//vector储存一个pari,每个pari里有一个string表示Test的名称,function返回一个test的指针

std::vector<std::pair<std::string, std::function<Test* ()>>> m_Tests;

};
}

Test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "Test.h"
#include "imgui/imgui.h"
namespace test {
TestMenu::TestMenu(Test*& currentTestPointer)
: m_CurrentTest(currentTestPointer) {
}

void TestMenu::OnImGuiRender() {
for (auto& test : m_Tests) {
if (ImGui::Button(test.first.c_str()))
m_CurrentTest = test.second();//按下按钮,则自动构造一个test实例,并把指针传递给m_CurrentTest
}
}

}

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
test::Test* currentTest = nullptr;
test::TestMenu* testMenu = new test::TestMenu(currentTest);
currentTest = testMenu;
//这样做的结果是,menu里的m_CurrentTest当前就储存的自己。
//因为我们没有必要每次都要设置一个初始的Test

//我们希望这样来设置Test,传入模板参数提供类型,以及对应的名称
//作者提出一种模块化coding的方式,就是先假装以及有这个功能,然后去使用,再回来实现
testMenu->RegisterTest<test::TestClearColor>("Clear Color");


while (!glfwWindowShouldClose(window))
{
GLCall(glClearColor(0.0, 0.0f, 0.0f, 1.0f));
renderer.Clear();

ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();

if (currentTest) {
currentTest->OnUpdate(0.0f);
currentTest->OnRender();
ImGui::Begin("Test");
if (currentTest != testMenu && ImGui::Button("<-")) {
delete currentTest;
currentTest = testMenu;
}//删除当前Test后,回到testMenu
currentTest->OnImGuiRender();
ImGui::End();
}

ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());


glfwSwapBuffers(window);
glfwPollEvents();
}
delete currentTest;//currentTest 的实例创建是模板的lambda里做的
if (currentTest != testMenu)
delete testMenu;

我们运行后,啥都没有

image-20220917170520171

但是点击Clear Color的button以后,就获得了前面的Clear Color的功能,并且包含一个回退按钮,再点击这个回退按钮

image-20220917170553457

再点击这个回退按钮,这个Test实例就被注销了,并且UI也回到原来的界面,但由于Opengl是状态机修改,所以背景色的状态没有变回去

image-20220917170625931

作者在循环开始时加上

1
GLCall(glClearColor(0.0, 0.0f, 0.0f, 1.0f));

这样就能保证不存在TestClearColor实例时,有一个固定的背景色。(即注销实例后,背景色会清空)

为了防止内存溢出,在循环结束后,我们还要把new出来的test删掉

1
2
3
delete currentTest;
if (currentTest != testMenu)
delete testMenu;

26 Creating a Texture Test

以上搭建的框架,其实就是建立了一个类似于沙盒,我们可以在里面设置各种测试Test

在之后的工作会进行Batch Rendering

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
#pragma once

#include "Test.h"
#include "VertexBuffer.h"
#include "VertexBufferLayout.h"
#include "Texture.h"

#include <memory>
namespace test {


class TestTexture2D : public Test {
public:
TestTexture2D();
~TestTexture2D();

void OnUpdate(float deltaTime) override;
void OnRender() override;
void OnImGuiRender() override;

private:
std::unique_ptr<VertexArray> m_VAO;
std::unique_ptr<IndexBuffer> m_IndexBuffer;
std::unique_ptr<Shader> m_Shader;
std::unique_ptr<Texture> m_Texture;
std::unique_ptr<VertexBuffer> m_VertexBuffer;
glm::mat4 m_Proj, m_View;
glm::vec3 m_TranslationA, m_TranslationB;
};
}

TestTexture2D.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
//
#include "TestTexture2D.h"
#include "Renderer.h"
#include "imgui/imgui.h"


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

namespace test {
TestTexture2D::TestTexture2D()
: m_Proj(glm::ortho(-2.0f, 2.0f, -1.5f, 1.5f, -1.0f, 1.0f)),
m_View(glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0))),
m_TranslationA((0, 0,0)), m_TranslationB((0,0,0))
{



float positions[16] = {
-2.0f, -1.5f, 0.0f, 0.0f,
2.0f, -1.5f, 1.0f, 0.0f,
2.0f, 1.5f, 1.0f, 1.0f,
-2.0f, 1.5f, 0.0f, 1.0f
};
unsigned int indices[] = {
0,1,2,
2,3,0
};
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);



m_VAO = std::make_unique<VertexArray>();
m_VertexBuffer = std::make_unique<VertexBuffer>(positions, 4 * 4 * sizeof(float));
VertexBufferLayout layout;
layout.Push<float>(2);
layout.Push<float>(2);
m_VAO->AddBuffer(*m_VertexBuffer, 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/2.png");
m_Texture->Bind();
m_Shader->SetUniform1i("u_Texture", 0);

}

TestTexture2D::~TestTexture2D() {
}

void TestTexture2D::OnUpdate(float deltaTime) {
}

void TestTexture2D::OnRender() {
GLCall(glClearColor(0.0f,0.0f,0.0f,1.0f));
GLCall(glClear(GL_COLOR_BUFFER_BIT));

Renderer renderer;

m_Texture->Bind();


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

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


}

void TestTexture2D::OnImGuiRender() {
ImGui::SliderFloat3("Translation A", &m_TranslationA.x, 0.0f, 10.0f);
ImGui::SliderFloat3("Translation B", &m_TranslationB.x, 0.0f, 10.0f);
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
}
}

在这里完成后,只需在main中注册。

Batch Rendering

  • 在处理Texture的时候要注意,由于最大的Texture slot数量的限制(比如上一章节讲texture里提过当前windows最大的数量32),同一个batch往往不能容纳足够的Texture,那么只能把多余的texture对应的网格渲染放到下一个batch,以此类推。

  • 假如100个submesh,对应100个texture,每32个texture对应的mesh合批,这样也只需要4个drawcall

  • 静态合批大抵就是如此(把不同mesh的vertex组合起来)

Dynamic Geometry

这一段就是讲动态合批。

  • 静态合批是我们一开始就设定好了合批的数据,每一帧只需要渲染这些已经传递到GPU的数据。

  • 而我们希望对于动态的物体也能进行合批。

  • 重要的依然是这两部分:vertex buffer和index buffer

    image-20220924145750783

    除了提前准备了这一部分数据,我们还使用

1
glBufferData(GL_ARRAY_BUFER,sizeof(vertices),vertices,GL_STATIC_DRAW)

在update前将数据储存到GPU。

而为了动态改变数据,我们可以只创建buffer,但是先不传递数据,即传递一个空指针。

我们也可以根据我们的顶点结构和需要的顶点数量来决定buffer的大小。

1
2
3
4
5
6
struct Vertex{
float Position[3];
float Color[4];
float TexCoords[2];
float TexID;
};

最后就是绘制方式,不再是Static,我们要换成dynamic

1
glBufferData(GL_ARRAY_BUFER,1000*sizeof(Vertex),nullptr,GL_DYNAMIC_DRAW)

其他关于顶点属性绑定之类都是一样的,只不过可以利用Vertex的结构来让它变得更通用

1
2
3
4
5
glEnableVertexArrayAttrib(m_QuadVB,0);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex), (const void*)offsetof(Vertex, Position));
glEnableVertexArrayAttrib(m_QuadVB,1);
glVertexAttribPointer(1,4,GL_FLOAT,GL_FALSE,sizeof(Vertex), (const void*)offsetof(Vertex, Color));
//......

IB在合批中也不应该被改变。虽然我们动态地改变了顶点的属性,但是这些跟IB有什么关系。

在数量庞大的vertices当中,往往也需要用for loop来构建indices。(去年可视化的性能优化工作就是手动做了静态合批这件事……虽然当时并不知道合批这个概念)

Update

image-20220924153010284

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Set dynamic buffer
//OnUpdate
//float vertices[] ={....};之后在这里我们每一帧都可能对顶点数据做出改变
//也可以用一些封装的方法
//这里作者也出现一个问题,就是前面Vertex的结构里定义的是数组,不能直接赋值一个结构,因此他改成了vec的结构
auto q0 = CreateQuad(-1.5f, -0.5f, 0.0f);//x,y,texID
auto q1 = CreateQuad(0.5f,-0.5f,1.0f);
Vertex vertices[8];
memcpy(vertices,q0.data(), q0.size()*sizeof(Vertex));
memcpy(vertices+q0.size(),q1.data(), q1.size()*sizeof(Vertex));

glBindBuffer(GL_ARRAY_BUFFER, m_QuadVB);
//glMapBuffer()我们可以用这个方法直接将数据写入缓冲
//也有glUnmapBuffer,把buffer从GPU上卸载下来
//但是这个方法会慢一点
glBufferSubData(GL_ARRAY_BUFFER,0,sizeof(vertices),vertices);
//这个方法和glBufferData很像,但它不分配缓冲,仅仅是将数据放进去

然后我们就可以通过各种方式来改变绘制的顶点数据

比如通过gui来控制

1
2
3
4
5
6
7
8
9
//OnImGuiRender
ImGui::Begin("Controls");
ImGui::DragFloat2("Quad Pos",m_QuadPosition, 0.1f);
ImGui::End();

//OnUpdate
//...
//q0 = Create(m_QuadPosition[0],m_QuadPosition[1],0.0f);
//...

这样就完成了动态的vertex buffer绘制,以及动态合批,我们只创建了一个vertex buffer,调用了一次drawcall。

Indices

对于两个quad,我们很简单地就可以直接写出合批的indices

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

但是对于数量更多的mesh,我们也可以有一定的规律来写出。

首先依然要决定我们要绘制多少个四边形,以1000个为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const size_t MaxQuadCount = 1000;
//这既决定Vertex的数量,也决定indices的数量
const size_t MaxVertexCount = MaxQuadCount * 4;
const size_t MaxIndexCount = MaxQuadCount * 6;
//......
glBufferData(GL_ARRAY_BUFFER,sizeof(Vertex) * MaxVertexCount,nullptr,GL_DYNAMIC_DRAW);
//......
uint32_t indices[MaxIndexCount];
for (size_t i=0;i<MaxIndexCount;i+=6){
indices[i+0]=0+offset;
indices[i+1]=1+offset;
indices[i+2]=2+offset;
indices[i+3]=2+offset;
indices[i+4]=3+offset;
indices[i+5]=0+offset;

offset += 4;
}