Hazel引擎学习(八)

我自己维护引擎的github地址在这里,里面加了不少注释,有需要的可以看看

参考视频链接在这里


Testing Hazel’s Performance

这节课主要是对当前的Hazel游戏引擎进行性能测试,拿了网友的一个Demo进行测试,整体来说,性能还不错。测试结果显示,对于Hazel引擎,Release下性能的大概是Debug下的十倍,这节课重点有:

  • 学会用Visual Studio自带的性能分析工具
  • 基于VS的性能分析工具,知道当前最大CPU用在了哪里,当前引擎的性能lagging点在哪
  • IMGUI在引擎里的用途

学会用Visual Studio自带的性能分析工具

参考:https://www.youtube.com/watch?v=X1-uHpEqNGM&ab_channel=MicrosoftVisualStudio

打开对应的项目,点击Debug->Performance Profiler,或者按Alt+F2,会出现下面的窗口:
在这里插入图片描述
选择分析CPU Usage,点击开始,此时会启动项目分析,可以看到这里有个图表,它代表了当前进程每帧占据总CPU的比例:
在这里插入图片描述
点击Enable Collection一段时间后,点击Stop Collection,然后点击得到的数据,可以分析对应的信息,界面如下图所示:
在这里插入图片描述
界面的说明在这里,感觉还挺复杂的。它这里是通过Sample CPU来实现分析的,可以在相关界面进行设置,默认的是1s Sample一千次,也就是说,图里面的CPU Unit就是1毫秒,Total CPU是指执行这个函数消耗的所有时间,而Self CPU是指执行该函数本身(不包含执行其内部函数的时间)所用的时间,比如说A调用B,A总共执行5ms,B自身执行了2ms,那么A的Self CPU就是3。

所以这里其实应该看Self CPU,我重新排序如下:
在这里插入图片描述
虽然我这个Demo只绘制了几个三角形,但还是可以看出来,目前性能最大消耗点在glm数学库的运算和imgui上


IMGUI在引擎里的用途

Hazel引擎里的ImGUI,跟Unity里的ImGUI的功能是一样的,都是方便快速搭建UI,主要是为了方便Debugging用,并不是游戏Runtime实际会跑的UI,而且它在runtime下的性能也比较差。

目前Hazel还是会将就用着imgui,也会用它来搭建Hazel Editor,不过后期会搭建自己的Runtime UI


Let’s Make Something in Hazel

这一章也没啥难的,有以下几点:

  • 写了个简易的Particle System,然后做了个很简单的Demo,就是鼠标点在哪里,就在那里释放Particles,这一部分的内容跟我之前做Flappy Rocket的Particle System是差不多的,无非这里是把它接入到支持Batch的Renderer2D里而已
  • 做了个Clipping的操作,就是停止绘制正交相机区域外的Quad,给正交相机添加了个GetBounds的函数,绘制的时候去判断是否有交集

细节就不多说了,没啥特别的。不过视频里提到了个Bug,可以注意一下,这是一个关于C++类的成员变量初始化顺序引起的Bug。

如下图所示,先写的m_Bounds(...),并不意味着就会先初始化m_Bounds,而是按照对应变量出现在C++类头文件定义的顺序先后来初始化的:
在这里插入图片描述


How Sprite Sheets/Texture Atlases Work

对于Hazel引擎,它已经写好了一个2D Renderer。目前的想法,是对现有的2D Renderer进行测试,上节课做了个粒子系统的测试,看上去还是OK的,但是测试还不够。为了测试,这里尽可能的,制作出一个真正的2D游戏里会出现的场景地图,因为它更贴近实际生产环境。

更长远的计划则是让Hazel能够在User不写C++的情况下,制作出任何的2D游戏,这就不多说。

制作2D地图需要很多精度不高的贴图,代表不同的地图块,为了制作地图,需要加入Texture Atlase系统,Texture Atlases可以把它们放到同一个大的Texture上,基本概念就不多介绍了,这章的目的有:

  • 介绍资源网站,可以用来获取免费的贴图资源
  • 如何分割Texture Atlas为小的Textures

贴图资源网站

链接为:https://www.kenney.nl/assets

这是一个有很多免费游戏资源的网站,不只是贴图,如下图所示:
在这里插入图片描述

如何分割Texture Atlas

做法比较暴力…就是把贴图在PS里打开,然后自己手动测量出每个小Texture的左下角的TexCoord和右上角的TexCoord,即手动计算每个要的子Texture的UV坐标,这个UV坐标是normalized坐标,在[0, 1]区间。

如下图所示,手动测呗,稍微注意一下,这里的宽度高度一般都是2的倍数,比如128、256啥的,也不是很难测:
在这里插入图片描述
其他的没啥区别了,这里额外注意一下图片的WrapMode是Linear还是Nearest即可,前者是进行了像素的融合,边缘会模糊柔和一些,而Nearest的图片边缘锯齿感会强一些

具体的代码不难,不多说了,就是改变UV坐标而已。



SubTextures

这节课也都是工程性的东西:

  • 为了方便处理SpriteSheet或者说Texture Atlas,可以添加一个额外的SubTexture类,它本质上就是一个Texture的Wrapper,然后添加了额外的四个TexCoord坐标,用于表示Texture对应部分区域的Texture,从而起到SubTexture的作用。
  • SubTexture是一个笼统的概念,虽然Texture是跨平台的(需要有OpenGLTexture等类),但它作为一个Wrapper,不需要跨平台(不需要有OpenGLSubTexture等类)
  • Renderer2D类里添加额外的DrawCall函数,用于支持SubTexture

还需要注意一点,目前的游戏引擎是没有合并Geometry的功能,就目前的2D Renderer来说,暂时是不需要的。因为2D Renderer里一般只会绘制看得见的东西,不会像3D游戏里,需要绘制到很多屏幕看不到的东西(Occlusion)。3D渲染才需要Geometry合并的功能,比如说它会把只看得见的部分合并成一个Mesh,然后把它绘制出来,从而优化性能。



Creating a Map of Tiles

这节课的重点:

  • 如何表示Tiles组成的地图,每个Tile用哪个SubTexture?
  • 二维数组优先行遍历还是优先列遍历,哪一种更Cache Friendly一些 (后面的附录写了)

如何表示Tiles组成的地图

方法其实很多,总之目的是为了表示出地图上用了哪些类型的Tile,以及每个Tile的位置,就目前所用的2D贴图而言,它最小的图形应该是128*128像素的,所以1920*1080(16:9)的屏幕最多也就是15*8.4375个Tile而已。

这里介绍了两种:

  • 用字符串表示,字符串里的不同字符用于代表不同类型的Tile
  • 用很小的像素图表示场景,一个Tile用一个像素表示,这样地图看起来比较直观,而且是用图片资源存储的,比较方便迭代

我用的是第一种,代码如下:

// 16行9列
static const char s_MapTiles[] =
{
// 这种写法其实代表一个长字符串, D代表Dirt土地Tile, W代表Water Tile, S代表路标Tile
// 注意第一个Tile为D, 虽然在数组里坐标为(0,0), 但是在屏幕上对应的坐标应该是(0,1)
"DDWWWWWWWWWWWWWW"
"DDWWWWWWWWWWWWWW"
"DDDDDDDDDDDWWWWW"
"DDDDDSDDDDDWWWWW"
"DDDDDDDDDDDWWWWW"
"DDWWWWWWWWWWWWWW"
"DDWWWWWWWWWDDSDD"
"DDWWWWWWWWWWWWWW"
"DDWWWWWWWWWWWWWW"
};



Next Steps + Dockspace

引擎接下来的方向是:

  • 制作Hazel Editor界面,让用户方便的创建物体,而不用写代码
  • 创建Scene系统
  • 创建ECS系统

这节课,在代码部分,加入了ImGUI的Dockspace作为程序启动的主窗口,取代了原本绘制的megenta的洋红色窗口。所以这节课的知识点其实是参照imgui_demo.cpp里的代码,调用对应绘制Dockspace的代码。

只要循环调用ImGui::ShowDemoWindow(true)代码,就可以看到示例窗口,里面展示了所有的ImGUI的基本功能:
在这里插入图片描述
找到的示例函数是ShowExampleAppDockSpace,它是在ShowDemoWindow里被直接调用的,下面有这么句话:

This function demonstrate using DockSpace() to create an explicit docking node within an existing window.DockSpace() is only useful to construct to a central location for your application.

根据注释,正常来说,ImGui的窗口之间都是互相支持拖拽的,它们本来就是支持Docking,无非DockSpace是可以用于Application窗口的中心hub。

最后,为了避免窗口里除了Dockspace和ImGui窗口,其他的啥也没有,这里通过ImGUI绘制一张贴图。需要调用ImGui::Image函数,这里为了支持跨平台,需要把textureID从原本的GLuint改成void*类型,注意它是直接取整型数作为一个指针,这个指针当然是无效的,而不是取GLuint的地址改成void*,具体原因可以看附录:

void Renderer2DTestLayer::OnImGuiRender()
{
	ImGui::Begin("Test");
	ImGui::ColorEdit4("Flat Color Picker", glm::value_ptr(m_FlatColor));

	auto& stats = Hazel::Renderer2D::GetStatistics();

	ImGui::Text("DrawCalls: %d", stats.DrawCallCnt);
	ImGui::Text("DrawQuads: %d", stats.DrawQuadCnt);
	ImGui::Text("DrawVertices: %d", stats.DrawVerticesCnt());
	ImGui::Text("DrawTiangles: %d", stats.DrawTrianglesCnt());

	ImGui::Image(m_Texture2D->GetTextureId(), { 1080, 720 });

	m_ProfileResults.clear();
	ImGui::End();
	...// 下面是绘制Dockspace的代码

显示如下:
在这里插入图片描述
注意,这里的图片是倒着的,要校正的话需要这么写:

// 应该是ImGui对于贴图的Y坐标的认知跟目前的Texture是相反的
ImGui::Image(m_Texture2D->GetTextureId(), { 1080, 720 }, ImVec2{ 0, 1 }, ImVec2{ 1, 0 });


Framebuffers

A framebuffer (frame buffer, or sometimes framestore) is a portion of random-access memory (RAM)[1] containing a bitmap that drives a video display. It is a memory buffer containing data representing all the pixels in a complete video frame.[2] Modern video cards contain framebuffer circuitry(电路) in their cores.

这章的目的是把当前不断变化的场景,通过framebuffer渲染到一张贴图上,然后通过imgui绘制出来,为了后续放到Viewport窗口里,步骤如下:

  • 了解什么是Framebuffer
  • 创建Framebuffer类,为其添加Texture作为默认的Color Attachment
  • 创建Framebuffer,把原本渲染的贴图渲染到这个framebuffer object上,再通过Framebuffer的GetColorAttachment的接口,得到对应的texture、最后让ImGUI绘制出来,说白了就是自行创建fbo,代替OpenGL默认的fbo,把动态变化的场景渲染到一张贴图上了

什么是Framebuffer

Framebuffer其实很简单,第一,它是一块buffer,也就是一块内存;第二,它是存储的是一帧的buffer数据。

Framebuffer的基础知识就不多介绍了,之前我写过一篇文章,里面有相关介绍。


创建Framebuffer类

Framebuffer类也是跨平台的类,写法跟Vertex Buffer类差不多,创建Framebuffer类,然后基于各个平台创建对应的子类,比如OpenGLFramebuffer类,初步版本的FrameBuffer的数据成员如下:

uint32_t Width, Height;

// 当RenderToScreen为true时, 在OpenGL里会执行glBindFramebuffer(0);
// 在Cherno的引擎版本里, 这个变量叫做SwapChainTarget, 这个叫法后续学习了vulkan应该更能理解
bool RenderToScreen = false;// whether this framebuffer should be renderer to the swap chain

Render Pass

后续的引擎还会添加Render Pass的概念,Render Pass在OpenGL里更像是一个抽象的概念,但在Vulkan里却是一个实实在在存在的类,它会有一个Framebuffer,和一个target。像上面的RenderToScreen为false的Framebuffer,其实就是一个渲染到屏幕上的Render Pass而已,渲染时的代码大概是:

renderer.BeginRenderPass();


Making a New C++ Project in Hazel

这节课没啥东西,就是创建了一个 新项目叫做Hazelnput,类似于Sandbox,我这里叫Hazel Editor。原本的Sandbox文件夹保留,用HazelEditor创建用于游戏的GameData,用Sandbox加载对应的data作为runtime测试环境。



Scene Viewport

这节课还挺重要的,主要做了以下工作:

  • 新建一个ImGui窗口,把它拖拽到Dockspace里,把frambebuffer渲染的贴图在里面显示出来
  • 添加Framebuffer接口,更新Framebuffer里作为output的Color Attachment
  • 利用ImGui的API,检测Viewport窗口的size,如果产生变化,则更新更新Framebuffer里的Color Attachment
  • 根据Viewport窗口的大小变化,调整Camera的Viewport

新建Viewport窗口

这么写就行了:

void EditorLayer::OnImGuiRender()
{
	// 先是绘制Dockspace的代码
	...
	
	// 再是绘制RenderStats的代码
	...

	// 绘制Viewport
	ImGui::Begin("Viewport");
	ImGui::Text("Viewport");
	ImGui::End();
}

然后手动把窗口都拖到Dockspace里存好就行了,相关UI布局配置应该都会存到imgui.init文件里,不必每次都去拖拽了。不过这里我拖拽Dockspace时,有时候可以拖拽,有时候不可以拖,还不确定是啥问题,可能是Dock Branch有bug,后面可能要考虑更新submodule。

目前是这个样子:
在这里插入图片描述


更新Framebuffer里的Color Attachment

由于Framebuffer里对应贴图是有固定大小的,这里需要添加API,更改里面的Texture的大小。不过视频里Cherno的做法是重新生成整个Framebuffer,我觉得没有必要。那么这就是一个OpenGL调用API的问题,就是如何更改Framebuffer里的Color Attachment的size,相关知识放在附录。

核心代码如下:

void OpenGLFramebuffer::ResizeColorAttachment(uint32_t width, uint32_t height)
{
	if (m_FramebufferId != -1)
	{
		// 注意, 这里不需要BindFramebuffer
		glBindTexture(GL_TEXTURE_2D, m_ColorAttachmentTextureId);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
	}
}

// 在EditorLayer的OnImguiRender函数里
void EditorLayer::OnImGuiRender()
{
	// 绘制Dockspace和RenderStats的代码
	...

	ImGui::Begin("Viewport");
	
	ImVec2 size = ImGui::GetContentRegionAvail();
	glm::vec2 viewportSize = { size.x, size.y };

	// 放前面先画, 是为了防止重新生成Framebuffer的ColorAttachment以后, 当前帧渲染会出现黑屏的情况
	if (viewportSize != m_LastViewportSize)
		m_Framebuffer->ResizeColorAttachment(viewportSize.x, viewportSize.y);

	ImGui::Image(m_Framebuffer->GetColorAttachmentTexture2DId(), size, { 0,1 }, { 1,0 });

	m_LastViewportSize = viewportSize;
	ImGui::End();
}

根据Viewport窗口的大小变化,调整Camera的Viewport

在做这一块功能之前,我仔细想了下,在游戏引擎里,通常都会有Viewport窗口。在Unity里叫Scene窗口,在UE4里就叫Viewport窗口。那么,在我拖拽改变对应窗口大小时,引擎的Viewport窗口视角应该怎么改变?

为此我跑到UE4和Unity里做了个测试,相关过程放到了附录,得到的结果是:

  • 游戏引擎的Viewport拖拽时,展示的结果,绝不是把Viewport展示的这张贴图进行Resize,然后展示出来这么简单
  • Unity和UE4拖拽viewport,得到的结果不完全相同,但核心思路都是改变窗口大小,意味着改变相机Frustum,包括更改Frustum的宽度和高度、修改Near平面和Far平面的值
  • 对于正交投影来说,改变窗口大小,只需要修改Frustum的宽度和高度即可

鉴于UE4和Unity俩引擎在这上面的做法都有区别,而且我测试的是透视投影。而目前Renderer2D用的是的正交投影,那么我这里也没必要太过纠结,Copy Cherno的就行。

具体代码如下:

// 当窗口调整大小时, 改变相机可以看到的区域
void OrthographicCamera::OnResize(uint32_t width, uint32_t height)
{
	m_AspectRatio = (float)width / (float)height;
	// 里面会调用glm::ortho(float left, float right, float bottom, float top)函数
	// 根据这段代码可知, camera看到的区域高度不会随着窗口大小而改变
	// 而是会随着鼠标滚动改变zoom值而变化
	SetProjectionMatrix(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);
}

// 绑定framebuffer时, 调整窗口的viewport跟framebuffer贴图大小一样
void OpenGLFramebuffer::Bind()
{
	glBindFramebuffer(GL_FRAMEBUFFER, m_FramebufferId);
	glViewport(0, 0, m_Width, m_Height);
}

所以目前我的Viewport窗口的效果是:

  • 改变Viewport窗口大小,竖直方向的区域会随着缩放,但竖直方向的视野不变;水平方向的区域也会缩放,但水平方向的区域视野也会变化

至于,为什么可以这么写,正交相机的投影矩阵为什么可以用AspectRatioZoomLevel来表示,附录里提到了。



Code Review + ImGui Layer Events

主要是:

  • 修一些Bug
  • 重构一些代码
  • 修复ImGui Layer事件与EditorLayer事件冲突的问题

Framebuffer析构函数为虚函数

基类的析构函数需要是虚函数,这是很常见的面试题目了。虽然我这里的Framebuffer类没有在堆上进行内存分配,但还是需要使用虚的析构函数。否则对于Framebuffer指针来说,即使其实际类型为派生类,该指针对应的对象析构时也不会调用派生类的析构函数。

// 比如下面这个情况
class EditorLayer : public Layer
{
	...
private:
	// 如果Framebuffer析构函数不为虚函数, 则EditorLayer析构时, m_Framebuffer析构不会调用派生类析构函数
	std::shared_ptr<Framebuffer> m_Framebuffer;
}

修改Input类

目前的Input类是这么写的

namespace Hazel
{
	// 引擎提供给用户查询Input的类, 里面的静态public函数是给用户使用的
	// 里面的protected函数是给Input的子类使用的, 具体需要根据用户所在的平台决定使用哪种子类对象
	// 这里的keycode是用int表示的, 具体哪个Key, 代表值为多少的int, 被统一定义在了引擎的KeyCode.h文件里
	// 比如#define HZ_KEY_D                  68, 这种写法借鉴于glfw3.h
	class HAZEL_API Input 
	{
	public:
		// 这种写法很乱
		inline static bool IsMouseButtonPressed(int button) { return s_Instance->IsMouseButtonPressedImp(button); }
		inline static bool IsKeyPressed(int keycode) { return s_Instance->IsKeyPressedImp(keycode); }
		inline static std::pair<float, float> GetMousePos() { return s_Instance->GetMousePosImp(); }

	protected:
		virtual bool IsKeyPressedImp(int keycode) = 0;
		virtual bool IsMouseButtonPressedImp(int button) = 0;
		virtual std::pair<float, float> GetMousePosImp() = 0;
	private:
		static Input* s_Instance;
	};
}

// 然后在对应的Platform下会有对应的派生类
class WindowsInput : public Input
{
public:
	virtual bool IsKeyPressedImp(int keycode);
	virtual bool IsMouseButtonPressedImp(int button);
	virtual std::pair<float, float> GetMousePosImp();
};

// 在WindowsInput.cpp里创建Singleton实例
namespace Hazel
{
	Input* Input::s_Instance = new WindowsInput();
	bool WindowsInput::IsKeyPressedImp(int keycode)
	{
		GLFWwindow* w = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
		auto r = glfwGetKey(w, keycode);
		
		return r == GLFW_PRESS || r == GLFW_REPEAT;
	}
	...
}

Cherno表示这种写法很垃圾,他更倾向于目前Renderer2D的写法,就是用一堆static函数,代替Singleton,原本Singleton的数据成员现在用一个struct表示,即创建一个对应static对象来存储Singleton的数据成员

那我可不可以像VertexBuffer类一样,它是根据RenderAPIType返回对应的VertexBuffer子类,那我这里也可以根据自己的操作系统返回对应的Input子类。感觉是可以,但是不太好。前面的VertexBuffer这么写,是因为Runtime可以改变RenderAPIType,但是对于Input派生类来说,只有一个子类会被编译到最终的exe里,这是个Compile Time决定的事情,不需要借助虚函数来实现,因为一个Application只可能同时在一个Platform上运行

所以更好的办法,是在Input类里创建一堆static函数,然后在各个平台对应的Input派生类类实现这些static函数,代码如下所示:

删除WindowsInput类对应的头文件,保留其cpp文件,在里面实现Input.h的static接口

// WindowsInput.cpp里
bool Input::IsKeyPressed(int keycode)
{
	GLFWwindow* w = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
	auto r = glfwGetKey(w, keycode);
	
	return r == GLFW_PRESS || r == GLFW_REPEAT;
}

bool Input::IsMouseButtonPressed(int button)
{
	GLFWwindow* w = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
	auto r = glfwGetMouseButton(w, button);
	return r == GLFW_PRESS || r == GLFW_REPEAT;
	return false;
}

std::pair<float, float> Input::GetMousePos()
{
	GLFWwindow* w = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
	double x, y;
	glfwGetCursorPos(w, &x, &y);
	return std::pair<float, float>((float)x, (float)y);
}


修复Framebuffer没有ClearColor的bug

我目前的代码是这样的:

void EditorLayer::OnUpdate(const Hazel::Timestep& ts)
{
	...
	// 这里清除的是OpenGL默认的framebuffer, 没有清除我自己的framebuffer
	Hazel::RenderCommand::Clear();
	Hazel::RenderCommand::ClearColor(glm::vec4(1.0f, 0.0f, 1.0f, 1.0f));// magenta color
	m_Framebuffer->Bind();
	Hazel::Renderer2D::BeginScene(m_OrthoCameraController.GetCamera());
	{
		...
	}
	Hazel::Renderer2D::EndScene();
	m_Framebuffer->UnBind();
}

这样写有问题,因为我的Clear操作,没有Clear我的Framebuffer,此时的画面上会出现很多不干净的东西,所以应该修改成:

void EditorLayer::OnUpdate(const Hazel::Timestep& ts)
{
	...
	// This is for the color for default window 
	// 保留原本默认窗口对应framebuffer的颜色, 注意, 一定要先设置ClearColor, 再去Clear
	Hazel::RenderCommand::SetClearColor(glm::vec4(1.0f, 0.0f, 1.0f, 1.0f));// 默认窗口颜色仍为magenta
	Hazel::RenderCommand::Clear();

	m_Framebuffer->Bind();
	Hazel::RenderCommand::SetClearColor(glm::vec4(0.1f, 0.1f, 0.1f, 1.0f));
	Hazel::RenderCommand::Clear();
	Hazel::Renderer2D::BeginScene(m_OrthoCameraController.GetCamera());
	{
		...
	}
	Hazel::Renderer2D::EndScene();
	m_Framebuffer->Unbind();
}

ImGui与Event

现在有个问题,我有俩窗口,一个是绘制Render Stats的窗口,另一个是Viewport窗口。但是我锁定了Render Stats窗口时,对应的键鼠事件还是会影响到Viewport窗口。

首先,梳理一下ImGui在引擎里的执行情况。目前的引擎里,我把ImGuiLayer的创建放到了基类Application的构造函数里,ImGuiLayer里面会有专门的ImGui的代码,比如ImGui的初始化与Update等。此外,每个普通Layer,比如我这里的EditorLayer,里面的OnImGuiRender函数里也可以写一些ImGui代码,我在EditorLayer.OnImGuiRender里绘制了Viewport窗口和Render Stats窗口。

关于ImGui,最好的入门方法还是参考An-introduction-to-the-Dear-ImGui-library,附录后面也写了。

至于Event,目前Hazel引擎处理Event的核心代码如下:

// WindowsWindow.cpp里, 登记glfw的窗口事件
// 当产生WindowCloseEvent时, 执行WindowsWindow类里记录的eventCallback这个函数指针对应的函数
glfwSetWindowCloseCallback(m_Window, [](GLFWwindow* window)
{
	WindowData &data = *(WindowData*)glfwGetWindowUserPointer(window);
	WindowCloseEvent closeEvent;
	data.eventCallback(closeEvent);
});

// Application.cpp里
Application::Application()
{
	s_Instance = this;

	m_Window = std::unique_ptr<Hazel::Window>(Hazel::Window::Create());
	// 这里会设置m_Window里的std::function<void(Event&)>对象, 当接受Event时, 会调用Application::OnEvent函数
	m_Window->SetEventCallback(std::bind(&Application::OnEvent, this, std::placeholders::_1));

	// Application应该自带ImGuiLayer, 这段代码应该放到引擎内部而不是User的Application派生类里
	m_ImGuiLayer = new ImGuiLayer();
	m_LayerStack.PushOverlay(m_ImGuiLayer);
}


// 当窗口触发事件时, 会调用此函数
void Application::OnEvent(Event& e)
{
	EventDispatcher dispatcher(e);

	// 1. 当接受窗口来的Event时, 首先判断是否是窗口关闭的事件
	// Dispatch函数只有在Event类型跟模板T匹配时, 才响应事件
	// std::bind其实是把函数和对应的参数绑定的一起
	dispatcher.Dispatch<WindowCloseEvent>(
		// std::bind第一个参数是函数指针, 第二个代表的类对象(因为是类的成员函数)
		// 第三个代表的是放到函数的第一位
		std::bind(&Application::OnWindowClose, this, std::placeholders::_1));
	dispatcher.Dispatch<WindowResizedEvent>(
		std::bind(&Application::OnWindowResized, this, std::placeholders::_1));

	// 2. 否则才传递到layer来执行事件, 逆序遍历是为了让ImGuiLayer最先收到Event
	uint32_t layerCnt = m_LayerStack.GetLayerCnt();
	for (int i = layerCnt - 1; i >= 0; i--)
	{
		if (e.IsHandled())
			break;

		m_LayerStack.GetLayer((uint32_t)i)->OnEvent(e);
	}
}

目前的思路是,既然ImGuiLayer是最先收到Event,当Focus的是Viewport对应的窗口时,那么ImGuiLayer不会处理这个Event,然后该Event会继续发送给剩下的Layer,也就是EditorLayer,让它来执行和处理这个Event。

// EditorLayer.cpp里
void EditorLayer::OnImGuiRender()
{
	...
	
	// 核心的ImGui代码如下:
	ImGui::Begin("Viewport");
	// BeginWindow之后, 这里返回的就是该Window的Focus状态了
	// 这里Begin()操作应该会把viewport设置为当前window
	m_ViewportFocused = ImGui::IsWindowFocused();
	m_ViewportHovered = ImGui::IsWindowHovered();
	// 相关状态存到ImGuiLayer里(感觉存到Applciation里更好)
	Hazel::Application::Get().GetImGuiLayer()->SetViewportFocusedStatus(m_ViewportFocused);
	Hazel::Application::Get().GetImGuiLayer()->SetViewportHoveredStatus(m_ViewportHovered);
	...
}

// 然后在ImGuiLayer.cpp里
void Hazel::ImGuiLayer::OnEvent(Event &e)
{
	// 只有鼠标在Viewport窗口上、且窗口被Focus时, Viewport窗口才可以接收到Event
	if (!(m_ViewportFocused && m_ViewportHovered))// Viewport区域以外的Event会被ImGui接受
		e.MarkHandled();
}

Code Review

不要把重要的最通用的宏定义放到头文件里
Cherno的Hazel引擎里的Core.h(后来改名为Base.h),内容如下:

#pragma once

#include <memory>

// Platform detection using predefined macros
#ifdef _WIN32
	/* Windows x64/x86 */
	#ifdef _WIN64
		/* Windows x64  */
		#define HZ_PLATFORM_WINDOWS
	#else
		/* Windows x86 */
		#error "x86 Builds are not supported!"
	#endif
#elif defined(__APPLE__) || defined(__MACH__)
	#include <TargetConditionals.h>
	/* TARGET_OS_MAC exists on all the platforms
	 * so we must check all of them (in this order)
	 * to ensure that we're running on MAC
	 * and not some other Apple platform */
	#if TARGET_IPHONE_SIMULATOR == 1
		#error "IOS simulator is not supported!"
	#elif TARGET_OS_IPHONE == 1
		#define HZ_PLATFORM_IOS
		#error "IOS is not supported!"
	#elif TARGET_OS_MAC == 1
		#define HZ_PLATFORM_MACOS
		#error "MacOS is not supported!"
	#else
		#error "Unknown Apple platform!"
	#endif
/* We also have to check __ANDROID__ before __linux__
 * since android is based on the linux kernel
 * it has __linux__ defined */
#elif defined(__ANDROID__)
	#define HZ_PLATFORM_ANDROID
	#error "Android is not supported!"
#elif defined(__linux__)
	#define HZ_PLATFORM_LINUX
	#error "Linux is not supported!"
#else
	/* Unknown compiler/platform */
	#error "Unknown platform!"
#endif // End of platform detection

#ifdef HZ_DEBUG
	#if defined(HZ_PLATFORM_WINDOWS)
		#define HZ_DEBUGBREAK() __debugbreak()
	#elif defined(HZ_PLATFORM_LINUX)
		#include <signal.h>
		#define HZ_DEBUGBREAK() raise(SIGTRAP)
	#else
		#error "Platform doesn't support debugbreak yet!"
	#endif
	#define HZ_ENABLE_ASSERTS
#else
	#define HZ_DEBUGBREAK()
#endif

#ifdef HZ_ENABLE_ASSERTS
	#define HZ_ASSERT(x, ...) { if(!(x)) { HZ_ERROR("Assertion Failed: {0}", __VA_ARGS__); HZ_DEBUGBREAK(); } }
	#define HZ_CORE_ASSERT(x, ...) { if(!(x)) { HZ_CORE_ERROR("Assertion Failed: {0}", __VA_ARGS__); HZ_DEBUGBREAK(); } }
#else
	#define HZ_ASSERT(x, ...)
	#define HZ_CORE_ASSERT(x, ...)
#endif

#define BIT(x) (1 << x)

#define HZ_BIND_EVENT_FN(fn) std::bind(&fn, this, std::placeholders::_1)

namespace Hazel {

	template<typename T>
	using Scope = std::unique_ptr<T>;
	template<typename T, typename ... Args>
	constexpr Scope<T> CreateScope(Args&& ... args)
	{
		return std::make_unique<T>(std::forward<Args>(args)...);
	}

	template<typename T>
	using Ref = std::shared_ptr<T>;
	template<typename T, typename ... Args>
	constexpr Ref<T> CreateRef(Args&& ... args)
	{
		return std::make_shared<T>(std::forward<Args>(args)...);
	}
}

可以看到,它定义了基本上Hazel在所有Platforms上的宏,但是它有致命的缺点:
它很难被所有cpp引用,即使把它放到pch里,它也只能保证被Hazel引擎内的代码使用,对于引擎使用的第三方库文件,不大可能会include这个Core.h文件。

更好的方法,是通过premake5.lua把宏定义放到项目的Preprocessing对应的属性栏里,如下图所示:
在这里插入图片描述


VertexArray在跨平台的图形API里并不存在
除了OpenGL 其他的渲染API是根本没有VertexArray这个概念 所以VertexArray这个类要大改 因为它不是一个Render跨平台通用的概念

VertezArray其实是用来描述vertex buffer的,其实DX和Vulkan的设计理念更好,就是让vertex layout绑定到shader,而OpenGL里,vertex layout是存在vertex array里,vertex arrray会绑定到context上,跟shader没有绑定关系



附录

遍历二维数组需要注意的问题

这个其实属于比较经典的问题,也会出现在一些面试题目里面,参考:https://stackoverflow.com/questions/33722520/why-is-iterating-2d-array-row-major-faster-than-column-major

C arrays are stored in a contiguous by row major order. This means if you ask for element x, then element x+1 is stored in main memory at a location directly following where x is stored.

结论是:应该按照行进行遍历,因为C++里数组是按行进行存储的

但是,注意代码里,应该是先是h,再是w,也就是第一个for loop里的是y,而不是x,如下所示:

// 先写列号y并不代表着先遍历列
for(int y = 0; y < h; y++)
	for(int x = 0; x < h; x++)
	{
		// 最内部的for loop才代表最早开始遍历的东西, 比如y = 0时遍历第一行
		...
	}

这样才能先遍历数组行元素



OpenGL里的RGBA32F

比如说:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

这样可以创建一个贴图,它的RGBA每个分量都是由一个32位浮点数组成的,这里的RGBA每个分量的范围不再是[0, 1]之间,而是任意的浮点数,所以这种贴图可以用来存储很多自定义数据


glTexImage2D和glTexStorage2D的区别

参考:https://stackoverflow.com/questions/23362497/how-can-i-resize-existing-texture-attachments-at-my-framebuffer

If you were using glTexImage2D (…) to allocate storage for your texture, it would be possible to re-allocate the storage for any image in the texture at any time without first deleting the texture.

  • glTexImage2D创建的texture的内存是可以变化的,如果想改贴图的大小,不需要delete再重新创建texture;而glTexStorage2D创建的贴图大小是固定的,它会创建一个immutable(不可变的)贴图对象,相关的贴图设置永远不可以再改变,如果执意更改其大小,会给一个GL_INVALID_OPERATION的报错
  • glTexStorage2D的速度会比glTexImage2D更快

glTexStorage2D specifies the storage requirements for all levels of a two-dimensional texture or one-dimensional texture array simultaneously. Once a texture is specified with this command, the format and dimensions of all levels become immutable unless it is a proxy texture. The contents of the image may still be modified, however, its storage requirements may not change. Such a texture is referred to as an immutable-format texture.
The behavior of glTexStorage2D depends on the target parameter.


Dear ImGui里的ImTextureID

参考:https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-how-can-i-display-an-image-what-is-imtextureid-how-does-it-work
参考:https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_opengl3.cpp

代码里是这么定义的,本质上是void*

// Other types
#ifndef ImTextureID                 // ImTextureID [configurable type: override in imconfig.h with '#define ImTextureID xxx']
typedef void* ImTextureID;          // User data for rendering backend to identify a texture. This is whatever to you want it to be! read the FAQ about ImTextureID for details.
#endif

参考文档里有这么两句:

  • You may use functions such as ImGui::Image(), ImGui::ImageButton() or lower-level ImDrawList::AddImage() to emit draw calls that will use your own textures.
  • OpenGL里:
    ImTextureID = GLuint
    See ImGui_ImplOpenGL3_RenderDrawData() function in imgui_impl_opengl3.cpp

想了想ImGUI是如何实现跨平台的,它应该是给了一批通用的头文件,然后在不同平台实现了对应的头文件,也就是说不同平台会有各自平台的cpp文件,比如OpenGL3对应的两个cpp文件为:imgui_impl_opengl3.cppimgui_impl_glfw.cpp,我参考了一下ImGui_ImplOpenGL3_RenderDrawData函数,发现它是在End函数里被调用的:

void Hazel::ImGuiLayer::End()
{        
	ImGuiIO& io = ImGui::GetIO();
	// Rendering
	ImGui::Render();
	ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

	// Update and Render additional Platform Windows
	// (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
	//  For this specific demo app we could also call glfwMakeContextCurrent(window) directly)
	if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
	{
		GLFWwindow* backup_current_context = glfwGetCurrentContext();
		ImGui::UpdatePlatformWindows();
		ImGui::RenderPlatformWindowsDefault();
		glfwMakeContextCurrent(backup_current_context);
	}
}

// 实际调用时
// 3. 最后调用ImGUI的循环
m_ImGuiLayer->Begin();
for (Hazel::Layer* layer : m_LayerStack)
{
	// 每一个Layer都在调用ImGuiRender函数
	// 目前有两个Layer, Sandbox定义的ExampleLayer和构造函数添加的ImGuiLayer
	layer->OnImGuiRender();
}
m_ImGuiLayer->End();

然后这里的ImGuiRender的函数,就会调用ImGui::Image()函数,内部是这样的:

// imGui_widgets.cpp里
void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
{
    ImGuiWindow* window = GetCurrentWindow();
    if (window->SkipItems)
        return;

    ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
    if (border_col.w > 0.0f)
        bb.Max += ImVec2(2, 2);
    ItemSize(bb);
    if (!ItemAdd(bb, 0))
        return;

	// 加入到DrawList里, 这也是类似于Batch的概念
    if (border_col.w > 0.0f)
    {
        window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f);
        window->DrawList->AddImage(user_texture_id, bb.Min + ImVec2(1, 1), bb.Max - ImVec2(1, 1), uv0, uv1, GetColorU32(tint_col));
    }
    else
    {
        window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col));
    }
}

最后会在ImGui::End函数里进行批处理和绘制,然后我看到了这个代码:

if (clip_rect.x < fb_width && clip_rect.y < fb_height && clip_rect.z >= 0.0f && clip_rect.w >= 0.0f)
{
    // Apply scissor/clipping rectangle
    glScissor((int)clip_rect.x, (int)(fb_height - clip_rect.w), (int)(clip_rect.z - clip_rect.x), (int)(clip_rect.w - clip_rect.y));

    // Bind texture, Draw, 看到没, 在这里把TextureId最终转换成了GLuint
    glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->TextureId);
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET 	
    if (g_GlVersion >= 320)
        glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)), (GLint)pcmd->VtxOffset);
    else
#endif
    glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)));
}

总之,这应该是为了支持跨平台用的,就是DrawImage的时候,把所有的TextureId的格式换成通用的void*,然后实际绘制时,根据跑的Platform的类型,解析该TextureId,各个平台的解析都不同:

  • OpenGL: ImTextureID = GLuint
  • DirectX9: ImTextureID = LPDIRECT3DTEXTURE9
  • DirectX11: ImTextureID = ID3D11ShaderResourceView*
  • DirectX12: ImTextureID = D3D12_GPU_DESCRIPTOR_HANDLE

感觉写法有点意思,注意一下,这里是直接把类型转换为void*,比如GLuint,得到的指针本身的地址就是这个数据的值,该指针所对应的地址的内容是无效的。



Delegating constructors

参考:https://docs.microsoft.com/en-us/cpp/cpp/delegating-constructors?view=msvc-170

像下面这种的,一个类的调用了另外的构造函数的构造函数,叫做Delegating constructor(委托构造函数),这是C++11的新特性,代码如下所示:

class class_c 
{
public:
    int max;
    int min;
    int middle;

    class_c(int my_max) 
    {
        max = my_max > 0 ? my_max : 10;
    }
    class_c(int my_max, int my_min) : class_c(my_max) 
    {
        min = my_min > 0 && my_min < max ? my_min : 1;
    }
    class_c(int my_max, int my_min, int my_middle) : class_c (my_max, my_min)
    {
        middle = my_middle < max && my_middle > min ? my_middle : 5;
	}
};
int main() 
{
    class_c c1{ 1, 3, 2 };
}

但是这么写,就会编译报错:

class class_a 
{
public:
    class_a() {}
    // member initialization here, no delegate
    class_a(string str) : m_string{ str } {}

    // can’t do member initialization here, 只能有class_a(str)这一个member-initializer
    // error C3511: a call to a delegating constructor shall be the only member-initializer
    class_a(string str, double dbl) : class_a(str) , m_double{ dbl } {}

    // only member assignment, 这种写法是对的
    class_a(string str, double dbl) : class_a(str) { m_double = dbl; }

    double m_double{ 1.0 };
    string m_string;
};

派生类无法使用Initializer list来初始化基类的对象

写了这么个简单代码,结果报错了:

class Framebuffer
{
public:
	uint32_t m_Width = 800;
	uint32_t m_Height = 600;
};

class OpenGLFramebuffer : public Framebuffer
{
public:
	OpenGLFramebuffer(uint32_t width, uint32_t height);
}

// 这句代码编译报错: "m_Width" is not a nonstatic data member or base class of class "OpenGLFramebuffer"
OpenGLFramebuffer::OpenGLFramebuffer(uint32_t width, uint32_t height): m_Width(width), m_Height(height)

报错信息表示,m_Width不是OpenGLFramebuffer的成员、也不是OpenGLFramebuffer的基类。但是这么写是可以的:

OpenGLFramebuffer::OpenGLFramebuffer(uint32_t width, uint32_t height)
{
	m_Width = width;
	m_Height = height;
}

事实就是这样的,派生类里继承于基类的成员变量,只可以通过调用基类的构造函数来初始化,可以在派生类的构造函数函数体内对其进行赋值(但是此时变量已经初始化好了)。

原因参考:https://stackoverflow.com/questions/2290733/initialize-parents-protected-members-with-initialization-list-c
参考:https://stackoverflow.com/questions/18479295/member-initializer-does-not-name-a-non-static-data-member-or-base-class

对于一个派生类,对于其不是delegating的构造函数,也就是不调用其他相同类构造函数的构造函数而言,它的初始化顺序是这样的:

  1. 首先,是先创建最深处的基类,然后慢慢创建到派生类,这个顺序应该很清楚
  2. 然后会创建该类的直接父类的构造函数,创建其对象
  3. 然后,会按照成员声明顺序,初始化该类的非static成员
  4. 最后,会执行该类的构造函数里的内容

不是特别懂,大概意思应该是,在执行:后面的代码时,该类的父类的构造函数还没被调用,所以不可以这么写,只可以在派生类里调用基类的构造函数,不允许在派生类的initializer_list里初始化继承来的成员变量



能不能直接把fbo得到的Texture进行Resize,作为Viewport

为了研究这个问题,我特意去看了Unity和UE4里Viewport是怎么Resize的。

对于Unity而言,好像是直接对Texture进行Resize的,因为我Viewport里看到的东西总是那么多,只是尺寸变了:

比如我创建了这么一个关卡,下面这是原图,为了保证这个窗口大小在我Resize之前都是一个大小的,我每次都会重置到Unity默认的窗口布局:
在这里插入图片描述
横向拉伸后,可以明显看到,这里的中心物体的大小完全没有变化,而如果单纯的拉长贴图,物体也会被拉伸,是不会产生这种效果的,:
在这里插入图片描述

纵向拉伸后:
在这里插入图片描述
纵向缩短后:
在这里插入图片描述
横向缩短后:
在这里插入图片描述

我还测试了UE4的viewport的拖拽情况,跟上面的操作方式差不多,得到以下结论:

  • 游戏引擎的Viewport拖拽时,展示的结果,绝不是把一张贴图进行Resize得到的效果,没有这么简单
  • Unity和UE4拖拽viewport,得到的结果不完全相同,但它们的核心思路都是改变相机Frustum,具体怎么修改的,两个引擎存在差异:
  1. 对于横轴缩放来说,当Unity的viewport进行横向缩小时,最开始的中心物体大小是不变的,但在某一个值后,再缩小Viewport窗口,物体则会整体变小,此时skybox对应的天际线也会降低(我猜测一开始会缩小Frustum的水平宽度,后来则会增加Far平面与Camera的距离);而横向放大viewport时,中心物体大小基本是不会变化的(只会增加Frustum的宽度);而UE4的viewport进行横向拖拽时,无论是缩小还是放大,物体大小一直是会随着viewport的宽度变大变小而对应变化的(不仅改变Frustum的宽度,好像还拉近了Far平面)。
  2. 对于竖轴缩放,又不一样。Unity竖轴缩放的效果跟UE4横轴缩放效果类似,物体大小会随着viewport的高度变大变小而对应变化(不仅改变Frustum的宽度,好像还拉近了Far平面);但UE4的竖轴缩放,不会更改物体大小,只会改变可视区间,也就是说画面里的物体不会有任何大小的变化,但是会产生竖直方向上的位移,缩小到最后只能看到部分内容(移动了Frustum的高度值,即y坐标值,且纵向改变了Frustum的长度),如下图所示:
    在这里插入图片描述

更改Framebuffer里的Color Attachment的size

参考:https://stackoverflow.com/questions/23362497/how-can-i-resize-existing-texture-attachments-at-my-framebuffer

方法很简单,就是用glTexImage2D来创建贴图,这样的贴图允许动态改变大小:

void OpenGLFramebuffer::ResizeColorAttachment(uint32_t width, uint32_t height)
{
	if (m_FramebufferId != -1)
	{
		glBindTexture(GL_TEXTURE_2D, m_ColorAttachmentTextureId);
		// 再次调用它即可
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
	}
}


正交矩阵与Zoom和Aspect Radio的关系

由于这里调用的是glm::ortho(left, right, bottom, top, -1.0f, 1.0f)left < 0right = - leftbottom < 0top = - bottom。这里作为2D的Camera,默认绘制的区间在横轴[-1.7778f, 1.778f],纵轴[-1, 1]之间,所以这里直接用ZoomLevel来表示top,因为拉近镜头时,ZoomLevel变大,而对应可见区域会变小,所以这里用ZoomLevel来表示top是一种很巧妙的方法。

而Aspect Radio就是bottom与left的比了,这个没啥



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

这个矩阵,应该是会把横轴为[left, right],纵轴为[bottom, top]的长方形区间,映射为横纵轴均为[-1, 1]的正方形区间。

函数签名我看有两种:

glm::ortho(xmin, xmax, ymin, ymax);
glOrtho( xmin, xmax, ymin, ymax, near, far);

我的几个问题:

  • xmin与xmax、ymin与ymax一定是互为相反数吗,如果不一定,那么结果会咋样(毕竟相机不是在原点吗,如果xmin与xmax不对称,那么,难道左右边的长度不一样?还能不能映射到[-1, 1]区间了)
  • 这个zmin和zmax有啥用,是不是如果不加,就代表绘制无限Z区间,而设置了,就只设置这个区间的2D投影

没有看到说明,我怀疑就是这样的,即:

  • xmin与xmax、ymin与ymax一般都是互为相反数
  • zmin和zma应该是,如果不加,就代表绘制无限Z区间,而设置了,就只设置这个区间的2D投影


Dear ImGui的hello world项目

参考:https://blog.conan.io/2019/06/26/An-introduction-to-the-Dear-ImGui-library.html
代码如下所示:

int main()
{
	// 一些对OpenGL的初始化
	...
	
	 // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO &io = ImGui::GetIO();
    // Setup Platform/Renderer bindings
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);
    // Setup Dear ImGui style
    ImGui::StyleColorsDark();

	 while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        glClearColor(0.45f, 0.55f, 0.60f, 1.00f);
        glClear(GL_COLOR_BUFFER_BIT);

		// 相当于Update
        // feed inputs to dear imgui, start new frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        // rendering our geometries, 这个shader应该是随便写的, 跟ImGui没啥关系
        triangle_shader.use();
        glBindVertexArray(vao);
        glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);

        // render your GUI, 调用ImGui在原本绘制的三角形上, 再画一个按钮
        ImGui::Begin("Demo window");
        ImGui::Button("Hello!");
        ImGui::End();

        // Render dear imgui into screen, 把按钮绘制出来
        ImGui::Render();
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        int display_w, display_h;
        glfwGetFramebufferSize(window, &display_w, &display_h);
        glViewport(0, 0, display_w, display_h);
        glfwSwapBuffers(window);
    }
}

// 在结束后, 调用
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
posted @ 2022-12-03 13:07  弹吉他的小刘鸭  阅读(272)  评论(0编辑  收藏  举报