1-15是些基础工作、如项目构建、添加子模块、使引擎具有日志系统、事件系统,可视化窗口等基础功能

16开始是游戏引擎最核心的部分-Rendering,使引擎能够真正绘制出图形,如果想直接看这部分点此链接跳转

一、  项目设置

新建HEngine和Sandbox项目,HEngine项目生成为dll,Sandbox项目生成为exe,运行此exe通过动态链接HEngine的dll,可以调用dll定义的函数并输出信息。

1.调整输出目录和中间的目录

1
2
$(SolutionDir)\bin\$(Configuration)-$(Platform)\$(ProjectName)\ 
$(SolutionDir)\bin-int\$(Configuration)-$(Platform)\$(ProjectName)\

生成的文件在bin目录下,生成的intermediate文件在bin-int目录下,大概是这么个目录结构

1
2
bin/Debug-x64/ProjectName/
bin-int/Debug-64x/ProjectName/

中间目录:存储一些obj、二进制文件,生成好dll、exe后可以删除此文件夹

2.Sandbox项目引用HEngine项目

C++:静态链接与动态链接
阅读的博客,讲的很通俗易懂:https://blog.csdn.net/kang___xi/article/details/80210717

  • 静态链接

    • 说明

      • 使用静态库方式链接,编译后链接时会将使用的库函数对应所包含库函数定义的.o目标文件都包含在exe文件中。
    • 优点

      • 执行速度快:因为可执行文件程序内部包含了所有需要执行的东西
    • 缺点

      • 浪费空间:因为多个可执行程序对同所需要的目标文件都有一份副本

      • 更新慢:如果有一个.o目标文件发生改变,那么对应的使用这个.o目标文件的多个可执行程序需要重新来一遍链接过程,即链接多个.o目标文件来实现生成可执行文件。

  • 动态链接

    • 说明

      • 使用动态库方式链接,编译后因为推迟链接不会将使用的库函数对应的dll文件都包含在exe文件中,而是在exe运行的时候将dll加载到内存CPU中再链接。
    • 优点

      • 节省空间:多个可执行程序对同所需要的库函数共享一份副本

      • 更新快:一个源文件发生改变,只需更新编译成dll文件,不用每个可执行程序需要重新来一遍链接过程,因为多个可执行程序在运行时时链接,且共享一份副本

    • 缺点

      • 启动速度慢:因为每次执行程序都需要链接

二、  程序入口

代码文件

  • HEngine项目

    • Application类

      引擎内部功能实现

    • Core.h

      来根据不同项目的条件编译,而写dll导入还是导出的宏定义

    • EntryPoint.h

      入口点,main函数

    • HEngine.h

      引入其它头文件,控制给Sandbox项目提供哪些引擎内部功能

  • Sandbox项目

    • SanboxApp.cpp

      应用层实现

关键代码

  • 在HEngine项目的Application.h中

    1
    2
    3
    4
    5
    6
    7
    8
    namespace HEngine 
    {
    class HENGINE_API Application{
    ....
    };
    // To be defined in CLIENT
    Application* CreateApplication();
    }

    在HEngine命名空间内声明了CreateApplication函数

  • 在Sandbox项目的SandboxApp.cpp中

    1
    2
    3
    4
    HEngine::Application* HEngine::CreateApplication()
    {
    return new Sandbox();
    }

    定义了CreateApplication函数

  • 在EntryPoint.h中调用

    1
    2
    3
    4
    5
    6
    extern HEngine::Application* HEngine::CreateApplication();
    int main()
    {
    auto app = HEngine::CreateApplication();
    ...
    }

    将CreateApplication函数声明为extern,表示此函数会在外部定义,接下来使用的这函数时将使用在外部定义的CreateApplication

包含头文件+条件编译和宏定义实现Dll导入与导出

前提操作

  • HEngine项目

​ 右键项目Proterties(所有配置) => C/C++ => Preprocessor(预处理器) => Definitions(预处理器定义)

​ 输入: HE_PLATFORM_WINDOWS; HE_BUILD_DLL

  • Sandbox项目

    • 同上

      输入: HE_PLATFORM_WINDOWS;

    • 右键属性(所有配置) => C/C++ => General(常规) => 附加包含目录

      输入: $(SolutionDir)HEngine\src

      为了Sandbox项目能引入HEngine项目的文件

      1
      #include <HEngine.h>
  • 在Core.h中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #pragma once
    #ifdef HE_PLATFORM_WINDOWS
    #ifdef HE_BUILD_DLL
    #define HEngine_API __declspec(dllexport)
    #else
    #define HEngine_API __declspec(dllimport)
    #endif
    #else
    #error HEngine only supports Windows!
    #endif

    根据条件编译定义HEngine_API是dll导入还是导出,HEngine项目将是__declspec(dllexport),Sandbox项目是__declspec(dllimport)

程序运行流程

  • EntryPoint.h定义了main函数,即写了入口点,所以程序会在这运行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #pragma once
    #ifdef HE_PLATFORM_WINDOWS
    extern HEngine::Application* HEngine::CreateApplication();
    int main()
    {
    auto app = HEngine::CreateApplication();
    app->Run();
    delete app;
    }
    #endif

    main函数中执行CreateApplication函数,将调用定义在SandboxApp.cpp中的CreateApplication函数

    1
    2
    3
    4
    HEngine::Application* HEngine::CreateApplication()
    {
    return new Sandbox();
    }

    这函数返回的指针是HEngine项目中的Application父类指针,所以auto app的类型是Application*。

  • 当执行app->Run()函数时,由于Run()函数没有声明为虚函数,所以会调用Application.cpp中的Run()函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    namespace HEngine 
    {
    Application::Application()
    {
    }
    Application::~Application()
    {
    }
    void Application::Run()
    {
    while (true);
    }
    }

三、  日志系统 + Premake

主要是以下几个任务

  1. 创建Log类,然后有s_CoreLogger和s_ClientLogger,分别处理引擎的log和client的log
  2. 使用spdlog,具体主要是怎么利用git submodule使用该库
  3. 使用宏来封装对应的log函数,使用宏可以更好的方便不同平台的应用
  4. 用lua脚本配置项目属性,使用premake运行程序一键生成VS项目及属性

游戏引擎少不了LogStystem,这里使用了别人做好的日志系统,叫做spdlog, 原作者是https://github.com/gabime/spdlog.git,因为我要在内部加入Premake5文件,所以fork到我自己的仓库再添加子模块,就变成了

1
git submodule add https://github.com/donghuiw/spdlog.git HEngine/vendor/spdlog

对于这个LogSystem,底层使用的是spdlog,上层封装一层HEngine的Log类,以下是核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace HEngine
{
std::shared_ptr<spdlog::logger> Log::s_CoreLogger;
std::shared_ptr<spdlog::logger> Log::s_ClientLogger;

void Log::Init()
{
spdlog::set_pattern("%^[%T] %n: %v%$");

s_CoreLogger = spdlog::stdout_color_mt("HEngine");
s_CoreLogger->set_level(spdlog::level::trace);
s_ClientLogger = spdlog::stdout_color_mt("APP");
s_ClientLogger->set_level(spdlog::level::trace);
}
}

额外的宏操作,这里的宏的写法可以实现函数的宏,代码如下所示:

1
2
3
4
5
6
7
inline static std::shared_ptr<spdlog::logger>& GetCoreLogger() { return s_CoreLogger; }
inline static std::shared_ptr<spdlog::logger>& GetClientLogger() { return s_ClientLogger; }

//core log macros
#define HE_CORE_TRACE(...) ::HEngine::Log::GetCoreLogger()->trace(__VA_ARGS__)
#define HE_CORE_INFO(...) ::HEngine::Log::GetCoreLogger()->info(__VA_ARGS__)
#define HE_CORE_WARN(...) ::HEngine::Log::GetCoreLogger()->warn(__VA_ARGS__)

Git删除子模块

因添加子模块时候,写错代码,导致子模块添加路径错误,需要删除子模块并重新添加。而删除子模块是个稍微麻烦的事,网上查阅后,在此记录一下

1. 删除submodule缓存

需要先暂存 .gitmodules 文件, 否则会报错: fatal: please stage your changes to .gitmodules or stash them to proceed

1
2
git add .gitmodules
git rm --cached submodule_name

若报什么index已经存在的错误,说明没有执行git rm --cached 命令。

若报什么please stage your changes to .gitmodules or stash them to proceed,说明没有执行git add .gitmodules命令

2. 修改.gitmodules

1
移除对应的submodule信息, 只有1个submodule信息的时候也可以删除该文件.

3. .git/modules

移除对应的submodule目录, 进入.git\modules\HEngine\vendor,删除对应的子模块文件夹

4. .git/config

移除config文件内对应的submodule目录

Premake5使用及问题汇总

Premake生成项目的部分单独写了一个blog,关于使用第三方库文件的一般会单独写一个详细的,也方便查阅


四、  事件系统

思路是由Application创建自己的窗口window,对应的window类不应该知道任何Application的信息,所以Application在创建自己的window时,还要同时创建一个callback,在这之前,需要知道以下内容:

  • std::function的用法
  • C++中的enum和enum class
  • ###在C++宏里的用法
  • C++虚函数的override关键字和相关用法

我把这种补充知识点放到游戏引擎开发补充知识点,方便查看

现在可以正式开始Event的设计了,首先需要定义的就是EventEventType类,这里把Event作为基类,EventType是enum class,包含了基本的外设事件,如下所示:

1
2
3
4
5
6
7
8
enum class EventType
{
None = 0,
WindowClose, WindowResize, WindowFocus, WindowLostFocus, WindowMoved,
AppTick, AppUpdate, AppRender,
KeyPressed, KeyReleased,
MouseButtonPressed, MouseButtonReleased, MouseMoved, MouseScrolled
};

对于Event类型,作为基类,那么最基本的两个接口应该为:

  • 获取该事件的类型
  • 获取该事件的名字

作为基类,这都是最基本的API,再者,为了方便使用,仿照C#的方式,C#语言里所有的Object都有一个ToString函数,方便我们打印一些消息,所以这个API我们也把它加入到Event基类里,如下所示:

1
2
3
4
5
6
7
class HENGINE_API Event
{
public:
virtual EventType GetEventType() const = 0;
virtual const char* GetName() const = 0;
virtual const char* ToString() const = 0;
};

目前就是这样,然后我们还需要一个EventCategory枚举,以后用flag来快速筛选特定的Event:

1
2
3
4
5
6
7
8
9
10
11
#define BIT(x) (1 << x)
// events filter
enum EventCategory
{
None = 0,
EventCategoryApplication = BIT(0),
EventCategoryInput = BIT(1),
EventCategoryKeyboard = BIT(2),
EventCategoryMouse = BIT(3),
EventCategoryMouseButton = BIT(4)
};

定义Event这个基类,就可以着手创建对应的子类的,拿鼠标事件举例,一共有MouseMovedMouseButtonPressedMouseButtonReleasedMouseButtonScrolled四种,那么我就建立四个子类,全放在MouseEvent.h文件下,拿MouseMoved举例,其类型为之前枚举定义的MouseMovedEvent,其ToString应该是打印出鼠标移动的offset值,由于该类的所有Event类型都是一样的,所以我们可以用一个static变量去存储该类型就够了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MouseMovedEvent : public Event
{
public:
static EventType GetStaticType() { return EventType::MouseMoved; }
const EventType GetEventType() const override { return GetStaticType(); }
const char* GetName() const override { return "MouseMoved"; }
std::string ToString() const override
{
std::stringstream a;
a << "MouseMovedEvent: xOffset = " << GetXOffset() << ", yOffset = " << GetYOffset();
return a.str();
}

inline float GetXOffset() const { return m_xOffset; }
inline float GetYOffset() const { return m_yOffset; }
private:
float m_xOffset, m_yOffset;
};

OK,写完了这个类,就可以继续写类似的鼠标事件的类了,但是我发现有一些代码都是非常类似的,写起来很麻烦,也很影响阅读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MouseMovedEvent : public Event
{
public:
static EventType GetStaticType() { return EventType::MouseMoved; }
const EventType GetEventType() const override { return GetStaticType(); }
const char* GetName() const override { return "MouseMoved"; }
...
}
class MouseButtonPressedEvent: public Event
{
public:
static EventType GetStaticType() { return EventType::MouseButtonPressed; }
const EventType GetEventType() const override { return GetStaticType(); }
const char* GetName() const override { return "MouseButtonPressed"; }
...
}

所以我学到了一个方法,用宏去代替我们编写这么长的语句,这个宏名就叫做EVENT_CLASS_TYPE(typename),来为我们生成对应的函数,通过###符号,可以达到这种效果,一个#是转换成字符串,两个#是原语句替换,所以就是这么简化:

1
2
3
4
5
6
7
8
9
10
11
12
// 用于简化代码, 因为很多类都有着相同的函数
#define EVENT_CLASS_TYPE(type) \
static EventType GetStaticType() { return EventType::##type; }\
const EventType GetEventType() const override { return GetStaticType(); }\
const char* GetName() const override { return #type; }

class MouseMovedEvent : public Event
{
public:
EVENT_CLASS_TYPE(MouseMoved)
...
}

就像这样,我们可以把所有鼠标事件的类定义好,接下来还需要的定义的输入类就是Window Event、ApplicationEvent和KeyEvent,先说前两种Event,最后着重提一下KeyEvent,键盘事件的输入处理并不像点击鼠标那么简单,通常(简单的事件系统里)我们是没有长按鼠标的操作的,但是却有长按键盘的操作,当我们按键盘时,会先打印一个字母,然后停顿一下,如果这个时候还按着按钮,就继续打印剩余的字母。

所以说,按键的时候,第一次会立马打印第一个字母,然后需要记录我按的次数(或者记录按的时间),当记录的值达到一定阈值(或时间)时,才会继续不停打印接下来的字母,这里我们不用时间记录,而是用一个int值,记录按相同键的次数。

设计KeyEvent类的时候,可以发现,KeyPressedEvent会比KeyReleasedEvent的数据多一个,前者会额外记录按下Key时,key走过的Loop的总数,所以这个时候可以设计一个基类叫做KeyEvent,这里放通用的数据,就是Key的keycode,用于存放Key类型共有的内容,设计思路如下所示:

1
2
3
4
5
6
7
8
9
10
11
class HENGINE_API KeyEvent : public Event
{
public:
inline int GetKeycode() const { return keycode;}

EVENT_CLASS_CATEGORY(EventCategoryKeyboard | EventCategoryInput)
protected:
// 构造函数设为Protected,意味着只有其派生类能够调用此函数
KeyEvent(int keycode): keycode(code){}
int keycode;
};

然后再写对应的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class HENGINE_API KeyPressedEvent : public KeyEvent
{
public:
KeyPressedEvent(int keycode, int repeatCount)
: KeyEvent(keycode), m_RepeatCount(repeatCount) {}

inline int GetRepeatCount() const { return m_RepeatCount; }

std::string ToString() const override
{
std::stringstream ss;
ss << "KeyPressedEvent: " << m_KeyCode << " (" << m_RepeatCount << " repeats)";
return ss.str();
}

EVENT_CLASS_TYPE(KeyPressed)
private:
int m_RepeatCount;
};

五、  预编译头文件

为了避免头文件被反复编译,需要加上pch文件,可以加快编译速度,主要有以下几点

  • 需要在VS工程里添加hepch.h和hepch.cpp文件,前者放所有常用的库的头文件,对于后者,一般的pch是不需要cpp文件的,但是VS工程里需要这个东西,所以得加上,然后让他引用hepch.h
  • 然后在premake5.lua文件里进行修改,添加两个参数,pchheader “…” 和 pchsource “…” ,后者一般只是VS工程需要,其他平台会忽略这个,再次Build工程后,项目属性配置里会显示,使用pch
  • 最后再把所有用到基本库的cpp(或者说所有cpp)里,都加上#include "hepch.h"

有两个重点注意:

第一个是在premake5.lua中添加pchheader可以不顾当前路径随便写,但是pchsource得明确指定路径,需要改成:

1
2
3
pchheader "hepch.h"
pchsource "hepch.cpp" ×
pchsource "%{prj.name}/Src/hepch.cpp" -- 根目录基于premake5.lua文件

第二个是,如果勾选了使用pch文件,项目内的所有.cpp文件都要添加include “pch.h”,而且一定要在第一行


六、  GlFW窗口

总体思路是: Application类调用创建窗口函数,而Window类使用glfw库创建真正的窗口。Window类检测glfw窗口的事件,并回调给Application的处理事件函数。

在做这个事情之前,下面这些概念都得熟悉:Vsync、Observe Pattern、回调函数、函数指针,都放在游戏引擎开发补充知识点,方便查看

步骤

  • 添加glfw子模块

    1
    git add submodule https://github.com/glfw/glfw HEngine/vendor/GLFW
  • 修改premake

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    IncludeDir = {}
    IncludeDir["GLFW"] = "Hengine/vendor/GLFW/include"
    -- 这个include,相当于把glfw下的premake5.lua内容拷贝到这里
    include "Hengine/vendor/GLFW"

    project "HEngine"
    location "Hengine"
    kind "SharedLib"

    includedirs{
    "%{prj.name}/src",
    "%{prj.name}/vendor/spdlog/include",
    "%{IncludeDir.GLFW}" -- 包含GLFW目录
    }

    links
    {
    "GLFW", -- HEngine链接glfw项目
    "opengl32.lib"
    }

Window类

Window类作为接口类,需要包含通用的窗口内容:

  • 一个Update函数,用于在loop里每帧循环

  • 窗口的长和宽,以及相应的Get函数

  • 设置窗口的Vsync和Get窗口的Vsync函数

  • 窗口的回调函数,当窗口接受事件输入时,会调用这个回调函数

  • 代码

    window.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
    namespace HEngine 
    {
    struct WindowProps
    {// 窗口初始化设置的内容
    std::string Title;
    unsigned int Width;
    unsigned int Height;
    WindowProps(const std::string& title = "HEngine Engine",
    unsigned int width = 1280,
    unsigned int height = 720)
    : Title(title), Width(width), Height(height){}
    };
    class HENGINE_API Window
    {
    public:
    using EventCallbackFn = std::function<void(Event&)>;
    virtual ~Window() {}
    virtual void OnUpdate() = 0;
    virtual unsigned int GetWidth() const = 0;
    virtual unsigned int GetHeight() const = 0;
    // Window attributes
    virtual void SetEventCallback(const EventCallbackFn& callback) = 0;
    virtual void SetVSync(bool enabled) = 0;
    virtual bool IsVSync() const = 0;
    // 在Window父类声明创建函数
    static Window* Create(const WindowProps& props = WindowProps());
    };
    }

    WindowsWindow.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    namespace HEngine 
    {
    class WindowsWindow : public Window
    {
    public:
    WindowsWindow(const WindowProps& props);
    virtual ~WindowsWindow();
    Window函数重载...
    private:
    virtual void Init(const WindowProps& props);
    virtual void Shutdown();
    private:
    GLFWwindow* m_Window;
    struct WindowData{
    std::string Title;
    unsigned int Width, Height;
    bool VSync;
    EventCallbackFn EventCallback;
    };
    WindowData m_Data;
    };
    }

    WindowsWindow.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
    namespace HEngine 
    {
    static bool s_GLFWInitialized = false;
    // 在WindowsWindow子类定义在Window父类声明的函数
    Window* Window::Create(const WindowProps& props)
    {
    return new WindowsWindow(props);
    }
    WindowsWindow::WindowsWindow(const WindowProps& props)
    {
    Init(props);
    }
    void WindowsWindow::Init(const WindowProps& props)
    {
    m_Data.Title = props.Title;
    m_Data.Width = props.Width;
    m_Data.Height = props.Height;
    HE_CORE_INFO("Creating window {0} ({1}, {2})", props.Title, props.Width, props.Height);
    if (!s_GLFWInitialized)
    {
    // TODO: glfwTerminate on system shutdown
    int success = glfwInit();
    HE_CORE_ASSERT(success, "Could not intialize GLFW!");
    s_GLFWInitialized = true;
    }
    // 创建窗口
    m_Window = glfwCreateWindow((int)props.Width, (int)props.Height, m_Data.Title.c_str(), nullptr, nullptr);
    // 设置glfw当前的上下文
    glfwMakeContextCurrent(m_Window);
    /*
    设置窗口关联的用户数据指针。这里GLFW仅做存储,不做任何的特殊处理和应用。
    window表示操作的窗口句柄。
    pointer表示用户数据指针。
    */
    glfwSetWindowUserPointer(m_Window, &m_Data);
    SetVSync(true);
    }
    void WindowsWindow::OnUpdate()
    {
    glfwPollEvents(); // 轮询事件
    glfwSwapBuffers(m_Window); // 交换缓冲
    }
    void WindowsWindow::SetVSync(bool enabled)
    {
    if (enabled)
    glfwSwapInterval(1);
    else
    glfwSwapInterval(0);

    m_Data.VSync = enabled;
    }
    .....
    }

其它修改

  • Application

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    namespace HEngine 
    {
    Application::Application()
    {
    m_Window = std::unique_ptr<Window>(Window::Create()); // 创建窗口
    }
    void Application::Run()
    {
    while (m_Running)
    {
    glClearColor(1, 0, 1, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    m_Window->OnUpdate(); // 更新glfw
    }
    }
    }

七、  GLFW事件

使用GLFW函数设置(拦截)真正窗口事件的回调函数,在回调函数中转换为我们自定义的事件,再回调给Application的OnEvent,OnEvent拦截对应的事件

WindowsWindow.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
static void GLFWErrorCallback(int error, const char* description)
{
HE_CORE_ERROR("GLFW Error ({0}): {1}", error, description);
}
void WindowsWindow::Init(const WindowProps& props)
{
if (!s_GLFWInitialized)
{
glfwSetErrorCallback(GLFWErrorCallback); //从glfw里设置自己的回调函数
}

//设置glfw事件回调=接收glfw窗口事件
glfwSetWindowSizeCallback(m_Window, [](GLFWwindow* window, int width, int height)
{
WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);
data.Width = width;
data.Height = height;

WindowResizeEvent event(width, height);
data.EventCallback(event);
});

glfwSetWindowCloseCallback(m_Window, [](GLFWwindow* window)
{
WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);
WindowCloseEvent event;
data.EventCallback(event);
});
...

Application.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define BIND_EVENT_FN(x) std::bind(&Application::x, this, std::placeholders::_1)

Application::Application()
{
m_Window = std::unique_ptr<Window>(Window::Create());
m_Window->SetEventCallback(std::bind(&Application::OnEvent, this, std::placeholders::_1));// 设置window的callback为此对象的OnEvent函数
}

void Application::OnEvent(Event& e)
{
EventDispatcher dispatcher(e);
dispatcher.Dispatch<WindowCloseEvent>(BIND_EVENT_FN(OnWindowClose));

HE_CORE_TRACE("{0}", e);
}

bool Application::OnWindowClose(WindowCloseEvent &e)
{
m_Running = false;
return true;
}

其中,EventDispatcher用于根据事件类型的不同,调用不同的函数:

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
// 当收到Event时,创建对应的EventDispatcher
class HENGINE_API EventDispatcher
{
template<typename T>
using EventHandler = std::function<bool(T&)>;//存储了一个输入为任意类型,返回值为bool的函数指针
public:
EventDispatcher(Event& event):
m_Event(event){}

// T指的是事件类型, 如果输入的类型没有GetStaticType会报错
template<typename T>
void Dispatch(EventHandler<T> handler)
{
if (m_Event.m_Handled)
return;

if (m_Event.GetEventType() == T::GetStaticType())
{
m_Event.m_Handled = handler(*(T*)&m_Event); //使用(T*)把m_Event转换成输入事件的指针类型
}
}

private:
Event& m_Event;//必须是引用,不可以是Event的实例,因为Event带有纯虚函数
};

遇到的问题:

编译时报错显示error C2338: Cannot format argument,初步判定是跟spdlog有关,经过排查发现问题出在这里

1
2
3
4
5
6
7
8
void Application::OnEvent(Event& e)
{
EventDispatcher dispatcher(e);
dispatcher.Dispatch<WindowCloseEvent>(BIND_EVENT_FN(OnWindowClose));

HE_CORE_TRACE("{0}", e); ××××××

}

是spdlog无法识别Event这个类型,需要我们自定义一个关于Event的模版,在Log.h最上方加入以下代码即可

1
2
3
4
#if FMT_VERSION >= 90000
#include "Events/Event.h"
template <> struct fmt::formatter<HEngine::Event> : ostream_formatter {};
#endif

八、  设计游戏的层级框架

设计完Window和Event之后,需要创建Layer类

  • Layer的理解

    想象同Ps中一张图有多个层级,每个层级都可以绘制不同的画面,最后合在一起展现出图片最终的样子

  • Layer的设计

    • 数据结构:vector

    • 渲染顺序

      从前往后渲染各个层的图像,这样后面渲染的会覆盖前面渲染的图像,在屏幕的最顶层。

    • 处理事件顺序

      从后往前依次处理事件,当一个事件被一个层处理完不会传递给前一个层,结合渲染顺序,这样在屏幕最顶层的(也就是在vector最后的layer)图像最先处理事件。

    • 例子解释

      比如常见的3D游戏有UI。

      渲染顺序:将3D图形先渲染,再渲染2DUI,这样屏幕上2DUI永远在3D图形上方,显示正确;

      事件顺序:点击屏幕的图形,应该是2DUI最先处理,如果是相应UI事件,处理完后不传递给前一个3D层,若不是自己的UI事件,才传递给前一个3D层。

项目相关

Layer接口设计如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HENGINE_API Layer
{
public:
Layer(const std::string& name = "Layer");
virtual ~Layer();
virtual void OnAttach() {} // 应用添加此层执行
virtual void OnDetach() {} // 应用分离此层执行
virtual void OnUpdate() {} // 每层更新
virtual void OnEvent(Event& event) {}// 每层处理事件
inline const std::string& GetName() const { return m_DebugName; }

protected:
std::string m_DebugName;
};

LayerStack.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class HENGINE_API LayerStack
{
public:
LayerStack();
~LayerStack();
void PushLayer(Layer* layer); // vector在头部添加一个层
void PushOverlay(Layer* overlay);// 在vector末尾添加一个覆盖层,在屏幕的最上方的层
void PopLayer(Layer* layer); // vector弹出指定层
void PopOverlay(Layer* overlay);// vector弹出覆盖层
std::vector<Layer*>::iterator begin() { return m_Layers.begin(); }
std::vector<Layer*>::iterator end() { return m_Layers.end(); }
private:
std::vector<Layer*>stack;
std::vector<Layer*>::iterator curStackItr;
};

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
void Application::PushLayer(Layer* layer)
{
m_LayerStack.PushLayer(layer);
}
void Application::PushOverlay(Layer* layer)
{
m_LayerStack.PushOverlay(layer);
}

void Application::OnEvent(Event& e)
{
...
// 从后往前顺序处理事件
for (auto it = m_LayerStack.end(); it != m_LayerStack.begin();)
{
(*--it)->OnEvent(e);
if (e.Handled) // 截取后发现是此层的事件,就不传入前一个层
break;
}

}
void Application::Run()
{
...
{
glClearColor(1, 0, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);

// 从前往后顺序更新层
for (Layer* layer : m_LayerStack)
layer->OnUpdate();
}

}

SandboxApp.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
class ExampleLayer : public HEngine::Layer
{
public:
ExampleLayer()
: Layer("Example")
{
}
void OnUpdate() override
{
HE_INFO("ExampleLayer::Update");
}
void OnEvent(HEngine::Event& event) override
{
HE_TRACE("{0}", event);
}
};

class Sandbox : public HEngine::Application
{
public:
Sandbox()
{
PushLayer(new ExampleLayer());
};
};

九、  添加GLAD

为了使用OpenGL,而OpenGL的函数定义在显卡中,大多数函数的定义位置都无法在编译时确定下来,所以需要在运行时查询,需要使用GLAD库在运行时获取OpenGL函数地址并将其保存在函数指针中供程序运行时使用。

可以使用glew,也可以使用glad库,二者的在效率上好像没啥区别,不过glad的库要更新一些,所以这里用glad库,具体步骤:

  • 上网站https://glad.dav1d.de/上下载对应版本的header和src文件,放在vendor文件夹下

  • 网站上下载的glad库没有premake5文件,所以按照glfw库的方式为其写一个,与glfw库相同,这里的glad库也是作为lib文件使用

  • 把glad库的premake5文件相关内容整合到整个工程的premake5文件里


十、  ImGui层

Dear ImGui主要用于程序员的图形Debug工具(类似于Unity的ImGUI),如果没有ImGUI,要调整好一个参数,要反复在代码里面修改数值,然后编译运行项目查看效果,这样很麻烦,而通过ImGui,就可以实现直接在图形界面调试参数的功能。

怎么使用ImGui的代码

ImGui的网址https://github.com/ocornut/imgui, 需要添加Premake,所以Fork到我的仓库再添加子模块

1
git add submodule https://github.com/donghuiw/imgui HEngine/vendor/imgui

ImGui的代码仓库里给了两种代码:

  • 一种是源代码,HEngine需要引用这部分代码,作为库文件,类似于引用GLFW和Glad库文件项目一样
  • 一种是使用ImGui的源代码的examples代码,就是教你怎么调用的

对于第一种代码,为其生成一个premake5.lua文件,然后为其生成一个Project即可

具体怎么在HEngine里用,就需要参考ImGui的例子代码了,由于我们用的是glfw库加上OpenGL3的版本,所以要参考的两个cpp文件为:imgui_impl_opengl3.cppimgui_impl_glfw.cpp

接下来在Platform文件夹下,创建OpenGL文件夹:

  • imgui_impl_opengl3的头文件和源文件放进去,更名为ImGuiOpenGLRenderer,用来存放ImGui调用OpenGL的代码。
  • 而原本用到的imgui_impl_glfw相关内容,就直接Copy和Paste到ImGuiLayer里。

创建ImGUILayer

ImGui可以帮助程序员进行Debug,基于上述的Layer系统,把ImGui也作为一个Layer,为其创建对应的ImGuiLayer.cpp和相关头文件。

ImGuiLayer.h

1
2
3
4
5
6
7
8
9
10
11
12
namespace HEngine 
{
class HENGINE_API ImGuiLayer : public Layer
{
ImGuiLayer();
~ImGuiLayer();
void OnAttach() override; //当layer添加到layer stack的时候会调用此函数
void OnDettach() override; //当layer从layer stack移除的时候会调用此函数
void OnEvent(Event&) override;
void OnUpdate() override;
};
}

ImGuiLayer.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
#include "imgui.h"
#include "Platform/OpenGL//ImGuiOpenGLRenderer.h"

namespace HEngine
{
ImGuiLayer::ImGuiLayer()
:Layer("ImGuiLayer")
{
}

ImGuiLayer::~ImGuiLayer()
{
}

void ImGuiLayer::OnAttach()
{
//这部分直接复制example_glfw_opengl3的文件里的main函数
ImGui::CreateContext();
ImGui::StyleColorsDark();

ImGuiIO& io = ImGui::GetIO();
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors;
io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos;

io.KeyMap[ImGuiKey_Tab] = GLFW_KEY_TAB;
io.KeyMap[ImGuiKey_LeftArrow] = GLFW_KEY_LEFT;
...

ImGui_ImplOpenGL3_Init("#version 410");
}

void ImGuiLayer::OnDetach()
{

}

void ImGuiLayer::OnEvent(Event &)
{
}

void ImGuiLayer::OnUpdate()
{
// 这里也是参照ImGui给的Example写Update的函数,主要做了以下功能
// 1. 创建Frame然后进行Render
// 2. 根据窗口大小,动态给ImGui设置窗口展示大小
// 3. 设置DeltaTime
ImGuiIO& io = ImGui::GetIO();
Application& app = Application::Get();
io.DisplaySize = ImVec2(app.GetWindow().GetWindowWidth(), app.GetWindow().GetWindowHeight());

float time = (float)glfwGetTime();
io.DeltaTime = m_Time > 0.0f ? (time - m_Time) : (1.0f / 60.0f);
m_Time = time;

ImGui_ImplOpenGL3_NewFrame();
ImGui::NewFrame();

static bool show = true;
ImGui::ShowDemoWindow(&show);

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

然后在SandboxApp.cpp添加这一层就可以了

1
2
3
4
5
6
7
8
9
class SandBox : public HEngine::Application
{
public:
SandBox()
{
PushLayer(new ExampleLayer());
PushOverlay(new HEngine::ImGuiLayer());
}
};

十一、 给ImGui添加事件

  • 此节目的

    为了让显示在屏幕上ImGui的UI能接收GLFW窗口事件。

如何写ImGui的事件

  • 搞清楚原理

    ImGui的事件是来自GLFW窗口的事件

    (GLFW提供了函数来捕捉窗口事件,并回调自定义的函数->我们已经实现在回调自定义函数中传递给Application再传给Layer层,在Layer层中进行捕获和处理事件)

  • 参考ImGui的imgui_impl_glfw.cpp

    这个cpp里写了imgui实现处理glfw事件的回调处理事件函数

    所以参考imgui_impl_glfw.cpp对应的回调处理事件函数重写为ImGuiLayer层自己的回调处理函数函数

项目相关

代码

  • Event增加接收字符事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class HENGINE_API KeyTypedEvent : public KeyEvent// 增加接收字符事件
    {
    public:
    KeyTypedEvent(int keycode)
    : KeyEvent(keycode) {}

    std::string ToString() const override
    {
    std::stringstream ss;
    ss << "KeyTypedEvent: " << m_KeyCode; // 输出在窗口
    return ss.str();
    }

    EVENT_CLASS_TYPE(KeyTyped)
    };
  • WindowsWindow.cpp增加接收字符窗口事件并回调给Application

    1
    2
    3
    4
    5
    6
    7
    // 输入字符事件
    glfwSetCharCallback(m_Window, [](GLFWwindow* window, unsigned int keycode){
    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);

    KeyTypedEvent event(keycode);
    data.EventCallback(event);
    });
  • Application把事件传给ImGuiLayer层

    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
    {
    // 从Application的Event传递过来的事件
    void ImGuiLayer::OnEvent(Event& event){
    // 参考imgui_impl_glfw.cpp对应的回调处理事件函数重写为ImGuiLayer层自己的回调处理函数函数
    // 和之前不同的一点是:ImGui拦截了事件并处理后,不标记为处理过了,而是return false标记为没处理,将其传递给前一个层
    EventDispatcher dispatcher(event);
    dispatcher.Dispatch<MouseButtonPressedEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnMouseButtonPressedEvent));
    dispatcher.Dispatch<MouseButtonReleasedEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnMouseButtonReleasedEvent));
    dispatcher.Dispatch<MouseMovedEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnMouseMovedEvent));
    dispatcher.Dispatch<MouseScrolledEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnMouseScrolledEvent));
    dispatcher.Dispatch<KeyPressedEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnKeyPressedEvent));
    dispatcher.Dispatch<KeyReleasedEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnKeyReleasedEvent));
    dispatcher.Dispatch<KeyTypedEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnKeyTypedEvent));
    dispatcher.Dispatch<WindowResizeEvent>(HE_BIND_EVENT_FN(ImGuiLayer::OnWindowResizeEvent));
    }
    bool ImGuiLayer::OnMouseButtonPressedEvent(MouseButtonPressedEvent& e){
    ImGuiIO& io = ImGui::GetIO();
    io.MouseDown[e.GetMouseButton()] = true;

    return false;// 不标记为处理过了,而是没处理,将其传递给前一个层
    }
    bool ImGuiLayer::OnKeyReleasedEvent(KeyReleasedEvent& e)
    {
    ImGuiIO& io = ImGui::GetIO();
    io.KeysDown[e.GetKeyCode()] = false;

    return false;
    }
    bool ImGuiLayer::OnKeyPressedEvent(KeyPressedEvent& e)
    {
    ImGuiIO& io = ImGui::GetIO();
    io.KeysDown[e.GetKeyCode()] = true;

    io.KeyCtrl = io.KeysDown[GLFW_KEY_LEFT_CONTROL] || io.KeysDown[GLFW_KEY_RIGHT_CONTROL];
    io.KeyShift = io.KeysDown[GLFW_KEY_LEFT_SHIFT] || io.KeysDown[GLFW_KEY_RIGHT_SHIFT];
    io.KeyAlt = io.KeysDown[GLFW_KEY_LEFT_ALT] || io.KeysDown[GLFW_KEY_RIGHT_ALT];
    io.KeySuper = io.KeysDown[GLFW_KEY_LEFT_SUPER] || io.KeysDown[GLFW_KEY_RIGHT_SUPER];
    return false;
    }
    .......
    }

十二、 输入事件轮询

Input接口类设计
现在要为引擎添加新的功能,我们的应用需要能够知道键盘的输入状态,比如Unity里按住W和鼠标右键就可以实现摄像机的推进,所以引擎需要能够知道键盘的W键是否被按下

思路是通过GLFW已经提供的输入事件检测函数来检测输入事件,创建一个Input接口类,这个类根据不同平台生成对应的的Input子类,比如Windows平台下有class WindowsInput : public Input,Input类的接口需要判断某个键的状态、鼠标点击状态等

由于一个系统不会存在两个同样的键,也不会有两个鼠标,所以把这些函数都设计为Static函数,用单例模式,单例暴露的接口是static函数,而实现的具体方法是单例的虚函数

代码

  • Input.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    namespace HEngine {
    class HENGINE_API Input{
    public:
    inline static bool IsKeyPressed(int keycode) { return s_Instance->IsKeyPressedImpl(keycode); }
    inline static bool IsMouseButtonPressed(int button) { return s_Instance->IsMouseButtonPressedImpl(button); }
    inline static std::pair<float, float> GetMousePosition() { return s_Instance->GetMousePositionImpl(); }
    inline static float GetMouseX() { return s_Instance->GetMouseXImpl(); }
    inline static float GetMouseY() { return s_Instance->GetMouseYImpl(); }
    protected:
    virtual bool IsKeyPressedImpl(int keycode) = 0;

    virtual bool IsMouseButtonPressedImpl(int button) = 0;
    virtual std::pair<float, float> GetMousePositionImpl() = 0;
    virtual float GetMouseXImpl() = 0;
    virtual float GetMouseYImpl() = 0;
    private:
    static Input* s_Instance; // 声明静态单例全局对象
    };
    }
  • WindowsInput

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    namespace HEngine {
    class WindowsInput : public Input{
    protected:
    virtual bool IsKeyPressedImpl(int keycode) override;

    virtual bool IsMouseButtonPressedImpl(int button) override;
    virtual std::pair<float, float> GetMousePositionImpl() override;
    virtual float GetMouseXImpl() override;
    virtual float GetMouseYImpl() override;
    };
    }
    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
    namespace HEngine {
    Input* Input::s_Instance = new WindowsInput(); // 定义静态单例全局对象
    bool WindowsInput::IsKeyPressedImpl(int keycode){
    // 获取GLFW原生窗口void*,转为GLFWwindow*
    auto window = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
    // 通过GLFW函数来获取按键状态
    auto state = glfwGetKey(window, keycode);
    return state == GLFW_PRESS || state == GLFW_REPEAT;
    }
    bool WindowsInput::IsMouseButtonPressedImpl(int button){
    auto window = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
    auto state = glfwGetMouseButton(window, button);
    return state == GLFW_PRESS;
    }
    std::pair<float, float> WindowsInput::GetMousePositionImpl(){
    auto window = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
    double xpos, ypos;
    glfwGetCursorPos(window, &xpos, &ypos);

    return { (float)xpos, (float)ypos };
    }
    float WindowsInput::GetMouseXImpl(){
    // C++17写法
    auto [x, y] = GetMousePositionImpl();
    return x;
    // C++14以下
    //auto x = GetMousePositionImpl();
    //return std::get<0>(x);
    }
    float WindowsInput::GetMouseYImpl(){
    auto [x, y] = GetMousePositionImpl();
    return y;
    }
    }

测试

  • Application

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void Application::Run()
    {
    while (m_Running)
    {
    auto [x, y] = Input::GetMousePosition();
    HE_CORE_TRACE("{0}, {1}", x, y);
    m_Window->OnUpdate(); // 更新glfw
    }
    }

十三、 添加Math库

游戏引擎里自然少不了Vector3、Matrix以及相关的计算,如果自己写Math库,也可以允许,但是运行效率会不尽如人意,因为好的Math库能够尽可能快的完成数学运算(比如通过一次CPU指令完成矩阵的运算),这(好像)也叫做smid,如下图所示:

1

为了保证效率和跨平台的能力,这里使用glm库作为引擎的数学库,glm不只是OpenGL的数学库,也可以单独抽出来使用。老样子添加到子模块,然后修改prmake,就不重复阐述了

1
git submodule add https://github.com/g-truc/glm HEngine/vendor/glm

十四、 ImGui停靠功能

游戏引擎,比如Unity、UE5里的窗口都是可以拖拽和停靠(Docking)的,这是编辑器最基本的功能,可以直接用ImGui来完成,之前使用submodule的时候,都是引用该Project,然后把该submodule的源文件放进来,然而这里由于imgui里的源文件很多是我们不需要的,所以这里把其中的重要文件放到了ImGuiBuild.cpp里,直接当作头文件include进来,源码就不加在project的source列表里了

所以需要做以下事情:

  • 清除之前在ImGuiLayer.cpp里粘贴的ImGuiOpengl3Renderer和ImGuiGlfw3的相关内容,然后建立一个ImGuiBuild.cpp,把相关文件include进来(类似UnityBuild的做法)

设计思路
之前的ImGuiLayer是在SandboxApp.cpp里加入的,而实际上ImGui应该是游戏引擎自带的东西,不应该是由用户定义添加到LayerStack里,所以需要为Application提供固有的ImGuiLayer成员,可以用宏括起来,Release游戏的时候,就不用这个东西,设计思路如下:

1
2
3
4
5
6
7
8
9
10
11
class HENGINE_API Application
{

private:
bool OnWindowClose(WindowCloseEvent& e);

std::unique_ptr<Window> m_Window;
ImGuiLayer* m_ImGuiLayer; //添加ImGuiLayer
bool m_Running = true;
LayerStack m_LayerStack;
};

为了让每一个Layer都有一个ImGuiLayer,让每一个Layer都继承一个接口,用于绘制ImGui的内容,同时让ImGuiLayer成为HEngine内在的部分,需要在Application里面加上对应的LayerStack,与其内部的Layer一一对应,设计思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Layer
{
public:
Layer (const std::string& name = "Layer");
virtual ~Layer ();
virtual void OnAttach() {};
virtual void OnDettach() {};
virtual void OnEvent(Event&) {};
virtual void OnUpdate() {};
virtual void OnImGuiRender() {}; //新增函数
private:
...
}

然后在Application里,先调用Layer的正常函数,再调用其ImGuiRender函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (m_Running)
{
for (Layer* layer : m_LayerStack)
{
layer->OnUpdate();
}

****
m_ImGuiLayer->Begin();
for (Layer* layer : m_LayerStack)
{
// 每一个Layer都在调用ImGuiRender函数
layer->OnImGuiRender();
}
m_ImGuiLayer->End();
****
}

遇到的问题

在完成上述功能后,就可以把ImGui对应的窗口任意拖拽了,但为了在SandboxApp展示的窗口,也就是原始的Windows的粉色窗口上绘制对应的内容,需要在ExampleLayer里的OnImGuiRender里进行绘制,代码如下所示:

Sandbox.cpp

1
2
3
4
5
6
7
include "imgui.h"
void OnImGuiRender() override
{
ImGui::Begin("Test");
ImGui::Text("Hello World");
ImGui::End();
}

然而运行后,会报错,如下所示:

1
SandboxApp.obj : error LNK2019: unresolved external symbol 

大概意思就是,Linker找不到Begin、Text和End函数的定义,这是为什么呢?

这是因为HEngine引擎做成了dll,从外部可以调用的类和函数都是用HENGINE_API定义的,而ImGUI的内容是作为lib文件链接到HEngine.dll里的,ImGUI的相关API并没有声明为dllexport,到了Sandbox这里当然是找不到的

所以需要对IMGUI进行处理,使IMGUI的工程里是dllexport,直接修改他的premake5.lua文件

定义IMGUI_API这个宏为__declspec(dllimport),就可以了。

1
2
3
4
defines 
{
"IMGUI_API=__declspec(dllexport)"
}

十五、 HEngine改为静态库

  • dll

    • 优点
      • 热更新,更改引擎代码后只需重新编译dll,让多个测试项目不用重新编译能使用最新引擎代码
      • 让客户端的链接更容易
    • 缺点
      • dll很多警告
      • exe动态链接dll启动速度慢
  • lib

    所有链接都构建到exe文件中

需要考虑dll的优点在使用引擎的角度,引擎代码已经完成了就不需要热更新了,dll的热更新优点没有了

如何将引擎从dll改为lib

这里是用premake构建的工程,直接在premake5.lua文件里进行修改HEngine工程的类型就可以了。

原来的HEngine的premake部分内容如下:

1
2
3
4
5
project "HEngine"
location "%{prj.name}" -- 这里的location是生成的vcproj的位置
kind "SharedLib"-- 类型为dll
staticruntime "on"
...

修改之后变为:

1
2
3
4
5
project "HEngine"
location "%{prj.name}" -- 这里的location是生成的vcproj的位置
kind "StaticLib"-- 类型为.lib
staticruntime "off"
...

同时,把之前的dllexport和dllimport的宏注释掉就行了,如下所示:

1
2
3
4
5
6
7
8
9
#if HE_DYNAMIC_LINK
#ifdef HE_BUILD_DLL
#define HENGINE_API __declspec(dllexport)
#else
#define HENGINE_API __declspec(dllimport)
#endif
#else
#define HENGINE_API //现在为空
#endif


十六、 渲染前准备工作

这里开始我们就要开始激动人心的渲染部分啦,我们选择使用OpenGL来开始工作,因为它是较为简单和容易的图形库。我们用抽象类封装渲染图形API的Context,后面可以根据不同渲染API,设置不同渲染的上下文。

代码

  • GraphicsContext.h

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

    namespace HEngine
    {
    class GraphicsContext
    {
    public:
    virtual void Init() = 0;
    virtual void SwapBuffers() = 0;
    };
    }
  • OpenGLContext.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #pragma once
    #include "HEngine/Renderer/GraphicsContext.h"
    struct GLFWwindow;
    namespace HEngine
    {
    class OpenGLContext : public GraphicsContext{
    public:
    OpenGLContext(GLFWwindow* windowHandle);
    virtual void Init() override;
    virtual void SwapBuffers() override;
    private:
    GLFWwindow* m_WindowHandle;
    };
    }
  • OpenGLContext.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include "OpenGLContext.h"
    namespace HEngine
    {
    OpenGLContext::OpenGLContext(GLFWwindow* windowHandle)
    : m_WindowHandle(windowHandle)
    {
    HE_CORE_ASSERT(windowHandle, "Window handle is numm!")
    }
    void OpenGLContext::Init()
    {
    glfwMakeContextCurrent(m_WindowHandle); // 设置当前线程的主上下文

    // 获取显卡OpenGL函数定义的地址
    int status = gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
    HE_CORE_ASSERT(status, "Failed to initialize Glad!");
    }
    void OpenGLContext::SwapBuffers()
    {
    glfwSwapBuffers(m_WindowHandle); // 交换缓冲
    }
    }
  • WindowsWindow.h

    1
    2
    3
    4
    5
    6
    class WindowsWindow : public Window{
    .....
    private:
    GLFWwindow* m_Window;
    GraphicsContext* m_Context;
    .....
  • WindowsWindow.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void WindowsWindow::Init(const WindowProps& props)
    {
    ......
    m_Window = glfwCreateWindow((int)props.Width, (int)props.Height, m_Data.Title.c_str(), nullptr, nullptr);

    m_Context = new OpenGLContext(m_Window); // 创建渲染上下文对象
    m_Context->Init();

    glfwSetWindowUserPointer(m_Window, &m_Data);
    ......
    }

十七、 渲染第一个三角形!

  • 此节介绍

    在屏幕上用OpenGL的函数成功显示一个三角形,以及显示显卡信息。

  • 此次渲染三角形,是直接调用OpenGL图形API,并没有抽象类来封装这些API,先了解有哪些API,后面再慢慢封装成一个个抽象类,关于OpenGL学习,视频我看的是YouTube上Cherno的,访问不了外网的话网站推荐OpenGL中文网,建议学习过之后再看以下代码

代码

  • Application

    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
    #include <glad/glad.h>
    ....
    Application::Application()
    {
    ...
    // 顶点数据
    float vertices[3 * 3] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f, 0.5f, 0.0f
    };
    unsigned int indices[3] = { 0, 1, 2 }; // 索引数据
    // 0.生成顶点数组对象VAO、顶点缓冲对象VBO、索引缓冲对象EBO
    glGenVertexArrays(1, &m_VertexArray);
    glGenBuffers(1, &m_VertexBuffer);
    glGenBuffers(1, &m_IndexBuffer);
    // 1. 绑定顶点数组对象
    glBindVertexArray(m_VertexArray);
    // 2. 把我们的CPU的顶点数据复制到GPU顶点缓冲中,供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, m_VertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 3. 复制我们的CPU的索引数据到GPU索引缓冲中,供OpenGL使用
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IndexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    // 4. 设定顶点属性指针,来解释顶点缓冲中的顶点属性布局
    glEnableVertexAttribArray(0);// 开启glsl的layout = 0输入
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
    }
    void Application::Run(){
    ......
    while (m_Running){
    glClearColor(0.1f, 0.1f, 0.1f, 1);
    glClear(GL_COLOR_BUFFER_BIT);

    // 5.绑定顶点数组对象,并开始绘制,默认使用一个白色的着色器
    glBindVertexArray(m_VertexArray);
    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr);

效果

4

OpenGLContext

1
2
3
4
5
6
7
8
9
void OpenGLContext::Init()
{
//打印当前exe使用的显卡信息
HE_CORE_INFO("OpenGL Info:");

HE_CORE_INFO(" Vendor: {0}", (const char*)glGetString(GL_VENDOR));
HE_CORE_INFO(" Renderer: {0}", (const char*)glGetString(GL_RENDERER));
HE_CORE_INFO(" Version: {0}", (const char*)glGetString(GL_VERSION));
}

Renderging负责在屏幕上的绘制工作,同时接受与外部Input的交互,为了表现更好的画面效果,需要使用Graphics Processing Unit(GPU),GPU的主要优点是:能并行处理、能很快的进行数学运算

对于一些电脑,可能这里不会默认使用独显,比如N卡可以在NVDIA ControlPanel里选择这个exe使用高性能的GPU处理器,如下图所示

3


十八、 添加着色器

  • 此节目的

    使用shader,让渲染的三角形有颜色,并且将关于shader的代码抽象到Shader类中

  • 关于Shader

    • 告诉GPU如何处理我们从CPU发送到GPU的顶点数据
    • 着色器(Shader)是运行在GPU上的小程序,分别对应渲染管理不同阶段。
    • 着色器是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
  • 官网介绍https://www.khronos.org/opengl/wiki/Shader_Compilation

代码

  • 增加Shader类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #pragma once
    #include "string"
    namespace HEngine
    {
    class Shader
    {
    public:
    Shader(const std::string& vertexSrc, const std::string& fragmentSrc);
    ~Shader();

    void Bind() const;
    void Unbind() const;
    private:
    uint32_t m_RendererID;
    };
    }

    Shader.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    namespace HEngine {
    Shader::Shader(const std::string& vertexSrc, const std::string& fragmentSrc)
    {
    // 1.1 创建顶点着色器对象
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    // 1.2 附加顶点着色器源码到顶点着色器对象中
    const GLchar* source = vertexSrc.c_str();
    glShaderSource(vertexShader, 1, &source, 0);
    glCompileShader(vertexShader); // 1.3 编译顶点着色器对象

    // 1.4 检查是否编译成功
    GLint isCompiled = 0;
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &isCompiled);
    if (isCompiled == GL_FALSE)
    {
    GLint maxLength = 0;
    glGetShaderiv(vertexShader, GL_INFO_LOG_LENGTH, &maxLength);
    // The maxLength includes the NULL character
    std::vector<GLchar> infoLog(maxLength);
    glGetShaderInfoLog(vertexShader, maxLength, &maxLength, &infoLog[0]);
    // We don't need the shader anymore.
    glDeleteShader(vertexShader);
    // Use the infoLog as you see fit.
    HE_CORE_ERROR("{0}", infoLog.data());
    HE_CORE_ASSERT(false, "Vertex shader compilation failure!");
    return;
    }
    // 片段着色器大部分和上面一样
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    source = fragmentSrc.c_str();
    glShaderSource(fragmentShader, 1, &source, 0);
    glCompileShader(fragmentShader);
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &isCompiled);
    //判断是否编译成功和上面一样,换个参数的事,就不写上来了

    // 3.1创建着色器程序对象
    m_RendererID = glCreateProgram();
    GLuint program = m_RendererID;
    // 3.2附加着色器对象给着色器程序对象
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    // 3.3链接着色器程序对象
    glLinkProgram(program);
    // 3.4可以检查链接是否成功
    GLint isLinked = 0;
    glGetProgramiv(program, GL_LINK_STATUS, (int*)&isLinked);
    if (isLinked == GL_FALSE)
    {
    GLint maxLength = 0;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength);
    std::vector<GLchar> infoLog(maxLength);
    glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]);
    glDeleteProgram(program);
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    HE_CORE_ERROR("{0}", infoLog.data());
    HE_CORE_ASSERT(false, "Shader compilation failure!");
    return;
    }
    // 4.删除着色器对象
    glDetachShader(program, vertexShader);
    glDetachShader(program, fragmentShader);
    }
    Shader::~Shader(){
    glDeleteProgram(m_RendererID);
    }
    void Shader::Bind() const{
    glUseProgram(m_RendererID);
    }
    void Shader::Unbind() const{
    glUseProgram(0);
    }
    }
  • 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
    Application::Application()
    {
    // 着色器代码
    std::string vertexSrc = R"(
    #version 450 core

    layout(location = 0) in vec3 a_Position;
    out vec3 v_Position;
    void main()
    {
    v_Position = a_Position;
    gl_Position = vec4(a_Position, 1.0);
    }
    )";
    std::string fragmentSrc = R"(
    #version 450 core

    layout(location = 0) out vec4 color;
    in vec3 v_Position;
    void main()
    {
    color = vec4(v_Position * 0.5 + 0.5, 1.0);
    }
    )";
    m_Shader.reset(new Shader(vertexSrc, fragmentSrc));
    // 在头文件的std::unique_ptr<Shader> m_Shader;
    }
    void Application::Run()
    {
    while (m_Running)
    {
    // 绑定着色器
    m_Shader->Bind();
    // 绑定顶点数组对象,并绘制
    glBindVertexArray(m_VertexArray);
    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr);
    }
    }
    • 解释

      • 片段着色器

        color = vec4(v_Position * 0.5 + 0.5, 1.0);

        三个顶点颜色被确定,其围成的区域片段的颜色将会根据三个顶点颜色线性插值

      • 着色器源码字符串书写方式

        1
        2
        3
        4
        5
        6
        7
        8
        9
        // 原始的-不美观
        string s = "#version 450 core\n"
        "asf\n"
        "adf\n"
        // 用R"()"包围
        string s = R"(
        #version 450 core
        ....
        )"

效果

5


十九、 封装Buffer类

  • 此节目的

    对于OpenGL的生成顶点缓冲索引缓冲这种原始代码抽象成类。

  • 如何设计类

    从想使用的API形式出发,先想像我要使用的API接口是什么样,写出调用,然后再去根据这个去写接口

  • 渲染接口的设计

    由于可以有多个渲染图形API:OpenGL、DX,若引擎支持两种渲染图形API,需要设计选择哪一个

    • 如果是在编译时确定选择

      缺点:如果更改渲染对象,需要重新编译引擎、且运行时不能切换

    • 如果是在运行时确定选择

      缺点:编译时两个渲染相关obj都要编译

      优点:能动态切换

      如何实现:采用C++的动态特性,基类指针指向子类对象实现动态多态

    如:VertexBuffer有静态Create函数,返回VertexBuffer*,根据选择不同渲染图形API调用return new OpenGLVertexBuffer还是DirectxVertexBuffer

代码修改

  • Renderer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #pragma once
    namespace HEngine
    {
    enum class RendererAPI
    {
    None = 0, OpenGL = 1
    };
    class Renderer
    {
    public:
    inline static RendererAPI GetAPI() { return s_RendererAPI; }
    private:
    static RendererAPI s_RendererAPI;
    };
    }

    Renderer.cpp

    1
    2
    3
    4
    5
    6
    #include "hepch.h"
    #include "Renderer.h"
    namespace HEngine
    {
    RendererAPI Renderer::s_RendererAPI = RendererAPI::OpenGL;
    }
  • 增加VertexBuffer与IndexBuffer类同放在Buffer文件中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #pragma once
    namespace HEngine
    {
    class VertexBuffer
    {
    public:
    virtual ~VertexBuffer() {}
    virtual void Bind() const = 0;
    virtual void UnBind() const = 0;
    static VertexBuffer* Create(float* vertices, uint32_t size);
    };
    class IndexBuffer
    {
    public:
    virtual ~IndexBuffer() {}
    virtual void Bind() const = 0;
    virtual void UnBind() const = 0;
    virtual uint32_t GetCount() const = 0;
    static IndexBuffer* Create(uint32_t* indices, uint32_t size);
    };
    }

    Buffer.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
    namespace HEngine
    {
    VertexBuffer* VertexBuffer::Create(float* vertices, uint32_t size)
    {
    switch (Renderer::GetAPI())
    {
    case RendererAPI::None: HE_CORE_ASSERT(false, "RendererAPI::None is currently not supported!"); return nullptr;
    case RendererAPI::OpenGL: return new OpenGLVertexBuffer(vertices, size);
    }

    HE_CORE_ASSERT(false, "Unknown RendererAPI!");
    return nullptr;
    }
    IndexBuffer* IndexBuffer::Create(uint32_t* indices, uint32_t size)
    {
    switch (Renderer::GetAPI())
    {
    case RendererAPI::None: HE_CORE_ASSERT(false, "RendererAPI::None is currently not supported!"); return nullptr;
    case RendererAPI::OpenGL: return new OpenGLIndexBuffer(indices, size);
    }

    HE_CORE_ASSERT(false, "Unknown RendererAPI!");
    return nullptr;
    }
    }
  • OpenGLBuffer.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
    namespace HEngine
    {
    class OpenGLVertexBuffer : public VertexBuffer
    {
    public:
    OpenGLVertexBuffer(float* vertices, uint32_t size);
    virtual ~OpenGLVertexBuffer();

    virtual void Bind() const;
    virtual void UnBind() const;
    private:
    uint32_t m_RendererID;
    };
    class OpenGLIndexBuffer : public IndexBuffer
    {
    public:
    OpenGLIndexBuffer(uint32_t* indices, uint32_t size);
    virtual ~OpenGLIndexBuffer();

    virtual void Bind() const;
    virtual void UnBind() const;

    virtual uint32_t GetCount() const { return m_Count; }
    private:
    uint32_t m_RendererID;
    uint32_t m_Count;
    };
    }

    OpenGLBuffer.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
    namespace HEngine {
    // VertexBuffer /
    OpenGLVertexBuffer::OpenGLVertexBuffer(float* vertices, uint32_t size)
    {
    // 1.创建顶点缓冲对象
    glCreateBuffers(1, &m_RendererID);
    // 2.绑定顶点缓冲对象
    glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
    // 3. 把我们的CPU的顶点数据复制到GPU顶点缓冲中,供OpenGL使用
    glBufferData(GL_ARRAY_BUFFER, size, vertices, GL_STATIC_DRAW);
    }
    OpenGLVertexBuffer::~OpenGLVertexBuffer()
    {
    glDeleteBuffers(1, &m_RendererID);
    }
    void OpenGLVertexBuffer::Bind() const
    {
    glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
    }
    void OpenGLVertexBuffer::Unbind() const
    {
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
    // IndexBuffer //
    OpenGLIndexBuffer::OpenGLIndexBuffer(uint32_t* indices, uint32_t count)
    : m_Count(count)
    {
    // 1.创建顶点缓冲对象
    glCreateBuffers(1, &m_RendererID);
    // 2.绑定顶点缓冲对象
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID);
    // 3. 复制我们的CPU的索引数据到GPU索引缓冲中,供OpenGL使用
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(uint32_t), indices, GL_STATIC_DRAW);
    }
    OpenGLIndexBuffer::~OpenGLIndexBuffer()
    {
    glDeleteBuffers(1, &m_RendererID);
    }
    void OpenGLIndexBuffer::Bind() const
    {
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID);
    }
    void OpenGLIndexBuffer::Unbind() const
    {
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    }
  • Application.h

    1
    2
    3
    4
    5
    6
    ...
    private:
    unsigned int m_VertexArray;

    std::unique_ptr<VertexBuffer> m_VertexBuffer;
    std::unique_ptr<IndexBuffer> m_IndexBuffer;

    Application.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    ......
    float vertices[3 * 3] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f, 0.5f, 0.0f
    };
    unsigned int indices[3] = { 0, 1, 2 };

    glGenVertexArrays(1, &m_VertexArray);
    glBindVertexArray(m_VertexArray);
    /* 删除
    glGenBuffers(1, &m_VertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, m_VertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    */
    m_VertexBuffer.reset(VertexBuffer::Create(vertices, sizeof(vertices))); //换成

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
    .....
    m_IndexBuffer.reset(IndexBuffer::Create(indices, sizeof(indices) //换成

二十、 设计顶点缓冲区布局

此节目的,抽象顶点缓冲布局类:给出对应顶点着色器输入一样的格式,使能够自动计算每个属性的偏移量、分量大小,总大小,而不用手动计算

代码

Buffer.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
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
// Shdader数据类型
enum class ShaderDataType{
None = 0, Float, Float2, Float3, Float4, Mat3, Mat4, Int, Int2, Int3, Int4, Bool
};
// 获取Shader数据类型的大小
static uint32_t ShaderDataTypeSize(ShaderDataType type){
switch (type){
case ShaderDataType::Float: return 4;
case ShaderDataType::Float2: return 4 * 2;
case ShaderDataType::Float3: return 4 * 3;
case ShaderDataType::Float4: return 4 * 4;
case ShaderDataType::Mat3: return 4 * 3 * 3;
case ShaderDataType::Mat4: return 4 * 4 * 4;
case ShaderDataType::Int: return 4;
case ShaderDataType::Int2: return 4 * 2;
case ShaderDataType::Int3: return 4 * 3;
case ShaderDataType::Int4: return 4 * 4;
case ShaderDataType::Bool: return 1;
}
HE_CORE_ASSERT(false, "Unknown ShaderDataType!");
return 0;
}
// Shader属性类
struct BufferElement{
std::string Name;
ShaderDataType Type;
uint32_t Size;
uint32_t Offset;
bool Normalized;
BufferElement() {}
BufferElement(ShaderDataType type, const std::string& name, bool normalized = false)
: Name(name), Type(type), Size(ShaderDataTypeSize(type)), Offset(0), Normalized(normalized)
{ }
// 获取此属性有几个分量
uint32_t GetComponentCount() const{
switch (Type) {
case ShaderDataType::Float: return 1;
case ShaderDataType::Float2: return 2;
case ShaderDataType::Float3: return 3;
case ShaderDataType::Float4: return 4;
case ShaderDataType::Mat3: return 3 * 3;
case ShaderDataType::Mat4: return 4 * 4;
case ShaderDataType::Int: return 1;
case ShaderDataType::Int2: return 2;
case ShaderDataType::Int3: return 3;
case ShaderDataType::Int4: return 4;
case ShaderDataType::Bool: return 1;
}
HE_CORE_ASSERT(false, "Unknown ShaderDataType!");
return 0;
}
};
// 顶点缓冲布局抽象类
class BufferLayout{
public:
BufferLayout() {}
// 用初始化列表构造BufferLayout对象
BufferLayout(const std::initializer_list<BufferElement>& elements)
: m_Elements(elements)
{
CalculateOffsetsAndStride();
}
inline uint32_t GetStride() const { return m_Stride; }
inline const std::vector<BufferElement>& GetElements() const { return m_Elements; }

std::vector<BufferElement>::iterator begin() { return m_Elements.begin(); }
std::vector<BufferElement>::iterator end() { return m_Elements.end(); }
std::vector<BufferElement>::const_iterator begin() const { return m_Elements.begin(); }
std::vector<BufferElement>::const_iterator end() const { return m_Elements.end(); }

private:
// 计算属性列表各个属性的偏移量,跨步长
void CalculateOffsetsAndStride(){
uint32_t offset = 0;
m_Stride = 0;
for (auto& element : m_Elements){
element.Offset = offset;
offset += element.Size;
m_Stride += element.Size;
}
}
private:
std::vector<BufferElement> m_Elements;
uint32_t m_Stride = 0;
};

OpenGLBuffer.h

1
2
3
4
5
6
7
8
public:
virtual void Bind() const override;
virtual void UnBind() const override;

virtual const BufferLayout& GetLayout() const override { return m_Layout; }
virtual void SetLayout(const BufferLayout& layout) override { m_Layout = layout; }
private:
BufferLayout m_Layout;

Application.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
static GLenum ShaderDataTypeToOpenGLBaseType(ShaderDataType type)
{
switch (type)
{
case HEngine::ShaderDataType::Float: return GL_FLOAT;
case HEngine::ShaderDataType::Float2: return GL_FLOAT;
case HEngine::ShaderDataType::Float3: return GL_FLOAT;
case HEngine::ShaderDataType::Float4: return GL_FLOAT;
case HEngine::ShaderDataType::Mat3: return GL_FLOAT;
case HEngine::ShaderDataType::Mat4: return GL_FLOAT;
case HEngine::ShaderDataType::Int: return GL_INT;
case HEngine::ShaderDataType::Int2: return GL_INT;
case HEngine::ShaderDataType::Int3: return GL_INT;
case HEngine::ShaderDataType::Int4: return GL_INT;
case HEngine::ShaderDataType::Bool: return GL_BOOL;
}
HE_CORE_ASSERT(false, "Unknow ShaderDataType!");
return 0;
}
/*删除
float vertices[3 * 3] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
*/
float vertices[3 * 7] = { //加上颜色数据
-0.5f, -0.5f, 0.0f, 0.8f, 0.2f, 0.8f, 1.0f,
0.5f, -0.5f, 0.0f, 0.2f, 0.3f, 0.8f, 1.0f,
0.0f, 0.5f, 0.0f, 0.8f, 0.8f, 0.2f, 1.0f

/*删除
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
*/
{
BufferLayout layout =
{
{ ShaderDataType::Float3, "a_Position"},
{ ShaderDataType::Float4, "a_Color" }
};
m_VertexBuffer->SetLayout(layout);
}

uint32_t index = 0;
const auto& layout = m_VertexBuffer->GetLayout();
for (const auto& element : layout)
{
glEnableVertexAttribArray(index);
glVertexAttribPointer(index,
element.GetComponentCount(),
ShaderDataTypeToOpenGLBaseType(element.Type),
element.Normalized ? GL_TRUE : GL_FALSE,
layout.GetStride(),
(const void*)element.Offset);

index++;
}

std::string vertexSrc = R"(
#version 450 core

layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
out vec3 v_Position;
out vec4 v_Color;
void main()
{
v_Position = a_Position;
v_Color = a_Color;
gl_Position = vec4(a_Position, 1.0);
}
)";
std::string fragmentSrc = R"(
#version 450 core
layout(location = 0) out vec4 color;
in vec3 v_Position;
in vec4 v_Color;
void main()
{
color = vec4(v_Position * 0.5 + 0.5, 1.0);
color = v_Color;
}
)";

效果:

6


二十一、添加顶点数组

OpenGL里的VAO,其实本身不包含任何Buffer的数据,它只是记录了Vertex Buffer和IndexBuffer的引用,并且使用glVertexAttribPointer函数来决定VAO通过哪种方式来挖取 VBO中的数据。

这节目的是创建Vertex Array类,由于OpenGL有VAO这个东西,而DX里完全没有这个概念,但是前期的HEngine引擎是极大程度依赖OpenGL的,所以目前是先创建VertexArray类

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
// 1. 创建VertexArray,这一段还没抽象化
glGenVertexArrays(1, &m_VertexArray);
glBindVertexArray(m_VertexArray);

// 这一段已经成功抽象化了
{
BufferLayout layout =
{
{ ShaderDataType::Float3, "a_Position"},
{ ShaderDataType::Float4, "a_Color" }
};
vertexBuffer->SetLayout(layout);
m_VertexArray->AddVertexBuffer(vertexBuffer);
}

BufferLayout layout = m_VertexBuffer->GetBufferLayout();
int index = 0;
// 2. 指定VAO挖数据的方法,这一段也没抽象化
for (const BufferElement& element : layout)
{
glEnableVertexAttribArray(index);
glVertexAttribPointer(index,
GetShaderTypeDataCount(element.GetType()),
GetShaderDataTypeToOpenGL(element.GetType()),
element.IsNormalized()? GL_TRUE : GL_FALSE,
layout.GetStride(),
(const void*)(element.GetOffset()));
index++;
}

接下来就是正式开始写代码了。

前面两句代码要把它抽象化,也就是把它变成跟平台无关的东西,跟之前创建的VertexBuffer和IndexBuffer都差不多

1
2
glGenVertexArrays(1, &m_VertexArray);
glBindVertexArray(m_VertexArray);

这里单独建一个VertexArray的cpp和h文件,之所以单独建立cpp和h文件,是因为还不确定相关的VertexArray的内容以后还会不会会保留,毕竟DX里是没有这个概念的

VertexArray.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 直接复制VertexBuffer的相关内容就行:
class VertexArray
{
public:
// 这些都是跟VertexBuffer和IndexBuffer的接口一样的
virtual ~VertexArray() {};
virtual void Bind() const = 0;
virtual void Unbind() const = 0;//Unbind函数一般用于debuging purposes

static VertexArray* Create();

// 由于一个VAO可以挖取多个VBO的数据,所以需要添加记录相关VBO引用的接口
virtual void AddVertexBuffer(const std::shared_ptr < VertexBuffer>& vertexBuffer) = 0;
virtual void SetIndexBuffer(const std::shared_ptr<IndexBuffer>& indexBuffer) = 0;

virtual const std::vector<std::shared_ptr<VertexBuffer>>& GetVertexBuffers() const = 0;
virtual const std::shared_ptr<IndexBuffer>& GetIndexBuffer() const = 0;
};

VertexArray.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace HEngine
{
VertexArray* VertexArray::Create()
{
switch (Renderer::GetAPI())
{
case RendererAPI::None: HE_CORE_ASSERT(false, "RendererAPI::None is currently not supported!"); return nullptr;
case RendererAPI::OpenGL: return new OpenGLVertexArray();
}
HE_CORE_ASSERT(false, "Unknow RendererAPI!");
return nullptr;
}
}

接下来就是创建OpenGLVertexArray的头文件和cpp文件了,放到Platform的文件夹里,实现过程跟OpenGLVertexBuffer差不多

OpenGLVertexArray.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
void OpenGLVertexArray::AddVertexBuffer(const std::shared_ptr<VertexBuffer>& vertexBuffer)
{
HE_CORE_ASSERT(vertexBuffer->GetLayout().GetElements().size(), "Vertex Buffer has no layout!");

glBindVertexArray(m_RendererID);
vertexBuffer->Bind();

uint32_t index = 0;
const auto& layout = vertexBuffer->GetLayout();
for (const auto& element : layout)
{
glEnableVertexAttribArray(index);
glVertexAttribPointer(index,
element.GetComponentCount(),
ShaderDataTypeToOpenGLBaseType(element.Type),
element.Normalized ? GL_TRUE : GL_FALSE,
layout.GetStride(),
(const void*)element.Offset);

index++;
}
m_VertexBuffers.push_back(vertexBuffer);
}
void OpenGLVertexArray::SetIndexBuffer(const std::shared_ptr<IndexBuffer>& indexBuffer)
{
glBindVertexArray(m_RendererID);
indexBuffer->Bind();

m_IndexBuffer = indexBuffer;
}

多个VBO来验证

做到这里其实就差不多了,但是为了验证之前做的是正确的,创建了一个应用场景,就是通过使用两个Shader,一个VAO,两个VBO,一个EBO来绘制出来

创建第二个VertexArray
前面画了个三角形,下面再画一个Quad,在Application类里创建:

1
std::shared_ptr<VertexArray> m_QuadVertexArray;

然后按照同样的方式,创建VAO、VBO和顶点数据,再创建一个Shader,这里做一个只输出蓝色的Shader,就可以了,最后在Loop里分别绑定VBO和Shader就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
float squareVertices[3 * 4] = {
-0.75f, -0.75f, 0.0f,
0.75f, -0.75f, 0.0f,
0.75f, 0.75f, 0.0f,
-0.75f, 0.75f, 0.0f
};
std::shared_ptr<VertexBuffer> squareVB;
squareVB.reset(VertexBuffer::Create(squareVertices, sizeof(squareVertices)));
squareVB->SetLayout({
{ ShaderDataType::Float3, "a_Position"}
});
m_SquareVA->AddVertexBuffer(squareVB);

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

std::string blueShaderVertexSrc = R"(
#version 450 core

layout(location = 0) in vec3 a_Position;

out vec3 v_Position;

void main()
{
v_Position = a_Position;
gl_Position = vec4(a_Position, 1.0);
}
)";

std::string blueShaderFragmentSrc = R"(
#version 450 core

layout(location = 0) out vec4 color;

in vec3 v_Position;

void main()
{
color = vec4(0.2, 0.3, 0.8, 1.0);
}
)";
m_BlueShader.reset(new Shader(blueShaderVertexSrc, blueShaderFragmentSrc));

m_BlueShader->Bind();
m_SquareVA->Bind();
glDrawElements(GL_TRIANGLES, m_SquareVA->GetIndexBuffer()->GetCount(), GL_UNSIGNED_INT, nullptr);

m_VertexArray->Bind();
glDrawElements(GL_TRIANGLES, m_VertexArray->GetIndexBuffer()->GetCount(), GL_UNSIGNED_INT, nullptr);

效果

7


二十二、设计渲染器架构

从无到有,绘制出了三角形,然后把相关的VerterBuffer、VertexArray、IndexBuffer进行了抽象化,也就是说目前Application里不会有具体的OpenGL这种平台相关的代码,只剩下里面的glDrawElements函数、glClear和glClearColor没有抽象化。

前面做的抽象化,比如VertexBuffer、VertexArray,这些都是渲染要用到的相关概念的类抽象,真正的跨平台 用于渲染的Renderer类还没有创建起来。

思考一下,一个Renderer需要干什么 它需要Render一个Geometry。Render一个Geometry需要以下内容:

  • 一个Vertex Array,包含了VertexBuffers和一个IndexBuffer
  • 一个Shader
  • 人物的视角,即Camera系统,本质上就是一个Projection和View矩阵
  • 绘制物体的所在的世界坐标,前面的VertexBuffer里记录的是局部坐标,也就是Model(World)矩阵
  • Cube表面的材质属性,wooden或者plastic,金属度等相关属性,这个也可以属于Shader的范畴
  • 环境信息:比如环境光照、比如Environment Map、Radiance Map

这些信息可以分为两类:

  • 环境相关的信息:渲染不同的物体时,环境信息也一般是相同的,比如环境光照、人物的视角等
  • 被渲染的物体相关的信息:不同物体的相关信息很多是不同的,比如VertexArray,也可能部分属性相同(比如材质),这些相同的内容可以在批处理里进行处理,从而优化性能

总结得到,一个Renderer应该具有以下功能

  • 设置环境相关的信息
  • 接受被渲染的物体,传入它对应的数据,比如Vertex Array、引用的Material和Shader
  • 渲染物体,调用DrawCall
  • 批处理,为了优化性能,把相同材质的物体一起渲染等

可以把Renderer每帧执行的任务分为四个步骤:

  • BeginScene: 负责每帧渲染前的环境设置
  • Submit:收集场景数据,同时收集渲染命令,提交渲染命令到队列里
  • EndScene:对收集到的场景数据进行优化
  • Render:按照渲染队列,进行渲染

具体步骤如下:
1. BeginScene
由于环境相关的信息是相同的,所以在Renderer开始渲染的阶段,需要先搭建相关环境,为此设计了一个Begin Scene函数。Begin Scene阶段,基本就是告诉Renderer,我要开始渲染一个场景,然后会设置其周围的环境(比如环境光照)、Camera。

2. Submit
这个阶段,就可以渲染每一个Mesh了,他们的Transform矩阵一般是不同的,依次传给Renderer就可以了,这里会把所有的渲染命令都commit到RenderCommandQueue里。

3. End Scene
应该是在这个阶段,在收集完场景数据后,做一些优化的操作,比如

  • 把使用相同的材质的物体合并到一起(Batch)
  • 把在Frustum外部的物体Cull掉
  • 根据位置进行排序

4. Render
在把所有的东西都commit到RenderCommandQueue里后,所有的Scene相关的东西,现在Renderer都处理好了,也都拥有了该数据,就可以开始渲染了。

整体四个过程的代码大体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在Render Run里
while (m_Running)
{
// 这个ClearColor是游戏最底层的颜色,一般不会出现在用户界面里,可能用得比较少
RenderCommand::SetClearColor();// 参数省略
RenderCommand::Clear();
RenderCommand::DrawIndexed();

Renderer::BeginScene();// 用于设置Camera、Environment和lighting等
Renderer::Submit();// 提交Mesh给Renderer
Renderer::EndScene();

// 在多线程渲染里,可能会在这个阶段用一个另外的线程执行Render::Flush操作,需要结合Render Command Queue
Renderer::Flush();
...
}

本节要做的实际内容

上面虽然介绍完了渲染架构,大体上是统一处理物体,然后统一渲染,但是由于目前相关的架构还没搭起来,所以这仍然是Bind一个VAO,然后调用一次DrawCall,以后会改进的。

目前就剩glClear、glClearColor和DrawCall的代码需要抽象化了,也就是这三句:

1
2
3
glClearColor(0.1f, 0.1f, 0.1f, 1);
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLES, m_QuadVertexArray->GetIndexBuffer()->GetCount(), GL_UNSIGNED_INT, nullptr);

这些代码,打算把它抽象为:

1
2
3
4
// 这个ClearColor是游戏最底层的颜色,一般不会出现在用户界面里,用洋红色这种offensive的颜色比较好
RenderCommand::SetClearColor(glm::vec4(1.0, 0.0, 1.0, 1.0));// 直接用glm里的vec4好了
RenderCommand::Clear();
RenderCommand::DrawIndexed();

然后对于原本的Renderer里的GetAPI函数,它应该由RendererAPI负责, 而不是Renderer负责,这里新建RendererAPI类,除了要有标识当前使用的API类型的函数外,还需要有很多与平台无关的渲染的API,比如清空Buffer、根据Vertex Array进行调用DrawCall等函数

RendererAPI.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace HEngine
{
class RendererAPI
{
public:
enum class API
{
None = 0, OpenGL = 1
};
public:
virtual void SetClearColor(const glm::vec4& color) = 0;
virtual void Clear() = 0;

virtual void DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray) = 0;

inline static API GetAPI() { return s_API; }
private:
static API s_API;
};
}
//在RendererAPI的cpp文件里进行初始化:
RendererAPI::API RendererAPI::s_API = RendererAPI::API::OpenGL;

RendererAPI是一个接口类,与平台无关,现在就可以实现OpenGL平台的OpenGLRendererAPI了,Platform文件夹下创建对应的cpp和h文件,跟之前的做法类似,不多说。

OpenGLRendererAPI.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace HEngine
{

void OpenGLRendererAPI::SetClearColor(const glm::vec4& color)
{
glClearColor(color.r, color.g, color.b, color.a);
}

void OpenGLRendererAPI::Clear()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void OpenGLRendererAPI::DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray)
{
glDrawElements(GL_TRIANGLES, vertexArray->GetIndexBuffer()->GetCount(), GL_UNSIGNED_INT, nullptr);
}

}

接下来,就是实现Renderer类了,目前这个类只实现了一个GetAPIType函数,类的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Renderer
{
public:
// TODO: 未来会接受Scene场景的相关参数,比如Camera、lighting, 保证shaders能够得到正确的环境相关的uniforms
static void BeginScene();
static void EndScene();
// TODO: 会把VAO通过RenderCommand下的指令,传递给RenderCommandQueue
// 目前偷个懒,直接调用RenderCommand::DrawIndexed()函数
static void Submit(const std::shared_ptr<VertexArray>&);

inline static RendererAPI::API GetAPIType() { return RendererAPI::GetAPIType(); }
};

后面会再去实现成员函数,现在先把类都声明好,还剩一个RenderCommand类了,同样创建一个RenderCommand.h头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RenderCommand
{
public:
// 注意RenderCommand里的函数都应该是单一功能的函数,不应该有其他耦合的任何功能
inline static void DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray)
{
// 比如这里不可以调用vertexArray->Bind()函数
s_RenderAPI->DrawIndexed(vertexArray);
}
// 同上,再实现Clear和ClearColor的static函数
...
private:
static RendererAPI* s_RenderAPI;
};

可以看到,RenderCommand类只是把RendererAPI的内容,做了一个静态的封装,这样做是为了以后把函数加入到RenderCommandQueue里做的架构设计,也是为了后面的多线程渲染做铺垫。

然后在Renderer的submit函数里实现下面的内容即可:

1
2
3
4
5
void Renderer::Submit(const std::shared_ptr<VertexArray>& vertexArray)
{
vertexArray->Bind();
RenderCommand::DrawIndexed(vertexArray);
}

到此我们渲染框架就大致完成了,Application里已经没有OpenGL原生函数就可以完成绘制了

Application.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Application::Run()
{
while (m_Running)
{
....
Renderer::BeginScene();
m_BlueShader->Bind();
Renderer::Submit(m_QuadVertexArray);

m_Shader->Bind();
Renderer::Submit(m_VertexArray);

Renderer::EndScene();
.....
}
}

二十三、正交摄像机实现

先介绍一些Camera的概念,相机系统的代码框架(architecture)很重要,它决定了游戏引擎能否将更多的时间花在渲染上,从而提高帧数。Camera除了与渲染相关,还与玩家有着交互, 比如User Input、比如玩家移动的时候,Camera往往也需要移动,所以说,Camera既受GamePlay影响,也会被Submit到Renderer做渲染工作

Camera本身是一个虚拟的概念,它的本质其实就是View和Projection矩阵的设置,其属性有:

  • 相机的位置
  • 相机的相关属性,比如FOV,比如Aspect Ratio
  • 还有MVP三个矩阵(以下个人理解不一定对),M是与模型密切相关的,但是不同模型在同一个相机下,V和P矩阵是相同的,所以说,VP矩阵属于相机的属性。举个简单的例子,在日常生活中你去拍照,你首先会取景,然后把你想拍的东西都摆放好,这一步叫做model transformation(模型变换)。接下来你肯定会挑选一个特定的角度摆放你的摄像机,这一步就叫做view transformation(视图变换)。然后按下快门后把图片拍下来,这一步就叫做projection transformation(投影变换)。

实际渲染时,默认相机都是在世界坐标系原点,朝向-z方向看的,当调整相机属性时,其实没有Camera这个实物,实际上是整个世界的物体在靠近相机,即往Camera这边平移;当我们向左移动相机的时候,,实际上我们是把所有世界的物体向右移,所以,相机的transform变化矩阵与物体的transform变化矩阵正好是互逆的。也就是说,我们可以通过记录相机的transformation矩阵,然后取逆矩阵,就可以得到对应的View矩阵了,这里只需要Position和Rotation,因为相机是没有缩放的。

根据投影方式的不同,分为了透视投影的Camera和正交投影的Camera,这一节先实现更简单的正交投影的Camera。

代码

  • OrthographicCamera.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class OrthographicCamera{
    public:
    OrthographicCamera(float left, float right, float bottom, float top);
    const glm::vec3& GetPosition() const { return m_Position; }
    void SetPosition(const glm::vec3& position) { m_Position = position; RecalculateViewMatrix(); }
    float GetRotation() const { return m_Rotation; }
    void SetRotation(float rotation) { m_Rotation = rotation; RecalculateViewMatrix(); }
    const glm::mat4& GetProjectionMatrix() const { return m_ProjectionMatrix; }
    const glm::mat4& GetViewMatrix() const { return m_ViewMatrix; }
    const glm::mat4& GetViewProjectionMatrix() const { return m_ViewProjectionMatrix; }
    private:
    void RecalculateViewMatrix();
    private:
    glm::mat4 m_ProjectionMatrix;
    glm::mat4 m_ViewMatrix;
    glm::mat4 m_ViewProjectionMatrix;
    glm::vec3 m_Position = { 0.0f, 0.0f, 0.0f };// 位置
    float m_Rotation = 0.0f; // 绕z轴的旋转角度
    };

    OrthographicCamera.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 初始化用glm计算正交投影矩阵
    OrthographicCamera::OrthographicCamera(float left, float right, float bottom, float top)
    : m_ProjectionMatrix(glm::ortho(left, right, bottom, top, -1.0f, 1.0f)), m_ViewMatrix(1.0f)
    {
    m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix;
    }
    // 投影观察矩阵计算
    void OrthographicCamera::RecalculateViewMatrix()
    {
    // 观察矩阵
    glm::mat4 transform = glm::translate(glm::mat4(1.0f), m_Position) *
    glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation), glm::vec3(0, 0, 1));

    m_ViewMatrix = glm::inverse(transform);
    m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix;
    }
  • Renderer.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Renderer{
    public:
    static void BeginScene(OrthographicCamera& camera);

    static void Submit(const std::shared_ptr<Shader>& shader, const std::shared_ptr<VertexArray>& vertexArray);

    private:
    struct SceneData {
    glm::mat4 ViewProjectionMatrix;
    };
    static SceneData* m_SceneData;
    };

    Renderer.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Renderer::SceneData* Renderer::m_SceneData = new Renderer::SceneData;
    void Renderer::BeginScene(OrthographicCamera& camera)
    {
    m_SceneData->ViewProjectionMatrix = camera.GetViewProjectionMatrix(); // 保存计算的Projection * view矩阵
    }
    void Renderer::Submit(const std::shared_ptr<Shader>& shader, const std::shared_ptr<VertexArray>& vertexArray)
    {
    shader->Bind(); // 着色器绑定
    shader->UploadUniformMat4("u_ViewProjection", m_SceneData->ViewProjectionMatrix);// 上传投影观察矩阵
    }
  • Shader.cpp

    1
    2
    3
    4
    void Shader::UploadUniformMat4(const std::string& name, const glm::mat4& matrix){
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
    }
  • Application

    1
    OrthographiCamera m_Camera;
    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
    Application::Application() : m_Camera(-1.6f, 1.6f, -0.9f, 0.9f)
    {
    std::string vertexSrc = R"(
    #version 450 core

    layout(location = 0) in vec3 a_Position;
    layout(location = 1) in vec4 a_Color;
    uniform mat4 u_ViewProjection; //新增
    out vec3 v_Position;
    out vec4 v_Color;
    void main()
    {
    v_Position = a_Position;
    v_Color = a_Color;
    gl_Position = u_ViewProjection * vec4(a_Position, 1.0); //新增
    }
    )";
    }
    void Application::Run()
    {
    while (m_Running){

    m_Camera.SetPosition({ 0.5f, 0.5f, 0.0f }); //新增
    m_Camera.SetRotation(45.0f); //新增

    Renderer::BeginScene(m_Camera);

    Renderer::Submit(m_BlueShader, m_SquareVA);
    Renderer::Submit(m_Shader, m_VertexArray);

    Renderer::EndScene();
    }
    }

遇到的问题

  • 正方形变成长方形

    窗口是1280 * 720,当glm::ortho(-1.0f,1.0f, -1.0f, 1.0f, -1.0f, 1.0f);时候,本来正方形的蓝色quad变为长方形

  • 修复变回正方形

    在1280*720下,left right需传入1280/720=1.7左右,将宽放大,从而左右视角变大,物体围成的宽范围缩小,从而变回正方形。

    1
    2
    Application::Application()
    :m_Camera(-1.6f, 1.6f, -0.9f, 0.9f){}

GLM库函数相关

  • glm::ortho(left,right, bottom, top, -1.0f, 1.0f);

    left = -1.0f;right = 1.0f;bottom = -1.0f;top = 1.0f

    得到的矩阵是

    1
    2
    3
    4
    1 0  0 0
    0 1 0 0
    0 0 -1 0
    0 0 0 1
  • glm::translate(glm::mat4(1.0f), m_Position);

    m_Position= {0.5f, 0.5f, 0.5f};

    glm::mat4(1.0f),是4x4的单位矩阵

    1
    2
    3
    4
    1 0 0 0
    0 1 0 0
    0 0 1 0
    0 0 0 1

    glm::translate(glm::mat4(1.0f), m_Position);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /*
    glm::translate函数中
    mat<4, 4, T, Q> Result(m);
    Result[3] = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3];
    Result[3]是第4行,m[0]是第1行,m[1]是第2行,m[2]是第3行。。。
    */
    1 0 0 0.5
    0 1 0 0.5
    0 0 1 0.5
    0 0 0 1

二十四、添加Timestep系统

现在的HEngine游戏引擎里,一秒内调用多少次OnUpdate函数,完全是取决于CPU的(当开启VSync则取决于显示器的频率)。假如我设计一个用代码控制相机移动的功能

此时会出现一个问题:如果同时在不同的机器上执行这段代码,性能更好的CPU,1s内循环跑的此时越多,相机会移动的更快。不同的机器的执行效果不一样,这肯定是不行的,所以要设计TimeStep系统。

三种不同的Timestep系统

一般来说,有三种Timestep系统,它们都用来帮助解决不同机器上循环执行速度不同的问题:

  • 固定delta time的Timestep系统
  • 灵活delta time的Timestep系统,delta time取决于此帧用时
  • 半固定delta time的Timestep系统(Semi-fixed timestep)

HEngine引擎里的Timestep系统

由于目前没有物理引擎部分,所以这里选择了上面说的第二种Timestep系统

第二种Timestep系统的设计原理是:虽然不同机器执行一次Loop函数的用时不同,但只要把每一帧里的运动,跟该帧所经历的时间相乘,就能抵消因为帧率导致的数据不一致的问题。

  • 计算deltatime

    HZ为60的,1/60 = 0.01666666

    HZ为100的,1/100 = 0.01

    由于 deltaTime 变小,高帧率时每帧移动距离会自动缩小,确保一致的运动。

所以只需要记录每帧的DeltaTime,然后在Movement里乘以它即可,具体其实就是把之前在循环里调用的函数,比如OnUpdate函数,从无参函数变成带一个TimeStep参数的函数而已,代码如下:

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
// ============== Timestep.cpp =============
// Timestep 实际就是一个float值的wrapper
class Timestep
{
public:
Timestep(float time = 0.0f)
: m_Time(time)
{
}

operator float() const { return m_Time; }
// 给float添加wrapper是方便进行秒和毫秒的转换
float GetSeconds() const { return m_Time; }
float GetMilliseconds() const { return m_Time * 1000.0f; }
private:
float m_Time;
};


// ============== Application.cpp =============
void Application::Run()
{
while(m_Running)
{
float time = (float)GetTime();

Timestep timestep = time - m_LastFrameTIme;
m_LastFrameTime = time;

...
layer->OnUpdate(timestep);
}
}

//============== Sandbox.cpp =============
void OnUpdate(HEngine::Timestep ts) override
{
HEngine::RenderCommand::SetClearColor({ 0.1f, 0.1f, 0.1f, 1 });
HEngine::RenderCommand::Clear();

m_Camera.SetPosition(m_CameraPosition);
m_Camera.SetRotation(m_CameraRotation);

HEngine::Renderer::BeginScene(m_Camera);

HEngine::Renderer::Submit(m_BlueShader, m_SquareVA);
HEngine::Renderer::Submit(m_Shader, m_VertexArray);

HEngine::Renderer::EndScene();
}

把Application.cpp里的内容移到Sandbox对应的Project

Application类应该主要负责进行While循环,在里面调用各个Layer的Update函数,而SandboxApp类虽然继承于Application类,但是也只是个大致的空壳而已,它的存在是为了把new出来的ExampleLayer加入到继承来的m_LayerStack里,具体的绘制Quad和Triangle的的操作应该放到Sandbox对应Project的Layer里,大概是这样:

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

class ExampleLayer : public HEngine::Layer
{
public:
ExampleLayer()
: Layer("Example")
: Layer("Example"), m_Camera(-1.6f, 1.6f, -0.9f, 0.9f), m_CameraPosition(0.0f)
{
m_VertexArray.reset(HEngine::VertexArray::Create());
float vertices[3 * 7] = {
-0.5f, -0.5f, 0.0f, 0.8f, 0.2f, 0.8f, 1.0f,
0.5f, -0.5f, 0.0f, 0.2f, 0.3f, 0.8f, 1.0f,
0.0f, 0.5f, 0.0f, 0.8f, 0.8f, 0.2f, 1.0f
};
std::shared_ptr<HEngine::VertexBuffer> vertexBuffer;
vertexBuffer.reset(HEngine::VertexBuffer::Create(vertices, sizeof(vertices)));
HEngine::BufferLayout layout = {
{ HEngine::ShaderDataType::Float3, "a_Position"},
{ HEngine::ShaderDataType::Float4, "a_Color" }
};
vertexBuffer->SetLayout(layout);
m_VertexArray->AddVertexBuffer(vertexBuffer);

unsigned int indices[3] = { 0, 1, 2 };
std::shared_ptr<HEngine::IndexBuffer> indexBuffer;
indexBuffer.reset(HEngine::IndexBuffer::Create(indices, sizeof(indices) / sizeof(uint32_t)));
m_VertexArray->SetIndexBuffer(indexBuffer);


m_SquareVA.reset(HEngine::VertexArray::Create());
float squareVertices[3 * 4] = {
-0.75f, -0.75f, 0.0f,
0.75f, -0.75f, 0.0f,
0.75f, 0.75f, 0.0f,
-0.75f, 0.75f, 0.0f
};
std::shared_ptr<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 };
std::shared_ptr<HEngine::IndexBuffer> squareIB;
squareIB.reset(HEngine::IndexBuffer::Create(squareIndices, sizeof(squareIndices) / sizeof(uint32_t)));
m_SquareVA->SetIndexBuffer(squareIB);

std::string vertexSrc = ...;
std::string fragmentSrc = ...;
m_Shader.reset(new HEngine::Shader(vertexSrc, fragmentSrc));

std::string blueShaderVertexSrc = ...;
std::string blueShaderFragmentSrc = ...;
m_BlueShader.reset(new HEngine::Shader(blueShaderVertexSrc, blueShaderFragmentSrc));
}
}

二十五、矩阵位置、旋转、缩放

目前的Transform都是World坐标系的Transform,没有层级父子关系,本质就是globalPosition,globalRotation和globalScale,可以组成一个矩阵来表示。这里做的很简单,甚至都没有单独创建一个Transform类,就是用矩阵代表Model矩阵,作为uniform传给Shader而已,很简单,这里重点修改的函数是Submit函数,原本的函数如下:

Renderer.cpp

1
void Renderer::Submit(const std::shared_ptr<Shader>& shader, const std::shared_ptr<VertexArray>& va)

现在要修改成:

1
2
3
4
5
6
7
8
9
// 在渲染Vertex Array的时候, 添加对应model的transform对应的矩阵信息
void Renderer::Submit(const std::shared_ptr<Shader>& shader, const std::shared_ptr<VertexArray>& vertexArray, const glm::mat4& transform)
{
shader->Bind();
shader->UploadUniformMat4("u_ViewProjection", s_SceneData->ViewProjectionMatrix);
shader->UploadUniformMat4("u_Transform", transform);

RenderCommand::DrawIndexed(va);
}

然后Sandbox.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
void OnUpdate(HEngine::Timestep ts) override
{
if (HEngine::Input::IsKeyPressed(HE_KEY_LEFT))
m_CameraPosition.x -= m_CameraMoveSpeed * ts;
else if (HEngine::Input::IsKeyPressed(HE_KEY_RIGHT))
m_CameraPosition.x += m_CameraMoveSpeed * ts;

if (HEngine::Input::IsKeyPressed(HE_KEY_UP))
m_CameraPosition.y += m_CameraMoveSpeed * ts;
else if (HEngine::Input::IsKeyPressed(HE_KEY_DOWN))
m_CameraPosition.y -= m_CameraMoveSpeed * ts;

if (HEngine::Input::IsKeyPressed(HE_KEY_A))
m_CameraRotation += m_CameraRotationSpeed * ts;
else if (HEngine::Input::IsKeyPressed(HE_KEY_D))
m_CameraRotation -= m_CameraRotationSpeed * ts;

m_Camera.SetPosition(m_CameraPosition);
m_Camera.SetRotation(m_CameraRotation);

HEngine::Renderer::BeginScene(m_Camera);

glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(0.1f));
for (int y = 0; y < 20; y++)
{
for (int x = 0; x < 20; x++)
{
glm::vec3 pos(x * 0.11f, y * 0.11f, 0.0f);
glm::mat4 transform = glm::translate(glm::mat4(1.0f), pos) * scale;
HEngine::Renderer::Submit(m_BlueShader, m_SquareVA, transform);
}
}

HEngine::Renderer::Submit(m_Shader, m_VertexArray);
HEngine::Renderer::EndScene();
}
std::string vertexSrc = R"(
#version 450 core
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;

uniform mat4 u_ViewProjection;
uniform mat4 u_Transform; //新增

out vec3 v_Position;
out vec4 v_Color;

void main()
{
v_Position = a_Position;
v_Color = a_Color;
gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0); //乘上Transform
}
)";

效果

8


二十六、着色器封装

  • 此节目的

    完成Shader的抽象,因为目前只有Shader类,应该像顶点数组、顶点缓冲一样完善Shader的抽象

    同之前抽象的结构一样:Shader是一个抽象类,有一个静态的Create方法,返回Shader指针,在这个函数中根据不同的预定义,实例化OpenGLShader还是DxShader。

关键代码

  • 动态指针强转

    1
    std::dynamic_pointer_cast<HEngine::OpenGLShader>(m_FlatShader)->UploadUniformFloat3("u_Color", m_SquareColor);

    因为要执行OpenGLShader(子类)独有的函数UploadUniformFloat3,而Shader(父类)里没有这个函数UploadUniformFloat3,所以需要动态指针强转,转为派生类的指针类型。

  • 由于上传的是vec3,所以fragment的代码uniform接受的是vec3,而color是vec4类型,所以要补充最后A通道

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    std::string blueShaderfragmentSrc = R"(
    #version 330 core

    layout(location = 0) out vec4 color;

    in vec3 v_Position;

    uniform vec3 u_Color;

    void main(){
    color = vec4(u_Color, 1.0f); // 补充最后A通道
    }
    )";

  • 使用imgui对应效果图的颜色选择器

    1
    2
    3
    4
    5
    virtual void OnImgGuiRender()override {
    ImGui::Begin("Settings");
    ImGui::ColorEdit3("Square Color",glm::value_ptr(m_SquareColor));
    ImGui::End();
    }

具体代码

  • Shader父类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Shader
    {
    public:
    virtual ~Shader() = default;

    virtual void Bind() const = 0;
    virtual void UnBind() const = 0;

    static Shader* Create(const std::string& vertexSrc, const std::string& fragmentSrc);
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    namespace HEngine 
    {
    Shader* Shader::Create(const std::string& vertexSrc, const std::string& fragmentSrc) {
    switch (Renderer::GetAPI())
    {
    case RendererAPI::API::None: HE_CORE_ASSERT(false, "RendererAPI:None is currently not supported!"); return nullptr;
    case RendererAPI::API::OpenGL: return new OpenGLShader(vertexSrc, fragmentSrc);
    }
    HE_CORE_ASSERT(false, "UnKnown RendererAPI!");
    return nullptr;
    }
    }
  • OpenGlShader子类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    namespace HEngine
    {
    class OpenGLShader : public Shader
    {
    public:
    OpenGLShader(const std::string& vertexSrc, const std::string& fragmentSrc);
    virtual ~OpenGLShader();

    virtual void Bind() const override;
    virtual void Unbind() const override;

    void UploadUniformInt(const std::string& name, int value);

    void UploadUniformFloat(const std::string& name, float value);
    void UploadUniformFloat2(const std::string& name, const glm::vec2& value);
    void UploadUniformFloat3(const std::string& name, const glm::vec3& value);
    void UploadUniformFloat4(const std::string& name, const glm::vec4& value);

    void UploadUniformMat3(const std::string& name, const glm::mat3& matrix);
    void UploadUniformMat4(const std::string& name, const glm::mat4& matrix);
    private:
    uint32_t m_RendererID;
    };
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    namespace HEngine {
    OpenGLShader::OpenGLShader(const std::string& vertexSrc, const std::string& fragmentSrc)
    {
    .......
    }
    OpenGLShader::~OpenGLShader()
    {
    glDeleteProgram(m_RendererID);
    }
    void OpenGLShader::Bind() const
    {
    glUseProgram(m_RendererID);
    }
    void OpenGLShader::UnBind() const
    {
    glUseProgram(0);
    }
    void OpenGLShader::UploadUniformInt(const std::string& name, int value)
    {
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform1i(location, value);
    }
    .......
    }

Sandbox.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string flatColorShaderFragmentSrc = R"(
#version 330 core

layout(location = 0) out vec4 color;

in vec3 v_Position;

uniform vec3 u_Color;

void main()
{
color = vec4(u_Color, 1.0); //换成输入的动态color
}
)";
virtual void OnImGuiRender() override
{
ImGui::Begin("Settings");
ImGui::ColorEdit3("Square Color", glm::value_ptr(m_SquareColor));
ImGui::End();
}

二十七、宏定义智能指针

此节只是将shared_ptr与unique_ptr给与别名

代码修改

  • Core.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #pragma once
    #include <memory>
    .....
    namespace HEngine {
    template<typename T>
    using Scope = std::unique_ptr<T>;

    template<typename T>
    using Ref = std::shared_ptr<T>;
    }

    将项目中的shared_ptr变为Ref名字

应该为哪种指针

  • 前置C++知识

    指针类型释放策略离开作用域时指针是否被释放
    shared_ptr引用计数是,当引用计数为0时
    unique_ptr独占所有权,不允许共享所有权
  • 思考ExampleLayer中的Shader指针为什么应该为unique_ptr,而不是shared_ptr

    • 目前运行机制是

      C++程序上传数据给shader去渲染,这一段过程需要时间

      而电脑屏幕显示的是上一帧,显卡现在在渲染当前帧

    • 假设这个类指针指向OpenGLShader,且这个指针是unique_ptr

      1. 在Renderer中需要用到OpenGLShader上传数据给显卡上的Shader程序,这需要时间。
      2. 但是若当前Renderer的容器ExampleLayer关闭了,指针离开作用域,那么这个OpenGLShader指针指向的内存也会被释放
      3. 而Renderer中并不知道,还在使用这个指针指向的内存,就会报错。
  • ExampleLayer中

    1
    2
    3
    std::shared_ptr<HEngine::Shader> m_Shader;			// shader类 指针

    HEngine::Renderer::Submit(m_Shader, m_VertexArray); // 在Onupdate函数中
  • Renderer的Submit函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void Renderer::Submit(const std::shared_ptr<Shader>& shader, const std::shared_ptr<VertexArray>& vertexArray, glm::mat4 transform)
    {
    vertexArray->Bind();
    shader->Bind();

    std::dynamic_pointer_cast<HEngine::OpenGLShader>(shader)->UploadUniformMat4("u_ViewProjection", m_SceneData->ViewProjectionMatrix);

    std::dynamic_pointer_cast<HEngine::OpenGLShader>(shader)->UploadUniformMat4("u_Transform", transform);

    RenderCommand::DrawIndexed(vertexArray);
    }

    可见:Renderer的Submit函数中需要保持m_Shader存在才可不会报错

  • 解释:OpenGLShader指针指向的内存不会被释放

    1. 函数形参是shared_ptr引用
    2. 由于Submit的形参是引用,接受实参时,并不会增加计数,当离开作用域时候,形参也不会减少计数,所以m_Shader的内存还存在

二十八、添加纹理

  • 此节目的: 为了给图形表面赋予纹理Texture

  • 如何实现

    • 顶点属性中需要有这个顶点的UV

    • stb_img加载图片数据

    • 片段着色器根据当前片段的UV采样图片,从而得到当前片段的纹理信息

      当图形所有的片段着色器运行完,效果就是图形表面被覆上一张图片

  • 具体说明纹理

    • 是属于材质Material的一部分

    • 纹理需要被

      采样

      1. 一组顶点位置包围了一个区域
      2. 需要为这个区域上色,这些颜色可以从纹理里来
      3. 而如何获得纹理的颜色,则需要使用采样方式
    • 纹理不止可以包含颜色,还可以包含高度什么的,可以不用法线与光源计算就可以得到更逼真一点的模型。

项目引入stb

  1. stb_imgage项目拷贝stb_image.h和cpp到vendor文件夹下

  2. stb_image.cpp文件定义宏

    1
    2
    #define STB_IMAGE_IMPLEMENTATION
    #include "stb_image.h"

代码:

创建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
namespace HEngine
{
class Texture
{
public:
virtual ~Texture() = default;

virtual uint32_t GetWidth() const = 0;
virtual uint32_t GetHeight() const = 0;

virtual void Bind(uint32_t slot = 0) const = 0;
};
class Texture2D : public Texture
{
public:
static Ref<Texture2D> Create(const std::string& path);
};
}
//cpp内的定义
Ref<HEngine::Texture2D> Texture2D::Create(const std::string& path)
{
switch (Renderer::GetAPI())
{
case RendererAPI::API::None: HE_CORE_ASSERT(false, "RendererAPI::None is currently not supported!"); return nullptr;
case RendererAPI::API::OpenGL: return std::make_shared<OpenGLTexture2D>(path);
}
HE_CORE_ASSERT(false, "Unknown RendererAPI!");
return nullptr;
}

创建子类OpenGLTexture

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
namespace HEngine
{
OpenGLTexture2D::OpenGLTexture2D(const std::string& path)
:m_Path(path)
{
int width, height, channels;
// 设置垂直翻转,由于OpenGL是从上往下渲染的,所以要把图片翻转过来
stbi_set_flip_vertically_on_load(1);
stbi_uc* data = stbi_load(path.c_str(), &width, &height, &channels, 0);
HE_CORE_ASSERT(data, "Failed to load image!");
m_Width = width;
m_Height = height;

glCreateTextures(GL_TEXTURE_2D, 1, &m_RendererID);
glTextureStorage2D(m_RendererID, 1, GL_RGB8, m_Width, m_Height);
glTextureParameteri(m_RendererID, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//纹理放大时用周围颜色的平均值过滤,不然尺寸小的纹理被放到大矩阵上会很糊//
glTextureParameteri(m_RendererID, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTextureSubImage2D(m_RendererID, 0, 0, 0, m_Width, m_Height, GL_RGB, GL_UNSIGNED_BYTE, data);

stbi_image_free(data);
}
OpenGLTexture2D::~OpenGLTexture2D()
{
glDeleteTextures(1, &m_RendererID);
}
void OpenGLTexture2D::Bind(uint32_t slot) const
{
glBindTextureUnit(slot, m_RendererID);
}
}

Sandbox.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
float squareVertices[5 * 4] = {
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f //添加了纹理坐标
};
squareVB->SetLayout({
{ HEngine::ShaderDataType::Float3, "a_Position"},
{ HEngine::ShaderDataType::Float2 ,"a_TexCoord"}
});
std::string textureShaderVertexSrc = R"(
#version 330 core

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

uniform mat4 u_ViewProjection;
uniform mat4 u_Transform;

out vec2 v_TexCoord;

void main()
{
v_TexCoord = a_TexCoord;
gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);
}
)";
std::string textureShaderFragmentSrc = R"(
#version 330 core

layout(location = 0) out vec4 color;

in vec2 v_TexCoord;

uniform sampler2D u_Texture;

void main()
{
color = texture(u_Texture, v_TexCoord);
}
)";
m_TextureShader.reset(HEngine::Shader::Create(textureShaderVertexSrc, textureShaderFragmentSrc));

m_Texture = HEngine::Texture2D::Create("assets/textures/Checkerboard.png");

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

效果:

9


二十九、添加混合效果

  • 此节目的:

    添加混合效果,使用OpenGL自带的函数即可

  • 什么是混合

    两张图片有一部分叠加在一起,需要得出这重叠的部分最终的颜色。

  • 如何混合

    根据两张图片的alpha通道,由公式推出来最终颜色。

代码

OpenGLRendererAPI.cpp

1
2
3
4
5
void OpenGLRendererAPI::Init()
{
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

不同的图片通道不一样,要先判断读取的图片几个通道的图片,不然会读取颜色出错,需要修改代码

OpenGLTexture.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
GLenum internalFormat = 0, dataFormat = 0;
if (channels == 4)
{
internalFormat = GL_RGBA8;
dataFormat = GL_RGBA;
}
else if (channels == 3)
{
internalFormat = GL_RGB8;
dataFormat = GL_RGB;
}
glTextureStorage2D(m_RendererID, 1, internalFormat, m_Width, m_Height);
glTextureSubImage2D(m_RendererID, 0, 0, 0, m_Width, m_Height, dataFormat,

三十、 优化着色器文件

目前的Shader是在代码里写死的,这样写的代码会作为static常量存在内存里。但是游戏引擎里有一个常见的需求,就是对Shader的热更,比如说我更改一个Shader,我想立马在游戏里看到更改之后的Shader的效果。如果把Shader写在单独的文件里,就可以重新单独Reload和编译这个新文件。还有个问题,游戏引擎,比如Unity,里面支持在编辑器里写Shader,目前的这种写法不满足这种用户需求。

之前学习OpenGL时,一个ShaderProgram是有多个文件的,分别存放vert shader、fragment shader等,而DX是都放在一个文件里的。感觉都放一个文件里更科学一点,所以这边创建文件是下面这样的格式:

1
2
3
4
5
#type vertex				// 注意:这是自己定义的字符串里分隔Shader的方法,不是官方写法
...//写原本的vertex shader

#type fragment
...//写原本的fragment shader

然后利用ifstream来读取文件,得到string,再寻找上面的#type ...这种东西,把一个大的string,分为多个string,每个细分后的string对应一种shader。

代码

OpenGLShader.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
namespace HEngine
{
static GLenum ShaderTypeFromString(const std::string& type)
{
if (type == "vertex")
return GL_VERTEX_SHADER;
if (type == "fragment" || type == "pixel")
return GL_FRAGMENT_SHADER;

HE_CORE_ASSERT(false, "Unknown shader type!");
return 0;
}
OpenGLShader::OpenGLShader(const std::string& filepath)
{
std::string source = ReadFile(filepath);
auto shaderSources = PreProcess(source);
Complie(shaderSources);
}
std::string OpenGLShader::ReadFile(const std::string& filepath)
{
std::string result;
std::ifstream in(filepath, std::ios::in, std::ios::binary);
if (in)
{
in.seekg(0, std::ios::end);
result.resize(in.tellg());
in.seekg(0, std::ios::beg);
in.read(&result[0], result.size());
in.close();
}
else
{
HE_CORE_ERROR("Could not open file '{0}'", filepath);
}
return result;
}

std::unordered_map<GLenum, std::string> OpenGLShader::PreProcess(const std::string& source)
{
std::unordered_map<GLenum, std::string>shaderSources;

const char* typeToken = "#type";
size_t typeTokenLength = strlen(typeToken);
size_t pos = source.find(typeToken, 0);
while (pos != std::string::npos)
{
size_t eol = source.find_first_of("\r\n", pos);
HE_CORE_ASSERT(eol != std::string::npos, "Syntax error");
size_t begin = pos + typeTokenLength + 1;
std::string type = source.substr(begin, eol - begin);
HE_CORE_ASSERT(ShaderTypeFromString(type), "Invalid shader type specified");

size_t nextLinePos = source.find_first_not_of("\r\n", eol);
pos = source.find(typeToken, nextLinePos);
shaderSources[ShaderTypeFromString(type)] = source.substr(nextLinePos, pos - (nextLinePos == std::string::npos ? source.size() - 1 : nextLinePos));
}
return shaderSources;
}

Shandr.cpp

1
2
3
4
5
6
7
8
9
10
11
Shader* Shader::Create(const std::string& filepath)
{
switch (Renderer::GetAPI())
{
case RendererAPI::API::None: HE_CORE_ASSERT(false, "RendererAPI::None is currently not supported!"); return nullptr;
case RendererAPI::API::OpenGL: return new OpenGLShader(filepath);
}

HE_CORE_ASSERT(false, "Unknown RendererAPI!");
return nullptr;
}

把之前SandboxApp中shader的字符串移到一个文件里面,后缀为glsl

然后SandboxApp就只用这一句就可以了

1
m_TextureShader.reset(HEngine::Shader::Create("assets/shaders/Texture.glsl"));