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)
{
Bind();
glReadBuffer(GL_COLOR_ATTACHMENT0 + colorAttachmentId);
int pixelData;
glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixelData);
return pixelData;
}
其他的内容就不多介绍了,看代码就行了
SPIR-V and the New Shader System
这节课内容比较多,核心目的有两个:
- 在引擎里只写一份Shader,但是该Shader能跑在不同的平台上,其实也就是引擎的Shader跨平台编译系统,更具体来说,目前是想让写的OpenGL的shader,能够跑在vulkan上
- 实现Shader的缓存,相当于编译出Shader在各个平台上的Binary文件,然后利用类似
glGetProgramBinary
去直接上传Shader,从而省去Runtime编译Shader的过程
具体分为以下步骤:
- 介绍SPIR-V (可以把shader翻译为跨平台中间语言的编译器)
- 介绍github上的shaderc项目(可以理解为封装了SPIR-V的github项目)
- 介绍Vulkan SDK,以及脚本一键安装Vulkan SDK环境
- 接入shaderc项目到Hazel里
- 修改现有的Shader文件,以支持vulkan
- Tip: Vulkan里的depth范围是[-1, 1],而OpenGL是[0, 1]
- 给调用的exe添加命令行参数
介绍SPIR-V
参考:8 reasons why SPIR-V makes a big difference
参考:why-do-we-need-spir-v
SPIR-V是一个Shader的编译器,它会把Shader编译为intermediate byte code。全称叫Standard Portable Intermediate Representation,是一种跨平台的中间语言。
SPIR-V is the first open standard, cross-API intermediate language for natively representing parallel compute and graphics.
SPIR-V的使用流程
实际使用的时候,比如我有一个vulkan用的shader,语法是glsl,那么怎么在OpenGL上用呢,具体步骤如下:
- 可以通过SPIR-V,把它编译为Binary文件
- 基于这个文件,通过SPIR-V的跨平台编译器,返回一个string,此时就是对应的OpenGL版本的shader(也是个文本文件),然后直接通过OpenGL来编译即可。
- 或者基于这个文件,通过SPIR-V的跨平台编译器,获取OpenGL版本的shader编译得到的binary文件,可以把它存到磁盘上,用的时候通过
glGetProgramBinary
类似的代码读取Shader。
从这个过程中可以看出,如果想要实现vulkan上的shader,在其他平台上使用,需要两个东西:
- SPIR-V的编译器,把vulkan的shader编译为中间语言
- SPIR-V的跨平台编译器,比如把它翻译为glsl在OpenGL上使用
我这里只是做个类比,任意平台上写的shader,应该都可以通过上面的步骤转换成别的平台的shader或shader binary.
SPIR-V对各个平台的shader转换的流程图
具体转化步骤如下图所示,转化流程需要从左往右看,这里的SPIR-V Tools代表的是shader被编译得到的中间语言,一些特定的平台,需要SPIRV-Cross负责把它再转化为其他的具体平台的语言:
具体到应用,为了在引擎里写OpenGL的shader,让它支持vulkan,参考上面的路径,应该是GLSL->glslang->SPIR-V Tools->Vulkan,所以这里需要:
- SPIR-V Tools
- glslang:负责编译glsl到SPIR-V Tools
这俩都是KhronosGroup的github仓库,好在谷歌为了方便使用,把上面两个库进行了封装,写在了shaderc里,直接把它加到引擎里即可。
介绍shaderc
shaderc is a collection of tools, libraries and tests for shader compilation
这个库封装了上面说的glslang和SPIRV-Tools两个库,目前包含了两个部分:
glslc
: 是在命令行执行的编译器,可以把GLSL/HLSL转成SPIR-Vlibshaderc
: 准确的说叫libshaderc_glslc
,a library API for accessing glslc functionality
所以说,为了把OpenGL的shader转换成vulkan支持的shader,引入这个shaderc就可以了,原本的想法是把该库作为submodule,类似于GLFW库一样集成到游戏引擎的子project里,但是可以借助Vulkan SDK
简化这个过程
介绍Vulkan SDK
这里并没有用原本的方法,把shaderc作为submodule,这是因为shaderc包含的内容比较大,没有必要加在引擎里。所以这里想要直接下载对应的Debug和Release下的库文件(类似于学习OpenGL时的操作)。
而Vulkan提供的SDK里面已经有了shaderc、glslang和SPIR-V Tools这些内容,所以我没有必要再去下载shaderc的源代码,再去编译出二进制文件。具体步骤如下:
- 通过官网提供的vulkan_sdk.exe,安装之后,可以得到上述的所有库文件,还有对应的头文件,如下图所示:
此时我们获取到的是头文件,以及对应库文件的Release版本,还需要去下载相同SDK版本的Debug版本,如下图所示:
可以看到,Vulkan SDK里面包含了所有我们需要的库文件,所以直接安装Vulkan SDK的本地环境,再配置到Hazel项目里即可。如下图所示,后面的字母d代表debug:
这里Cherno选择使用py脚本在SetUp阶段下载对应的SDK文件,这种做法也是其他游戏引擎或大型C++项目里常见的操作,就是准备一个SetUp的批处理文件,用于负责配置项目的环境。
顺便说句题外话,之前学习OpenGL的时候也有类似的操作,因为要使用GLFW库,同样可以选择类似的两种方式:
- 直接下载GLFW的lib文件,然后Link到项目里,同时把头文件的路径Include到项目里
- 下载GLFW的工程,作为子模块link到其他project上
脚本一键自动安装Vulkan SDK
作为引擎来说,这玩意儿是必要的,因为引擎的构建过程必然需要很多SDK配置环境,我不可能给一堆链接让用户自行下载。所以这里使用了python脚本,让批处理文件调用它,然后去指定的链接里下载对应的SDK文件,思路很简单:
- 先找到Vulkan SDK的下载路径
- 本机上确保存在python环境,否则无法调用py脚本
- 写py脚本,在里面验证py环境,如果不全,则重新下载python,完成此步骤后,同理验证Vulkan SDK环境
找到Vulkan SDK的下载路径
官网上,点击下载时,右键选择复制链接地址:
这样就得到了我要下载的文件路径了:
https://sdk.lunarg.com/sdk/download/1.2.170.0/windows/VulkanSDK-1.2.170.0-DebugLibs.zip
写py文件自动下载对应的资源
先写一个批处理文件,来运行对应的py脚本:
@echo off
python Setup.py
PAUSE
再写调用的py脚本,Cherno这里分了多个python文件,然后通过import PyFileName
的写法执行对应的PyFileName.py
文件,我不喜欢这种很复杂的脚本嵌套方式,就全部写在一个SetUp.py
文件里了:
# os代表操作系统, 为了让脚本能与os打交道, 这里相当于创建了一个OS模块的对象os
import os
# 创建subprocess模块对象, 可以调用其他的exe
import subprocess
# ============================= 1.Check Python environment ======================================
# The pkg_resources module provides runtime facilities for finding, introspecting, activating and using installed Python distributions.
import pkg_resources
def install(package):
print(f"Installing {package} module...")
subprocess.check_call(['python', '-m', 'pip', 'install', package])
def ValidatePackage(package):
required = { package }
installed = {pkg.key for pkg in pkg_resources.working_set}
missing = required - installed
# 安装缺失的package
if missing:
install(package)
def ValidatePackages():
ValidatePackage('requests')
ValidatePackage('fake-useragent')
# Make sure everything we need is installed, 调用它的ValidatePackage函数
# 这里是保证python环境是完整的
ValidatePackages()
# ============================= 2.define Utils function ======================================
import requests
import sys
import time
from fake_useragent import UserAgent
# 创建一个帮助显示下载进度的函数
def DownloadFile(url, filepath):
with open(filepath, 'wb') as f:
ua = UserAgent()
headers = {'User-Agent': ua.chrome}
response = requests.get(url, headers=headers, stream=True)
total = response.headers.get('content-length')
if total is None:
f.write(response.content)
else:
downloaded = 0
total = int(total)
startTime = time.time()
for data in response.iter_content(chunk_size=max(int(total/1000), 1024*1024)):
downloaded += len(data)
f.write(data)
done = int(50*downloaded/total)
percentage = (downloaded / total) * 100
elapsedTime = time.time() - startTime
avgKBPerSecond = (downloaded / 1024) / elapsedTime
avgSpeedString = '{:.2f} KB/s'.format(avgKBPerSecond)
if (avgKBPerSecond > 1024):
avgMBPerSecond = avgKBPerSecond / 1024
avgSpeedString = '{:.2f} MB/s'.format(avgMBPerSecond)
sys.stdout.write('\r[{}{}] {:.2f}% ({}) '.format('█' * done, '.' * (50-done), percentage, avgSpeedString))
sys.stdout.flush()
sys.stdout.write('\n')
def YesOrNo():
while True:
reply = str(input('[Y/N]: ')).lower().strip()
if reply[:1] == 'y':
return True
if reply[:1] == 'n':
return False
# ============================= 3.Check Vulkan SDK environment ======================================
# Vulkan是在Setup.py相同路径下的Vulkan.py文件
from pathlib import Path
from io import BytesIO
# urllib.requests是python提供打开和读取URL的库
from urllib.request import Request, urlopen
from zipfile import ZipFile
# VULKAN_SDK代表系统环境变量里存的VULKAN_SDK的版本
VULKAN_SDK = os.environ.get('VULKAN_SDK')
# 官网上下载的url路径
VULKAN_SDK_INSTALLER_URL = 'https://sdk.lunarg.com/sdk/download/1.2.170.0/windows/vulkan_sdk.exe'
HAZEL_VULKAN_VERSION = '1.2.170.0'
# 下载到本地的相对路径
VULKAN_SDK_EXE_PATH = 'Hazel/vendor/VulkanSDK/VulkanSDK.exe'
# 核心的安装SDK的函数
def InstallVulkanSDK():
print('Downloading {} to {}'.format(VULKAN_SDK_INSTALLER_URL, VULKAN_SDK_EXE_PATH))
DownloadFile(VULKAN_SDK_INSTALLER_URL, VULKAN_SDK_EXE_PATH)
print("Done!")
print("Running Vulkan SDK installer...")
os.startfile(os.path.abspath(VULKAN_SDK_EXE_PATH))
print("Re-run this script after installation")
# 当SDK未被安装, 或版本不对时, 调用此函数, 进而调用InstallVulkanSDK
def InstallVulkanPrompt():
print("Would you like to install the Vulkan SDK?")
install = YesOrNo()
if (install):
InstallVulkanSDK()
quit()
# 检查Vulkan SDK是否被安装
def CheckVulkanSDK():
if (VULKAN_SDK is None):
print("You don't have the Vulkan SDK installed!")
InstallVulkanPrompt()
return False
# 目前所用的sdk版本是1.2.170.0
elif (HAZEL_VULKAN_VERSION not in VULKAN_SDK):
print(f"Located Vulkan SDK at {VULKAN_SDK}")
print(f"You don't have the correct Vulkan SDK version! (Hazel requires {HAZEL_VULKAN_VERSION})")
# 调用此文件内的InstallVulkanPrompt函数, 可以在命令行安装Vulkan SDK
InstallVulkanPrompt()
return False
print(f"Correct Vulkan SDK located at {VULKAN_SDK}")
return True
VulkanSDKDebugLibsURL = 'https://sdk.lunarg.com/sdk/download/1.2.170.0/windows/VulkanSDK-1.2.170.0-DebugLibs.zip'
OutputDirectory = "Hazel/vendor/VulkanSDK"
TempZipFile = f"{OutputDirectory}/VulkanSDK.zip"
def CheckVulkanSDKDebugLibs():
shadercdLib = Path(f"{OutputDirectory}/Lib/shaderc_sharedd.lib")
if (not shadercdLib.exists()):
print(f"No Vulkan SDK debug libs found. (Checked {shadercdLib})")
print("Downloading", VulkanSDKDebugLibsURL)
# with关键词是为了在urlopen失败时
req = Request(url=VulkanSDKDebugLibsURL, headers={'User-Agent': 'Toby'})
#with urlopen(VulkanSDKDebugLibsURL) as zipresp:
with urlopen(req) as zipresp:
with ZipFile(BytesIO(zipresp.read())) as zfile:
zfile.extractall(OutputDirectory)
print(f"Vulkan SDK debug libs located at {OutputDirectory}")
return True
# 调用CheckVulkanSDK函数
if (not CheckVulkanSDK()):
print("Vulkan SDK not installed.")
if (not CheckVulkanSDKDebugLibs()):
print("Vulkan SDK debug libs not found.")
# ============================= 4.Call premake5.exe to build solution ======================================
print("Running premake...")
subprocess.call(["vendor/bin/premake/premake5.exe", "vs2019"])
执行完此脚本,就可以看到对应的库文件都会在这个文件夹下了:
修改Premake5.lua以使用shaderc库
梳理一下目前的情况,使用这些跨平台shader编译的东西,项目需要这三个内容:
- Debug下的库文件
- Release下的库文件
- 头文件
这里的Debug下的库文件好说,我们直接下载的zip文件,解压到了这里:
至于后面两个,其实是调用的Vulkan_SDK.exe,安装的路径是用户自定义的,比如我这里:
那么我肯定是要在lua里include这个头文件的,而且要link这个路径下的lib的,重点在于,安装Vulkan_SDK.exe时,会添加如下所示的环境变量:
VULKAN_SDK F:\VulkanSDK\1.2.170.0
所以我在lua里调用系统API,获取路径即可,写法如下:
VULKAN_SDK = os.getenv("VULKAN_SDK")
那此时依赖的这三个东西路径分别为:
-- Debug下的库文件
"%{wks.location}/Hazel/vendor/VulkanSDK/Lib"
-- Release下的库文件
"%{VULKAN_SDK}/Lib"
-- 头文件
"%{VULKAN_SDK}/Include"
升级SDK版本
看了下Cherno提供的版本,里面的debug对应的lib文件是dll的导入库文件,而缺少对应的dll,感觉用1.3.216的VulkanExe更合适,如下图所示,这里可以顺便把对应的环境都安装好,不用自己再去下Debug的zip文件了:
添加编译出Shader Cache的函数
需要修改原本的OpenGLShader类,添加新功能,可以把现有的Shader2D.glsl编译为OpenGL平台用的Binary Cache,以及Vulkan平台用的Binary Cache,新加的接口如下:
namespace Hazel
{
class OpenGLShader : public Shader
{
public:
OpenGLShader(const std::string& path);
...
private:
void CompileOrGetOpenGLBinaries();
void CompileOrGetVulkanBinaries(const std::unordered_map<ShaderType, std::string>& shaderSources);
void Reflect(ShaderType stage, const std::vector<uint32_t>& shaderData);
private:
// m_VulkanSPIRVCache相当于一个缓存的unordered_map, key是子着色器类型
// value是一个uint32的数组, 代表着Spir-V对应的编译结果
std::unordered_map<ShaderType, std::vector<uint32_t>> m_VulkanSPIRVCache;
std::unordered_map<ShaderType, std::vector<uint32_t>> m_OpenGLSPIRVCache;
std::unordered_map<ShaderType, std::string> m_OpenGLSourceCode;
std::string m_FilePath;
};
}
核心函数代码如下:
// shaderSources的每个pair对应一个shader, 比如fragment shader, 本函数会为每个pair生成对应的vulkan类型的binary文件
void OpenGLShader::CompileOrGetVulkanBinaries(const std::unordered_map<ShaderType, std::string>& shaderSources)
{
GLuint program = glCreateProgram();
// 1. 创建编译器: shaderc提供的glslang编译器
shaderc::Compiler compiler;
shaderc::CompileOptions options;
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
const bool optimize = true;
if (optimize)
options.SetOptimizationLevel(shaderc_optimization_level_performance);
std::filesystem::path cacheDirectory = Utils::GetShaderCacheDirectory();
std::unordered_map<ShaderType, std::vector<uint32_t>>& shaderData = m_VulkanSPIRVCache;
shaderData.clear();
std::filesystem::path shaderPath = m_FilePath;
const std::string& shaderName = shaderPath.replace_extension().filename().string();
// 2. 遍历并编译每个shader, 比如vert或frag, 为其生成一个单独的binary文件
for (auto&& [stage, source] : shaderSources)
{
std::filesystem::path shaderFilePath = m_FilePath;
std::filesystem::path cachedPath = Utils::GetCurrentDirectory() + "/" + cacheDirectory.string() + "/" + shaderName + "/" + Utils::ShaderTypeToString(stage) + ".shadercache";
std::ifstream in(cachedPath, std::ios::in | std::ios::binary);
// 如果有现成的缓存文件, 那么直接读取该文件, 存到data里
if (in.is_open())
{
in.seekg(0, std::ios::end);
auto size = in.tellg();
in.seekg(0, std::ios::beg);
auto& data = shaderData[stage];
data.resize(size / sizeof(uint32_t));
in.read((char*)data.data(), size);
}
// 否则编译出新的文件
else
{
// 把glsl文件编译为Spir-v
///shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, Utils::GLShaderStageToShaderC(stage), m_FilePath.c_str(), options);
shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, (shaderc_shader_kind)0, m_FilePath.c_str(), options);
if (module.GetCompilationStatus() != shaderc_compilation_status_success)
{
CORE_LOG_ERROR(module.GetErrorMessage());
HAZEL_CORE_ASSERT(false, "");
}
shaderData[stage] = std::vector<uint32_t>(module.cbegin(), module.cend());
std::ofstream out(cachedPath, std::ios::out | std::ios::binary);
if (out.is_open())
{
auto& data = shaderData[stage];
out.write((char*)data.data(), data.size() * sizeof(uint32_t));
out.flush();
out.close();
}
}
}
// 3. 所有的shader都读到shaderData, 目前只是从compiler里获取每个shader里的信息而已(严格来说, 这段代码不应该出现在编译shader函数里)
for (auto&& [stage, data] : shaderData)
Reflect(stage, data);
}
void OpenGLShader::CompileOrGetOpenGLBinaries()
{
auto& shaderData = m_OpenGLSPIRVCache;
// 一些谷歌提供的shaderc的写法
shaderc::Compiler compiler;
shaderc::CompileOptions options;
options.SetTargetEnvironment(shaderc_target_env_opengl, shaderc_env_version_opengl_4_5);
const bool optimize = false;
if (optimize)
options.SetOptimizationLevel(shaderc_optimization_level_performance);
// 获取shader缓存目录
std::filesystem::path cacheDirectory = Utils::GetShaderCacheDirectory();
shaderData.clear();
std::filesystem::path shaderPath = m_FilePath;
const std::string& shaderName = shaderPath.replace_extension().filename().string();
//m_OpenGLSourceCode.clear();
for (auto&& [stage, spirv] : m_VulkanSPIRVCache)
{
std::filesystem::path shaderFilePath = m_FilePath;
std::filesystem::path cachedPath = Utils::GetCurrentDirectory() + "/" + cacheDirectory.string() + "/" + shaderName + "/" + Utils::ShaderTypeToString(stage) + ".shadercache";
std::ifstream in(cachedPath, std::ios::in | std::ios::binary);
if (in.is_open())
{
in.seekg(0, std::ios::end);
auto size = in.tellg();
in.seekg(0, std::ios::beg);
auto& data = shaderData[stage];
data.resize(size / sizeof(uint32_t));
in.read((char*)data.data(), size);
}
else
{
spirv_cross::CompilerGLSL glslCompiler(spirv);
m_OpenGLSourceCode[stage] = glslCompiler.compile();
auto& source = m_OpenGLSourceCode[stage];
//shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, Utils::GLShaderStageToShaderC(stage), m_FilePath.c_str());
shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, (shaderc_shader_kind)0, m_FilePath.c_str());
if (module.GetCompilationStatus() != shaderc_compilation_status_success)
{
CORE_LOG_ERROR(module.GetErrorMessage());
HAZEL_CORE_ASSERT(false, "");
}
shaderData[stage] = std::vector<uint32_t>(module.cbegin(), module.cend());
std::ofstream out(cachedPath, std::ios::out | std::ios::binary);
if (out.is_open())
{
auto& data = shaderData[stage];
out.write((char*)data.data(), data.size() * sizeof(uint32_t));
out.flush();
out.close();
}
}
}
}
void OpenGLShader::Reflect(ShaderType stage, const std::vector<uint32_t>& shaderData)
{
spirv_cross::Compiler compiler(shaderData);
// 通过编译器获取ShaderResources
spirv_cross::ShaderResources resources = compiler.get_shader_resources();
//CORE_LOG("OpenGLShader::Reflect - {0} {1}", Utils::GLShaderStageToString(stage), m_FilePath);
CORE_LOG(" {0} uniform buffers", resources.uniform_buffers.size());
CORE_LOG(" {0} resources", resources.sampled_images.size());
CORE_LOG("Uniform buffers:");
for (const auto& resource : resources.uniform_buffers)
{
const auto& bufferType = compiler.get_type(resource.base_type_id);
uint32_t bufferSize = compiler.get_declared_struct_size(bufferType);
uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding);
int memberCount = bufferType.member_types.size();
CORE_LOG(" {0}", resource.name);
CORE_LOG(" Size = {0}", bufferSize);
CORE_LOG(" Binding = {0}", binding);
CORE_LOG(" Members = {0}", memberCount);
}
}
修改现有的Shader文件,以支持vulkan
现有的shader内容如下:
#type vertex
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTex;
layout(location = 2) in vec4 aCol;
layout(location = 3) in int aTexIndex;
layout(location = 4) in float aTilingFactor;
layout(location = 5) in int aInstanceId;
uniform mat4 u_ViewProjection;
out vec2 v_TexCoord;
out vec4 v_Color;
flat out int v_TexIndex;
out float v_TilingFactor;
flat out int v_InstanceId;
void main()
{
gl_Position = u_ViewProjection * vec4(aPos, 1.0);
v_TexCoord = aTex;
v_Color = aCol;
v_TexIndex = aTexIndex;
v_TilingFactor = aTilingFactor;
v_InstanceId = aInstanceId;
}
#type fragment
#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_InstanceId;
}
如果想用这个glsl文件去编译得到Vulkan的binary,编译会出现如下报错:
// error: 'non-opaque uniforms outside a block' : not allowed when using GLSL for Vulkan
uniform mat4 u_ViewProjection;
// error: 'location' : SPIR-V requires location for user input/output
out vec2 v_TexCoord;
这两种报错,都可以在前面加上layout(location = x)来解决,加上之后OpenGL的代码仍然可以跑
uniform升级为Uniform Buffer
虽然这里的vulkan sdk能帮助我们把OpenGL版本的shader翻译为Vulkan的binary文件,vulkan也是用的glsl语言,但它的glsl写法与OpenGL的是不相同的,二者都有一些对方没有的shader概念,比如这里的uniform,写法是不对的。vulkan里对于uniform跟OpenGL是不一样的,vulkan里没有Uniforms这个概念,但有Uniform buffer这个概念。
关于Uniform Buffer,其实跟我理解的Uniform是差不多的,它就是一块位于GPU上的内存,可以被所有的Shader共享。问题就在于,目前的做法是,对于每个Shader Program,我都通过glUniformMatrix4fv
函数进行Upload Uniform,这样是不合理的,它其实应该没有用到Uniform Buffer,而是在每个Shader Program里设置了uniform对应的内存,应该直接从CPU端在GPU上设置一块uniform buffer,存储场景里的Camera的VP矩阵,因为这玩意儿是所有Shader共享的东西,没有必要单独存在于每个Shader Program里。
其实OpenGL里面也是有Uniform Buffer的概念,写法如下:
// OpenGL里的uniform也是一种buffer, 只不过类型为GL_UNIFORM_BUFFER
glCreateBuffers(1, &m_RendererID);
glNamedBufferData(m_RendererID, size, nullptr, GL_DYNAMIC_DRAW); // TODO: investigate usage hint
glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_RendererID);
然后修改对应的Shader文件即可:
uniform mat4 u_Transform;
需要写成:
// push一个constant的uniform buffer, std140代表layout
layout(std140, push_constant) uniform Transform
{
mat4 Transform;
}
或者写成:
// 这里的binding类似于layout(location = 1)的location,应该是绑定到0号槽位的uniform buffer上
layout(std140, binding = 0) uniform Transform
{
mat4 Transform;
}
再在绘制的时候,每帧的BeginScene里传一下数据就行了:
glNamedBufferSubData(m_RendererID, 0, sizeof(glm::mat4), glm::value_ptr(s_SceneData.ViewProjectionMatrix));
读取Shader Cache文件
最后再把Cache读取进来就行了,核心API为:
std::vector<GLuint> shaderIDs;
for (auto&& [stage, spirv] : m_OpenGLSPIRVCache)
{
GLuint shaderID = shaderIDs.emplace_back(glCreateShader(Utils::ShaderTypeToOpenGL(stage)));
glShaderBinary(1, &shaderID, GL_SHADER_BINARY_FORMAT_SPIR_V, spirv.data(), spirv.size() * sizeof(uint32_t));
glSpecializeShader(shaderID, "main", 0, nullptr, nullptr);
glAttachShader(m_RendererID, shaderID);
}
不过现在应该有个问题,在存在Shader Cache的情况下,如果Shader Source改写了,应该也不会重新编译Shader,后面再解决这个问题吧
给调用的exe添加命令行参数
修改main函数,大概改为这个样子:
int main(int argc,char * argv[]); //int main(int argc,char ** argv);
比较基础,就不多说了
附录
尝试Build imguizmo项目
他这里其实没有留下创建Solution的东西,我尝试了一下,最后还是手动创建VS项目,Add对应文件进来的,不过这个过程走了不少弯路,在这里记录一下。
这个imguizmo需要使用vcpkg和cmake来build工程,所以这里研究一下:
参考:https://gamefromscratch.com/vcpkg-cpp-easy-mode-step-by-step-tutorial/
参考:https://vcpkg.readthedocs.io/en/latest/users/integration/
首先,git clone对应的github工程,然后执行批处理后,输出integrate
指令:
bootstrap-vcpkg.bat
vcpkg integrate install
然后输入指令,搜索需要的package,貌似vcpkg里有两千多个项目可以被搜索到:
// vcpkg search imguizmo // 默认是32位的
vcpkg search imguizmo:x64-windows // 这里应该输入这个
如下图所示是我的搜索结果:
既然搜索到了,安装即可:
vcpkg install imguizmo
输入vcpkg list
可以看到已经安装好的package,此时可以在对应子目录里看到安装的东西了,如下图所示,bin文件夹里有个可以Run的exe文件:
exe跑起来如下图所示:
At this point vcpkg will do the magic of downloading and installing the raylib library for you. Due to the “integrate install” command we ran earlier your library is ready to go in Visual Studio, no further actions require. No need to configure the linker, simple add the appropriate #include to the top of your code and presto, you’re ready to go!
其实做到这里,我并没有得到最终的Solution,前面这一系列的操作,基于vcpkg integrate install
,实际上是帮我把imguizmo这个库安装到了我本机的电脑上,如下图所示,我随便创建任何一个cpp solution,里面都会自动link我这个安装的环境(应该是使用到就link),我这里的include文件都能索引到imguizmo和它依赖的imgui代码:
看了下,应该跟vcpkg没有关系,所以我接着输入vcpkg integrate remove
移除掉了这个全局的Link。我需要的是根据这个Makefile创建出对应的Solution,一开始我下载了NMake,配置了到了System Path里,不过没有作用,显示编译报错,就是NMake不太能识别我这个Makefile的语法:
makefile(14) : fatal error U1033: syntax error : ':' unexpected
Stop.
最后看到这个文件的提交记录:
下载安装MinGw,添加到系统Path,然后在路径里输入mingw32-make.exe,就可以Build出exe了,不过还是没啥卵用… Makefile应该是帮助build的,对于创建Visual Studio里的Solution工程而言,并没有什么卵用。最后我还是乖乖创建VS项目,把头文件和源文件都加进去了。。。
Decompose Transfomation Matrix和glm::decompose函数
单独写在文章里了
EditorCamera的移动
我在引擎里写了这么一段代码,可能需要额外解释一下:
// EditorCamera.h文件里
class EditorCamera
{
...
// 在一开始指定Editor相机的位置和朝向
glm::vec3 m_Position = glm::vec3(0, 0, 3.0f); // 正交投影下的相机位置不重要
// WXYZ, 初始方向朝-Z方向
glm::quat m_Rotation = { 0, 1, 0, 0 };// 正交投影下的相机只会有绕Z轴的旋转
// 这里是用0,0,1代表right, 1,0,0代表foward
glm::vec3 GetLocalForward() { return m_Rotation * glm::vec3{ 0, 0, 1 }; }
glm::vec3 GetLocalRight() { return m_Rotation * glm::vec3{ 1, 0, 0 }; }
glm::vec3 GetLocalUp() { return m_Rotation * glm::vec3{ 0, 1, 0 }; }
}
然后我这么写相机的移动代码:
// EditorCameraController.cpp里
if (Input::IsMouseButtonPressed(1))// 按住鼠标右键时
{
if (m_Camera.IsOrthographicCamera())
{
...
}
else if (m_Camera.IsProjectiveCamera())
{
// 直接告诉我, 这里左移就用减号
if (Input::IsKeyPressed(HZ_KEY_A))
m_CameraPosition -= m_Camera.GetLocalRight() * (float)ts;
else if (Input::IsKeyPressed(HZ_KEY_D))
m_CameraPosition += m_Camera.GetLocalRight() * (float)ts;
else if (Input::IsKeyPressed(HZ_KEY_W))
m_CameraPosition += m_Camera.GetLocalForward() * (float)ts;
else if (Input::IsKeyPressed(HZ_KEY_S))
m_CameraPosition -= m_Camera.GetLocalForward() * (float)ts;
...
}
}
我发现有个问题,就是我按A键,相机没有左移,而是右移,仔细思考了一下,发现是坐标系的原因,OpenGL里都是右手坐标系(Unity和UE都是用的左手坐标系),比如我一开始相机的LocalAxis是这样的,跟世界坐标系相同:
为了让相机看向-z方向,需要把它绕Y轴旋转180°:
旋转过后的效果如下图所示,黄色方框是相机要看的内容:
很明显,此时的相机的LocalRight在左边,这意味着如果我想向右移动相机,应该是这么写代码:
// LocalRight其实在左边
if (Input::IsKeyPressed(HZ_KEY_A))
m_CameraPosition += m_Camera.GetLocalRight() * (float)ts;
else if (Input::IsKeyPressed(HZ_KEY_D))
m_CameraPosition -= m_Camera.GetLocalRight() * (float)ts;
可能这就是游戏引擎不用右手坐标系的原因吧。。。。
Dear ImGui的GetPos函数
看到了好几个API,比如这些:
// 返回的是鼠标在显示器上的坐标
ImGui::GetMousePos();
// 获取当前窗口(左上角)相对于屏幕坐标系的位置
ImGui::GetWindowPos();
// 获取Cursor相对于屏幕坐标系的位置
ImGui::GetCursorScreenPos();
// 获取Cursor相对于屏幕的local位置(include scroll offset)
ImGui::GetCursorPos();
但是这个Cursor Pos好像不会随着我鼠标移动而改变,可能我的窗口少个flag还是啥的
Dear ImGui获取当前window在屏幕坐标系的Rect
代码如下:
ImGui::Begin("Viewport");
// 计算viewport窗口内部可视范围的相对坐标(去掉Margin之后的可视部分)
auto viewportMinRegion = ImGui::GetWindowContentRegionMin();
auto viewportMaxRegion = ImGui::GetWindowContentRegionMax();
auto viewportOffset = ImGui::GetWindowPos();
// 计算Viewport窗口在屏幕坐标系的Rect
m_ViewportBounds[0] = { viewportMinRegion.x + viewportOffset.x, viewportMinRegion.y + viewportOffset.y };
m_ViewportBounds[1] = { viewportMaxRegion.x + viewportOffset.x, viewportMaxRegion.y + viewportOffset.y };
ImGui::End();
Python的一些语法
with关键字
参考:with statement in Python
with statement in Python is used in exception handling to make the code cleaner and much more readable. It simplifies the management of common resources like file streams. Observe the following code example on how the use of with statement makes code cleaner.
with关键字会让python自动去处理文件读取失败时的Dispose函数,类似于调用fclose函数
as关键字
其实就是创建一个变量,as后面跟的是变量名
下载和解压下载得到的文件
参考:Downloading and Unzippig a Zip File
下载403报错
http error 403: forbidden occurs,是因为下载的时候没添加自己的header,参考Problem HTTP error 403 in Python 3 Web Scraping,是这么写:
# urllib.requests是python提供打开和读取URL的库
from urllib.request import Request, urlopen
def CheckVulkanSDKDebugLibs():
shadercdLib = Path(f"{OutputDirectory}/Lib/shaderc_sharedd.lib")
if (not shadercdLib.exists()):
print(f"No Vulkan SDK debug libs found. (Checked {shadercdLib})")
print("Downloading", VulkanSDKDebugLibsURL)
# with关键词是为了在urlopen失败时
req = Request(url=VulkanSDKDebugLibsURL, headers={'User-Agent': 'Toby'})
#with urlopen(VulkanSDKDebugLibsURL) as zipresp:
with urlopen(req) as zipresp:
with ZipFile(BytesIO(zipresp.read())) as zfile:
zfile.extractall(OutputDirectory)
print(f"Vulkan SDK debug libs located at {OutputDirectory}")
return True
判断lib文件是静态库文件还是dll的导入库文件
参考:https://blog.csdn.net/Liuqz2009/article/details/107789424
起初是因为我在跑HazelEditor时,有这么一个dll找不到的报错:
我明明是用的该文件的lib,但是显示的却是找不到dll文件,最后发现这个文件不是库的static版本,而是对应的dll匹配的lib文件(方便快速定位dll里的symbols的位置的lib)
所以这里介绍一下,如何判断一个.lib文件是静态库还是动态库的导入库
微软的Visual Studio提供了lib.exe,可以帮助判断,我存在的目录为:
或者是这个路径,总之是你自己安装MSVC的路径:
D:\VS2019\VC\Tools\MSVC\14.28.29910\bin\Hostx64\x64
运行 lib /list hello.lib
如果输出: hello.obj,则是静态库
如果输出: hello.dll,则是动态库的导入库。
我是这么输入的:
lib /list C:\GitRepository\Hazel\Hazel\Hazel\vendor\VulkanSDK\Lib\shaderc_sharedd.lib
最终打印消息为:
shaderc_sharedd.dll
shaderc_sharedd.dll
shaderc_sharedd.dll
...// 很多个重复的信息
shaderc_sharedd.dll
shaderc_sharedd.dll
shaderc_sharedd.dll
shaderc_sharedd.dll
所以说明我下的这个lib文件不对劲,它其实是dll文件匹配的lib导入库,但是真正的dll文件,也不一定需要放在exe的同名目录,还可以放到别的地方
Visual Studio是怎么找dll的
参考:Visual Studio提示由于找不到dll,无法继续执行代码的问题解决
Windows在查找Visual Studio的dll时,会按照如下路径来查找
1. 包含EXE文件的目录
2. 进程的当前工作目录
3. Windows系统目录
4. Windows目录
5. 列在Path环境变量中的一系列目录
所以为了解决上面的HazelEditor时dll找不到的报错,应该保证对应的dll被加到windows的环境变量里,就可以了