三十一、设计着色器库

  • 为什么要Shader库

    为了重复使用shader,把shader存储在cpu中,需要的时候可以直接使用,不用再创建。

代码修改

  • Shader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class ShaderLibrary {
    public:
    void Add(const std::string& name, const Ref<Shader>& shader);
    void Add(const Ref<Shader>& shader);
    Ref<Shader> Load(const std::string& filepath);
    Ref<Shader> Load(const std::string& name, const std::string& filepath);

    Ref<Shader> Get(const std::string &name);

    bool Exists(const std::string& name) const;
    private:
    std::unordered_map<std::string, Ref<Shader>> m_Shaders;
    };
    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
    void ShaderLibrary::Add(const std::string& name, const Ref<Shader>& shader)
    {
    HE_CORE_ASSERT(!Exists(name), "Shader already exists!");
    m_Shaders[name] = shader;
    }
    void ShaderLibrary::Add(const Ref<Shader>& shader)
    {
    auto& name = shader->GetName();
    Add(name, shader);
    }
    HEngine::Ref<HEngine::Shader> ShaderLibrary::Load(const std::string& filepath)
    {
    auto shader = Shader::Create(filepath);
    Add(shader);
    return shader;
    }
    HEngine::Ref<HEngine::Shader> ShaderLibrary::Load(const std::string& name, const std::string& filepath)
    {
    auto shader = Shader::Create(filepath);
    Add(name,shader);
    return shader;
    }
    HEngine::Ref<HEngine::Shader> ShaderLibrary::Get(const std::string& name)
    {
    HE_CORE_ASSERT(Exists(name), "Shader not found!");
    return m_Shaders[name];
    }
    bool ShaderLibrary::Exists(const std::string& name) const
    {
    return m_Shaders.find(name) != m_Shaders.end();
    }

  • Sandboxapp.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    m_Shader = HEngine::Shader::Create("VertexPosColor", vertexSrc, fragmentSrc);

    m_FlatColorShader = HEngine::Shader::Create("FlatColor",flatColorShaderVertexSrc, flatColorShaderFragmentSrc);

    auto textureShader = m_ShaderLibrary.Load("assets/shaders/Texture.glsl");

    std::dynamic_pointer_cast<HEngine::OpenGLShader>(textureShader)->Bind();
    std::dynamic_pointer_cast<HEngine::OpenGLShader>(textureShader)->UploadUniformInt("u_Texture", 0);

    auto textureShader = m_ShaderLibrary.Get("Texture");

三十二、添加相机控制器

  • 此节目的

    在SandboxApp里面有camera移动旋转的方法,希望能将Camera抽象成一个

关键代码

  • 宽高比

    在1280720下的界面,宽1280>高720,宽明显比高 像素占位*多

    需传入1280/720=1.7左右,将宽放大,从而左右视角变大,达到正常比例。

    1
    ExampleLayer() : Layer("Example"), m_CameraController(1280.0f / 720.0f, true)
  • m_ZoomLevel视野影响物体大小

    1
    m_Camera(-m_AspectRatio * m_ZoomLevel, m_AspectRatio* m_ZoomLevel,-m_ZoomLevel, m_ZoomLevel)
    • 视野放大,物体缩小
    • 视野缩小,物体放大
  • m_ZoomLevel视野影响摄像机移动速度

    1
    2
    // 视野放大,摄像机移动速度变快,视野缩小,摄像机移动速度变慢
    m_CameraTranslationSpeed = m_ZoomLevel;
    • 视野放大,物体缩小,摄像机移动速度变
    • 视野缩小,物体放大,摄像机移动速度变

代码修改

  • 新建OrthographicCameraController类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class OrthographicCameraController
    {
    public:
    OrthographicCameraController(float aspectRatio, bool rotation = false);

    void OnUpdate(Timestep ts);
    void OnEvent(Event& e);

    OrthographiCamera& GetCamera(){ return m_Camera; }
    const OrthographiCamera& GetCamera() const { return m_Camera; }
    private:
    bool OnMouseScrolled(MouseScrolledEvent& e);
    bool OnWindowResized(WindowResizeEvent& e);
    private:
    float m_AspectRatio;
    float m_ZoomLevel = 1.0f;
    OrthographiCamera m_Camera;

    bool m_Rotation;

    glm::vec3 m_CameraPosition = { 0.0f, 0.0f, 0.0f };
    float m_CameraRotation = 0.0f;
    float m_CameraTranslationSpeed = 10.0f, m_CameraRotationSpeed = 180.0f;
    };

    OrthographicCameraController.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
    namespace HEngine
    {
    OrthographicCameraController::OrthographicCameraController(float aspectRatio, bool rotation)
    : m_AspectRatio(aspectRatio), m_Camera(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel), m_Rotation(rotation)
    {
    }

    void OrthographicCameraController::OnUpdate(Timestep ts)
    {
    if (Input::IsKeyPressed(HE_KEY_A))
    m_CameraPosition.x -= m_CameraTranslationSpeed * ts;
    else if (Input::IsKeyPressed(HE_KEY_D))
    m_CameraPosition.x += m_CameraTranslationSpeed * ts;

    if (Input::IsKeyPressed(HE_KEY_W))
    m_CameraPosition.y += m_CameraTranslationSpeed * ts;
    else if (Input::IsKeyPressed(HE_KEY_S))
    m_CameraPosition.y -= m_CameraTranslationSpeed * ts;

    if (m_Rotation)
    {
    if (Input::IsKeyPressed(HE_KEY_Q))
    m_CameraRotation += m_CameraRotationSpeed * ts;
    else if (Input::IsKeyPressed(HE_KEY_E))
    m_CameraRotation -= m_CameraRotationSpeed * ts;

    m_Camera.SetRotation(m_CameraRotation);
    }

    m_Camera.SetPosition(m_CameraPosition);

    m_CameraTranslationSpeed = m_ZoomLevel;
    }

    void OrthographicCameraController::OnEvent(Event& e)
    {
    EventDispatcher dispatcher(e);
    dispatcher.Dispatch<MouseScrolledEvent>(HE_BIND_EVENT_FN(OrthographicCameraController::OnMouseScrolled));
    dispatcher.Dispatch<WindowResizeEvent>(HE_BIND_EVENT_FN(OrthographicCameraController::OnWindowResized));
    }

    bool OrthographicCameraController::OnMouseScrolled(MouseScrolledEvent& e)
    {
    m_ZoomLevel -= e.GetYOffset() * 0.25f;
    m_ZoomLevel = std::max(m_ZoomLevel, 0.25f);
    m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);
    return false;
    }

    bool OrthographicCameraController::OnWindowResized(WindowResizeEvent& e)
    {
    m_AspectRatio = (float)e.GetWidth() / (float)e.GetHeight();
    m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);
    return false;
    }
    }
  • SandboxApp.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
ExampleLayer()
: Layer("Example"), m_CameraController(1280.0f / 720.0f, false)
{...}
void OnUpdate(HEngine::Timestep ts) override
{
m_CameraController.OnUpdate(ts);

HEngine::Renderer::BeginScene(m_CameraController.GetCamera());
}
void OnEvent(HEngine::Event& e) override
{
m_CameraController.OnEvent(e);
}

三十三、窗口大小调整功能

三十二节虽然设置了CameraController的窗口resize事件,但是OpenGL并没有改变渲染的位置,依旧是原先的大小和位置,所以需要调整OpenGL的渲染视口。

后期我们需要为引擎实现不依靠停留在窗口也可以渲染的区域,需要用到OpenGL的frame buffer

  • 此节目的

    为了实现调整窗口大小后

    • OpenGL绘图的区域也会相应调整

      窗口调为0*0,OpenGL不应该渲染图形了

    • 摄像机也会根据窗口大小的变换,依旧保持正确的宽高比

关键代码

  • Application.cpp

    使用事件调度器捕捉响应Window调整大小事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    dispatcher.Dispatch<WindowResizeEvent>(BIND_EVENT_FN(OnWindowResize));// 捕捉响应Window调整大小事件

    bool Application::OnWindowResize(WindowResizeEvent& e)
    {
    if (e.GetWidth() == 0 || e.GetHeight() == 0)
    {
    m_Minimized = true;
    return false;
    }

    m_Minimized = false;
    Renderer::OnWindowResize(e.GetWidth(), e.GetHeight());

    return false;
    }

    if (!m_Minimized) {
    for (Layer* layer : m_LayerStack) {
    layer->OnUpdate(timestep);
    }
    }
  • OpenGL改变视口:绘图的区域改变

    1
    2
    3
    4
    void OpenGLRendererAPI::SetViewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height)
    {
    glViewport(x, y, width, height);
    }
  • 窗口大小调整,摄像机需要依旧保持正确的宽高比

    1
    2
    3
    4
    5
    6
    7
    bool HEngine::OrthographicCameraController::OnWindowResized(WindowResizeEvent& e)
    {
    // 重新计算宽高比
    m_AspectRatio = (float)e.GetWidth() / (float)e.GetHeight();
    m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);
    return false;
    }

三十四、创建SandBox2D

  • 此节所做

    将Sandbox项目中的ExampleLayer渲染代码移到在Sandbox项目中新创建的SandBox2D(Layer)类中。

    在SandBox2D类中调用HEngine项目中的OpenGLRenderer类。

代码修改

Sandbox2D.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Sandbox2D : public HEngine::Layer
{
public:
Sandbox2D();
virtual ~Sandbox2D() = default;

virtual void OnAttach() override;
virtual void OnDetach() override;

void OnUpdate(HEngine::Timestep ts) override;
virtual void OnImGuiRender() override;
void OnEvent(HEngine::Event& e) override;
private:
HEngine::OrthographicCameraController m_CameraController;

//Temp
HEngine::Ref<HEngine::VertexArray> m_SquareVA;
HEngine::Ref<HEngine::Shader> m_FlatColorShader;

glm::vec4 m_SquareColor = { 0.2f, 0.3f, 0.8f, 1.0f };
};

Sandbox2D.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
Sandbox2D::Sandbox2D()
:Layer("Sandbox2D"), m_CameraController(1280.0f / 720.0f)
{
}
void Sandbox2D::OnAttach()
{
m_SquareVA = HEngine::VertexArray::Create();

float squareVertices[5 * 4] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.5f, 0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};

HEngine::Ref<HEngine::VertexBuffer> squareVB;
squareVB.reset(HEngine::VertexBuffer::Create(squareVertices, sizeof(squareVertices)));
squareVB->SetLayout({
{HEngine::ShaderDataType::Float3, "a_Position"}
});
m_SquareVA->AddVertexBuffer(squareVB);

uint32_t squareIndices[6] = { 0,1, 2, 2, 3, 0 };
HEngine::Ref<HEngine::IndexBuffer> squareIB;
squareIB.reset(HEngine::IndexBuffer::Create(squareIndices, sizeof(squareIndices) / sizeof(uint32_t)));
m_SquareVA->SetIndexBuffer(squareIB);

m_FlatColorShader = HEngine::Shader::Create("assets/shaders/FlatColor.glsl");
}
void Sandbox2D::OnUpdate(HEngine::Timestep ts)
{
// Update
m_CameraController.OnUpdate(ts);

// Render
HEngine::RenderCommand::SetClearColor({ 0.1f, 0.1f, 0.1f, 1 });
HEngine::RenderCommand::Clear();

HEngine::Renderer::BeginScene(m_CameraController.GetCamera());

std::dynamic_pointer_cast<HEngine::OpenGLShader>(m_FlatColorShader)->Bind();
std::dynamic_pointer_cast<HEngine::OpenGLShader>(m_FlatColorShader)->UploadUniformFloat4("u_Color", m_SquareColor);

HEngine::Renderer::Submit(m_FlatColorShader, m_SquareVA, glm::scale(glm::mat4(1.0f), glm::vec3(1.5f)));

HEngine::Renderer::EndScene();
}
void Sandbox2D::OnImGuiRender()
{
ImGui::Begin("Settings");
ImGui::ColorEdit4("Square Color", glm::value_ptr(m_SquareColor));
ImGui::End();
}
void Sandbox2D::OnEvent(HEngine::Event& e)
{
m_CameraController.OnEvent(e);
}

三十五、创建Renderer2D

  • 此节所作
    • 在Sandbox2D类的基础上再做一次封装,将去除关于着色器、顶点缓冲这些关于代码,只需使用简单的Api调用就能绘制单个quad图形
    • 新建一个Renderer2D类来渲染单个2Dquad图形,而不是用Rednerer类来渲染一个大场景。

2D渲染类的函数是静态的解释

  1. OpenGL绘图是一个设置状态顺序的过程
  2. 在2D渲染类中只是简单的调用设置OpenGL状态,并不需要实例化
  3. 不需要让一个2D渲染类开始场景,另一个2D渲染类绘制
  4. 综上,只需要一个就行,所以静态即可

代码改变

  • Renderer2D

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    namespace HEngine
    {
    class Renderer2D
    {
    public:
    static void Init();
    static void Shutdown();
    static void BeginScene(const OrthographiCamera& camera);
    static void EndScene();
    //Primitives
    static void DrawQuad(const glm::vec2& position, const glm::vec2 size, const glm::vec4& color);
    static void DrawQuad(const glm::vec3& position, const glm::vec2 size, const glm::vec4& color);
    };
    }

    Renderer2D.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
    struct Renderer2DStorage
    {
    Ref<VertexArray> QuadVertexArray;
    Ref<Shader> FlatColorShader;
    };

    static Renderer2DStorage* s_Data;
    void Renderer2D::BeginScene(const OrthographiCamera& camera)
    {
    std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->Bind();
    std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->UploadUniformMat4("u_ViewProjection", camera.GetViewProjectionMatrix());
    std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->UploadUniformMat4("u_Transform", glm::mat4(1.0f));
    }
    void Renderer2D::DrawQuad(const glm::vec2& position, const glm::vec2 size, const glm::vec4& color)
    {
    DrawQuad({ position.x,position.y,0.0f }, size, color);
    }

    void Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2 size, const glm::vec4& color)
    {
    std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->Bind();
    std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->UploadUniformFloat4("u_Color", color);

    s_Data->QuadVertexArray->Bind();
    RenderCommand::DrawIndexed(s_Data->QuadVertexArray);
    }

Sandbox2D.cpp

1
2
3
4
5
6
void Sandbox2D::OnUpdate(HEngine::Timestep ts)
{
HEngine::Renderer2D::BeginScene(m_CameraController.GetCamera());
HEngine::Renderer2D::DrawQuad({ 0.0f,0.0f }, { 1.0f,1.0f }, { 0.8f,0.2f,0.3f,1.0f });
HEngine::Renderer2D::EndScene();
}

三十六、实现单个着色器

目前的Renderer2D有两个Shader,FlatColorShader和TextureShader,其实可以把两个Shader合为一个Shader,毕竟Shader的编译还是要消耗不少性能的。

这么做的思路是,使用两个uniform,一个是代表color的float4的uniform,一个是代表texture的sampler2D的uniform,片元着色器如下所示:

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

layout(location = 0) out vec4 color;

in vec2 v_TexCoord;

uniform vec4 u_Color;
uniform sampler2D u_Texture;

void main()
{
color = texture(u_Texture, v_TexCoord * 10.0) * u_Color;
}

核心思路是,当我想要把Shader用作TextureShader时,那么传入我要渲染的Texture,u_Color传入(1,1,1,1)即可;当我想要把Shader用作FlatShader时,u_Color里传入我要显示的FlatColor,同时传入一个特殊的WhiteTexture,这个Texture的Sample的返回值永远是(1,1,1,1)。

Renderer2D.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
struct Renderer2DStorage
{
std::shared_ptr<VertexArray> QuadVertexArray;
// std::shared_ptr<Shader> FlatColorShader; 删除
std::shared_ptr<Shader> TextureShader;
std::shared_ptr<Texture2D> WhiteTexture;// 这张图是白色的, 用作FlatColor
};

void Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2 size, const glm::vec4& color)//不要纹理时只传入Color
{
s_Data->TextureShader->SetFloat4("u_Color", color);
s_Data->WhiteTexture->Bind();

glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::scale(glm::mat4(1.0f), { size.x, size.y,1.0f });
s_Data->TextureShader->SetMat4("u_Transform", transform);

s_Data->QuadVertexArray->Bind();
RenderCommand::DrawIndexed(s_Data->QuadVertexArray);
}
void Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2 size, const Ref<Texture2D>& texture)
{
s_Data->TextureShader->SetFloat4("u_Color", glm::vec4(1.0f));
texture->Bind();

glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::scale(glm::mat4(1.0f), { size.x, size.y,1.0f });
s_Data->TextureShader->SetMat4("u_Transform", transform);
}

Sandbox2D.cpp

1
2
3
4
5
6
void Sandbox2D::OnUpdate(HEngine::Timestep ts)
{
HEngine::Renderer2D::DrawQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, { 0.8f, 0.2f, 0.3f, 1.0f });
HEngine::Renderer2D::DrawQuad({ 0.5f, -0.5f }, { 0.5f, 0.75f }, { 0.2f, 0.3f, 0.8f, 1.0f });
HEngine::Renderer2D::DrawQuad({ 0.0f, 0.0f, -0.1f }, { 10.0f, 10.0f }, m_CheckerboardTexture);
}

三十七、添加程序性能检测功能

对于C++程序的性能优劣,程序所用时长是一个很重要的指标,为了方便预览和分析程序里想要分析的代码片段每帧所花的时间,这里选了一个很简单的方法,就是在Runtime把Profile信息写到一个JSON文件里,然后利用Chrome提供的Chrome Tracing工具(只要在谷歌浏览器里输入chrome://tracing即可打开它),来帮忙分析这个JSON文件。

实现写入JSON文件的类叫instrumentor,这个单词在英语里其实并不存在,它源自于单词instrumentation,本意是一套仪器、仪表,在CS里的意思是对程序性能、错误等方面的监测

所以instrumentor可以翻译为程序性能检测者,代码如下:

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
namespace HEngine {
struct ProfileResult
{
std::string Name;
long long Start, End;
uint32_t ThreadID;
};

struct InstrumentationSession
{
std::string Name;
};

class Instrumentor
{
private:
InstrumentationSession* m_CurrentSession;
std::ofstream m_OutputStream;
int m_ProfileCount;
public:
Instrumentor()
: m_CurrentSession(nullptr), m_ProfileCount(0)
{
}

void BeginSession(const std::string& name, const std::string& filepath = "results.json")
{
m_OutputStream.open(filepath);
WriteHeader();
m_CurrentSession = new InstrumentationSession{ name };
}

void EndSession()
{
WriteFooter();
m_OutputStream.close();
delete m_CurrentSession;
m_CurrentSession = nullptr;
m_ProfileCount = 0;
}

void WriteProfile(const ProfileResult& result)
{
if (m_ProfileCount++ > 0)
m_OutputStream << ",";

std::string name = result.Name;
std::replace(name.begin(), name.end(), '"', '\'');

m_OutputStream << "{";
m_OutputStream << "\"cat\":\"function\",";
m_OutputStream << "\"dur\":" << (result.End - result.Start) << ',';
m_OutputStream << "\"name\":\"" << name << "\",";
m_OutputStream << "\"ph\":\"X\",";
m_OutputStream << "\"pid\":0,";
m_OutputStream << "\"tid\":" << result.ThreadID << ",";
m_OutputStream << "\"ts\":" << result.Start;
m_OutputStream << "}";

m_OutputStream.flush();
}

void WriteHeader()
{
m_OutputStream << "{\"otherData\": {},\"traceEvents\":[";
m_OutputStream.flush();
}

void WriteFooter()
{
m_OutputStream << "]}";
m_OutputStream.flush();
}

static Instrumentor& Get()
{
static Instrumentor instance;
return instance;
}
};

class InstrumentationTimer
{
public:
InstrumentationTimer(const char* name)
: m_Name(name), m_Stopped(false)
{
m_StartTimepoint = std::chrono::high_resolution_clock::now();
}

~InstrumentationTimer()
{
if (!m_Stopped)
Stop();
}

void Stop()
{
auto endTimepoint = std::chrono::high_resolution_clock::now();

long long start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimepoint).time_since_epoch().count();
long long end = std::chrono::time_point_cast<std::chrono::microseconds>(endTimepoint).time_since_epoch().count();

uint32_t threadID = std::hash<std::thread::id>{}(std::this_thread::get_id());
Instrumentor::Get().WriteProfile({ m_Name, start, end, threadID });

m_Stopped = true;
}
private:
const char* m_Name;
std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimepoint;
bool m_Stopped;
};
}

#define HE_PROFILE 1
#if HE_PROFILE
#define HE_PROFILE_BEGIN_SESSION(name, filepath) ::HEngine::Instrumentor::Get().BeginSession(name, filepath)
#define HE_PROFILE_END_SESSION() ::HEngine::Instrumentor::Get().EndSession()
#define HE_PROFILE_SCOPE(name) ::HEngine::InstrumentationTimer timer##__LINE__(name);
#define HE_PROFILE_FUNCTION() HE_PROFILE_SCOPE(__FUNCSIG__)
#else
#define HE_PROFILE_BEGIN_SESSION(name, filepath)
#define HE_PROFILE_END_SESSION()
#define HE_PROFILE_SCOPE(name)
#define HE_PROFILE_FUNCTION()
#endif

三十八、改进Renderer2D

  • 此节目的,对Renderer2D API进行改进,重载DrawQuad方法进行添加旋转的功能。

改进

  • glsl纹理采样坐标从0-1到0-10应由CPP绘图API控制

    1
    color = texture(u_Texture, v_TexCoord * 10.0) * u_Color;	

    改为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #type fragment
    #version 450 core

    layout(location = 0) out vec4 color;

    in vec2 v_TexCoord;
    uniform vec4 u_Color;
    uniform float u_TilingFactor;// 由这个控制

    uniform sampler2D u_Texture;

    void main() {
    color = texture(u_Texture, v_TexCoord * u_TilingFactor) * u_Color;
    }

代码

  • Renderer2D.cpp新增和扩展API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void Renderer2D::DrawRotatedQuad(const glm::vec2& position, const glm::vec2 size, float rotation, const glm::vec4& color)
    {
    DrawRotatedQuad({ position.x,position.y,0.0f }, size, rotation, color);
    }

    void Renderer2D::DrawRotatedQuad(const glm::vec3& position, const glm::vec2 size, float rotation, const glm::vec4& color)
    {
    HE_PROFILE_FUNCTION();

    s_Data->TextureShader->SetFloat4("u_Color", color);
    s_Data->TextureShader->SetFloat("u_TilingFactor", 1.0f);
    s_Data->WhiteTexture->Bind();

    glm::mat4 transform = glm::translate(glm::mat4(1.0f), position)
    * glm::rotate(glm::mat4(1.0f), rotation, { 0.0f, 0.0f, 1.0f })
    * glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });
    s_Data->TextureShader->SetMat4("u_Transform", transform);
    s_Data->QuadVertexArray->Bind();
    RenderCommand::DrawIndexed(s_Data->QuadVertexArray);
    }
    ...
  • Sandbox2D.cpp

    1
    2
    3
    4
    5
    void Sandbox2D::OnUpdate(HEngine::Timestep ts)
    {
    HEngine::Renderer2D::DrawRotatedQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, 45.0f, { 0.8f, 0.2f, 0.3f, 1.0f });
    HEngine::Renderer2D::DrawQuad({ 0.0f, 0.0f, -0.1f }, { 10.0f, 10.0f }, m_CheckerboardTexture, 10.0f, glm::vec4(1.0f, 0.9f, 0.9f, 1.0f));
    }

三十九、实现批处理2D渲染

前言

  • 目的

    实现OpenGL的批处理渲染,减少OpenGL绘制命令的调用,用一次OpenGL绘制命令,绘制多个图形

  • 大致思路

    CPU和GPU都开辟同样大小的一大块内存(为了存储顶点信息)

    索引在程序运行时生成对应规则后绑定到索引缓冲中

    动态生成顶点信息(现在改成Drawquad只是确定图形顶点的位置)

    然后在Endscene,将CPU的动态生成的顶点数据上传给GPU,然后再绘制出来。

关键代码流程

  • CPU和GPU都开辟同样大小的一大块内存(为了存储顶点信息)

    1
    2
    3
    4
    5
    // 在CPU开辟存储s_Data.MaxVertices个的QuadVertex的内存
    s_Data.QuadVertexBufferBase = new QuadVertex[s_Data.MaxVertices];

    // 创建顶点缓冲区,先在GPU开辟一块s_Data.MaxVertices * sizeof(QuadVertex)大小的内存
    s_Data.QuadVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(QuadVertex));
  • 索引在程序运行时生成对应规则后绑定到索引缓冲中

    (规则就是2个三角形组成的Quad,本来2个三角形共6个顶点,用索引后可以重复利用顶点,从而减少到4个顶点组成一个四方形)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    uint32_t* quadIndices = new uint32_t[s_Data.MaxIndices];

    // 一个quad用6个索引,012 230
    uint32_t offset = 0;
    for (uint32_t i = 0; i < s_Data.MaxIndices; i += 6) {
    quadIndices[i + 0] = offset + 0;
    quadIndices[i + 1] = offset + 1;
    quadIndices[i + 2] = offset + 2;

    quadIndices[i + 3] = offset + 2;
    quadIndices[i + 4] = offset + 3;
    quadIndices[i + 5] = offset + 0;

    offset += 4;
    }

    Ref<IndexBuffer> flatIB = IndexBuffer::Create(quadIndices, s_Data.MaxIndices);

    s_Data.QuadVertexArray->SetIndexBuffer(flatIB);
    delete[] quadIndices;
  • 动态生成顶点信息,主要是位置、纹理坐标

    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
    void HEngine::Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2& size, const glm::vec4& color)
    {
    // quad的左下角为起点
    s_Data.QuadVertexBufferPtr->Position = position;
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 0.0f };
    s_Data.QuadVertexBufferPtr++;

    s_Data.QuadVertexBufferPtr->Position = { position.x + size.x, position.y, 0.0f };
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 1.0f, 0.0f };
    s_Data.QuadVertexBufferPtr++;

    s_Data.QuadVertexBufferPtr->Position = { position.x + size.x, position.y + size.y, 0.0f };
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 1.0f, 1.0f };
    s_Data.QuadVertexBufferPtr++;

    s_Data.QuadVertexBufferPtr->Position = { position.x, position.y +size.y , 0.0f };
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 1.0f };
    s_Data.QuadVertexBufferPtr++;

    s_Data.QuadIndexCount += 6;// 每一个quad用6个索引
    }
  • 然后在Endscene,将CPU的动态生成的顶点数据(主要是位置信息)上传给GPU,然后再绘制出来

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void HEngine::Renderer2D::EndScene()
    {
    // 计算当前绘制需要多少个顶点数据
    uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
    // 截取部分CPU的顶点数据上传OpenGL
    s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);

    Flush();
    }

    void Renderer2D::Flush()
    {
    RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
    }
    ......
    void OpenGLVertexBuffer::SetData(const void* data, uint32_t size)
    {
    glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
    // 截取部分CPU的顶点数据上传OpenGL
    glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
    }
  • Glsl的代码

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

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

    uniform mat4 u_ViewProjection;
    // uniform mat4 u_Transform;

    out vec4 v_Color;
    out vec2 v_TexCoord;

    void main() {
    v_Color = a_Color;
    v_TexCoord = a_TexCoord;
    // 由规则动态生成的顶点位置(基于本地空间)没有涉及transform变换顶点位置
    // gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);
    gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
    }

四十、 Batch Textures

前言

  • 回顾纹理采样个人理解。

    1. 加载纹理资源,设置这个纹理资源开辟的缓冲区m_RendererID绑定在纹理槽0号上
    2. glsl上采样的纹理,在纹理槽0号上采样,纹理槽0号指向了第1步的纹理资源,这样就完成对这个加载的纹理采样。
  • OpenGL的纹理槽

    OpenGL限制了一次drawcall能使用多少个纹理槽,HEngine设置为32

    若想超过32有40个纹理需要使用,可以:

    • 分两次,第一次drawcall32个,然后清空重新加载纹理再drawcall
    • 用纹理集,texture altas

大致流程

  1. 先提前为此次的shader上传一个默认的大小为32的采样数组

    u_Textures[i] = i,u_Textures[1] = 1表示片段着色器采样纹理槽1号上的纹理

  2. 加载一个纹理得到纹理对象,用数组保存这个纹理对象

  3. 在绘制带有纹理的quad图形时,判断数组中是否有这个纹理对象

  4. 设置当前顶点采样的纹理单元是 i,后续会将这个纹理槽号 i 从顶点阶段传入到fragment片段着色器阶段

  5. 在Drawcall前,TextureSlots数组上存储已经加载的纹理,按照顺序依次绑定到对应的纹理槽

  6. Drawcall时,在片段着色器上,读取采样对应纹理槽号i上的纹理

代码

  • 先记住数据结构

    • std::array TextureSlots;

      是一个32大小的数组,数组的元素是纹理对象指针,用来存储加载好了的纹理对象的

    • int32_t samplers[s_Data.MaxTextureSlots];

      是一个32大小的数组,数组的元素是int值,用来上传给glsl的

  1. 先提前为此次的shader上传一个默认的大小为32的采样数组,u_Textures[i] = j, 其中i = j,u_Textures[1] = 1表示采样纹理槽1号上的纹理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 纹理的shader
    s_Data.TextureShader = Shader::Create("assets/shaders/Texture.glsl");

    int32_t samplers[s_Data.MaxTextureSlots];
    for (uint32_t i = 0; i < s_Data.MaxTextureSlots; i++) {
    samplers[i] = i;
    }
    s_Data.TextureShader->Bind();
    // 为TextureShader上传一个默认的大小为32的采样数组
    s_Data.TextureShader->SetIntArray("u_Textures", samplers, s_Data.MaxTextureSlots);
  2. 加载一个纹理得到纹理对象,用数组保存这个纹理对象

    1
    2
    3
    4
    5
    6
    7
    8
    // 白色纹理
    // 创建一个白色Texture
    s_Data.WhiteTexture = Texture2D::Create(1, 1);
    uint32_t whiteTextureData = 0xffffffff;
    s_Data.WhiteTexture->SetData(&whiteTextureData, sizeof(uint32_t));

    // 0号纹理槽对应白色纹理缓冲区
    s_Data.TextureSlots[0] = s_Data.WhiteTexture;
  3. 在当前绘制quad图形时,判断TextureSlots数组是否有这个纹理,有就取出 i 下标,没有就加在数组已有纹理的末尾,并且记录下标 i

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    float textureIndex = 0.0f;
    for (uint32_t i = 1; i < s_Data.TextureSlotIndex; i++)
    {
    // 当前纹理,如果已经存储在纹理槽,就直接读取
    if (*s_Data.TextureSlots[i].get() == *texture.get()) {
    textureIndex = (float)i;
    break;
    }
    }
    if (textureIndex == 0.0f) {
    textureIndex = (float)s_Data.TextureSlotIndex;
    s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
    s_Data.TextureSlotIndex++;
    }
  4. 在顶点数据数组设置当前顶点采样的纹理单元是 i,后续在第6步会将这个纹理槽号 i 传入到fragment阶段

    1
    2
    3
    4
    5
    6
    7
    // quad的左下角为起点
    s_Data.QuadVertexBufferPtr->Position = position;
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 0.0f };
    s_Data.QuadVertexBufferPtr->TexIndex = textureIndex; // 采样的纹理槽号
    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data.QuadVertexBufferPtr++;

    glsl相关

    1
    2
    3
    4
    5
    6
    7
    8
    9
    in float v_TexIndex;
    in float v_TilingFactor;

    uniform sampler2D u_Textures[32];

    void main()
    {
    color = texture(u_Textures[int(v_TexIndex)], v_TexCoord * v_TilingFactor) * v_Color;
    }
  5. 在Drawcall前,TextureSlots数组上存储已经加载的纹理,按照顺序依次绑定到对应的纹理槽

    这样就与第3、4步设置当前顶点采样的纹理槽号i是TextureSlots数组上的i号纹理

    1
    2
    3
    4
    5
    6
    7
    8
    void Renderer2D::Flush()
    {
    // 对应i的texture绑定到i号纹理槽
    for (uint32_t i = 0; i < s_Data.TextureSlotIndex; i++) {
    s_Data.TextureSlots[i]->Bind(i);
    }
    RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
    }

四十一、优化批处理功能

前言

  • 前两节写的批处理留下来的问题

    1. 批处理没有处理Quad旋转的函数
    2. 设计的顶点位置是基于自身空间的,没有经过transform变换位置,所以无旋转效果
  • 此节所做

    1. 处理Quad旋转函数

    2. 将顶点位置在Cpu上计算,通过transform矩阵(平移、缩放、旋转)变换到世界空间

      与060之前不同,之前将顶点从局部空间转换到世界空间是在GPU(GLSL代码)上运行的。

  • 流程:

    1. 给所有quad以原点为初始位置(局部空间

    2. 再接受旋转角度,用初始位置transform矩阵 = 平移旋转缩放,再乘以顶点转换到世界空间

      (提下,写代码顺序是:平移旋转缩放,但解读顺序是:从右往左读,先进行缩放,再进行旋转最后平移)

    3. 将最终世界空间的位置上传到GLSL的顶点着色器阶段

关键代码

Renderer2D.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
void Renderer2D::DrawrRotatedQuad(const glm::vec3& position, const glm::vec2& size, float rotation, const Ref<Texture2D>& texture, float tilingFactor, const glm::vec4& tintColor)
{
constexpr glm::vec4 color = { 1.0f, 1.0f, 1.0f, 1.0f };

float textureIndex = 0.0f;
for (uint32_t i = 1; i < s_Data.TextureSlotIndex; i++)
{
if (*s_Data.TextureSlots[i].get() == *texture.get()) {
textureIndex = (float)i;
break;
}
}
if (textureIndex == 0.0f) {
textureIndex = (float)s_Data.TextureSlotIndex;
s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
s_Data.TextureSlotIndex++;
}
// 设置transform
glm::mat4 tranform = glm::translate(glm::mat4(1.0f), position) *
glm::rotate(glm::mat4(1.0f), glm::radians(rotation), { 0.0f, 0.0f, 1.0f }) *
glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });

s_Data.QuadVertexBufferPtr->Position = tranform * s_Data.QuadVertexPosition[0];
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 0.0f };
s_Data.QuadVertexBufferPtr->TexIndex = textureIndex;
s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
s_Data.QuadVertexBufferPtr++;

s_Data.QuadVertexBufferPtr->Position = tranform * s_Data.QuadVertexPosition[1];
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 1.0f, 0.0f };
s_Data.QuadVertexBufferPtr->TexIndex = textureIndex;
s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
s_Data.QuadVertexBufferPtr++;

s_Data.QuadVertexBufferPtr->Position = tranform * s_Data.QuadVertexPosition[2];
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 1.0f, 1.0f };
s_Data.QuadVertexBufferPtr->TexIndex = textureIndex;
s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
s_Data.QuadVertexBufferPtr++;

s_Data.QuadVertexBufferPtr->Position = tranform * s_Data.QuadVertexPosition[3];
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 1.0f };
s_Data.QuadVertexBufferPtr->TexIndex = textureIndex;
s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
s_Data.QuadVertexBufferPtr++;

s_Data.QuadIndexCount += 6;// 每一个quad用6个索引
}

Sandbox2D.cpp

1
2
3
4
5
6
7
8
9
10
void Sandbox2D::OnUpdate(HEngine::Timestep ts)
{
{
static float rotation = 0.0f;
rotation += ts * 50.0f;

HEngine::Renderer2D::DrawRotatedQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, 45.0f, { 0.9f, 0.2f, 0.3f, 1.0f });
HEngine::Renderer2D::DrawRotatedQuad({ -0.5f, -0.5f, 0.0f }, { 1.0f, 1.0f }, rotation, m_CheckerboardTexture, 20.0f);
}
}

四十二、重置绘画缓存

前言

  • 之前批处理设计带来的问题

    当绘画的图形所占的内存超过我们得预先给定的空间,应该分两次Drawcall

    第二次drawcall时候需要重置内存数据,以便能开始下一轮批处理。

此节代码思路

  1. 需要知道当前渲染信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Renderer2D.h
    // 当前渲染的信息
    struct Statistics {
    uint32_t DrawCalls = 0;
    uint32_t QuadCount = 0;

    uint32_t GetTotalVertexCount() { return QuadCount * 4; }
    uint32_t GetTotalIndexCount() { return QuadCount * 6; }
    };
  2. 设置最大绘制数量

    1
    2
    3
    4
    5
    6
    7
    Renderer2D.cpp
    struct Renderer2DData {
    static const uint32_t MaxQuads = 20000;// 一次绘制多少个Quad
    .....

    Renderer2D::Statistics Stats;
    };
  3. 当绘画的图形所占的内存超过我们得预先给定的空间,需要有判定(提交渲染和重置)

    1
    2
    3
    4
    5
    if (s_Data.QuadIndexCount >= Renderer2DData::MaxIndices) {// 判断需要提交渲染和重置
    FlushAndReset();
    }
    ......
    s_Data.Stats.QuadCount++;
  4. 当一次渲染超过这个数量,分两次渲染,第二次渲染时候需要重置内存数据

    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
    // 内存不够为了分批渲染要做的drawcall绘制和重置
    void Renderer2D::FlushAndReset()
    {
    EndScene();
    //重置
    s_Data.QuadIndexCount = 0;
    s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;
    s_Data.TextureSlotIndex = 1;
    }
    void Renderer2D::EndScene{
    // 计算上传大小
    uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
    s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);

    Flush();
    }
    void Renderer2D::Flush(){
    // Bind textures
    for (uint32_t i = 0; i < s_Data.TextureSlotIndex; i++)
    s_Data.TextureSlots[i]->Bind(i);

    RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
    s_Data.Stats.DrawCalls++;
    }
    .........................
    void OpenGLVertexBuffer::SetData(const void* data, uint32_t size){
    glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);

    glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
    }
  5. 调用绘制Render2D API代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void Sandbox2D::OnUpdate(HEngine::Timestep ts)
    {
    HE_PROFILE_FUNCTION();

    m_CameraController.OnUpdate(ts);

    // 渲染信息初始化
    HEngine::Renderer2D::ResetStats();

    ......

    HEngine::Renderer2D::BeginScene(m_CameraController.GetCamera());
    HEngine::Renderer2D::DrawrRotatedQuad({ 1.0f, 0.5f }, { 0.8f, 0.8f },30.0f, m_FlatColor);
    HEngine::Renderer2D::DrawQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, m_FlatColor);
    HEngine::Renderer2D::DrawQuad({ 0.5f, -0.5f }, { 0.5f, 0.8f }, {0.2f, 0.8f, 0.9f, 1.0f});
    HEngine::Renderer2D::DrawQuad({ 0.0f, 0.0f, -0.1f }, { 20.0f, 20.0f }, m_SquareTexture, 10.0f);
    HEngine::Renderer2D::DrawrRotatedQuad({ -0.5f, -1.5f, 0.0f }, { 1.0f, 1.0f }, rotation, m_SquareTexture, 20.0f);
    HEngine::Renderer2D::EndScene();
    ......

四十三、 使用ImGui渲染纹理

前言

  • 接下来想做的
    1. 使HEngine成为一个独立的工具
    2. 可以打开成窗口应用程序
    3. 可以操作程序添加场景
    4. 在场景上放入精灵、实体,再写脚本语言给实体添加一些行为
    5. 以及给实体添加组件
    6. 然后可以导出为游戏执行文件,可以在编辑器之外运行

Dockspace

  • 什么是dockspace

    imgui提供的可以停靠在窗口上,能够实现重新布局窗口的功能。

  • 缺点

    • 问题所在

      使用了Imgui的dockspace,OpenGL绘制的场景不见了,因为imgui接管了窗口

    • 如何解决

      让Opengl渲染到framebuffer帧缓冲中,作为texture纹理,然后Imgui再渲染这个texture。

    • 代码:

      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
      void Sandbox2D::OnImgGuiRender()
      {
      .....
      {
      ImGui::Begin("Settings");

      auto stats = HEngine::Renderer2D::GetStats();
      ImGui::Text("Renderer2D Stats:");
      ImGui::Text("Draw Calls: %d", stats.DrawCalls);
      ImGui::Text("Quads: %d", stats.QuadCount);
      ImGui::Text("Vertices: %d", stats.GetTotalVertexCount());
      ImGui::Text("Indices: %d", stats.GetTotalIndexCount());

      ImGui::ColorEdit4("Square Color", glm::value_ptr(m_SquareColor));

      uint32_t textureID = m_CheckerboardTexture->GetRendererID();
      ImGui::Image((void*)textureID, ImVec2{ 256.0f, 256.0f });
      ImGui::End();

      ImGui::End();
      }
      else
      {
      ImGui::Begin("Settings");

      auto stats = HEngine::Renderer2D::GetStats();
      ImGui::Text("Renderer2D Stats:");
      ImGui::Text("Draw Calls: %d", stats.DrawCalls);
      ImGui::Text("Quads: %d", stats.QuadCount);
      ImGui::Text("Vertices: %d", stats.GetTotalVertexCount());
      ImGui::Text("Indices: %d", stats.GetTotalIndexCount());

      ImGui::ColorEdit4("Square Color", glm::value_ptr(m_SquareColor));

      uint32_t textureID = m_CheckerboardTexture->GetRendererID();
      ImGui::Image((void*)textureID, ImVec2{ 256.0f, 256.0f });
      ImGui::End();
      }
      }

四十四、实现自定义帧缓冲

前言

帧缓冲(我的理解)

  1. 可以将OpenGL渲染的场景放在这个帧缓冲中
  2. 然后可以把这个帧缓冲当做是颜色或者纹理采样区(取决于帧缓冲附加的缓冲附件类型
  3. 在ImGui把这个帧缓冲当做颜色纹理渲染出来,就在ImGui界面上显示了原本应显示在屏幕上的场景

代码流程

  • 创建帧缓冲并附加纹理、深度与模板缓冲纹理

    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
    void OpenGLFramebuffer::Invalidate()
    {
    // 1.创建帧缓冲
    glCreateFramebuffers(1, &m_RendererID);
    glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID); // 绑定这个帧缓冲

    // 2.创建纹理
    glCreateTextures(GL_TEXTURE_2D, 1, &m_ColorAttachment);;
    glBindTexture(GL_TEXTURE_2D, m_ColorAttachment);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Specification.Width, m_Specification.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // 1.1纹理附加到帧缓冲
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_ColorAttachment, 0);

    // 3.创建深度模板缓冲纹理附加到帧缓冲中
    glCreateTextures(GL_TEXTURE_2D, 1, &m_DepthAttachment);;
    glBindTexture(GL_TEXTURE_2D, m_DepthAttachment);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, m_Specification.Width, m_Specification.Height);

    // 1.2深度模板缓冲纹理附加到帧缓冲中
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, m_DepthAttachment, 0);

    HE_CORE_ASSERT(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "帧缓冲未创建完成");

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }
  • 给创建的帧缓冲写入数据

    1. 绑定自定义的帧缓冲
    2. 正常渲染图形,将本由OpenGL渲染在屏幕上的图像写入到自定义的帧缓冲中
    3. 解绑帧缓冲
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void Sandbox2D::OnUpdate(HEngine::Timestep ts)
    {
    HEngine::Renderer2D::ResetStats();
    {
    m_Framebuffer->Bind();

    HEngine::RenderCommand::SetClearColor({ 0.1f, 0.1f, 0.1f, 1 });
    HEngine::RenderCommand::Clear();
    HEngine::Renderer2D::DrawrRotatedQuad({ 1.0f, 0.5f }, { 0.8f, 0.8f },30.0f, m_FlatColor);
    HEngine::Renderer2D::DrawQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, m_FlatColor);
    .......
    // 3.解绑帧缓冲
    m_Framebuffer->Unbind();
    }
    ......
    }
  • Imgui渲染自定义的帧缓冲

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ImGui::Begin("Settings");

    auto stats = HEngine::Renderer2D::GetStats();
    ImGui::Text("Renderer2D Stats:");
    ImGui::Text("Draw Calls: %d", stats.DrawCalls);
    ImGui::Text("Quads: %d", stats.QuadCount);
    ImGui::Text("Vertices: %d", stats.GetTotalVertexCount());
    ImGui::Text("Indices: %d", stats.GetTotalIndexCount());

    ImGui::ColorEdit4("Square Color", glm::value_ptr(m_SquareColor));
    //在执行完Invalidate方法后,所有渲染内容都会存储到这个FBO的附件(ColorAttachment)中
    //此时只要渲染这个纹理就会得出原来屏幕的画面
    uint32_t textureID = m_Framebuffer->GetColorAttachmentRendererID();
    ImGui::Image((void*)textureID, ImVec2{ 1280, 720 }, ImVec2{ 0, 1}, ImVec2{ 1, 0 });

    ImGui::End();

BUG:ImGui上显示OpenGL渲染的图像

  • Bug说明

    可视化界面的wasd摄像机移动,显示相反(按a却成d的效果)。

  • Bug分析

    由于imgui的uv默认是左下角为01,右下角为11,左上角为00,右上角是10(起始点在左上角)

    而我们绘制的quad的uv是左下角为00,右下角10,左上角01,右上角11(起始点在左下角)

  • 调整

    1
    2
    3
    4
    5
    /*
    ImVec2(0, 1):设置左上角点的uv是 0 1
    ImVec2(1, 0):设置右下角点的uv是 1 0
    */
    ImGui::Image((void*)textureID, ImVec2( 1280, 720 ), ImVec2(0, 1), ImVec2(1, 0));

补充:创建HEngine-Editor新项目并设置为启动项目, 用来做可视化界面,构建游戏,原先Sandbox项目的操作移动到Editor里面


四十五、调整视口大小处理

前言

  • 此节目标
    • 实现调整ImGui视口大小后,摄像机、绘图的区域、帧缓冲纹理也相应调整

代码流程

  • EditorLayer.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ImVec2 viewportPanelSize = ImGui::GetContentRegionAvail();
    if (m_ViewportSize != *((glm::vec2*)&viewportPanelSize)) { // 如果改变了窗口大小
    // 调整帧缓冲区
    m_Framebuffer->Resize((uint32_t)viewportPanelSize.x, (uint32_t)viewportPanelSize.y);
    m_ViewportSize = { viewportPanelSize.x, viewportPanelSize.y };
    // 调整摄像机
    m_CameraController.OnResize(viewportPanelSize.x, viewportPanelSize.y);
    }

    uint32_t textureID = m_Framebuffer->GetColorAttachmentRendererID();
    ImGui::Image((void*)textureID, ImVec2(m_ViewportSize.x, m_ViewportSize.y), ImVec2(0, 1), ImVec2(1, 0));

    OpenGLFramebuffer.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void OpenGLFramebuffer::Resize(uint32_t width, uint32_t height)
    {
    m_Specification.Width = width;
    m_Specification.Height = height;

    Invalidate(); //重新生成帧缓冲
    }

    // 在下次帧缓冲被绑定时,OpenGL视口大小被设置为imgui视口大小
    void OpenGLFramebuffer::Bind()
    {
    glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID);
    glViewport(0, 0, m_Specification.Width, m_Specification.Height);
    }

四十六、事件触发判定优化

发现一个bug,如鼠标放在ImGui的Settings窗口区域滑动鼠标滚轮,渲染方块的Viewport窗口也会响应,这就有问题了,应该是我鼠标放在窗口区域内并且点击了代表我选择这个窗口了,才能响应事件

解决方法:由ImGui视口能获取焦点鼠标停留bool值,用这两个bool值来处理

代码:

EditorLayer.cpp

1
2
3
4
5
6
7
8
9
10
11
void EditorLayer::OnUpdate(HEngine::Timestep ts)
{
if(m_ViewportFocused) //只有鼠标选中当前区域才能触发摄像机移动等事件
m_CameraController.OnUpdate(ts);
}
void EditorLayer::OnImGuiRender()
{
m_ViewportFocused = ImGui::IsWindowFocused();
m_ViewportHovered = ImGui::IsWindowHovered();
Application::Get().GetImGuiLayer()->BlockEvents(!m_ViewportFocused || !m_ViewportHovered);
}

ImGuiLayer.cpp

1
2
3
4
5
6
if (m_BlockEvents)	//在鼠标既不在渲染窗口位置也没点击选中时才能响应ImGui其他窗口
{
ImGuiIO& io = ImGui::GetIO();
e.Handled |= e.IsInCategory(EventCategoryMouse) & io.WantCaptureMouse;
e.Handled |= e.IsInCategory(EventCategoryKeyboard) & io.WantCaptureKeyboard;
}

四十七、设计实体组件系统

关于ECS的介绍单独写了比较详细的文章,实体组件系统(ECS)

HEngine选择使用提供ECS模式的Entt库来实现我们的实体组件系统,Entt相关的内容可以直接看对应的github仓库的介绍,这里是他的Wiki

该库所有的内容,应该都放到一个叫entt.hpp的文件里了,我看了下,这个文件非常大,一共有17600行,就把它当头文件用就行了

代码:

创建Scene类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <entt.hpp>
namespace HEngine
{
class Scene
{
public:
Scene();
~Scene();

entt::entity CreateEntity();

//TEMP
entt::registry& Reg() { return m_Registry; }

void OnUpdate(Timestep ts);
private:
entt::registry m_Registry;
};
}

Scene.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
// 用于后面的Callback例子, 当Transform组件被创建时调用, 会加到entity上
static void OnTransformConstruct(entt::registry& registry, entt::entity entity){}

// 创建一个registry, 可以把它理解为vector<entity>, 也就是包含所有entity的容器
entt::registry m_Registry;

// 创建一个entity, entt::entity其实是uint32_t
entt::entity entity = m_Registry.create();

// emplace等同于AddComponent, 这里给entity添加TransformComponent
m_Registry.emplace<TransformComponent>(entity, glm::mat4(1.0f));

// entt提供的Callback, 当TransformComponent被创建时, 调用OnTransformConstruct函数
m_Registry.on_construct<TransformComponent>().connect<&OnTransformConstruct>();

// 判断entity上是否有TransformComponent
if (m_Registry.all_of<TransformComponent>(entity))
// 从entity上get TransformComponent
TransformComponent& transform = m_Registry.get<TransformComponent>(entity);

// 获取所有带有TransformComponent的entity数组
auto view = m_Registry.view<TransformComponent>();
for (auto entity : view)
{
TransformComponent& transform = view.get<TransformComponent>(entity);
}

// group用来获取同时满足拥有多个Component的Entity数组
auto group = m_Registry.group<TransformComponent>(entt::get<SpriteRendererComponent>);
for (auto entity : group)
{
auto& [transform, spriteRenderer] = group.get<TransformComponent, SpriteRendererComponent>(entity);
}

ComPonents.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct TransformComponent
{
glm::mat4 Transform{ 1.0f };

TransformComponent() = default;
TransformComponent(const TransformComponent&) = default;
TransformComponent(const glm::mat4& transform)
: Transform(transform) {}

operator glm::mat4& () { return Transform; }
operator const glm::mat4& () const { return Transform; }
};
struct SpriteRendererComponent
{
glm::vec4 Color{ 1.0f,1.0f,1.0f,1.0f };

SpriteRendererComponent() = default;
SpriteRendererComponent(const SpriteRendererComponent&) = default;
SpriteRendererComponent(const glm::vec4& color)
:Color(color) {}
};

四十八、添加Entity类

前言

  • 此节目的

    • 创建出Entity实体类,作为中介连接场景与组件,能添加、删除组件
  • 思考Entt的API优缺点来设计出Scene、Entity类,之前学习到一个思路,设计一个API的时候应该先想象他需要拥有什么功能,然后再根据列出来的功能逐个实现就好了

    游戏引擎开发人员的角度,是场景拥有实体、场景给实体添加组件,所以以下调用Entt代码是合理的。

    1
    2
    3
    // 场景给square实体添加组件
    m_ActiveScene->Reg().emplace<TransformComponent>(square); // 先要获取注册表才能添加组件
    m_ActiveScene->Reg().emplace<SpriteRendererComponent>(square, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));

    但是,这代码太长了,引擎开发人员应该站在游戏开发人员角度思考

    1. 场景虽然拥有实体,可以获取实体给它添加组件
    2. 实体也应拥有组件,只需简单的考虑实体添加组件这个行为,而忽略场景这个东西
    3. 这有利于我们简化代码,目标是简化成Unity那样给gameobject添加组件的函数
    1
    2
    3
    4
    5
    6
    // 原先:场景给square实体添加组件
    m_ActiveScene->Reg().emplace<TransformComponent>(square); // 先要获取注册表才能添加组件
    m_ActiveScene->Reg().emplace<SpriteRendererComponent>(square, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));
    // 化简为:square实体添加组件,忽略场景
    square.AddComponent<TransformComponent>();
    square.AddComponent<SpriteRendererComponent>(glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));

代码

  • Entity.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
    namespace HEngine
    {
    class Entity
    {
    public:
    Entity() = default;
    Entity(entt::entity handle, Scene* scene);
    Entity(const Entity& other) = default;

    template<typename T, typename... Args>
    T& AddComponent(Args&&... args)
    {
    HE_CORE_ASSERT(!HasComponent<T>(), "Entity already has component!");
    return m_Scene->m_Registry.emplace<T>(m_EntityHandle, std::forward<Args>(args)...);
    }

    template<typename T>
    T& GetComponent()
    {
    HE_CORE_ASSERT(HasComponent<T>(), "Entity does not have component!");
    return m_Scene->m_Registry.get<T>(m_EntityHandle);
    }

    template<typename T>
    bool HasComponent()
    {
    return m_Scene->m_Registry.all_of<T>(m_EntityHandle);
    }

    template<typename T>
    void RemoveComponent()
    {
    HE_CORE_ASSERT(HasComponent<T>(), "Entity does not have component!");
    m_Scene->m_Registry.remove<T>(m_EntityHandle);
    }

    operator bool() const { return (uint32_t)m_EntityHandle != 0; }
    private:
    entt::entity m_EntityHandle{ null };
    Scene* m_Scene = nullptr;
    };
    }

    Entity.cpp

    1
    2
    3
    4
    5
    6
    7
    namespace HEngine
    {
    Entity::Entity(entt::entity handle, Scene* scene)
    : m_EntityHandle(handle), m_Scene(scene)
    {
    }
    }
  • Scene.h

    1
    2
    3
    4
    5
    entt::entity CreateEntity();//删除
    Entity CreateEntity(const std::string& name = std::string());

    private:
    friend class Entity;

    Scene.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    Entity Scene::CreateEntity(const std::string& name)
    {
    Entity entity = { m_Registry.create(), this };
    entity.AddComponent<TransformComponent>();
    auto& tag = entity.AddComponent<TagComponent>();
    tag.Tag = name.empty() ? "Entity" : name;
    return entity;
    }
  • EditorLayer.cpp使用

    1
    2
    3
    4
    5
    6
    7
    ImGui::Separator();
    auto& tag = m_SquaerEntity.GetComponent<TagComponent>().Tag;
    ImGui::Text("%s", tag.c_str());

    auto& squareColor = m_SquaerEntity.GetComponent<SpriteRendererComponent>().Color;
    ImGui::ColorEdit4("Square Color", glm::value_ptr(squareColor));
    ImGui::Separator();

四十九、添加摄像头组件

  • 此节内容

    • 设计一个Camera组件

    • 这个Camera组件需拥有Camera类指针

      Camera类拥有的属性与行为

      1. 设置正交投影projection矩阵
      2. 是否主相机
      3. 获取projection投影矩阵

Camera类与Camera组件设计

  • 创建Camera.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    namespace HEngine
    {
    class Camera
    {
    public:
    Camera(const glm::mat4& projection)
    : m_Projection(projection) {}

    const glm::mat4& GetProjection() const { return m_Projection; }

    private:
    glm::mat4 m_Projection;
    };
    }
  • Components.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct CameraComponent {
    Camera camera;
    bool primary = true;

    CameraComponent() = default;
    CameraComponent(const CameraComponent&) = default;
    CameraComponent(const glm::mat4 & projection)
    : camera(projection) {}
    };

场景切换到主摄像机视角代码流程

  • EditorLayer层添加一个实体

    1
    2
    3
    4
    Entity m_CameraEntity;			
    Entity m_SecondCamera;

    bool m_PrimaryCamera = true;
  • EditorLayer.cpp

    1
    2
    3
    4
    5
    6
    m_CameraEntity = m_ActiveScene->CreateEntity("Camera Entity");
    m_CameraEntity.AddComponent<CameraComponent>(glm::ortho(-16.0f, 16.0f, -9.0f, 9.0f, -1.0f, 1.0f));

    m_SecondCamera = m_ActiveScene->CreateEntity("Clip-Space Entity");
    auto& cc = m_SecondCamera.AddComponent<CameraComponent>(glm::ortho(-1.0f,1.0f,-1.0f,1.0f,-1.0f,1.0f));
    cc.Primary = false;
  • 在scene onupdate方法中寻找主摄像机,以及获取主摄像机的transform用来计算视图矩阵

    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
    void Scene::OnUpdate(Timestep ts)
    {
    Camera* mainCamera = nullptr;
    glm::mat4* cameraTransform = nullptr;
    {
    auto group = m_Registry.view<TransformComponent,CameraComponent>();
    for (auto entity : group)
    {
    auto& [transform, camera] = group.get<TransformComponent, CameraComponent>(entity);
    if (camera.Primary)
    {
    mainCamera = &camera.Camera;
    cameraTransform = &transform.Transform;
    break;
    }
    }
    }
    if(mainCamera)
    {
    Renderer2D::BeginScene(mainCamera->GetProjection(), *cameraTransform);

    auto group = m_Registry.group < TransformComponent>(entt::get<SpriteRendererComponent>);
    for (auto entity : group)
    {
    auto& [transform, sprite] = group.get<TransformComponent, SpriteRendererComponent>(entity);

    Renderer2D::DrawQuad(transform, sprite.Color);
    }
    Renderer2D::EndScene();
    }
    }
  • Renderer2D.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void Renderer2D::BeginScene(const Camera& camera, const glm::mat4& transform)
    {
    HE_PROFILE_FUNCTION();
    glm::mat4 viewProj = camera.GetProjection() * glm::inverse(transform);

    s_Data.TextureShader->Bind();
    s_Data.TextureShader->SetMat4("u_ViewProjection", viewProj);

    s_Data.QuadIndexCount = 0;
    s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;

    s_Data.TextureSlotIndex = 1;
    }

五十、 Scene Camera

  • 此节目的

    当ImGui视口大小发生改变时,场景中所有实体所拥有的摄像机都能正确的设置宽高比使图像不会变形

    • 此节所作
      • 新建了一个SceneCamera类继承自Camera类,作为摄像机组件的摄像机类属性
      • 视口改变,场景内所有摄像机都能更新宽高比

代码流程

  • 修改了Camera.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    namespace HEngine 
    {
    class Camera
    {
    public:
    Camera() = default;
    virtual ~Camera() = default;
    protected:
    glm::mat4 m_Projection = glm::mat4(1.0f);
    };
    }
  • 新添加了SceneCamera

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    namespace HEngine
    {
    class SceneCamera : public Camera
    {
    public:
    SceneCamera();
    virtual ~SceneCamera() = default;

    void SetOrthographic(float size, float nearClip, float farClip);

    void SetViewportSize(uint32_t width, uint32_t height);

    float GetOrthographicSize() const { return m_OrthographicSize; }
    void SetOrthographicSize(float size) { m_OrthographicSize = size; RecalculateProjection(); }
    private:
    void RecalculateProjection();
    private:
    float m_OrthographicSize = 10.0f;
    float m_OrthographicNear = -1.0f, m_OrthographicFar = 1.0f;

    float m_AspectRatio = 0.0f;
    };
    }
    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
    namespace HEngine
    {
    SceneCamera::SceneCamera()
    {
    RecalculateProjection();
    }
    void SceneCamera::SetOrthographic(float size, float nearClip, float farClip)
    {
    m_OrthographicSize = size;
    m_OrthographicNear = nearClip;
    m_OrthographicFar = farClip;
    RecalculateProjection();
    }
    void SceneCamera::SetViewportSize(uint32_t width, uint32_t height)
    {
    m_AspectRatio = (float)width / (float)height;
    RecalculateProjection();
    }
    void SceneCamera::RecalculateProjection()
    {
    float orthoLeft = -m_OrthographicSize * m_AspectRatio * 0.5f;
    float orhoRight = m_OrthographicSize * m_AspectRatio * 0.5f;
    float orthoBottom = -m_OrthographicSize * 0.5f;
    float orthoTop = m_OrthographicSize * 0.5f;

    m_Projection = glm::ortho(orthoLeft, orhoRight,
    orthoBottom, orthoTop, m_OrthographicNear, m_OrthographicFar);
    }
    }

    Scene.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void Scene::OnViewportResize(uint32_t width, uint32_t height)
    {
    m_ViewportWidth = width;
    m_ViewportHeight = height;

    //Resize our non-FixedAspectRatio cameras
    auto view = m_Registry.view<CameraComponent>();
    for (auto entity : view)
    {
    auto& cameraComponent = view.get<CameraComponent>(entity);
    if (!cameraComponent.FixedAspectRatio)
    cameraComponent.Camera.SetViewportSize(width, height);
    }
    }

    EditorLayer.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void EditorLayer::OnUpdate(Timestep ts)
    {
    if (FramebufferSpecification spec = m_Framebuffer->GetSpecification();
    m_ViewportSize.x > 0.0f && m_ViewportSize.y > 0.0f && //zero sized framebuffer is invalid
    (spec.Width != m_ViewportSize.x || spec.Height != m_ViewportSize.y))
    {
    m_Framebuffer->Resize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
    m_CameraController.OnResize(m_ViewportSize.x, m_ViewportSize.y);

    m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
    }
    }
    void EditorLayer::OnImGuiRender()
    {
    {
    auto& camera = m_SecondCamera.GetComponent<CameraComponent>().Camera;
    float orhoSize = camera.GetOrthographicSize();
    if (ImGui::DragFloat("Second Camera OrthO Size", &orhoSize))
    camera.SetOrthographicSize(orhoSize);
    }
    }

五十一、提供C++脚本功能

前言

  • 此节目标

    需要给实体添加脚本组件Native Scripting,实现可以用cpp语言来编写脚本

  • 此节所作

    给摄像机实体添加脚本组件,在引擎内的Scene视口中引擎人员和游戏开发人员都可以控制摄像机移动

  • 为什么需要添加cpp脚本组件

    • 引擎开发人员需要给引擎某个实体添加脚本功能,从而实现测试时候能运行想要的脚本功能。

实现在Scene视口操作摄像机移动

  • EditorLayer.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
    class CameraController : public ScriptableEntity
    {
    public:
    void OnCreate()
    {
    }

    void OnDestroy()
    {
    }

    void OnUpdate(Timestep ts)
    {
    auto& transform = GetComponent<TransformComponent>().Transform;
    float speed = 5.0f;

    if (Input::IsKeyPressed(KeyCode::A))
    transform[3][0] -= speed * ts;
    if (Input::IsKeyPressed(KeyCode::D))
    transform[3][0] += speed * ts;
    if (Input::IsKeyPressed(KeyCode::W))
    transform[3][1] += speed * ts;
    if (Input::IsKeyPressed(KeyCode::S))
    transform[3][1] -= speed * ts;
    }
    };

    m_CameraEntity.AddComponent<NativeScriptComponent>().Bind<CameraController>();
  • Component.h添加NativeScriptComponent脚本组件接收到CameraController作为参数T

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct NativeScriptComponent
    {
    ScriptableEntity* Instance = nullptr;

    std::function<void()> InstantiateFunction;
    std::function<void()> DestroyInstanceFunction;

    std::function<void(ScriptableEntity*)> OnCreateFunction;
    std::function<void(ScriptableEntity*)> OnDestroyFunction;
    std::function<void(ScriptableEntity*, Timestep)> OnUpdateFunction;

    template<typename T>
    void Bind()
    {
    InstantiateFunction = [&]() { Instance = new T(); };
    DestroyInstanceFunction = [&]() { delete (T*)Instance; Instance = nullptr; };

    OnCreateFunction = [](ScriptableEntity* instance) { ((T*)instance)->OnCreate(); };
    OnDestroyFunction = [](ScriptableEntity* instance) { ((T*)instance)->OnDestroy(); };
    OnUpdateFunction = [](ScriptableEntity* instance, Timestep ts) { ((T*)instance)->OnUpdate(ts); };
    }
    };
  • 在Scene.cpp的OnUpdate函数中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    m_Registry.view<NativeScriptComponent>().each([=](auto entity, auto& nsc)
    {
    if (!nsc.Instance)
    {
    nsc.InstantiateFunction();
    nsc.Instance->m_Entity = Entity{ entity, this };

    if (nsc.OnCreateFunction)
    nsc.OnCreateFunction(nsc.Instance);
    }

    if (nsc.OnUpdateFunction)
    nsc.OnUpdateFunction(nsc.Instance, ts);
    });
    }
  • ScriptableEntity.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    namespace HEngine 
    {
    class ScriptableEntity
    {
    public:
    template<typename T>
    T& GetComponent()
    {
    return m_Entity.GetComponent<T>();
    }
    private:
    Entity m_Entity;
    friend class Scene;
    };
    }

五十二、优化C++脚本函数

前言

  • 这节目的

    将上一节在NativeScriptComponent写的function执行的功能替换为虚函数(OnCreate、OnUpdate、OnDestroy)和函数指针(Instantiate、Destroy)

代码

  • ScriptableEntity.h 函数定义为虚函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ScriptableEntity {
    public:
    virtual ~ScriptableEntity() {}

    protected:
    virtual void OnCreate() {}
    virtual void OnDestroy() {}
    virtual void OnUpdate(Timestep ts) {}
    };
  • EditorLayer.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class CameraController : public ScriptableEntity
    {
    public:
    virtual void OnCreate() override
    {
    auto& transform = GetComponent<TransformComponent>().Transform;
    transform[3][0] = rand() % 10 - 5.0f;
    }
    virtual void OnDestory() override
    {
    }
    virtual void OnUpdate(Timestep ts) override
    {
    ...
    }
    }
  • Components.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct NativeScriptComponent 
    {
    ScriptableEntity* (*InstantiateScript)();
    void (*DestroyScript)(NativeScriptComponent*);

    template<typename T>
    void Bind()
    {
    InstantiateScript = []() {return static_cast<ScriptableEntity*>(new T()); };
    DestroyScript = [](NativeScriptComponent* nsc) {delete nsc->Instance; nsc->Instance = nullptr; };
    }
    };
  • Scene.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    void Scene::OnUpdate(Timestep ts)
    {
    // Update scripts
    {
    m_Registry.view<NativeScriptComponent>().each([=](auto entity, auto& nsc)
    {
    //TODO: Move to Scene::OnScenePlay
    if (!nsc.Instance)
    {
    nsc.Instance = nsc.InstantiateScript();
    nsc.Instance->m_Entity = Entity{ entity, this };

    nsc.Instance->OnCreate();
    }
    nsc.Instance->OnUpdate(ts);
    });
    }
    }

五十三、场景Hierarchy面板

前言

  • 此节目的

    给引擎添加hierarchy界面面板,用ECS遍历当前场景的实体,用imgui显示当前场景存活的实体

代码

  • EditorLayer.h

    1
    SceneHierarchyPanel m_SceneHierarchyPanel;
  • EditorLayer.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    void EditorLayer::OnAttach()
    {
    m_SceneHierarchyPanel.SetContext(m_ActiveScene);// 设置上下文
    }
    void EditorLayer::OnImGuiRender()
    {
    m_SceneHierarchyPanel.OnImGuiRender();
    }
  • SceneHierarchyPanel.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    namespace HEngine
    {
    class SceneHierarchyPanel
    {
    public:
    SceneHierarchyPanel() = default;
    SceneHierarchyPanel(const Ref<Scene>& scene);

    void SetContext(const Ref<Scene>& scene);

    void OnImGuiRender();
    private:
    void DrawEntityNode(Entity entity);
    private:
    Ref<Scene> m_Context;
    Entity m_SelectionContext;
    };
    }
  • SceneHierarchyPanel.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
    namespace HEngine
    {
    SceneHierarchyPanel::SceneHierarchyPanel(const Ref<Scene>& context)
    {
    SetContext(context);
    }
    void SceneHierarchyPanel::SetContext(const Ref<Scene>& context)
    {
    m_Context = context;
    }
    void SceneHierarchyPanel::OnImGuiRender()
    {
    ImGui::Begin("Scene Hierarchy");

    for (auto entity : m_Context->m_Registry.view<entt::entity>())
    {
    DrawEntityNode( { entity, m_Context.get() });
    }

    ImGui::End();
    }
    void SceneHierarchyPanel::DrawEntityNode(Entity entity)
    {
    auto& tag = entity.GetComponent<TagComponent>().Tag;
    /*
    ImGuiTreeNodeFlags_Selected:如果选中这个实体,则高亮这个实体。
    ImGuiTreeNodeFlags_OpenOnArrow:使节点可以点击箭头展开,而不是整个区域都能展开。
    */
    ImGuiTreeNodeFlags flags = ((m_SelectionContext == entity) ? ImGuiTreeNodeFlags_Selected : 0) | ImGuiTreeNodeFlags_OpenOnArrow;
    //TreeNodeEx 用于绘制一个可展开的树节点。
    bool opened = ImGui::TreeNodeEx((void*)(uint64_t)(uint32_t)entity, flags, tag.c_str());
    if (ImGui::IsItemClicked())
    {
    m_SelectionContext = entity;
    }

    if (opened)//该节点是否被展开
    {
    ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow;
    bool opened = ImGui::TreeNodeEx((void*)888888, flags, tag.c_str());
    if (opened)
    ImGui::TreePop();
    ImGui::TreePop();
    }
    }
    }

五十四、添加属性面板

前言

  • 此节目的

    写出像Unity点击Cube,显示的Inspector面板,上面显示Cube的所有组件:transform、Mesh等。

代码流程

  • 如果当前点击的实体有效

    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
    void SceneHierarchyPanel::OnImGuiRender()
    {
    // 优化:若当前在hierarchy面板并且没点击到实体,属性面板清空
    if (ImGui::IsMouseDown(0) && ImGui::IsWindowHovered()) {
    m_SelectionContext = {};
    }
    //新增面板
    ImGui::Begin("Properties");
    if (m_SelectionContext) {
    DrawComponents(m_SelectionContext);
    }
    ImGui::End();
    }
    void SceneHierarchyPanel::DrawComponents(Entity entity)
    {
    if (entity.HasComponent<TagComponent>())
    {
    auto& tag = entity.GetComponent<TagComponent>().Tag;

    char buffer[256];
    memset(buffer, 0, sizeof(buffer));
    strcpy_s(buffer, sizeof(buffer), tag.c_str());
    if (ImGui::InputText("Tag", buffer, sizeof(buffer)))
    {
    tag = std::string(buffer);
    }
    }

    if (entity.HasComponent<TransformComponent>())
    {
    if (ImGui::TreeNodeEx((void*)typeid(TransformComponent).hash_code(), ImGuiTreeNodeFlags_DefaultOpen, "Transform"))
    {
    auto& transform = entity.GetComponent<TransformComponent>().Transform;
    ImGui::DragFloat3("Position", glm::value_ptr(transform[3]), 0.1f);

    ImGui::TreePop();
    }
    }
    }

五十五、摄像头 UI

前言

  • 此节目的
    • 在属性面板上实现点击摄像机实体,属性面板显示摄像机实体的摄像机组件参数。
    • 修改摄像机的参数,摄像机会做出相应的显示
    • 给摄像机添加透视投影类型。

代码

  • SceneHierarchyPanel.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
    void SceneHierarchyPanel::DrawComponents(Entity entity)
    {
    if (entity.HasComponent<CameraComponent>())
    {
    if (ImGui::TreeNodeEx((void*)typeid(CameraComponent).hash_code(), ImGuiTreeNodeFlags_DefaultOpen, "Camera"))
    {
    auto& cameraComponent = entity.GetComponent<CameraComponent>();
    auto& camera = cameraComponent.Camera;

    ImGui::Checkbox("Primary", &cameraComponent.Primary);

    const char* projectionTypeStrings[] = { "Perspective", "Orthographic" };
    const char* currentProjectionTypeString = projectionTypeStrings[(int)camera.GetProjectionType()];
    if (ImGui::BeginCombo("Projection", currentProjectionTypeString))
    {
    for (int i = 0; i < 2; i++)
    {
    bool isSelected = currentProjectionTypeString == projectionTypeStrings[i];
    if (ImGui::Selectable(projectionTypeStrings[i], isSelected))
    {
    currentProjectionTypeString = projectionTypeStrings[i];
    //设置摄像头类型,重新计算投影矩阵
    camera.SetProjectionType((SceneCamera::ProjectionType)i);
    }
    if (isSelected)
    ImGui::SetItemDefaultFocus();
    }
    ImGui::EndCombo();
    }

    if (camera.GetProjectionType() == SceneCamera::ProjectionType::Perspective)
    {
    float verticalFov = glm::degrees(camera.GetPerspectiveVerticalFOV());
    if (ImGui::DragFloat("Vertical FOV", &verticalFov))
    camera.SetPerspectiveVerticalFOV(glm::radians(verticalFov));

    float orthoNear = camera.GetPerspectiveNearClip();
    if (ImGui::DragFloat("Near", &orthoNear))
    camera.SetPerspectiveNearClip(orthoNear);

    float orthoFar = camera.GetPerspectiveFarClip();
    if (ImGui::DragFloat("Far", &orthoFar))
    camera.SetPerspectiveFarClip(orthoFar);
    }

    if (camera.GetProjectionType() == SceneCamera::ProjectionType::Orthographic)
    {
    float orthoSize =camera.GetOrthographicSize();
    if (ImGui::DragFloat("Size", &orthoSize))
    camera.SetOrthographicSize(orthoSize);

    float orthoNear = camera.GetOrthographicNearClip();
    if (ImGui::DragFloat("Near", &orthoNear))
    camera.SetOrthographicNearClip(orthoNear);

    float orthoFar = camera.GetOrthographicFarClip();
    if (ImGui::DragFloat("Far", &orthoFar))
    camera.SetOrthographicFarClip(orthoFar);

    ImGui::Checkbox("Fixed Aspect Ratio", &cameraComponent.FixedAspectRatio);
    }
    }
    }
    }
  • 在Scene中绘制时,获取到主摄像机的投影矩阵,计算得到投影视图矩阵传入给OpenGL计算绘制的图形在世界的最终位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void SceneCamera::RecalculateProjection()
    {
    // 区分是正交还是透视
    if (m_ProjectionType == ProjectionType::Perspective)
    {
    //glm::perspective()计算透视投影矩阵
    m_Projection = glm::perspective(m_PerspectiveFOV, m_AspectRatio, m_PerspectiveNear, m_PerspectiveFar);
    }
    else
    {
    float orthoLeft = -m_OrthographicSize * m_AspectRatio * 0.5f;
    float orthoRight = m_OrthographicSize * m_AspectRatio * 0.5f;
    float orthoBottom = -m_OrthographicSize * 0.5f;
    float orthoTop = m_OrthographicSize * 0.5f;

    m_Projection = glm::ortho(orthoLeft, orthoRight, orthoBottom,
    orthoTop, m_OrthographicNear, m_OrthographicFar);
    }
    }

五十六、Sprite UI

前言

  • 此节目的

    点击实体,在属性面板里显示这个实体的spriterenderer组件的属性,应包括颜色、texture、shader什么的(纹理),但目前先只显示颜色

代码

SceneHierarchyPhanel.cpp

1
2
3
4
5
6
7
8
// 实体transform组件
if (entity.HasComponent<SpriteRendererComponent>()) {
if (ImGui::TreeNodeEx((void*)typeid(SpriteRendererComponent).hash_code(), ImGuiTreeNodeFlags_DefaultOpen, "Sprite Renderer")) {
auto& src = entity.GetComponent<SpriteRendererComponent>();
ImGui::ColorEdit4("Color", glm::value_ptr(src.Color));
ImGui::TreePop();
}
}

五十七、TransForm UI

前言

  • 此节目的

    点击实体,在属性面板显示实体的Transform组件的位置、缩放、旋转属性。

    对UI有要求

    1. 文字需在左边,属性按钮在右边
    2. x不是label,而是按钮且带有颜色,且点击按钮可以复原值。

代码

  • Components.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    struct TransformComponent 
    {
    glm::vec3 Translation = { 0.0f, 0.0f, 0.0f };
    glm::vec3 Rotation = { 0.0f, 0.0f,0.0f };
    glm::vec3 Scale = { 1.0f, 1.0f, 1.0f };

    TransformComponent(const glm::vec3& translation)
    : Translation(translation) {}

    glm::mat4 GetTransform()const {
    glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), Rotation.x, { 1,0,0 })
    * glm::rotate(glm::mat4(1.0f), Rotation.y, { 0, 1, 0 })
    * glm::rotate(glm::mat4(1.0f), Rotation.z, { 0, 0, 1 });

    return glm::translate(glm::mat4(1.0f), Translation)
    * rotation
    * glm::scale(glm::mat4(1.0f), Scale);
    }
    };
  • SceneHierarchyPanel.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
    static void DrawVec3Control(const std::string& label, glm::vec3& values, float resetValue = 0.0f, float columnWidth = 100.0f)
    {
    ImGui::PushID(label.c_str()); // 为每个控件组推入唯一ID,防止命名冲突

    ImGui::Columns(2); // 设置两列布局
    ImGui::SetColumnWidth(0, columnWidth); // 第一列的宽度
    ImGui::Text(label.c_str()); // 显示标签
    ImGui::NextColumn(); // 切换到第二列

    ImGui::PushMultiItemsWidths(3, ImGui::CalcItemWidth()); // 设置 3 个元素的宽度
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{ 0, 0 }); // 设置控件间距为 0

    // 计算按钮大小(基于字体大小)
    float lineHeight = GImGui->Font->FontSize + GImGui->Style.FramePadding.y * 2.0f;
    ImVec2 buttonSize = { lineHeight + 3.0f, lineHeight };

    // X 轴按钮(红色)
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{ 0.8f, 0.1f, 0.15f, 1.0f });
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{ 0.9f, 0.2f, 0.2f, 1.0f });
    ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{ 0.8f, 0.1f, 0.15f, 1.0f });
    if (ImGui::Button("X", buttonSize)) // 点击按钮时重置 X 值
    values.x = resetValue;
    ImGui::PopStyleColor(3); // 恢复按钮颜色

    ImGui::SameLine(); // 让 X 轴拖动条与按钮在同一行
    ImGui::DragFloat("##X", &values.x, 0.1f, 0.0f, 0.0f, "%.2f"); // 拖动条,调整 X 值
    ImGui::PopItemWidth(); // 弹出 X 轴控件宽度
    ImGui::SameLine(); // 让 Y 轴按钮在同一行

    // Y 轴按钮(绿色)
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{ 0.2f, 0.7f, 0.2f, 1.0f });
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{ 0.3f, 0.8f, 0.3f, 1.0f });
    ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{ 0.2f, 0.7f, 0.2f, 1.0f });
    if (ImGui::Button("Y", buttonSize))
    values.y = resetValue;
    ImGui::PopStyleColor(3);

    ImGui::SameLine();
    ImGui::DragFloat("##Y", &values.y, 0.1f, 0.0f, 0.0f, "%.2f");
    ImGui::PopItemWidth();
    ImGui::SameLine();

    // Z 轴按钮(蓝色)
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{ 0.1f, 0.25f, 0.8f, 1.0f });
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{ 0.2f, 0.35f, 0.9f, 1.0f });
    ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{ 0.1f, 0.25f, 0.8f, 1.0f });
    if (ImGui::Button("Z", buttonSize))
    values.z = resetValue;
    ImGui::PopStyleColor(3);

    ImGui::SameLine();
    ImGui::DragFloat("##Z", &values.z, 0.1f, 0.0f, 0.0f, "%.2f");
    ImGui::PopItemWidth();

    ImGui::PopStyleVar(); // 恢复原始的 ItemSpacing

    ImGui::Columns(1); // 结束列布局

    ImGui::PopID(); // 弹出 ID,防止影响其他控件
    }

    void SceneHierarchyPanel::DrawComponents(Entity entity)
    {
    if (entity.HasComponent<TransformComponent>())
    {
    auto& tc = entity.GetComponent<TransformComponent>();
    DrawVec3Control("Translation", tc.Translation);
    glm::vec3 rotation = glm::degrees(tc.Rotation);
    DrawVec3Control("Rotation", rotation);
    tc.Rotation = glm::radians(rotation);
    DrawVec3Control("Scale", tc.Scale, 1.0f);
    }
    }


五十八、实体和组件增删的UI

前言

  • 目的

    此节需要完成像Unity那样

    • 右键实体可以弹出菜单可以删除实体,右键空白地方可以弹出菜单添加实体
    • 在属性面板显示实体的组件,有添加组件按钮,点击弹出菜单项可以添加相应组件。
    • 在属性面板显示实体的组件,每个组件的下拉有按钮,点击弹出菜单项可以删除这个组件

代码

  • 删除、添加实体

    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
    void SceneHierarchyPanel::OnImGuiRender()
    {
    // 判断当前点击的实体是否存在
    ImGui::Begin("Properties");
    if (m_SelectionContext) { // operator uint32_t() 的是const,不然不会调用operator bool(),而是调用uint32_t()
    DrawComponents(m_SelectionContext);
    // 弹出菜单: 在属性面板显示添加组件按钮
    if (ImGui::Button("Add Component")) {
    ImGui::OpenPopup("AddComponent");// AddComponent只是id
    }
    if (ImGui::BeginPopup("AddComponent")) {
    if (ImGui::MenuItem("Camera")) {
    m_SelectionContext.AddComponent<CameraComponent>();
    ImGui::CloseCurrentPopup();
    }
    if (ImGui::MenuItem("Sprite Renderer")) {
    m_SelectionContext.AddComponent<SpriteRendererComponent>();
    }
    ImGui::EndPopup();
    }
    }
    ImGui::End();
    }
    void SceneHierarchyPanel::DrawEntityNode(Entity entity)
    {
    auto& tag = entity.GetComponent<TagComponent>().Tag;
    // 若是被点击标记为选中状态|有下一级
    ImGuiTreeNodeFlags flags = ((m_SelectionContext == entity) ? ImGuiTreeNodeFlags_Selected : 0) | ImGuiTreeNodeFlags_OpenOnArrow;
    // 第一个参数是唯一ID 64的,
    bool opened = ImGui::TreeNodeEx((void*)(uint64_t)(uint32_t)entity, flags, tag.c_str());
    if (ImGui::IsItemClicked()) {
    m_SelectionContext = entity; // 记录当前点击的实体
    }
    // 右键实体 弹出菜单:删除实体
    bool entityDeleted = false;
    if (ImGui::BeginPopupContextItem()) {
    if (ImGui::MenuItem("Delete Entity")) {
    entityDeleted = true;
    }
    ImGui::EndPopup();
    }
  • ImGui弹出菜单与延迟删除

    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
    void SceneHierarchyPanel::OnImGuiRender()
    {
    if (!m_SelectionContext) //When item is not selected
    {
    // 右击空白面板-弹出菜单。0是ID 1是右键
    if (ImGui::BeginPopupContextWindow(0, 1, false)) {
    if (ImGui::MenuItem("Create Empty Entity")) {
    m_Context->CreateEntity("Empty Entity");
    }
    ImGui::EndPopup();
    }
    }
    if (m_SelectionContext)
    {
    if (ImGui::Button("Add Component"))
    ImGui::OpenPopup("AddComponent");

    if (ImGui::BeginPopup("AddComponent"))
    {
    if (ImGui::MenuItem("Camera"))
    {
    m_SelectionContext.AddComponent<CameraComponent>();
    ImGui::CloseCurrentPopup();
    }
    if (ImGui::MenuItem("Sprite Renderer"))
    {
    m_SelectionContext.AddComponent<SpriteRendererComponent>();
    ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
    }
    }
    }

    void SceneHierarchyPanel::DrawEntityNode(Entity entity)
    {
    bool entityDeleted = false;
    if (ImGui::BeginPopupContextItem()) //选中Item并右键触发
    {
    if (ImGui::MenuItem("Delete Entity"))
    entityDeleted = true;

    ImGui::EndPopup();
    }
    if (entityDeleted)
    {
    m_Context->DestoryEntity(entity);
    if (m_SelectionContext == entity)
    m_SelectionContext = {};
    }
    }


    if (entity.HasComponent<SpriteRendererComponent>())
    {
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{ 4, 4 });
    bool open = ImGui::TreeNodeEx((void*)typeid(SpriteRendererComponent).hash_code(), treeNodeFlags, "Sprite Renderer");
    ImGui::SameLine(ImGui::GetWindowWidth() - 25.0f);
    if (ImGui::Button("+", ImVec2{ 20, 20 }))
    {
    ImGui::OpenPopup("ComponentSettings");
    }
    ImGui::PopStyleVar();

    bool removeComponent = false;
    if (ImGui::BeginPopup("ComponentSettings"))
    {
    if (ImGui::MenuItem("Remove component"))
    removeComponent = true;

    ImGui::EndPopup();
    }

    if (removeComponent)
    entity.RemoveComponent<SpriteRendererComponent>();
    }
  • 添加摄像机组件的时候,设置视口大小计算投影矩阵

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Scene.h
    private:
    template<typename T>
    void OnComponentAdded(Entity entity, T& component);
    Scene.cpp
    // 模板类定义在cpp中
    template<typename T>
    void Scene::OnComponentAdded(Entity entity, T& component)
    {
    // 静态断言:false,代表在编译前就会执行, 但是编译器这里不会报错,说明这段代码不会编译吧。。
    // 而且打了断点,也不行,证明这段代码只是声明作用吧。
    static_assert(false);
    }
    template<>
    void Scene::OnComponentAdded<CameraComponent>(Entity entity, CameraComponent& component)
    {
    entity.GetComponent<TransformComponent>().Translation = { 0, 0, 5.0f };
    component.camera.SetViewportSize(m_ViewportWidth, m_ViewportHeight);
    }

五十九、新的主题外观

前言

  • 此节目的

    如此节标题,让编辑器更好看,主要有:

    1. 更换字体
    2. 按钮字体加粗
    3. 调整布局
    4. 控制最小宽度
    5. 界面颜色
    6. 用模板函数去除重复代码

关键代码

  • ImGui相关

    • 更换字体

      1
      2
      3
      // 完善UI:设置界面主题字体
      io.Fonts->AddFontFromFileTTF("assets/fonts/opensans/OpenSans-Bold.ttf", 18.0f);
      io.FontDefault = io.Fonts->AddFontFromFileTTF("assets/fonts/opensans/OpenSans-Regular.ttf", 18.0f);
    • 按钮字体加粗

      1
      2
      3
      4
      5
      6
      7
      // 完善UI:加粗字体
      ImGui::PushFont(boldFont);
      if (ImGui::Button("X", buttonSize) ){
      values.x = resetValue;
      }
      ImGui::PopFont();
      ImGui::PopStyleColor(3);
    • 控制最小宽度

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 完善UI:设置面板最小宽度
      float minWinSizeX = style.WindowMinSize.x;
      style.WindowMinSize.x = 370.f;
      if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable)
      {
      ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
      ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
      }
      style.WindowMinSize.x = minWinSizeX; // 恢复
    • 界面颜色

      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
      void ImGuiLayer::SetDarkThemeColors()
      {
      auto& colors = ImGui::GetStyle().Colors;
      colors[ImGuiCol_WindowBg] = ImVec4{ 0.1f, 0.105f, 0.11f, 1.0f };

      // Headers
      colors[ImGuiCol_Header] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };
      colors[ImGuiCol_HeaderHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f };
      colors[ImGuiCol_HeaderActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };

      // Buttons
      colors[ImGuiCol_Button] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };
      colors[ImGuiCol_ButtonHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f };
      colors[ImGuiCol_ButtonActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };

      // Frame BG
      colors[ImGuiCol_FrameBg] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };
      colors[ImGuiCol_FrameBgHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f };
      colors[ImGuiCol_FrameBgActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };

      // Tabs
      colors[ImGuiCol_Tab] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
      colors[ImGuiCol_TabHovered] = ImVec4{ 0.38f, 0.3805f, 0.381f, 1.0f };
      colors[ImGuiCol_TabActive] = ImVec4{ 0.28f, 0.2805f, 0.281f, 1.0f };
      colors[ImGuiCol_TabUnfocused] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
      colors[ImGuiCol_TabUnfocusedActive] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };

      // Title
      colors[ImGuiCol_TitleBg] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
      colors[ImGuiCol_TitleBgActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
      colors[ImGuiCol_TitleBgCollapsed] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
      }
  • 用模板函数去除重复代码

  • SceneHierarchyPanel.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
    template<typename T, typename UIFunction>
    static void DrawComponent(const std::string& name, Entity entity, UIFunction uiFunction)
    {
    const ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_FramePadding;
    if (entity.HasComponent<T>())
    {
    auto& component = entity.GetComponent<T>();
    ImVec2 contentRegionAvailable = ImGui::GetContentRegionAvail();

    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{ 4, 4 });
    float lineHeight = GImGui->Font->FontSize + GImGui->Style.FramePadding.y * 2.0f;
    ImGui::Separator();
    bool open = ImGui::TreeNodeEx((void*)typeid(T).hash_code(), treeNodeFlags, name.c_str());
    ImGui::PopStyleVar();

    ImGui::SameLine(contentRegionAvailable.x - lineHeight * 0.5f);
    if (ImGui::Button("+", ImVec2{ lineHeight, lineHeight }))
    {
    ImGui::OpenPopup("ComponentSettings");
    }

    bool removeComponent = false;
    if (ImGui::BeginPopup("ComponentSettings"))
    {
    if (ImGui::MenuItem("Remove component"))
    removeComponent = true;

    ImGui::EndPopup();
    }

    if (open)
    {
    uiFunction(component);
    ImGui::TreePop();
    }

    if (removeComponent)
    entity.RemoveComponent<T>();
    }
    }
    -------------------------------------

    DrawComponent<TransformComponent>("Transform", entity, [](auto& component)
    {
    DrawVec3Control("Translation", component.Translation);
    glm::vec3 rotation = glm::degrees(component.Rotation);
    DrawVec3Control("Rotation", rotation);
    component.Rotation = glm::radians(rotation);
    DrawVec3Control("Scale", component.Scale, 1.0f);
    });

    DrawComponent<CameraComponent>("Camera", entity, [](auto& component)
    {
    auto& camera = component.Camera;
    ImGui::Checkbox("Primary", &component.Primary);

    const char* projectionTypeStrings[] = { "Perspective", "Orthographic" };
    const char* currentProjectionTypeString = projectionTypeStrings[(int)camera.GetProjectionType()];
    @@ -256,41 +298,13 @@ namespace HEngine
    if (ImGui::DragFloat("Far", &orthoFar))
    camera.SetOrthographicFarClip(orthoFar);

    ImGui::Checkbox("Fixed Aspect Ratio", &component.FixedAspectRatio);
    }
    });
    ...

六十、 保存和加载场景

前言

  • 目的

    为了实现像unity那样保存场景到本地从本地加载场景的功能,这功能其实就是将场景的实体和组件用文本信息保存起来,加载场景的话就是读取文本信息,根据文本存储的信息,创建实体并且设置组件和组件数据

将yaml项目作为子模块融入项目中

  1. cmd

    1
    git submodule add https://github.com/jbeder/yaml-cpp
  2. 打开yaml-cpp文件夹下的premake.lua,将staticruntime “off”改为

    1
    staticruntime "on"

    目的是使yaml-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
    void SceneSerializer::Serialize(const std::string& filepath)
    {
    YAML::Emitter out;
    out << YAML::BeginMap;

    out << YAML::Key << "Scene" << YAML::Value << "Untitled";
    out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;// 开始序列化
    m_Scene->m_Registry.each([&](auto entityID) {
    Entity entity = { entityID ,m_Scene.get() };
    if (!entity)
    return;
    // 序列化实体
    SerializeEntity(out, entity);
    });
    out << YAML::EndSeq; // 结束序列化

    out << YAML::EndMap;

    std::ofstream fout(filepath);
    fout << out.c_str();
    }
    static void SerializeEntity(YAML::Emitter& out, Entity entity) {
    out << YAML::BeginMap;
    out << YAML::Key << "Entity" << YAML::Value << "16816816816888";
    if (entity.HasComponent<TagComponent>()) {
    out << YAML::Key << "TagComponent";
    out << YAML::BeginMap;

    auto& tag = entity.GetComponent<TagComponent>().Tag;
    out << YAML::Key << "Tag" << YAML::Value << tag;

    out << YAML::EndMap;
    }
    // 其它组件类似
    out << YAML::EndMap;
    }
  • 解析

    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
    std::ifstream stream(filepath);
    std::stringstream strStream;
    strStream << stream.rdbuf(); // strStream流对象中的流重定向到字符串输出流strStream

    // 转换为node对象
    YAML::Node data = YAML::Load(strStream.str());
    if (!data["Scene"]) {
    return false;
    }

    std::string sceneName = data["Scene"].as<std::string>();
    HE_CORE_TRACE("Deserializing scene '{0}'", sceneName);

    auto entities = data["Entities"];
    if (entities) {
    for (auto entity : entities) {
    uint64_t uuid = entity["Entity"].as<uint64_t>();

    std::string name;
    auto tagComponent = entity["TagComponent"];
    if (tagComponent) {
    name = tagComponent["Tag"].as<std::string>();
    }
    HE_CORE_TRACE("Deserialized entity with ID = {0}, name = {1}", uuid, name);

    Entity deserializedEntity = m_Scene->CreateEntity(name);;

    auto transformComponent = entity["TransformComponent"];
    if (transformComponent) {
    // 添加实体,默认有transform组件
    auto& tc = deserializedEntity.GetComponent<TransformComponent>();
    tc.Translation = transformComponent["Translation"].as<glm::vec3>();
    tc.Rotation = transformComponent["Rotation"].as<glm::vec3>();
    tc.Scale = transformComponent["Scale"].as<glm::vec3>();
    }
    .....

  • 存储的文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Scene: Untitled
    Entities:
    - Entity: 16816816816888
    TagComponent:
    Tag: Camera B
    - Entity: 16816816816888
    TagComponent:
    Tag: Camera A
    ......

关于yaml的BeginSeq

开了<< YAML::BeginSeq

1
2
3
4
5
6
7
8
Scene: Untitled
Entities:
- Entity: 16816816816888
TagComponent:
Tag: Camera B
- Entity: 16816816816888
TagComponent:
Tag: Camera A

没开<< YAML::BeginSeq

1
2
3
4
5
6
7
8
Scene: Untitled
Entities:
Entity: 12837192831273
TagComponent:
Tag: Camera B
? Entity: 12837192831273
TagComponent:
Tag: Camera A

可以看出,BeginSeq是在实体信息前加一个“-”,代表着是一个实例