HEngine游戏引擎(01-30)
1-15是些基础工作、如项目构建、添加子模块、使引擎具有日志系统、事件系统,可视化窗口等基础功能
16开始是游戏引擎最核心的部分-Rendering,使引擎能够真正绘制出图形,如果想直接看这部分点此链接跳转
一、 项目设置
新建HEngine和Sandbox项目,HEngine项目生成为dll,Sandbox项目生成为exe,运行此exe通过动态链接HEngine的dll,可以调用dll定义的函数并输出信息。
1.调整输出目录和中间的目录
1 | $(SolutionDir)\bin\$(Configuration)-$(Platform)\$(ProjectName)\ |
生成的文件在bin目录下,生成的intermediate文件在bin-int目录下,大概是这么个目录结构
1 | bin/Debug-x64/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
8namespace HEngine
{
class HENGINE_API Application{
....
};
// To be defined in CLIENT
Application* CreateApplication();
}在HEngine命名空间内声明了CreateApplication函数
在Sandbox项目的SandboxApp.cpp中
1
2
3
4HEngine::Application* HEngine::CreateApplication()
{
return new Sandbox();
}定义了CreateApplication函数
在EntryPoint.h中调用
1
2
3
4
5
6extern 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
在Core.h中
1
2
3
4
5
6
7
8
9
10
根据条件编译定义HEngine_API是dll导入还是导出,HEngine项目将是declspec(dllexport),Sandbox项目是declspec(dllimport)
程序运行流程
EntryPoint.h定义了main函数,即写了入口点,所以程序会在这运行
1
2
3
4
5
6
7
8
9
10
extern HEngine::Application* HEngine::CreateApplication();
int main()
{
auto app = HEngine::CreateApplication();
app->Run();
delete app;
}main函数中执行CreateApplication函数,将调用定义在SandboxApp.cpp中的CreateApplication函数
1
2
3
4HEngine::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
13namespace HEngine
{
Application::Application()
{
}
Application::~Application()
{
}
void Application::Run()
{
while (true);
}
}
三、 日志系统 + Premake
主要是以下几个任务
- 创建Log类,然后有s_CoreLogger和s_ClientLogger,分别处理引擎的log和client的log
- 使用spdlog,具体主要是怎么利用git submodule使用该库
- 使用宏来封装对应的log函数,使用宏可以更好的方便不同平台的应用
- 用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 | namespace HEngine |
额外的宏操作,这里的宏的写法可以实现函数的宏,代码如下所示:
1 | inline static std::shared_ptr<spdlog::logger>& GetCoreLogger() { return s_CoreLogger; } |
Git删除子模块
因添加子模块时候,写错代码,导致子模块添加路径错误,需要删除子模块并重新添加。而删除子模块是个稍微麻烦的事,网上查阅后,在此记录一下
1. 删除submodule缓存
需要先暂存 .gitmodules 文件, 否则会报错: fatal: please stage your changes to .gitmodules or stash them to proceed
1 | git add .gitmodules |
若报什么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
的设计了,首先需要定义的就是Event
和EventType
类,这里把Event
作为基类,EventType
是enum class,包含了基本的外设事件,如下所示:
1 | enum class EventType |
对于Event
类型,作为基类,那么最基本的两个接口应该为:
- 获取该事件的类型
- 获取该事件的名字
作为基类,这都是最基本的API,再者,为了方便使用,仿照C#的方式,C#语言里所有的Object都有一个ToString函数,方便我们打印一些消息,所以这个API我们也把它加入到Event
基类里,如下所示:
1 | class HENGINE_API Event |
目前就是这样,然后我们还需要一个EventCategory
枚举,以后用flag来快速筛选特定的Event
:
1 |
|
定义Event这个基类,就可以着手创建对应的子类的,拿鼠标事件举例,一共有MouseMoved
、MouseButtonPressed
、MouseButtonReleased
、MouseButtonScrolled
四种,那么我就建立四个子类,全放在MouseEvent.h
文件下,拿MouseMoved
举例,其类型为之前枚举定义的MouseMovedEvent
,其ToString应该是打印出鼠标移动的offset值,由于该类的所有Event
类型都是一样的,所以我们可以用一个static变量去存储该类型就够了:
1 | class MouseMovedEvent : public Event |
OK,写完了这个类,就可以继续写类似的鼠标事件的类了,但是我发现有一些代码都是非常类似的,写起来很麻烦,也很影响阅读:
1 | class MouseMovedEvent : public Event |
所以我学到了一个方法,用宏去代替我们编写这么长的语句,这个宏名就叫做EVENT_CLASS_TYPE(typename)
,来为我们生成对应的函数,通过#
和##
符号,可以达到这种效果,一个#
是转换成字符串,两个#
是原语句替换,所以就是这么简化:
1 | // 用于简化代码, 因为很多类都有着相同的函数 |
就像这样,我们可以把所有鼠标事件的类定义好,接下来还需要的定义的输入类就是Window Event、ApplicationEvent和KeyEvent,先说前两种Event,最后着重提一下KeyEvent,键盘事件的输入处理并不像点击鼠标那么简单,通常(简单的事件系统里)我们是没有长按鼠标的操作的,但是却有长按键盘的操作,当我们按键盘时,会先打印一个字母,然后停顿一下,如果这个时候还按着按钮,就继续打印剩余的字母。
所以说,按键的时候,第一次会立马打印第一个字母,然后需要记录我按的次数(或者记录按的时间),当记录的值达到一定阈值(或时间)时,才会继续不停打印接下来的字母,这里我们不用时间记录,而是用一个int值,记录按相同键的次数。
设计KeyEvent类的时候,可以发现,KeyPressedEvent
会比KeyReleasedEvent
的数据多一个,前者会额外记录按下Key时,key走过的Loop的总数,所以这个时候可以设计一个基类叫做KeyEvent,这里放通用的数据,就是Key的keycode,用于存放Key类型共有的内容,设计思路如下所示:
1 | class HENGINE_API KeyEvent : public Event |
然后再写对应的子类
1 | class HENGINE_API KeyPressedEvent : public KeyEvent |
五、 预编译头文件
为了避免头文件被反复编译,需要加上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 | pchheader "hepch.h" |
第二个是,如果勾选了使用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
20IncludeDir = {}
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
28namespace 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
22namespace 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
53namespace 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
16namespace 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 | static void GLFWErrorCallback(int error, const char* description) |
Application.cpp
1 |
|
其中,EventDispatcher
用于根据事件类型的不同,调用不同的函数:
1 | // 当收到Event时,创建对应的EventDispatcher |
遇到的问题:
编译时报错显示error C2338: Cannot format argument,初步判定是跟spdlog有关,经过排查发现问题出在这里
1 | void Application::OnEvent(Event& e) |
是spdlog无法识别Event这个类型,需要我们自定义一个关于Event的模版,在Log.h最上方加入以下代码即可
1 |
|
八、 设计游戏的层级框架
设计完Window和Event之后,需要创建Layer类
Layer的理解
想象同Ps中一张图有多个层级,每个层级都可以绘制不同的画面,最后合在一起展现出图片最终的样子
Layer的设计
数据结构:vector
渲染顺序
从前往后渲染各个层的图像,这样后面渲染的会覆盖前面渲染的图像,在屏幕的最顶层。
处理事件顺序
从后往前依次处理事件,当一个事件被一个层处理完不会传递给前一个层,结合渲染顺序,这样在屏幕最顶层的(也就是在vector最后的layer)图像最先处理事件。
例子解释
比如常见的3D游戏有UI。
渲染顺序:将3D图形先渲染,再渲染2DUI,这样屏幕上2DUI永远在3D图形上方,显示正确;
事件顺序:点击屏幕的图形,应该是2DUI最先处理,如果是相应UI事件,处理完后不传递给前一个3D层,若不是自己的UI事件,才传递给前一个3D层。
项目相关:
Layer接口设计如下
1 | class HENGINE_API Layer |
LayerStack.h
1 | class HENGINE_API LayerStack |
Application.cpp
1 | void Application::PushLayer(Layer* layer) |
SandboxApp.cpp
1 | class ExampleLayer : public HEngine::Layer |
九、 添加GLAD
为了使用OpenGL,而OpenGL的函数定义在显卡中,大多数函数的定义位置都无法在编译时确定下来,所以需要在运行时查询,需要使用GLAD库在运行时获取OpenGL函数地址并将其保存在函数指针中供程序运行时使用。
可以使用glew,也可以使用glad库,二者的在效率上好像没啥区别,不过glad的库要更新一些,所以这里用glad库,具体步骤:
网站上下载的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.cpp
和imgui_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 | namespace HEngine |
ImGuiLayer.cpp
1 |
|
然后在SandboxApp.cpp添加这一层就可以了
1 | class SandBox : public HEngine::Application |
十一、 给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
15class 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
42namespace 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
19namespace 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
11namespace 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
34namespace 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
9void 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,如下图所示:
为了保证效率和跨平台的能力,这里使用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 | class HENGINE_API Application |
为了让每一个Layer都有一个ImGuiLayer,让每一个Layer都继承一个接口,用于绘制ImGui的内容,同时让ImGuiLayer成为HEngine内在的部分,需要在Application里面加上对应的LayerStack,与其内部的Layer一一对应,设计思路如下:
1 | class Layer |
然后在Application里,先调用Layer的正常函数,再调用其ImGuiRender函数,如下所示:
1 | while (m_Running) |
遇到的问题
在完成上述功能后,就可以把ImGui对应的窗口任意拖拽了,但为了在SandboxApp展示的窗口,也就是原始的Windows的粉色窗口上绘制对应的内容,需要在ExampleLayer里的OnImGuiRender里进行绘制,代码如下所示:
Sandbox.cpp
1 | include "imgui.h" |
然而运行后,会报错,如下所示:
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 | defines |
十五、 HEngine改为静态库
dll
- 优点
- 热更新,更改引擎代码后只需重新编译dll,让多个测试项目不用重新编译能使用最新引擎代码
- 让客户端的链接更容易
- 缺点
- dll很多警告
- exe动态链接dll启动速度慢
- 优点
lib
所有链接都构建到exe文件中
需要考虑dll的优点在使用引擎的角度,引擎代码已经完成了就不需要热更新了,dll的热更新优点没有了
如何将引擎从dll改为lib
这里是用premake构建的工程,直接在premake5.lua文件里进行修改HEngine工程的类型就可以了。
原来的HEngine的premake部分内容如下:
1 | project "HEngine" |
修改之后变为:
1 | project "HEngine" |
同时,把之前的dllexport和dllimport的宏注释掉就行了,如下所示:
1 |
十六、 渲染前准备工作
这里开始我们就要开始激动人心的渲染部分啦,我们选择使用OpenGL来开始工作,因为它是较为简单和容易的图形库。我们用抽象类封装渲染图形API的Context,后面可以根据不同渲染API,设置不同渲染的上下文。
代码
GraphicsContext.h
1
2
3
4
5
6
7
8
9
10
11
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
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
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
6class WindowsWindow : public Window{
.....
private:
GLFWwindow* m_Window;
GraphicsContext* m_Context;
.....WindowsWindow.cpp
1
2
3
4
5
6
7
8
9
10
11void 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
....
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);
效果
OpenGLContext
1 | void OpenGLContext::Init() |
Renderging负责在屏幕上的绘制工作,同时接受与外部Input的交互,为了表现更好的画面效果,需要使用Graphics Processing Unit(GPU),GPU的主要优点是:能并行处理、能很快的进行数学运算
对于一些电脑,可能这里不会默认使用独显,比如N卡可以在NVDIA ControlPanel里选择这个exe使用高性能的GPU处理器,如下图所示
十八、 添加着色器
此节目的
使用shader,让渲染的三角形有颜色,并且将关于shader的代码抽象到Shader类中
关于Shader
- 告诉GPU如何处理我们从CPU发送到GPU的顶点数据
- 着色器(Shader)是运行在GPU上的小程序,分别对应渲染管理不同阶段。
- 着色器是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
代码
增加Shader类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
73namespace 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
38Application::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
....
)"
效果
十九、 封装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
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
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
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
25namespace 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
28namespace 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
47namespace 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 | // Shdader数据类型 |
OpenGLBuffer.h
1 | public: |
Application.cpp
1 | static GLenum ShaderDataTypeToOpenGLBaseType(ShaderDataType type) |
效果:
二十一、添加顶点数组
OpenGL里的VAO,其实本身不包含任何Buffer的数据,它只是记录了Vertex Buffer和IndexBuffer的引用,并且使用glVertexAttribPointer
函数来决定VAO通过哪种方式来挖取 VBO中的数据。
这节目的是创建Vertex Array类,由于OpenGL有VAO这个东西,而DX里完全没有这个概念,但是前期的HEngine引擎是极大程度依赖OpenGL的,所以目前是先创建VertexArray类
Application.cpp
1 | // 1. 创建VertexArray,这一段还没抽象化 |
接下来就是正式开始写代码了。
前面两句代码要把它抽象化,也就是把它变成跟平台无关的东西,跟之前创建的VertexBuffer和IndexBuffer都差不多
1 | glGenVertexArrays(1, &m_VertexArray); |
这里单独建一个VertexArray
的cpp和h文件,之所以单独建立cpp和h文件,是因为还不确定相关的VertexArray的内容以后还会不会会保留,毕竟DX里是没有这个概念的
VertexArray.h
1 | // 直接复制VertexBuffer的相关内容就行: |
VertexArray.cpp
1 | namespace HEngine |
接下来就是创建OpenGLVertexArray的头文件和cpp文件了,放到Platform的文件夹里,实现过程跟OpenGLVertexBuffer差不多
OpenGLVertexArray.cpp
1 | void OpenGLVertexArray::AddVertexBuffer(const std::shared_ptr<VertexBuffer>& vertexBuffer) |
多个VBO来验证
做到这里其实就差不多了,但是为了验证之前做的是正确的,创建了一个应用场景,就是通过使用两个Shader,一个VAO,两个VBO,一个EBO来绘制出来
创建第二个VertexArray
前面画了个三角形,下面再画一个Quad,在Application类里创建:
1 | std::shared_ptr<VertexArray> m_QuadVertexArray; |
然后按照同样的方式,创建VAO、VBO和顶点数据,再创建一个Shader,这里做一个只输出蓝色的Shader,就可以了,最后在Loop里分别绑定VBO和Shader就可以了:
1 | float squareVertices[3 * 4] = { |
效果
二十二、设计渲染器架构
从无到有,绘制出了三角形,然后把相关的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 | // 在Render Run里 |
本节要做的实际内容
上面虽然介绍完了渲染架构,大体上是统一处理物体,然后统一渲染,但是由于目前相关的架构还没搭起来,所以这仍然是Bind一个VAO,然后调用一次DrawCall,以后会改进的。
目前就剩glClear、glClearColor和DrawCall的代码需要抽象化了,也就是这三句:
1 | glClearColor(0.1f, 0.1f, 0.1f, 1); |
这些代码,打算把它抽象为:
1 | // 这个ClearColor是游戏最底层的颜色,一般不会出现在用户界面里,用洋红色这种offensive的颜色比较好 |
然后对于原本的Renderer里的GetAPI函数,它应该由RendererAPI负责, 而不是Renderer负责,这里新建RendererAPI类,除了要有标识当前使用的API类型的函数外,还需要有很多与平台无关的渲染的API,比如清空Buffer、根据Vertex Array进行调用DrawCall等函数
RendererAPI.h
1 | namespace HEngine |
RendererAPI是一个接口类,与平台无关,现在就可以实现OpenGL平台的OpenGLRendererAPI了,Platform文件夹下创建对应的cpp和h文件,跟之前的做法类似,不多说。
OpenGLRendererAPI.cpp
1 | namespace HEngine |
接下来,就是实现Renderer类了,目前这个类只实现了一个GetAPIType函数,类的声明如下:
1 | class Renderer |
后面会再去实现成员函数,现在先把类都声明好,还剩一个RenderCommand类了,同样创建一个RenderCommand.h头文件
1 | class RenderCommand |
可以看到,RenderCommand类只是把RendererAPI的内容,做了一个静态的封装,这样做是为了以后把函数加入到RenderCommandQueue里做的架构设计,也是为了后面的多线程渲染做铺垫。
然后在Renderer的submit函数里实现下面的内容即可:
1 | void Renderer::Submit(const std::shared_ptr<VertexArray>& vertexArray) |
到此我们渲染框架就大致完成了,Application里已经没有OpenGL原生函数就可以完成绘制了
Application.cpp
1 | void Application::Run() |
二十三、正交摄像机实现
先介绍一些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
19class 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
12class 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
10Renderer::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
4void 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
33Application::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变为长方形
修复变回正方形
在1280720下,left right需传入1280/720=1.7左右,将宽放大,从而左右视角变大,物体围成的宽范围*缩小,从而变回正方形。
1
2Application::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
41 0 0 0
0 1 0 0
0 0 -1 0
0 0 0 1glm::translate(glm::mat4(1.0f), m_Position);
m_Position= {0.5f, 0.5f, 0.5f};
glm::mat4(1.0f),是4x4的单位矩阵
1
2
3
41 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1glm::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 | // ============== Timestep.cpp ============= |
把Application.cpp里的内容移到Sandbox对应的Project
Application
类应该主要负责进行While循环,在里面调用各个Layer的Update函数,而SandboxApp
类虽然继承于Application
类,但是也只是个大致的空壳而已,它的存在是为了把new出来的ExampleLayer加入到继承来的m_LayerStack
里,具体的绘制Quad和Triangle的的操作应该放到Sandbox对应Project的Layer里,大概是这样:
1 |
|
二十五、矩阵位置、旋转、缩放
目前的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 | // 在渲染Vertex Array的时候, 添加对应model的transform对应的矩阵信息 |
然后Sandbox.cpp中修改
1 | void OnUpdate(HEngine::Timestep ts) override |
效果
二十六、着色器封装
此节目的
完成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
14std::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
5virtual 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
10class 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
12namespace 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
24namespace 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
24namespace 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 | std::string flatColorShaderFragmentSrc = R"( |
二十七、宏定义智能指针
此节只是将shared_ptr与unique_ptr给与别名
代码修改
Core.h
1
2
3
4
5
6
7
8
9
10
.....
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
- 在Renderer中需要用到OpenGLShader上传数据给显卡上的Shader程序,这需要时间。
- 但是若当前Renderer的容器ExampleLayer关闭了,指针离开作用域,那么这个OpenGLShader指针指向的内存也会被释放
- 而Renderer中并不知道,还在使用这个指针指向的内存,就会报错。
ExampleLayer中
1
2
3std::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
11void 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指针指向的内存不会被释放
- 函数形参是shared_ptr引用
- 由于Submit的形参是引用,接受实参时,并不会增加计数,当离开作用域时候,形参也不会减少计数,所以m_Shader的内存还存在
二十八、添加纹理
此节目的: 为了给图形表面赋予纹理Texture
如何实现
顶点属性中需要有这个顶点的UV
stb_img加载图片数据
片段着色器根据当前片段的UV采样图片,从而得到当前片段的纹理信息
当图形所有的片段着色器运行完,效果就是图形表面被覆上一张图片
具体说明纹理
是属于材质Material的一部分
纹理需要被
采样
- 一组顶点位置包围了一个区域
- 需要为这个区域上色,这些颜色可以从纹理里来
- 而如何获得纹理的颜色,则需要使用采样方式
纹理不止可以包含颜色,还可以包含高度什么的,可以不用法线与光源计算就可以得到更逼真一点的模型。
项目引入stb
从stb_imgage项目拷贝stb_image.h和cpp到vendor文件夹下
stb_image.cpp文件定义宏
1
2
代码:
创建Texture类
1 | namespace HEngine |
创建子类OpenGLTexture
1 | namespace HEngine |
Sandbox.cpp添加图片纹理
1 | float squareVertices[5 * 4] = { |
效果:
二十九、添加混合效果
此节目的:
添加混合效果,使用OpenGL自带的函数即可
什么是混合
两张图片有一部分叠加在一起,需要得出这重叠的部分最终的颜色。
如何混合
根据两张图片的alpha通道,由公式推出来最终颜色。
代码
OpenGLRendererAPI.cpp
1 | void OpenGLRendererAPI::Init() |
不同的图片通道不一样,要先判断读取的图片几个通道的图片,不然会读取颜色出错,需要修改代码
OpenGLTexture.cpp
1 | GLenum internalFormat = 0, dataFormat = 0; |
三十、 优化着色器文件
目前的Shader是在代码里写死的,这样写的代码会作为static常量存在内存里。但是游戏引擎里有一个常见的需求,就是对Shader的热更,比如说我更改一个Shader,我想立马在游戏里看到更改之后的Shader的效果。如果把Shader写在单独的文件里,就可以重新单独Reload和编译这个新文件。还有个问题,游戏引擎,比如Unity,里面支持在编辑器里写Shader,目前的这种写法不满足这种用户需求。
之前学习OpenGL时,一个ShaderProgram是有多个文件的,分别存放vert shader、fragment shader等,而DX是都放在一个文件里的。感觉都放一个文件里更科学一点,所以这边创建文件是下面这样的格式:
1 | #type vertex // 注意:这是自己定义的字符串里分隔Shader的方法,不是官方写法 |
然后利用ifstream
来读取文件,得到string,再寻找上面的#type ...
这种东西,把一个大的string,分为多个string,每个细分后的string对应一种shader。
代码
OpenGLShader.cpp
1 | namespace HEngine |
Shandr.cpp
1 | Shader* Shader::Create(const std::string& filepath) |
把之前SandboxApp中shader的字符串移到一个文件里面,后缀为glsl
然后SandboxApp就只用这一句就可以了
1 | m_TextureShader.reset(HEngine::Shader::Create("assets/shaders/Texture.glsl")); |