引言
前面一周一直在写optix光追渲染器,并且渲出来了不错的效果。但是后面还有局部光源、disney pbr材质、mis多重重要性采样、体渲染要写,仍旧有不小的工作量,身心俱疲,所以想换个项目玩玩。
都说图形学无非就是光栅管线渲染和光追渲染两个主流技术,现在我们光追玩腻了,就决定反过来玩玩光栅管线。一开始我想从opengl和directx里选一个,做一个实时光栅渲染器的(本科的课上学了opengl,读研后导师又开了个directx的小课,所以对两个api都略有了解),但是后来想了一下,光栅渲染器最大的优势是啥?是实时,为了凸显实时,我们就要能在场景中漫游,并实现物体交互、各种动态效果,这样才有实时引擎的感觉。所以我们不仅要有渲染模块,还要尽量加入自由镜头、物理交互、动态光源、粒子等等效果,我寻思这再拓展一下编辑功能,不就是游戏引擎了吗?
所以这几天刷了下games104,又看了几节cherno的课,属实受益匪浅,理解了什么才是真正的工程。当然,这种规模的工程必不可能一蹴而就,需要慢慢打基础,然后逐层进行开发、接口对接。当然以我现在对引擎的理解水平,完全不足以自己手撕一个引擎出来,所以我准备参考cherno的系列教程来深入学习。
由于引擎开发的代码量太大,而且很多代码都没有展示并逐行解读的意义,所以这个系列我大概率不会大段大段放代码,只是记录一下每一P中cherno讲的核心理论、开发的架构和流程,当然如果某部分代码特别重要的话,也有可能顺手把代码粘贴进blog里。
P1 教程介绍
cherno:好多人给我打钱,那我就来出个系列教程吧。
P2 什么是游戏引擎
游戏引擎的定义很广泛,比如“能够快速制作出游戏的工具”,“能够给世界场景建模”,“能够堆砌所有游戏元素”。实际上游戏引擎是一种多功能开发平台,它既可以面向游戏开发人员去做游戏,也可以面向建筑公司做建筑可视化、面向产品公司做VR项目、面向实验室做仿真模拟等等。因此广义上讲,游戏引擎是可以根据各种输入,将一系列数据通过自定义的运算或交互,将结果可视化到屏幕上的应用程序平台。
所以游戏引擎的核心任务简言之就是,将一段数据处理成资产后,以另一种数据的形式展示出来,即“我不创造数据,我只是数据的搬运工”。(这也是为什么游戏引擎一般不会预置建模、绘图功能的原因)只不过要处理的数据种类有很多,例如三维模型、图像纹理、音频、序列化场景等等,所以引擎自上而下还要分很多很多的平台层来各司其职。
游戏引擎的意义和重要性就不用提了,如果没有游戏引擎,每次游戏开发人员都要从游戏的底层写起,尽管不考虑这样的成本有多高,每次做新游戏都重复相同的工作才是最不能接受的。因此游戏引擎极大化地把一些机械的、泛化的内容封装起来,从而提供游戏开发人员一个较高的开发起点。
P3 设计游戏引擎
首先先要规划一下引擎的各个模块:
1、Entry Point,即入口点,就是当我们启动游戏引擎的时候,我们需要让引擎做什么;
2、Application Layout,即应用层,用来管理或处理各种程序的生命周期、运行循环,以unity举例就比如控制每帧调用update函数,每帧进行frame渲染刷新;此外还可以管理各种事件系统、用户输入;
3、Window Layout,即窗口层,首要任务就是渲染图像。同时,他还要管理+传递在这个窗口上发生的消息和事件,从事件管理器中处理传播用户在窗口中的输入等。以unity举例就比如其中的广播系统;
4、Renderer,即渲染器,顾名思义,实现实时渲染的层。该系列教程不打算讲opengl3D渲染器的实现;
5、Render API abstract,即渲染api抽象层,因为我们的引擎可能会用到各种api,因此我们要为渲染器系统设计一个抽象层,能够对接不同的渲染api,从而使得顶层的Renderer变得api无关化。(一个漂亮的解耦)
6、Debugging system,与VS自带的调试系统不同,这里我们要自定义一个调试系统,来针对自己设计的事件系统进行监测和打印,并可以分析系统的性能;
7、Scripting layout,即脚本层,就是提供给用户来编写游戏逻辑的层,以unity举例就是它的C#脚本;
8、Memory system,即内存管理系统,越大的工程越需要良好的内存管理;
9、Entity-Component system,即ECS架构,将游戏对象的属性模块化,分离成不同的组件,用过unity的人肯定对这种架构再熟悉不过了;
10、Physics solution,即物理解算,顾名思义,要对场景中的物理行为和交互进行计算;
11、File Input/Output&VFS,即文件读写和虚拟文件系统;
12、Build system,即资产构建系统,将模型、纹理等数据转换成一种低数据冗余的格式保存在工程内,并且要易于更新、替换,资产要能够直接被游戏高效使用;
P4 建立项目
这一节就是在github上创建一个仓库,然后在vs开一个新的空白c++项目。
由于引擎需要依赖于大量的lib静态库,因此按照Cherno的规划,引擎将会编译一个dll,将全部静态依赖都链接到dll中。在用游戏引擎做出游戏后,游戏本身也会直接调用这个dll,从而链接到那成百上千个lib静态库上。这样子设计,就可以把引擎或引擎做出游戏的exe和那些乱七八糟的静态库解耦,可以自由调用外部的dll来链接那些静态库。
现在我们把当前开启的新项目作为上述的dll工程(我自己起名叫Tibbie,大家按自己的引擎名字来给dll工程起名即可)。首先我们配置一下vs项目的属性,禁用32位支持,然后把配置类型从exe改成dll;将输出目录改为$(SolutionDir)bin\$(Configuration)-$(Platform)\$(ProjectName);将中间件目录改为$(SolutionDir)bin-int\$(Configuration)-$(Platform)\$(ProjectName)
第二步我们要再创建一个exe工程SandBox。同理,先禁用32位支持,配置类型保持exe,然后输出目录和中间件目录都使用上一步的字符串即可。
保存解决方案并关闭,现在我们要改一些解决方案的设置,将SandBox设为启动项目。用vsCode打开.sln文件,将SandBox的Project放到Dll Project的前面去。
然而我发现vs2019只是换个顺序还不够,还需要右键解决方案-设置启动项目,将单启动项目下的选项改成SandBox。
重新打开解决方案,右键SandBox-添加-引用,把dll工程勾选上引用。这样当Dll工程编译出lib和dll后,SandBox将自动引用新编译出来的这些库。
第三步我们在两个项目中各创建一个src文件夹用来放源码。现在我们开始写一些测试文件,在dll工程中创建一组.h/.cpp文件:
Test.h:
#pragma once
namespace Tibbie {
__declspec(dllexport) void Print();
}
__declspec(dllexport)标识符表示的是,编译后的dll将允许该函数被外部调用。如果想要SandBox工程的exe能够调用dll中的Print函数就必须加这个标识符。
Test.cpp:
#include "Test.h"
#include <stdio.h>
namespace Tibbie {
void Print() {
printf("Hello World!\n");
}
}
写完后,我们先构建dll项目,这样就得到了我们想要的dll文件。然后我们来到SandBox项目,创建一个Application.cpp。
Application.cpp:
#include <stdio.h>
namespace Tibbie {
__declspec(dllimport) void Print();
}
void main()
{
Tibbie::Print();
getchar();
}
首先我们通过__declspec(dllimport)标识符从链接到的dll中绑定Print函数,然后在main函数中调用它。为了防止控制台一闪而过,所以补充了个getchar()。
然后我们尝试编译运行,然而会报错:找不到dll!这是因为我们还没有设置SandBox工程查找dll的路径,或者说SandBox默认只寻找当前文件夹的dll。因此我们直接把dll复制到SandBox的工程目录下即可。
至此,dll工程和exe工程就都配置完成了。
P5 引擎入口点
这一节直接把上一节的文件全删了重构,对项目重新做了下规划,文件结构如下:
这里我就不放代码了,大家可以看Cherno的原视频。先来看TibbieEngine项目,这里定义了个Application的基类来描述抽象的应用程序,里面包含Run()之类的运行方法,该类需要由客户端(sandbox项目)去继承并override。同时定义了一个CreateApplication创建方法用来创建应用程序对象,具体创建的是什么应用程序对象肯定也是交由客户端定义的,所以要把函数实现留到Sandbox里去写;
Core.h目前是用来预编译+宏定义的,比如我们将烦人的dllexport和dllimport统一定义成宏:TibbieAPI;
EntryPoint.h就是引擎入口点,main函数就在里面。在这里通过extern将CreateApplication方法传到sandbox客户端,等客户端完成了创建方法的定义后,在这里调用创建方法,从而得到一个客户端定义的对象,并执行;
TibbieEngine.h就是一个帮助客户端来链接TibbieEngine内各种库的中介,里面几乎引用了各种TibbieEngine的库,客户端直接引用该头文件就可以获取Engine的绝大部分库文件了。
最后我们的客户端SandboxApp来定义一个继承Application类的子类:Sandbox,然后完成CreateApplication创建方法的定义。
其实到这里,我基本理解了为什么要把项目分成dll(核心core)和exe(客户端client)两部分。因为对于各种游戏引擎以及各种游戏而言,他们有很多方法和成员都是完全一致的,比如都要从entrypoint启动应用程序,都要由应用程序调用run函数,都要刷新画面等等,对于这种大家都要做的通用行为,就可以将这些函数定义或基类封装到这个dll工程里,形成一个完整的通用架构;而应用程序的具体设计方案、游戏玩法等等这些互不相同的、具体落地的内容,就开放给客户端进行override。总言之就是,客户端是在把dll中那些抽象、没有具体定义的内容具象化,而最终所运行的架构流水,都是在dll中定义死的、通用的内容。
从这个例子就可以看出来,比如entrypoint内创建application这个行为,就是所有引擎和游戏都必须第一步做的通用行为,那么我们就将这段方法封装进dll中,而客户端要干的就是定义一下创建的应用程序具体是什么类对象。
P6 日志系统
这一节主要讲怎么向引擎中引入日志系统。这里使用的日志库叫做spdlog:gabime/spdlog: Fast C++ logging library. (github.com),将其引入我们的工程后先建立一个静态类<Log>,在类内为引擎核心和客户端分别分配一个静态spdlog指针,并分别配置日志属性。
#pragma once
#include <memory>
#include "Core.h"
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"
namespace Tibbie {
class TB_API Log
{
public:
static void Init();
inline static std::shared_ptr<spdlog::logger>& GetCoreLogger() { return s_CoreLogger; }
inline static std::shared_ptr<spdlog::logger>& GetClientLogger() { return s_ClientLogger; }
private:
static std::shared_ptr<spdlog::logger> s_CoreLogger;
static std::shared_ptr<spdlog::logger> s_ClientLogger;
; };
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("TBCore");
s_CoreLogger->set_level(spdlog::level::trace);
spdlog::get("TBCore")->info("loggers can be retrieved from a global registry using the spdlog::get(logger_name)");
s_ClientLogger = spdlog::stdout_color_mt("ClientAPP");
s_ClientLogger->set_level(spdlog::level::trace);
}
}
#define TB_CORE_ERROR(...) ::Tibbie::Log::GetCoreLogger()->error(__VA_ARGS__)
#define TB_CORE_WARN(...) ::Tibbie::Log::GetCoreLogger()->warn(__VA_ARGS__)
#define TB_CORE_INFO(...) ::Tibbie::Log::GetCoreLogger()->info(__VA_ARGS__)
#define TB_CORE_TRACE(...) ::Tibbie::Log::GetCoreLogger()->trace(__VA_ARGS__)
#define TB_CORE_FATAL(...) ::Tibbie::Log::GetCoreLogger()->fatal(__VA_ARGS__)
#define TB_CLIENT_ERROR(...) ::Tibbie::Log::GetClientLogger()->error(__VA_ARGS__)
#define TB_CLIENT_WARN(...) ::Tibbie::Log::GetClientLogger()->warn(__VA_ARGS__)
#define TB_CLIENT_INFO(...) ::Tibbie::Log::GetClientLogger()->info(__VA_ARGS__)
#define TB_CLIENT_TRACE(...) ::Tibbie::Log::GetClientLogger()->trace(__VA_ARGS__)
#define TB_CLIENT_FATAL(...) ::Tibbie::Log::GetClientLogger()->fatal(__VA_ARGS__)
P7 premake
教程里用的基于lua的premake,但个人还是习惯于使用Cmake,所以这节跳过
P8 计划事件系统
这一节没有代码,主要是统筹规划事件系统的大纲。
首先事件顾名思义,就是发生的特殊事情,例如点击鼠标、键盘,或者开始渲染新一帧,或者开关窗口等等。当我们检测到事件触发后,我们就应该对事件产生响应,例如当我们按下alt+F4 窗口就会关闭;当窗口发生了尺寸变化,就应该立刻改变画布大小。但是事实上,需要相应事件的并非“一层”,例如点击窗口时,不仅窗口层需要响应,gui层也要响应,因此当监听到事件后,应该逐层传递事件去依次进行响应。
首先,我们需要在应用层定义事件系统,对每一种会发生的事件定义一个专有的类;同时需要定义一个处理各种事件(例如将这些事件送入队列)用的核心函数OnEvent。接下来,应用层需要向能够监听到事件的实例(例如window窗口)派发OnEvent的函数指针,当对应实例(如窗口)监听到事件时,需要用上述函数指针来调用OnEvent,这样就把监听到的事件送入了分发队列,再传递给其他层。
这样就形成了一个架构顺序:Application定义OnEvent(event)负责应用层的事件响应,然后将OnEvent函数指针派发给Window;Window负责监听发生的事件ins_event,并使用上述函数指针 调用OnEvent(ins_event)完成应用层的响应。等应用层处理完ins_event后,再调用下一层(GUI)的OnEvent(ins_event)去响应。
P9 事件系统实现
这一节首先定义了所有事件类型Event Type和事件目录Event Category的枚举,事件类型指的是各种可能会触发的事件条目,例如窗口尺寸改变、键盘按下、应用程序渲染等等;事件目录主要是将同类型的事件合并为一个大的目录中方便管理。
接下来定义事件系统的抽象类Event,纯虚函数包括 获取事件类型、获取事件名称、获取事件目录等,然后再定义一个检查是否在某目录中的检测函数;
有了抽象类Event,我们就可以通过继承Event来定义各种具体的事件类了,这节主要定义了应用程序事件(窗口尺寸改变、窗口关闭、应用程序逐帧响应、应用程序更新、应用程序提交渲染)、键盘事件(键盘基础事件类,以及派生出来的键盘按下、键盘松开、键盘类型)和鼠标事件(鼠标移动、滚轮事件、按键事件,其中按键事件可以派生出按下按键和松开按键两个子类)。
最后定义一个Event的Dispatcher(调度器)用来“监护并调度”某个事件对象,其中的Dispatch函数输入了一个func,然后Dispatcher会检查自己所监护的事件对象是否与目标func中事件的类型一致,如果一致就执行这个func。但是这一节还没有看到func是什么样的、谁会调用这个Dispatch函数,所以暂时不需要去理解它的用处。
P10 预编译头文件
这一节主要就是将我们所引用的各种外部库放到预编译头中提前编译,这样可以节省下来更多的编译时间。首先创建tbpch.h来引用所有需要的库,然后创建tbpch.cpp并设置成“创建预编译头”类型。最后将整个项目的“使用预编译头”指向tbpch.h。
#pragma once
#include <iostream>
#include <memory>
#include <utility>
#include <functional>
#include <sstream>
#include <string>
#include <vector>
#ifdef TB_PLATFORM_WINDOWS
#include <Windows.h>
#endif // TB_PLATFORM_WINDOWS
P11 窗口抽象与glfw
这一节开始引入glfw并实现最基础的窗口。首先创建了一个Window类,定义了窗体的基础属性以及一些常用的窗体方法,但是考虑到跨平台的因素,Window类只是一个抽象类,需要后面定义不同平台下继承该Window所形成的 对应平台的窗口类。这里我们先只定义Windows平台下使用opengl的情况,首先定义了一个WindowsWindow类来继承Window抽象类,然后分别在Init函数中创建窗口、OnUpdate函数中处理窗口事件+交换双缓冲、Shutdown函数中处理关闭窗口事件、SetVSync函数中设置垂直同步。具体opengl的api就不解读了,大部分和我们之前在opengl渲染器中介绍的api相重合(毕竟都用的是glfw)
在应用程序类中,我们可以在构造函数中创建一个窗口,然后在run函数中不断地调用窗口的OnUpdate函数,这样就完成了应用程序创建并控制窗口的过程了。
芜湖,现在窗口就创建出来啦!
P12 窗口事件与回调
这一节就是在复现P8所描述的事情。首先我们在Application类的构造函数中,将OnEvent的函数指针绑定给Window类的回调指针上(用的是std::bind方法),Window通过glfw来监听窗口上发生的事情。当发生了鼠标按下、移动、滚轮,键盘按下、松开等等事件后,定义一下对应类型的Event对象,然后使用回调函数指针来调用OnEvent函数,参数就是对应类型的Event对象。
先简单在OnEvent函数中print事件的逻辑,如下图所示:
这里我们的Window已经成功监听到了glfw窗口中的事件,并成功通过函数指针调用了OnEvent从而将对应事件输出到了控制台中。
那么回归正题,我们在OnEvent函数中真正要做的事情是分发Dispatch事件,即识别事件类型并根据类型去调用实际的响应函数。首先我们定义一个分发器Dispatcher来绑定该事件,分发器的分发逻辑是这样的:
bool Dispatch(EventFn<T> func)
{
if (m_Event.GetEventType() == T::GetStaticType())
{
m_Event.Handled |= func(*(T*)&m_Event);
return true;
}
return false;
}
简单说就是,分发器要判断当前绑定的事件是否匹配某个响应函数,只需要看这个响应函数所响应事件的类型T是否与当前所管理的事件类型所一致。如果一致,那么就允许调用对应的响应函数,并为该事件做上Handled记号。
举个例子,如果我们要响应关闭窗口行为,那么在OnEvent可以这样写:
void Application::OnEvent(Event& e) {
EventDispatcher dispatcher(e);
dispatcher.Dispatch<WindowCloseEvent>(std::bind(&Application::OnWindowClose, this, std::placeholders::_1));
TB_CORE_INFO(e.ToString());
}
bool Application::OnWindowClose(WindowCloseEvent& e) {
m_running = false;
}
首先我们定义了关闭窗口的具体相应逻辑OnWindowClose,然后我们在OnEvent中处理分发逻辑,实现方法就是用分发器Dispatcher去检测当前绑定的事件e是否适配WindowCloseEvent类型,如果不适配,那么就不响应;适配就响应。
P13 层级
如P3所讲解,我们需要将游戏引擎分为很多个层,每一层有一个独立的抽象;针对于渲染、命令等事件,需要按照层的顺序进行逐层传递。这一节主要就是定义层的概念,并定义用于梳理层级关系的层堆栈。
Layer层具体的定义比较简单,成员变量只有一个Name,成员方法主要就是附着层、取消附着层、刷新层、层事件效应OnEvent;
LayerStack其实就是将各个Layer排列起来的通道(栈),所以我们需要维护这个栈的迭代器(正向迭代和反向迭代),以及对应的入栈弹出逻辑。不过这里的Layer栈分为了两部分,前一部分是普通层,处于bgein()~begin()+m_LayerInsertIndex;后一部分是叠加层,处于begin()+m_LayerInsertIndex~end(),具体为什么分成普通层和叠加层目前还没解释。
接下来我们在Application中定义一个LayerStack来管理所有层,并定义了PushLayer的方法;然后在run函数中循环调用每个层的Update函数。然后我们回到SandBox项目,定义一个实例层叫testLayer,调用PushLayer方法将它加入到Stack中,就可以看到如下结果了:
基本就搞定了,虽然目前的层级还没有接收具体的Event。
P14 现代Opengl与Glad
这一节就是配置Glad了,之前在学习learnOpengl的时候配过一次了就不多言了。唯一需要提示的是,我们之前默认让所有.c文件都使用tbpch.h预编译头,对于glad.c文件我们需要取消使用预编译头。
P15 整合ImGUI
这一节主要是引入了ImGui库ocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies (github.com),并且创建了ImGui对应的层。
在本例中,我们使用ImGui_ImplOpenGL3作为opengl的imgui库参考,对于OnAttach函数主要进行imgui的初始化、上下文定义、风格定义,对于OnUpdate函数主要进行每帧imgui控件的绘制。定义完了ImGui层后,在SandBox的应用程序中将ImGui层添加到LayerStack中去,从而每帧调用该层的OnUpdate函数完成绘制。这一节大部分都是ImGui的各种api调用,不多赘述。
唯一要注意的是,ImGui的绘制事件应该在刷新画布之后、窗口绘制之前,这样才能顺利显示在窗口上。
最后效果:
但是现在的ImGui不能点击交互。
P16 ImGui事件
这一节主要是做ImGui的事件系统。首先应用层的OnEvent需要额外做一件事,就是将收到的Event传播给其他层的OnEvent函数中,这样其他层也可以对收到的事件进行响应。
然后我们就开始定义ImGui层的事件,包括鼠标点击、滚轮、键盘按下等等事件,然后在OnEvent函数中进行dispatch分发到各个响应函数,响应函数就是直接对ImGuiIO进行赋值从而模拟ImGui控件的交互:
P17 P18 Github相关
这个跟项目关系不大,直接跳过
P19 输入循环
现在我们的输入仍然存在一个问题,举个例子,我希望点击鼠标左键是移动,按住Alt时点击鼠标左键是旋转,这意味着Application等层需要主动调用glfw的窗口api来检查某一按键是否已经按下。但是前面提到过,我们不希望在这些层中出现glfw相关的api,因为从应用层开始我们需要抽象api,所以我们就必须定义一套抽象的Input输入系统,当Application等层需要检查某些按键是否按下时,由这个抽象的Input来提供;而Input的具体实现则是根据api和操作系统而异,各自实现各自的提供按键检测的方法。这样我们就保持了上层的抽象性。
首先定义一个Input抽象类,它的作用就是检测键鼠输入并管理输入结果。然后根据操作系统和api继承出来一个Window平台+opengl的WindowsInput类来检测glfw窗口中的输入行为。具体代码依旧是事件回调里处理输入的那套东西。
P20 键盘鼠标代码
上一节定义了键鼠的Input系统,不过还是存在一个问题:例如我现在想知道Alt键是否被按下,那我必须要先知道Alt的键值,然后才能去和glfw窗口中获得的按键键值去配对比较,所以我们这节要连带键盘输入和鼠标输入的键值一起定义完全,创建一个KeyCodes.h和MouseCodes.h来存储所有键值映射(这部分代码都可以从glfw或imgui中复制)。有了这个后,我们的Input系统便可以快速将用户想要的按键翻译成键值去做匹配了。
(说句实话,我感觉这一节的意义并不是特别明显,只是多做了一层封装,属于可做可不做的内容)
P21 数学
因为之后要进入渲染章节了,所以需要提前把glm数学库配置好。然后这一节基本就没有开发内容了。
P22 ImGui修正与更新
这一节主要是对ImGui部分代码的一个维护更新,主要分为两部分:
1、Cherno发现ImGui的源文件中的imgui_impl_glfw.h/.cpp其实包含了处理ImGui控件在glfw窗口中输入的例子,囊括了处理鼠标输入、键盘输入来交互ImGui控件的方法。所以我们在P16中自己定义的ImGui事件处理函数基本都是白写了。将这部分内容删除,改为追加调用imgui_impl_glfw源文件;
2、对于ImGui控件,我们希望它可以在客户端中被override,让用户决定它如何渲染、要渲染哪些控件。但是OnUpdate里关于初始化画布、执行渲染的方法都是定死的,为了避免让用户定义这些定死的内容,我们可以将OnUpdate方法拆分成begin函数(负责画布初始化)、OnImGuiRender函数(交给用户override要渲染的控件)和end函数(执行渲染)。其中还Begin和End函数都是内置定死的函数,只有OnImGuiRender函数可以交给用户自定义。
同时,我们每一层可能都涉及ImGui的渲染,例如物理层有物理层的ImGui,图形层有图形层的ImGui,所以对于OnImGuiRender这个函数,我们需要在Layer中将它定义成虚函数,供所有层去override。
做完这步后,我们要对Application做一点修改,将ImGuiLayer绑定为LayerStack外的一个独立的层,这样Application便可以先调用其Begin函数来初始化ImGui画布,再调用各个层的OnImGuiRender函数来完成各个层的控件渲染,最后调用End函数来执行渲染。言外之意,ImGuiLayer是内置定死的一个层,有且只有一个ImGuiLayer层。
现在ImGui部分的架构就比较成熟了。
接下来将要进入到Opengl渲染环节了,不过我的计划并不是跟着Cherno一点一点学Opengl,而是去学一下LearnOpengl,等到搓出一个简单的实时渲染器后再进行Cherno下一章的学习。
后记1. 架构总结
引擎分为两部分:Dll和Exe两个工程,Dll负责完成所有基础定义,有些需要交给用户自由定义的方法外放给Exe项目完成。
层级:目前只有一层ImGui层,但是同时有两个“抽象层”分别是应用层和窗口层。①关于事件:窗口层负责监听窗口事件,并将事件回调给应用层,应用层再分发传递给UI层;②关于绘制:应用层负责控制各个层的刷新,先刷新各个层的图形(目前这部分还没写),再刷新UI层的绘制,最后是窗口层交换双缓冲完成渲染。因为目前只明确定义了一个UI层所以层级关系不是那么明显。
抽象掉的东西:①窗口类,opengl、dx和vulkan都有不同的实现窗口和窗口回调机制;②UI,imGui为不同的图形api提供了不同的渲染方法;③Input,不同api的获取用户输入的api不同。
后记2. C++技巧总结
1、子类使用父类默认的构造函数:
class TestLayer : Tibbie::Layer{
public:
TestLayer(): Layer("Test"){}
2、通过预编译头来决定宏定义的内容:
#ifdef TB_PLATFORM_WINDOWS
#ifdef TB_BUILD_DLL
#define TB_API __declspec(dllexport)
#else
#define TB_API __declspec(dllimport)
#endif
#endif
3、绑定某个类对象中的成员函数指针:
std::bind(&Application::OnEvent, this, std::placeholders::_1)
4、导出某个未定义的函数声明,由外部调用该dll的应用程序完成定义:
dll:
extern Tibbie::Application* Tibbie::CreateApplication();
exe:
Tibbie::Application* Tibbie::CreateApplication() {
return new Sandbox();
}
5、静态类,以及对应的内联静态函数:
class TB_API Log
{
public:
static void Init();
inline static std::shared_ptr<spdlog::logger>& GetCoreLogger() { return s_CoreLogger; }
inline static std::shared_ptr<spdlog::logger>& GetClientLogger() { return s_ClientLogger; }
private:
static std::shared_ptr<spdlog::logger> s_CoreLogger;
static std::shared_ptr<spdlog::logger> s_ClientLogger;
; };
6、非静态类的静态函数 + 宏定义多个函数:
#define EVENT_CLASS_TYPE(type) static EventType GetStaticType() { return EventType::type; }\
virtual EventType GetEventType() const override { return GetStaticType(); }\
virtual const char* GetName() const override { return #type; }
Comments | NOTHING