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重命名为
EditorCamera
和EditorCameraController
类 - 并完善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引擎学习的主要内容,如果未能解决你的问题,请参考以下文章