Hazel引擎学习

Posted 弹吉他的小刘鸭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hazel引擎学习相关的知识,希望对你有一定的参考价值。

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


参考视频链接在这里


经过了漫长的过程,终于把无聊的UI部分结束了,本阶段继续搭建引擎的基础部分,包括:

  • 场景序列化
  • Viewport里的显示与操作:包括鼠标点选GameObject和Transform Gizmos的绘制
  • 跨平台的Shader编译系统

Saving and Loading Scenes

具体以下几点:

  • 为什么要把Scene存成文本文件
  • 添加Yaml-cpp作为submodule
  • 创建SceneSerializer类,负责把Scene与Text文件之间的互换,还有Scene与Binary文件之间的互换

把Scene存成文本文件

Unity把场景存成了Yaml格式的文本文件,GameObject与Components之间的信息、以及每个Component在inspector上显示的内容,都会存在Scene文件里;而UE里的场景对应文件为.umap格式,貌似并没有完全把场景存为文本文件,如下图所示:

其实还可以用JSON来存储,但是这种格式在merge的时候,没有Yaml格式好,而且JSON文件很容易写漏掉


添加Yaml-cpp作为submodule

用这玩意儿主要是为了方便将C++数据与Yaml数据互相转换,这里的仓库地址为:https://github.com/jbeder/yaml-cpp

这里有个问题,Yaml模块应该放到Hazel下,还是Hazel Editor工程下。直观感觉是把它放到后者里,因为它只是一个Editor用到的东西,Runtime下为了更好的效率,应该会把场景数据(打包)成二进制数据。但Yaml模块应该放到引擎下更好,这样可以提供Runtime下的Debug功能,否则如果全是二进制数据,Runtime下数据的预览将会很困难。

接下来很简单,自己Fork一个Yaml cpp工程,在里面加上自己的premake5.lua文件,然后再在Hazel里:

  • 添加SUBMODULE
  • 修改premake5.lua文件
  • 加宏,把Yaml-cpp工程设置为static library

Yaml-cpp里yaml的写法参考:https://github.com/jbeder/yaml-cpp/wiki/How-To-Emit-YAML


Scene的序列化与反序列化

序列化的顺序按范围从大到小,先是Scene,再是GameObject,然后是Component,类声明为:

namespace Hazel

	class Scene;
	class GameObject;
	class SceneSerializer
	
	public:	
		static void Serialize(std::shared_ptr<Scene>, const char* path);
		static bool Deserialize(std::shared_ptr<Scene> scene, const char* path);
		static void SerializeGameObject(YAML::Emitter& out, const GameObject&);
		static GameObject& DeserializeGameObject(YAML::Emitter& out);
	;

具体实现就不多说了,很简单。值得一提的是,Yaml里也是用Tree这种结构来表示父子结构的,每一个Map区间貌似对应一个Node,如下所示是我序列化得到的场景数据:



Open/Save File Dialogs

具体步骤:

  • 通过Windows API,出现出搜寻文件的创建,可以加载指定路径的.scene文件,将其反序列化得到Scene对象
  • 点击New Scene按钮,创建新场景
  • 给相关操作添加快捷键

搜索文件的窗口

这里的需求是打开窗口,选择需要的.scene文件加载,这里有个问题,就是是否使用各个平台自带的搜索文件的功能,比如Windows上的很好用,但是不可以兼容在别的操作系统上。这里的选择是,根据各个平台的不同,使用各个平台自带的窗口API,像Unity和UE都是这么做的,UE里做了个内部记录,但还是可以点击打开Windows的搜索界面,如下图所示:

所以这里的做法是在引擎里创建一个FileDialogue类的头文件,然后在Platform的各个平台实现FileDialogue类,代码如下:

#pragma once
#include "hzpch.h"
#include "Hazel/Utils/PlatformUtils.h"
#include "Hazel/Core/Application.h"

// 使用windows sdk里提供的头文件
#include <commdlg.h>
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>

// TODO: if def window platform
namespace Hazel

	// 打开搜索文件的窗口
	std::optional<std::string> FileDialogWindowUtils::OpenFile(const char* filter)
	
		// 打开搜索文件的窗口, 需要传入一个OPENFILENAMEA对象
		OPENFILENAMEA ofn;
		CHAR szFile[260] =  0 ;
		ZeroMemory(&ofn, sizeof(OPENFILENAME));

		// 填充ofn的信息
		ofn.lStructSize = sizeof(OPENFILENAME);
		ofn.hwndOwner = glfwGetWin32Window((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow());
		ofn.lpstrFile = szFile;
		ofn.nMaxFile = sizeof(szFile);
		ofn.lpstrFilter = filter;
		ofn.nFilterIndex = 1;
		// 
		ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
		// 调用win32 api
		if (GetOpenFileNameA(&ofn) == TRUE)
			return ofn.lpstrFile;

		return ;
	

	std::optional<std::string> FileDialogWindowUtils::SaveFile(const char* filter)
	
		OPENFILENAMEA ofn;
		CHAR szFile[260] =  0 ;
		ZeroMemory(&ofn, sizeof(OPENFILENAME));
		ofn.lStructSize = sizeof(OPENFILENAME);
		ofn.hwndOwner = glfwGetWin32Window((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow());
		ofn.lpstrFile = szFile;
		ofn.nMaxFile = sizeof(szFile);
		ofn.lpstrFilter = filter;
		ofn.nFilterIndex = 1;
		ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
		if (GetSaveFileNameA(&ofn) == TRUE)
			return ofn.lpstrFile;

		return ;
	

调用这些代码的时候,有个Windows的搜寻窗口的filter,写法参考:https://docs.microsoft.com/en-us/office/vba/api/excel.application.getopenfilename,我这里的写法为:

// 前面的Hazel Scene(*.scene)是展示在filter里的text, 后面的*.scene代表显示的文件后缀类型
FileDialogWindowUtils::OpenFile("Hazel Scene (*.scene)\\0*.scene\\0");


给相关操作添加快捷键

这里既包括在菜单栏的UI上显示快捷键的按法,也包括在EditorLayer里实现对应的事件响应,比如快捷键保存Scene。暂时不是非常有必要,先不加了



Transformation Gizmos

这节课旨在绘制出代表物体的三条轴的gizmos,暂时不包括mouse picking,这里借用了第三方插件ImGuizmo来绘制gizmos,这是一位作者基于Dear ImGui开发出来的Gizmos项目,具体步骤如下:

  • 介绍ImGuizmo
  • 还是Fork该项目,然后添加并下载ImGuizmo项目到Hazel里
  • 集成ImGuizmo到Hazel里,添加filter,让ImGuizmo项目不使用PCH
  • 为Hierarchy里选中的GameObject绘制gizmos,分别是Translation、Rotation、Scale
  • 添加QWER快捷键

关于ImGuizmo

这个项目基于Dear ImGui开发了非常多的Gizmos的内容,甚至还有播放Timeline窗口和节点编辑器窗口,做了很多的UI工作,牛逼!如下图所示:




集成ImGuizmo到Hazel里

这里的ImGuizmo项目是依赖于ImGui项目的,而且代码量级不大,所以本身不适合作为单独的Project。之前有过对类似库的处理,就是glm库和stb_image库,如下所示:

虽然这俩也是vendor里的东西,glm是submodule,而stb_image不是,如下图所示:

glm里都是头文件,所以这里用的submodule,而stb_image就一个头文件,还有一个我们自己加的cpp文件用于定义宏,所以它就不是submodule了:

// stb_image.cpp里的文件
#include "hzpch.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

这里要使用到的ImGuizmo,既有.h,也有.cpp文件,还依赖于ImGui,所以放Hazel项目里是比较合适的,原本的项目有不少文件,这里Cherno只选择里面的两个文件放进来:

相关的premake文件如下:

	files
	
		"%prj.name/Src/**.h",
		"%prj.name/Src/**.cpp",
		
		"%prj.name/vendor/imguizmo/ImGuizmo.h",
		"%prj.name/vendor/imguizmo/ImGuizmo.cpp"

		-- 项目只加入了这俩文件
		"vendor/ImGuizmo/ImGuizmo.h",
		"vendor/ImGuizmo/ImGuizmo.cpp"
	

	-- 对于ImGuizmo里的cpp, 不使用PCH, 因为我并不想去修改Submodule里它的cpp文件
	-- 改了以后, 单独的submodule就不可以编译了
	filter "files:Hazel/vendor/imguizmo/ImGuizmo.cpp"
	--filter "files:%prj.name/vendor/imguizmo/ImGuizmo.cpp" -- 这么写是行不通的, 不知道为啥

为Hierarchy里选中的GameObject绘制gizmos

Cherno展示的代码如下:

// Gizmos
Entity selectedEntity = m_SceneHierarchyPanel.GetSelectedEntity();
if (selectedEntity && m_GizmoType != -1)

	// ImGui的摄像机投影为正交投影
			ImGuizmo::SetOrthographic(false);
			ImGuizmo::SetDrawlist();

			float windowWidth = (float)ImGui::GetWindowWidth();
			float windowHeight = (float)ImGui::GetWindowHeight();
			ImGuizmo::SetRect(ImGui::GetWindowPos().x, ImGui::GetWindowPos().y, windowWidth, windowHeight);

			// Camera
			auto cameraEntity = m_ActiveScene->GetPrimaryCameraEntity();
			const auto& camera = cameraEntity.GetComponent<CameraComponent>().Camera;
			const glm::mat4& cameraProjection = camera.GetProjection();
			glm::mat4 cameraView = glm::inverse(cameraEntity.GetComponent<TransformComponent>().GetTransform());

			// Entity transform
			auto& tc = selectedEntity.GetComponent<TransformComponent>();
			glm::mat4 transform = tc.GetTransform();

			// 绘制的时候, 需要传入camera的v和p矩阵, 再传入要看物体的transform矩阵即可, 就会绘制出
			// 其localGizmos
			ImGuizmo::Manipulate(glm::value_ptr(cameraView), glm::value_ptr(cameraProjection),
				ImGuizmo::OPERATION::TRANSLATE, ImGuizmo::LOCAL, glm::value_ptr(transform));
		

我这边先给SceneHierarchyPanel类提供了Get和Set选中GameObject的函数

namespace Hazel

	class Scene;
	class GameObject;
	class SceneHierarchyPanel
	
		const uint32_t INVALID_INSTANCE_ID = 999999;

	public:
		...

		void SetSelectedGameObject();
		GameObject& GetSelectedGameObject();
		...
	

然后调用绘制函数即可:

void EditorLayer::OnImGuiRender()

	...
	
	ImGui::Begin("Viewport");
	...
	uint32_t id = m_SceneHierarchyPanel.GetSelectedGameObjectId();
	bool succ;
	GameObject& selected = m_Scene->GetGameObjectById(id, succ);
	if (succ)
	
		ImGuizmo::SetOrthographic(true);
		ImGuizmo::BeginFrame();

		glm::mat4 v = m_OrthoCameraController.GetCamera().GetViewMatrix();
		glm::mat4 p = m_OrthoCameraController.GetCamera().GetProjectionMatrix();
		glm::mat4 trans = selected.GetTransformMat();
		EditTransform((float*)(&v), (float*)(&p), (float*)(&trans), true);
		selected.SetTransformMat(trans);
	
	ImGui::End();

这个EditTransform函数是核心函数,是我从Imguizmo项目里的main.cpp里抄过来的,改成了下面这样,作为EditorLayer类的成员函数:

// 绘制Viewport对应的窗口, 从而绘制gizmos, 传入的是camera的V和P矩阵, matrix的Transform对应的矩阵
void EditorLayer::EditTransform(float* cameraView, float* cameraProjection, float* matrix, bool editTransformDecomposition)

	switch (m_Option)
	
	case ToolbarOptions::Default:
		break;
	case ToolbarOptions::Translate:
		mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
		break;
	case ToolbarOptions::Rotation:
		mCurrentGizmoOperation = ImGuizmo::ROTATE;
		break;
	case ToolbarOptions::Scale:
		mCurrentGizmoOperation = ImGuizmo::SCALE;
		break;
	default:
		break;
	

	static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::LOCAL);
	static bool useSnap = false;
	static float snap[3] =  1.f, 1.f, 1.f ;
	static float bounds[] =  -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f ;
	static float boundsSnap[] =  0.1f, 0.1f, 0.1f ;
	static bool boundSizing = false;
	static bool boundSizingSnap = false;

	ImGuiIO& io = ImGui::GetIO();
	float viewManipulateRight = io.DisplaySize.x;
	float viewManipulateTop = 0;
	static ImGuiWindowFlags gizmoWindowFlags = 0;

	ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing);
	ImGui::SetNextWindowPos(ImVec2(400, 20), ImGuiCond_Appearing);

	ImGuizmo::SetDrawlist();

	// ImGuizmo的绘制范围应该与Viewport窗口相同, 绘制(相对于显示器的)地点也应该相同
	float windowWidth = (float)ImGui::GetWindowWidth();
	float windowHeight = (float)ImGui::GetWindowHeight();
	ImGuizmo::SetRect(ImGui::GetWindowPos().x, ImGui::GetWindowPos().y, windowWidth, windowHeight);
	
	ImGuiWindow* window = ImGui::GetCurrentWindow();
	gizmoWindowFlags = ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect
		(window->InnerRect.Min, window->InnerRect.Max) ? ImGuiWindowFlags_NoMove : 0;


	//ImGuizmo::DrawGrid(cameraView, cameraProjection, identityMatrix, 100.f);
	//ImGuizmo::DrawCubes(cameraView, cameraProjection, &objectMatrix[0][0], gizmoCount);
	ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, NULL, 
		useSnap ? &snap[0] : NULL, boundSizing ? bounds : NULL, boundSizingSnap ? boundsSnap : NULL);

	//viewManipulateRight = ImGui::GetWindowPos().x + windowWidth;
	//viewManipulateTop = ImGui::GetWindowPos().y;
	//float camDistance = 8.f;
	//ImGuizmo::ViewManipulate(cameraView, camDistance, ImVec2(viewManipulateRight - 128, viewManipulateTop), ImVec2(128, 128), 0x10101010);

这样就可以在viewport里通过gizmos修改Transform了



Editor Camera

Runtime下应该用CameraComponent渲染,而Editor下使用Scene对应的Camera渲染,我跟Cherno的做法不完全一样,但是思路差不多,具体有:

  • 把原本用作EditorCamera的OrthographicCamera和OrthographicCameraController重命名为EditorCameraEditorCameraController
  • 并完善EditorCamera类,专门负责Viewport的绘制,EditorCamera不再是原本的Orthographic Camera,而是支持两种模式,而且默认为透视投影
  • 添加快捷键,让EditorCamera在Viewport里移动、旋转
  • 神秘二次函数,在图像论坛上获取的,用于根据Viewport窗口大小,调整合适的Pan Speed,就是用中键来调整Viewport缩放

没啥特殊的,都是些搬砖的工作



Multiple Render Targets and Framebuffer Refactor——Clicking To Select Entities

我把这中间五节课的内容放到了一章来写,因为前面的很多节课都是为了实现某种功能而做的代码铺垫,我个人比较偏向于为了开发什么功能,而去修改引擎,而不是一下子把引擎的基础设施都搭建好。

本章核心内容,是为了能在Viewport里点选对应的GameObject,做了以下事情:

  • 介绍Select GameObject的原理,思路是渲染出ID贴图,贴图上每个像素代表了存储的GameObejct的id
  • 给现有的Framebuffer添加第二个Color Attachment,在一次pass里输出两个color output
  • 重构Framebuffer相关代码,主要是在创建framebuffer时输出指定的参数
  • Viewport里点击时,获取鼠标相对于Viewport的相对坐标,读取渲染得到的Color Attachment,从上面直接获取GameObject的id


Select GameObject的原理

思路是把每个Pixel上显示的GameObject的Id写在贴图上,意味着渲染出一张特殊的贴图,从而可以在鼠标点选时,根据相对窗口坐标,采样贴图得到点选的GameObject的id。

那么具体应该怎么做呢?

还是应该跟渲染逻辑一样,逐个渲染物体,把它们的ID渲染到贴图上,然后渲染时开启Depth Test,这个逻辑也是跟渲染相同,离相机最近的才写入。还要注意一个问题,如何在绘制物体的时候,在Fragment Shader里知道物体的ID,有两种直观思路:

  • 通过Vertex Shader传入
  • 通过Uniform传入

仔细想想,第二种方法其实是不可行的。因为引擎里已经实现了批处理,就是多个物体绘制走的一个Draw Call,它们是按照同一个合并后的大物体计算的,如果是第二种方法,比如绘制A、B、C,即使我在提交绘制命令时分别Upload各自的Uniform信息,最后ABC绘制出来的ID都会是C的ID。



给现有的Framebuffer添加第二个Color Attachment

Cherno写的引擎应该只用了一个fbo,我这里用到了俩,一个负责Render Viewport,一个负责Render CameraComponent。这里为了渲染出新的Instance ID的贴图,没必要开第三个fbo,因为它的VP矩阵跟Viewport是一样的,只需要加个Color Atttachment即可,可以在一个Pass里完成这个操作。

具体分为这么几个步骤:

  • CPU给framebuffer生成俩Color attachment
  • glsl的fragment shader里输出两个output
  • CPU端提供读取Instance贴图的函数

CPU给framebuffer生成俩Color attachment

貌似以前学OpenGL的时候也没写过这个操作,但之前学到的生成并绑定Texture Attachment的写法是这样的:

// 前面的代码其实就是创建普通的texture
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
  
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

// attach it to the frame buffer, 作为输出的texture
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); 

生成和绑定多个Color Attachment,是类似的,只需要修改这里的GL_COLOR_ATTACHMENT0就行了



Shader同时输出多个Color

参考:concept-what-is-the-use-of-gldrawbuffer-and-gldrawbuffers
参考:glDrawBuffers - state change or command?

准确的说是Fragment Shader同时输出多个Color,glsl的写法如下:

// Fragment shader
#version 330 core

in vec2 v_TexCoord;
in vec4 v_Color;
flat in int v_TexIndex;
in float v_TilingFactor;
flat in int v_InstanceId;


layout (location = 0) out vec4 out_color;
layout (location = 1) out int out_InstanceId;

uniform sampler2D u_Texture[32];

void main()

	out_color = texture(u_Texture[v_TexIndex], v_TexCoord * v_TilingFactor) * v_Color;
	out_InstanceId= v_TexIndex;

这里假设我有三个color attachment,但是我只需要在一个pass里填充其中的俩color attachment,那么我需要使用glDrawBuffers来同时渲染得到多个Texture,调用代码如下:

// shader里的第一个output会是GL_COLOR_ATTACHMENT2对应的color buffer
const GLenum buffers[] GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT0 ;
glDrawBuffers(2, buffers);

需要注意的是,glDrawBuffers函数并不是一个Draw Call命令,而是一个状态机参数设置的函数,它的作用是告诉OpenGL,把绘制output put填充到这些Attachment对应的Buffer里,所以这个函数在创建Framebuffer的时候就可以被调用了。



CPU读取Color Attachment里的pixel值

需要借助glReadPixels函数,该函数负责从framebuffer里读取a block of pixels,示例代码如下:

m_Framebuffer->Bind();
glReadBuffer(GL_COLOR_ATTACHMENT0 + attachmentIndex);
int pixelData;
glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixelData);```

我提供的代码如下:

// 获取单点pixel的像素值
int OpenGLFramebuffer::ReadPixel(uint32_t colorAttachmentId, int x, int y

搜索引擎技术学习

搜索引擎技术学习

搜索引擎技术学习#搜索引擎#技术#搜索推广

搜索引擎技术学习

以上是关于Hazel引擎学习的主要内容,如果未能解决你的问题,请参考以下文章

为何要学习游戏引擎底层技术

我是如何学习游戏引擎的?

我是如何学习游戏引擎的?

我是如何学习游戏引擎的?

我是如何学习游戏引擎的?

游戏开发入门游戏引擎架构