好久不见!
个人库的地址:(GitHub - JJJJJJJustin/Nut: The game_engine which learned from Cherno),可以看到我及时更新的结果。
-------------------------------Saving & Loading scene------------------------------------
》》》》更改 Premake 文件构架
这一集中 Cherno 对 premake 文件进行了操作,不过此时 Premake 文件的构架发生了改变(现在每个项目的 premake 被放置在项目的文件夹下,而不是集中放置在 Nut 根目录下的 Premake 文件中),这是因为之前的一次 pull request。
本来准备先完善引擎 UI ,后面集中对引擎进行维护,现在看来就先提交一下这个更改吧。
具体可以参考:( https://github.com/TheCherno/Hazel/pull/320 )
》》》》一个问题:关于 premake 文件中的命名
当我将 yaml-cpp 作为键(Key) ,并以此来索引存储的值 (Value),此时会出现一个错误:
Error: [string "return IncludeDir.yaml-cpp"]:1: attempt to perform arithmetic on a nil value (field 'yaml') in token: IncludeDir.yaml-cpp | ( 错误:[string“return IncludeDir.yaml-cpp”]:1:尝试对令牌中的零值(字段“yaml”)执行算术运算:IncludeDir.yaml-cpp ) |
编译器似乎将 '-' 识别为算术运算符,而不是文本符号,这导致他尝试进行算术运算操作。
但是当我将 '-' 更改为 '_' 时,这样的问题便消失了。
》》》》关于最新的 YAML 导致链接错误的解决方案
编译器疑似在以动态库的方式尝试运行 yaml-cpp 库,并发出了很多警告
初步解决方案:
》》 AND..
首先我已经在 yaml-cpp 的 premake 文件中声明了 "YAML_CPP_STATIC_DEFINE" ,并且打开了 staticruntime,但我发现没有作用。
接着解决:
问题是,你还需要在你所使用项目的 premake 文件中再次声明 "YAML_CPP_STATIC_DEFINE"
总结:
》》》》什么是 .editorconfig 文件?有什么作用?
问题引入:在深入研究这次提交时,一个以 .editorconfig 署名的文件映入眼帘,这是什么文件?
文件介绍:
EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readable and they work nicely with version control systems. 来自 <EditorConfig> | 翻译: EditorConfig 可帮助多个开发人员在不同的编辑器或 IDE 上维护同一个项目的编码风格,使其保持一致。EditorConfig 项目包含一个用于定义编码风格的文件格式和一组文本编辑器插件,这些插件可让编辑器读取文件格式并遵循定义的风格。EditorConfig 文件易于阅读,并且可与版本控制系统完美配合。 |
作用:
通过使用 EditorConfig 文件,团队中的每个成员可以确保他们的代码遵循相同的格式,降低因代码风格不一致而引起的问题。许多现代代码编辑器和 IDE(如 Visual Studio Code、Atom、JetBrains 系列等)都支持 EditorConfig,可以自动读取这些规则并应用到打开的文件中。
使用规范:
文件名: | 文件名为 .editorconfig,通常放在项目根目录。 |
键值对格式: | 使用 key = value 的形式定义规则,每条规则占一行。 空行和以 # 开始的行会被视为注释。 |
范围选择器: | 使用 [*] 表示应用于所有文件,也可以使用其他模式如 *.js 或 *.py 来指定特定文件类型。 |
支持的属性:(支持的键值对) | 常用属性包括: root:指示是否为顶层文件。 end_of_line:指定行结束符(如 lf, crlf, cr)。 insert_final_newline:是否在文件末尾插入换行符。 indent_style:设置缩进样式(如 tab 或 space)。 indent_size:指定缩进的大小,可以是数字或 tab。 charset:文件字符集(如 utf-8, latin1 等)。 trim_trailing_whitespace:是否修剪行尾空白。 |
详情参考文档:( EditorConfig Specification — EditorConfig Specification 0.17.2 documentation )
代码理解:
root = true: | 指示这是一个顶层的 EditorConfig 文件,编辑器在找到此文件后不会再向上查找其他 EditorConfig 文件。 |
[*]: | 表示应用于所有文件类型的规则。 |
end_of_line = lf: | 指定行结束符为 Unix 风格的换行符(LF,Line Feed)。这通常在类 Unix 系统(如 Linux 和 macOS)中使用。 |
insert_final_newline = true: | 指定在每个文件的末尾插入一个换行符。这是一种良好的编码习惯,许多项目标准要求这样做。 |
indent_style = tab: | 指定缩进样式为制表符(tab),而不是空格。这会影响代码的缩进方式。 |
《《《《拓展:什么是 Hard tabs?什么是 Soft tabs?
Hard Tabs | 是使用制表符进行缩进,具有灵活性但可能导致跨环境的不一致。 |
Soft Tabs | 是使用空格进行缩进,保证了一致性但文件体积可能更大。 |
选择使用哪种方式通常取决于团队的编码标准或个人偏好。
》》》》 Y A M L U know what I'm saying
》》》》YAML YAML YAML
》》》》关于这次 premake 构架的维护,我只上传了一部分,剩下的留到之后维护时再做。现在我去了解一下 YAML。
》》》》YAML, What is yaml ? What we can do by yaml ?
介绍:
YAML is a human-readable data serialization language that is often used for writing configuration files. Depending on whom you ask, YAML stands for yet another markup language or YAML ain't markup language (a recursive acronym), which emphasizes that YAML is for data, not documents. 来自 <What is YAML?> | YAML 是一种人类可读的数据序列化语言,通常用于编写配置文件。根据使用的对象,YAML 可以代表另一种标记语言或者说 YAML 根本不是标记语言(递归缩写),这强调了 YAML 用于数据,而不是文档。 |
理解:
在程序中,我们可以使用 yaml 对文件进行两种操作:序列化和反序列化(Serialize & Deserialize)。
序列化意味着我们可以将复杂的数据转变为字节流,进而可以将其轻易保存到文件或数据库中。
反序列化则意味着我们可以对已经序列化的数据进行逆处理,进而将数据转换回原始的数据结构或对象状态。
基础:
基本结构
映射(Map):键值对的集合。 |
|
序列(Sequence):有序的元素列表。 |
|
2. 嵌套结构
YAML 支持嵌套映射和序列,可以组合使用: | person: |
3. 数据类型
YAML 支持多种数据类型, 包括:字符串,数字,布尔值,Null 值。 | 例如: string: "Hello, World!" |
》》》》yaml-cpp 的使用(详情请阅览: https://github.com/jbeder/yaml-cpp/blob/master/docs/Tutorial.md )
在 C++ 中使用 yaml-cpp 库,可以方便地处理 YAML 数据的读取和写入。(以下是读取 Yaml 文件和写入 Yaml 文件的示例)
读取 YAML |
|
写入 YAML: 使用 YAML::Emitter 可以生成 YAML 文件 |
|
YAML::Node
定义:YAML::Node 是 YAML-CPP 中的一个核心类,表示 YAML 文档中的一个节点。一个节点可以是标量(单个值)、序列(列表)或映射(键值对)。通过 YAML::Node,你可以以编程方式访问和操作 YAML 数据结构。 | 创建和使用 YAML::Node: Eg.
|
Sequences 和 Maps
Sequences(序列) 是一个有序列表,表示一组无命名的值。它们在 YAML 中用短横线表示: |
|
在 YAML-CPP 中,你可以这样处理序列: | Eg.
|
Maps(映射) 是一组键值对,表示命名的值。它们在 YAML 中用冒号分隔表示: |
|
在 YAML-CPP 中,你可以这样处理映射: | Eg.
|
Sequences 和 Maps 的不同之处
序列和映射都是 YAML::Node 的一种。你可以在一个映射中嵌套序列,反之亦然。
不同之处:
序列: | 没有键,每个项都有顺序。 |
映射: | 每个项都有唯一的键,顺序不重要。 |
Converting To/From Native Data Types
YAML-CPP 提供了方便的方法来将 YAML::Node 转换为 C++ 的原生数据类型。你可以使用 as<T>() 方法进行转换。
示例:从 YAML::Node 转换到原生数据类型 |
|
示例:从原生数据类型转换到 YAML::Node |
|
》》由此引出两个疑惑:
问题一:
查阅文档时,我发现当插入的索引超出当前序列的范围时,YAML-CPP 会将节点视为映射,而不是继续保持序列
结论:动态类型:YAML::Node 的类型是动态的,可以在运行时根据操作的不同而变化。当你使用整数索引时,它保持序列。当你使用非连续的索引或字符串键时,它会转变为映射。
问题二:如何为Node添加一个映射?
在 YAML::Node node = YAML::Load("[1, 2, 3]"); 的情况下,使用 node[1] = 5 是不合适的.
如果你想让 node[1] 表示一个映射,node[1] = 5 会将序列中索引为 1 的元素(即第二个元素)设置为整数 5,而不是将其更改为一个映射。
如果你想在该位置设置一个映射,你可以这样做: |
|
结构: |
- 1 - key: value - 3
|
或者: |
|
结构: | - 1 - 2: 5 - 3 |
注意:
如果你使用了 node["1"] = 5,由于 "1" 是一个字符串键,而不是数字索引,这将使程序尝试在 node 中以 "1" 为键插入值 5。 node 原本是一个序列,但它会因此转变为一个映射。 | 最终结果会是 { 0: 1, 1: 2, 2: 3, "1": 5},其中 "1" 是一个新的字符串键。 |
》》》》ifstream 和 ofstream 之间的关系
二者定义在 <fstream> 头文件中,管理文件流。
std::ifstream: | 用于从文件中读取数据(输入文件流)。 |
Std::ofstream: | 用于向文件中写入数据(输出文件流)。 |
易混淆:<iostream>和文件流没有关系,<iostream> 是提供输入或输出流的标准库,主要包括 std::cin, std::cout, std::cerr 等。
》》 ofstream 的使用:std::ofstream 用于创建和写入文件
》》ifstream 的使用:std::ifstream 用于读入文件,进而对读入的文件进行一些处理。
(图例:逐行读取文件内容到字符串中)
或者(比上述方法更加高效,迅捷)
《《《《 什么是 rdbuf();
在 C++ 中,rd 通常是 "read" 的简写,意味着与读取操作相关的函数。
rdbuf()
释义: | rdbuf() 是 C++ 中的一个成员函数,可以直接访问流的底层缓冲区。它通常用于与输入输出流(如 std::ifstream, std::ofstream, std::iostream 等)交互。 |
返回类型: | std::streambuf* 返回指向与流关联的 std::streambuf 对象的指针。该指针可以用于直接进行低级别的输入输出操作。 |
优点:直接访问缓冲区 | rdbuf() 返回一个指向当前流缓冲区的指针(即 std::streambuf 对象),允许你直接从流中读取或写入数据。 这意味着,你可以将整个文件的内容一次性读入,而不需要逐行或逐字符地读取,从而提高了效率。 当处理大型文件时,逐行读取会涉及多次 I/O 操作,这可能导致性能瓶颈。而使用 rdbuf() 可以减少这些 I/O 操作,因为它一次性读取整个缓冲区的数据。 |
类似的 rd 开头的函数还有 rdstate() | 含义:rdstate() 是一个成员函数,用于获取流的状态标志。它返回一个整数,表示流的当前状态,包括是否已达到文件结束、是否发生了错误等。 |
》》》》FIEL STRUCTURE U know what I'm saying
》》》》YAML YAML YAML
》》》》YAML 文件构架,YAML 文件设置思路
因此我们也可解释 Deserialize() 函数中做出的操作:从 data(map) 中取出序列 entities(seq) ,然后通过 For 循环对序列中的 entity(map)进行读取,随后根据读取的数据去复现场景。
需要提醒的是: Map 中的元素不能重复, Seq 中的元素可以重复。
》》》》 YAML::Emitter out << YAML::Flow;
概念:out << YAML::Flow 是 C++ 中使用 YAML 库(如 yaml-cpp)时的一种语法,它用来设置 YAML 输出的格式为“流式”(flow style)。
详解:YAML::Flow 是一个常量,指示输出的 YAML 数据应采用流式表示形式。
流式表示形式将集合(如数组和映射)以更紧凑的方式表示,例如使用方括号 [] 表示数组,使用花括号 {} 表示映射。
使用场景:
当你想要以更紧凑的格式输出数据时,可以使用流式表示形式。相比于块状表示(block style),流式表示在视觉上更简洁,适用于小型数据结构或在单行内表示数据。
假设你有一个简单的 YAML 数据结构,如果不使用 YAML::Flow,输出可能是这样的(块状表示): | items: |
而如果使用 YAML::Flow,此时,输出将是: | items: [item1, item2] |
示例代码:
#include <yaml-cpp/yaml.h>
#include <iostream>
int main() {
YAML::Emitter out;
out << YAML::Flow; // 设置为流式格式
out << YAML::BeginMap
<< YAML::Key << "items" << YAML::Value
<< YAML::BeginSeq
<< "item1" << "item2"
<< YAML::EndSeq
<< YAML::EndMap;
std::cout << out.str() << std::endl;
}
》》》》设计上的理解
1.Serialize
2.Yaml data
3.Deserialize
》》 There are few issues you need to know:
1.字符匹配:查找value所用的key需要正确无误,比如你想查找yaml文件中的 Fixed Aspect Ratio,你就必须用 Fixed Aspect Ratio 作为索引,而不是 FixedAspectRatio.
2.Map访问:如果你在map中查找其中存储的元素,你只能访问顶层的元素,而不能访问靠底层的元素。比如 CameraComponent
图例此时处于 cameraComponent,你通过这两个 key 访问其中的 value:Camera 和 Primary(比如调用 cameraComponent["Camera"] )。可是如果你想通过 CameraComponent 访问 projectionType,就不能使用 cameraComponent["ProjectionType"] 这样的语句,而必须使用 cameraComponent["Camera"]["ProjectionType"],否则会报错。
》》》》Cherno 将文件保存在 .hazel 后缀的文件中,这可以吗?为什么?只有Hazel 才能处理这种文件吗??
这取决于你的打开方式,现在 Hazel (或者 Nut) 有能力接受这种文件,通过我们定义的函数,Hazel(或者 Nut)通过文件流合理读入文件,然后识别文件内容并且做出了相应操作。如果你使用 word 打开这种文件,应用程序就会通过文本格式打开这个文件,这样也是被允许的,因为我们就是以文本的形式去设置了 yaml 文件。不过使用其他打开方式可能就会出错。
》》》》注意事项/可改进事项
AND
------------------------------------------ Save/Open file dialog ----------------------------------------------
》》》》什么是 commdlg 库?
概念: commdlg.h 是 Windows API 中的一个头文件,它用于实现标准对话框功能,如文件打开和文件保存对话框。这个头文件定义了与这些对话框相关的结构、常量和函数,使开发者能够方便地在应用程序中集成文件选择功能。
在使用 commdlg.h 时,通常会涉及到以下几个函数:
GetOpenFileName: | 用于显示“打开文件”对话框。 |
GetSaveFileName: | 用于显示“保存文件”对话框。 |
》》》》 glfw3.h 和 glfw3native.h 这两个库之间的不同
不同:glfw3.h 和 glfw3native.h 之间的区别在于:glfw3.h 用于创建 glfw 类型的窗口, glfw3native.h 用于获取原生操作系统中的窗口的句柄,以此来进行更底层的操作。
1. <GLFW/glfw3.h> 目的:这是 GLFW 的主要头文件,提供了创建窗口、处理输入、管理 OpenGL 上下文、以及其他与窗口和输入相关的功能。 内容:包含了 GLFW 的所有核心功能,比如:创建和管理窗口和上下文、处理键盘、鼠标等输入事件、管理 OpenGL 扩展、定时器等功能 | 2. <GLFW/glfw3native.h> 目的:这个头文件提供了平台特定的功能,通常用于访问底层原生窗口句柄或其他系统级别的功能。 内容:包含了一些函数,这些函数允许你获取与操作系统相关的窗口句柄。例如,在 Windows 系统上,你可以通过它获得 HWND 句柄;在 X11 上,你可以获得相应的 X11 窗口 ID。 使用场景:如果你需要与操作系统的原生 API 进行交互(例如,在窗口中嵌入第三方控件,或与其他库集成),则可能需要使用这个头文件。 |
》》》》关于 glfw3native.h 和 #define GLFW_EXPOSE_NATIVE_WIN32
首先:glfwGetWin32Window() 是 glfw3native.h 中的函数(以下是其定义)。不过该函数在使用之前,需要通过定义 GLFW_EXPOSE_NATIVE_WIN32 将其暴露出来。
》》》》对话框函数设计思路:
这个函数 FileDialogs::OpenFile 的作用是打开一个文件对话框,让用户选择一个文件,并返回所选文件的路径。
下面逐句解释代码的功能:(标蓝的函数/类型名/变量有额外笔记)
std::string FileDialogs::OpenFile(const char* filter) | 定义一个名为 OpenFile 的静态成员函数,接受一个字符串参数 filter,该参数用于指定文件类型过滤器(例如仅显示文本文件或图像文件 / 或者 Cherno 填入的:"Hazel Scene (*.hazel)\0*.hazel\0" ) |
{ | 创建一个 OPENFILENAMEA 结构体实例 ofn,该结构体用于存储文件对话框的各种信息。 |
CHAR szFile[260] = { 0 }; | 声明一个字符数组 szFile,用于存储用户选择的文件路径,大小为 260 字节(这是 Windows 系统中路径的最大长度限制)。 |
ZeroMemory(&ofn, sizeof(OPENFILENAME)); | 将 ofn 结构体的内存清零,以确保它的所有字段都被初始化为零。 |
ofn.lStructSize = sizeof(OPENFILENAME); | 设置 ofn 结构体的大小,以便 Windows 知道使用哪个版本的结构体 |
ofn.hwndOwner = glfwGetWin32Window((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow()); | 获取当前窗口的句柄并赋值给 ofn.hwndOwner,这样文件对话框会相对于这个窗口显示。 |
ofn.lpstrFile = szFile; | 将 szFile 的地址赋值给 ofn.lpstrFile,以便在用户选择文件后可以将文件路径写入这个数组中。 |
ofn.nMaxFile = sizeof(szFile); | 设置 ofn.nMaxFile 为 szFile 的大小,以告诉对话框可以存储的最大路径长度。 |
ofn.lpstrFilter = filter; | 设置 ofn.lpstrFilter 为传入的 filter 参数,以定义可见的文件类型(例如 ".txt;.cpp")。 |
ofn.nFilterIndex = 1; | 设定过滤器的索引,通常设为 1 表示使用第一个过滤器。 |
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; | 设置对话框的标志:
|
if (GetOpenFileNameA(&ofn) == TRUE) | 调用 Windows API 函数 GetOpenFileNameA 显示文件对话框。如果用户选择了一个文件,返回值为 TRUE。 |
{ | 如果文件选择成功,返回 ofn.lpstrFile 中存储的文件路径。 |
return std::string(); | 如果用户取消了操作或发生错误,返回一个空的字符串。 |
《《 关于 OPENFILENAMEA 的定义
《《 关于 ZeroMemory 函数的定义
在 minwinbase.h 函数中:
《《 ofn.lpstrFilter = filter; 之中,filter 为什么是 const char* ? 填入的时候有什么规范?
lpstrFilter 格式
示例:
const char *filter = "Hazel Files (*.hazel)\0*.hazel\0All Files (*.*)\0*.*\0\0";
格式:
每一组文件类型描述由两部分组成 -> 描述字符串和扩展名字符串:
描述字符串是用户在对话框中看到的文件类型名称,扩展名字符串指定了可以被选择的文件扩展名。各组之间用 \0 分隔,最后以两个 \0 结束。
对话框如何识别和表示:
Hazel Files (*.hazel)\0*.hazel\0 -> 在文件对话框中,用户会看到“Hazel Files (*.hazel)”作为文件类型的选项。当选择这个选项后,对话框会过滤出所有以 .hazel 结尾的文件。
All Files (*.*)\0*.*\0 -> 如果用户选择“所有文件 (.)”,则会显示所有文件,包括 .hazel 文件。
例如:
Nut Scene(*.yaml) 为显示的文本提示,通常设置为你可以选择的过滤器,通过文本提示,代码会索引到合适的过滤器
.hazel 则会根据你选择的文本索引到你设置的过滤器,然后对所有文件进行过滤,如果符合.hazel 后缀,便显示在对话窗口中。
《《 GetOpenFileNameA 函数的作用:
GetOpenFileNameA 是 Windows API 中的一个函数,用于显示一个标准的“打开文件”对话框,让用户选择一个文件。
函数原型 | BOOL GetOpenFileNameA(LPOPENFILENAMEA lpofn); |
参数 | lpofn: 指向 OPENFILENAMEA 结构体的指针,该结构体包含了对话框的配置信息和用户选择的文件路径。 |
返回值 | 如果用户成功选择了一个文件并点击“确定”,函数返回非零值(通常是 TRUE)。 如果用户取消对话框或发生错误,返回值为零(FALSE)。可以通过调用 GetLastError 来获取更多错误信息。 |
《《 Flags 详细解释:
在 OPENFILENAME 结构体中,可以设置多个标志(Flags)来控制对话框的行为。
OFN_PATHMUSTEXIST: | 含义:用户输入的路径必须存在。如果用户在对话框中输入了一个路径(而不是从浏览器中选择),这个路径必须是有效的。 触发情况:当用户输入一个不存在的路径并尝试打开文件时,会出现错误提示,说明路径无效。 |
OFN_FILEMUSTEXIST: | 含义:用户选择的文件必须存在。即使用户在对话框中选择了文件,如果该文件不存在,就会阻止选择。 触发情况:当用户选择的文件实际上在磁盘上不存在时,系统会显示错误提示,说明所选文件不存在。 |
OFN_NOCHANGEDIR: | 含义:打开对话框时不改变当前工作目录。默认情况下,打开文件对话框可能会改变程序的当前工作目录以便于访问文件。 触发情况:这个标志的作用是确保选择文件后,程序的当前工作目录保持不变,即使用户选择了不同的文件路径。 |
《 关于 OFN_NOCHANGEDIR 的详细说明
背景:在 Windows 应用程序中,当前工作目录是指程序在文件系统中默认访问的位置。当应用程序启动时,它会有一个初始的工作目录,通常是可执行文件所在的位置。
使用场景:
当用户打开文件对话框并选择一个文件时,默认情况下,Windows 会将当前工作目录更改为用户选择的文件所在的路径。这意味着如果用户选择了一个不同位置的文件,之后程序的所有文件访问操作都会基于这个新的工作目录。
然而,在某些情况下,这种行为可能会导致问题。例如:
- 依赖于相对路径:如果程序中的文件操作使用相对路径,工作目录的变化可能会导致文件访问失败。
- 多次调用:如果你的应用程序需要频繁打开文件,改变工作目录可能会使管理变得复杂,特别是在需要回到原始路径时。
假设你有一个文本编辑器应用程序,用户可以打开和编辑多个文件。在这个程序中,用户最开始可能在 C:\Documents 中打开一个文件。
OPENFILENAME ofn; // common dialog box structure
char szFile[260]; // buffer for file name
// Initialize OPENFILENAME
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.lpstrFile[0] = '\0';
ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = "Text Files\0*.TXT\0All Files\0*.*\0";
ofn.nFilterIndex = 1;
ofn.lpstrFileTitle = NULL;
ofn.nMaxFileTitle = 0;
ofn.lpstrInitialDir = NULL;
ofn.lpstrTitle = "Open File";
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
// Display the Open dialog box
if (GetOpenFileName(&ofn)) {
// 用户选择了文件
// 这里可以进行文件读取等操作
}
在此示例中:
如果没有设置 OFN_NOCHANGEDIR,选择一个位于 D:\OtherFiles 的文件可能会把当前工作目录从 C:\Documents 改为 D:\OtherFiles。
这意味着接下来如果程序尝试以相对路径访问文件(例如,读取 data.txt),它会在 D:\OtherFiles 查找,而不是在用户原本的路径 C:\Documents。
加入 OFN_NOCHANGEDIR 后的效果:
通过加入 OFN_NOCHANGEDIR,即使用户选择了位于不同文件夹的文件,程序的当前工作目录仍然保持在 C:\Documents。这样,任何依赖于该目录的文件操作都不会受到影响。
》》》》问题:触发断点 -> 为什么将 CreateRef 改为 Ref 时会触发断点异常?
因为 Ref(std::shared_ptr<T>() ) 创建了一个智能指针,但未分配内存;
而 CreateRef(std::make_shared<T>() )创建并初始化一个智能指针,同时分配内存并构造对象。
》》》》关于快捷键触发这个事件函数的设计:
1.GetRepeat() 在这里的作用:防止处理重复按键事件。
e.GetRepeatCount() 函数用于获取按键的重复次数。当一个按键被按下并保持时(比如长按),操作系统会不断生成按键按下事件,这样会导致该事件被多次触发。
所以当检测到重复按键时,函数直接返回 false,意味着后续的代码(如处理快捷键的逻辑)将不会被执行。这可以防止同一个快捷键被多次触发,从而避免造成意外的重复操作。
2.这个函数为什么只在事件重复时返回一个布尔量,其他条件下不返回值呢。为什么 GetRepeat() 需要设置在函数最前面?
在事件触发之后,我们运行实际逻辑代码(OpenScene/NewScene),但是我们不能返回 true,因为 true 不会阻止事件运行,这将会导致事件会被连续触发。
其次,如果在逻辑代码之后返回 false,而不是在函数开头返回 false,都会导致事件被触发第二次,因为在判断是否重复触发时依旧会先运行逻辑代码,随后判断出来结果是 false(应该阻塞),不过此时已经没有用了。
3.GetKeyCode 和 GetRepeatCount 中返回的 keycode 和 count 是在哪里自动获取的,所以我们才能使用其返回值来进行判断
在之前设置的回调函数中:WindowsWindow.cpp 中
4.为什么只有在鼠标单击了视口这个区域时(聚焦在 Viewport 窗口时),快捷键才能使用?
因为是在 EditorLayer::OnEvent() 函数中设置的事件分发。
--------------------------------------- ImGuizmo ---------------------------------------------
》》》》这段 premake 代码什么意义?
》》》》为什么Nut-editor 中没有包含 ImGui 库目录却可以使用 ImGui,但是没有包含 yaml-cpp 库目录的时候却不能使用 yaml-cpp?
因为实际上 ImGui 在 "%{wks.location}/Nut/vendor" 中已经被包含了。
》》》》代码设计:gizmo库中对于 ImGuizmo.cpp 的更改:( https://github.com/TheCherno/ImGuizmo/commit/218d60bde7d22061ac525d0d71e05360b4dcf978)
我猜想Cherno做的是一些对于 ImGuizmo 样式的更改,并且由于时间原因,最新的 ImGuizmo.cpp 结构发生了很多改变,这让我不容易同步这个更改。
所以我决定先保持默认值,后续再查看是否需要同步此更改。
》》》》什么是万向锁,什么情况下会触发万向锁?什么是四元数,为什么四元数可以解决万向锁? glm/gtx/quaternion.hpp 中处理四元数的函数怎么使用?
万向锁(Gimbal Lock)
万向锁是一种在三维空间中旋转时遇到的现象,通常发生在使用欧拉角表示旋转的情况下。它指的是在某些特定的旋转配置下,系统的自由度减少,使得无法进行预期的旋转。
万向锁通常在以下情况下发生:
旋转轴对齐: | 当一个旋转轴与另一个旋转轴对齐时,例如在俯仰角为 ±90° 时,两个旋转轴重合,这会导致系统失去一个自由度。 |
极限位置: | 当物体达到某个极限位置(如直立或水平),就可能会导致无法实现某些方向的旋转。 |
万向锁的影响:
当发生万向锁时,物体可能会无法按预期方向旋转,或者某些旋转会变得不可用。这在动画、飞行控制和机器人运动等应用中会造成问题。
参考文档:( https://medium.com/@lalesena/euler-angles-rotations-and-gimbal-lock-brief-explanation-de1d4764170 )
参考图片:
https://miro.medium.com/v2/resize:fit:498/format:webp/1*7JT7g5dLeZ-Q5uOYnX8hCw.gif
https://miro.medium.com/v2/resize:fit:400/format:webp/1*OCkqKWmTtmDtzrukAX4hSA.gif
四元数(Quaternion)是一种扩展了复数的数学结构,通常用于表示三维空间中的旋转。它由一个实数部分和三个虚数部分组成,形式为:
q=w+xi+yj+zkq=w+xi+yj+zk
其中:w 是实数部分,x,y,z 是虚数部分(向量部分),i,j,k 是虚单位。
四元数通过以下方式解决万向锁问题:
四维表示: | 四元数使用四个参数(一个实数和三个虚数)来表示旋转,而不是依赖于三个独立的角度(如俯仰、偏航和滚转)。这种表示方法避免了两条旋转轴重合的情况。 |
组合旋转: | 四元数之间的乘法可以直接组合多个旋转,而不受单一旋转轴限制。这使得在任何方向上进行旋转都不会遭遇自由度的丧失。 |
插值平滑: | 四元数支持球形线性插值(Slerp),这使得在动画中能够平滑地过渡旋转,从而避免因插值引起的万向锁问题。 |
参考文档---涉及到数理知识:( 3D数学:欧拉角、万向锁、四元数 - HighDefinition - 博客园 )
》》》》代码设计: TransformComponent 中的更改:
这可以防止通过 imGuizmo 调整物体旋转时图像极速发生变化的问题(通过 ImGuizmo 可视化工具轻轻更改欧拉角,角度却会极速增加,这会导致图像迅速旋转并闪烁)
并且防止可能出现的万向锁问题(可以查看上述的: 万向锁(Gimbal Lock) )。
》》》》一些 ImGuizmo 函数:
》》》》 ImGuizmo::SetRect()
释义:ImGuizmo::SetRect() 是 ImGuizmo 库中的一个函数,用于设置 ImGuizmo 操作区域的矩形。
在使用 ImGuizmo 进行变换操作时,你需要定义一个矩形区域来限制 ImGuizmo 的界面和交互区域。这通常是你在应用程序中想要显示和使用的 GUI 区域。
函数原型: void SetRect(float x, float y, float width, float height);
参数解析:
x: | 矩形区域的左上角的 X 坐标(屏幕坐标)。 |
y: | 矩形区域的左上角的 Y 坐标(屏幕坐标)。 |
width: | 矩形区域的宽度。 |
height: | 矩形区域的高度。 |
》》》》ImGuizmo::SetDrawList();
ImGui 的绘制列表
定义: 在 ImGui 中,绘制列表(ImDrawList)是一个存储了绘制命令的对象,包含了要渲染的所有图形的信息(如线条、矩形、文本等)。
功能: 每个 ImDrawList 都包含了多种绘图命令和状态信息,例如:
顶点数据
颜色
纹理….
ImGuizmo::SetDrawList()
释义:
作用于 Gizmo: | 这个函数的主要作用是让 ImGuizmo 知道在哪个 ImDrawList 上进行绘制。这意味着,所有由 ImGuizmo 创建的图形(例如,表示变换的箭头、框和其他形状)都将被绘制到指定的绘制列表上。 |
控制绘制顺序: | 通过选择不同的绘制列表,开发者可以控制 gizmo 的绘制顺序。例如,你可以将 gizmo 绘制在某个特定 UI 元素的上方或下方,这样可以实现更好的视觉效果和用户体验。 |
原型:static void SetDrawList(ImDrawList* draw_list);
参数:
draw_list: 指向 ImDrawList 的指针,这是一个 ImGui 的绘制列表,用于管理和记录图形的绘制命令。
示例:
ImDrawList* backgroundDrawList = ImGui::GetBackgroundDrawList();
ImDrawList* foregroundDrawList = ImGui::GetForegroundDrawList();
// 在背景绘制列表中绘制某个背景元素
backgroundDrawList->AddRectFilled(ImVec2(0, 0), ImVec2(100, 100), IM_COL32(50, 50, 50, 255));
// 在前景绘制列表中设置 gizmo 绘制
ImGuizmo::SetDrawList(foregroundDrawList);
ImGuizmo::SetRect(0, 0, ImGui::GetWindowWidth(), ImGui::GetWindowHeight());
ImGuizmo::Manipulate(viewMatrix, projectionMatrix, ImGuizmo::OPERATION::TRANSLATE, ImGuizmo::MODE::LOCAL, &modelMatrix[0][0]);
》》》》 ImGuizmo::Manipulate()
释义:ImGuizmo::Manipulate 是 ImGuizmo 库中用于处理物体变换的函数。这个函数可以在用户界面中允许用户通过拖动来对物体进行平移、旋转和缩放操作。
函数原型:
bool Manipulate(const float* view, const float* projection,
OPERATION operation, MODE mode,
float* matrix, float* deltaMatrix,
const float* snap = nullptr, const float* pivot = nullptr);
参数解析:
view: | 相机的视图矩阵(cameraView),表示相机的位置和方向。 |
projection: | 相机的投影矩阵(cameraProjection),用于确定场景中物体的透视效果。 |
operation: | 变换操作类型(平移、旋转或缩放),通过 (ImGuizmo::OPERATION)m_GizmoType 指定。 |
mode: | 变换模式,通常是 ImGuizmo::LOCAL(局部变换)或 ImGuizmo::GLOBAL(全局变换)。 |
matrix: | 物体的变换矩阵(transform),表示物体在场景中的位置、旋转和缩放。 |
deltaMatrix: | 可选参数,表示变化的矩阵。 |
snap: | 可选参数,指定对变换进行捕捉的值(例如,步长)。如果为 nullptr,则不进行捕捉。 |
pivot: | 可选参数,指定旋转的中心点。 |
》》》》ImGuizmo::OPERATION 的定义
》》》》代码设计:窗口响应
更改前:只要不满足 聚焦于窗口上/悬停在窗口上 的任一一个条件,便会阻塞当前窗口中的事件(不再捕获鼠标或键盘的活动)
更改后:只有 窗口没有被聚焦且鼠标没有悬停在窗口上的时候,才会阻塞事件。
这让我们在结构面板中选中实体之后,只需要将鼠标悬停在 viewport 窗口上,便可以通过键盘调整/响应 Gizmo,这在实际使用中很舒服( 本人亲测 :>
》》》》为什么这里需要使用 Const 标识?
可能是因为不需要对 cameraComponent 进行更改吧。
》》》》这个函数的意义?
》》》》这段代码的意义?
------------------------------------ Editor camera ------------------------------------------
》》》》 关于引擎的操作界面和简单操纵,我们已经完成了很多,Cherno 在接下来将开始实现鼠标选择实体,不过我可能会将之前遗漏的维护补充一下。
》》》》总之先让我们开始这一集吧。
》》》》这一集中,主要围绕 编辑时摄像机 和 运行时摄像机 两个概念来设计。
我目前是这样理解的:
- 编辑时摄像机:用于在引擎中编辑物体,此摄像机为默认存在。只要你需要对物体进行编辑,那么直接建立一个 sprite 实体,在编辑器中便应该能够直接查看和编辑该实体,而不需要额外添加摄像机(运行时摄像机),这符合游戏引擎的使用逻辑。
- 运行时摄像机:用于在实际游戏程序运行时添加,此摄像机需要手动添加。游戏中不需要“上帝视角”的编辑时摄像机,而只需要用来观察物体的运行时摄像机,该摄像机不像编辑时摄像机一样默认存在,因为编辑器和游戏程序的逻辑是不同的。
这两个摄像机不是同时存在的:在编辑器中,底层默认存在一个编辑时摄像机,该相机只在编辑器运行时存在并更新;同样的,游戏程序中不使用编辑时摄像机,而是用运行时摄像机,该相机只在游戏程序运行时存在并更新。
由于我们现在在编辑器中设计程序,所以只需要对所有实体使用编辑时摄像机即可。对于游戏,我想我们应该需要在编辑器中添加一个游戏运行时摄像机,然后打开游戏程序,进行游玩。
---------------------------- Multiple Render Targets and Framebuffer refactor----------------------------------
》》》》很久没 commit 了,之前准备先做维护,奈何 Cherno 后面实现的内容太过诱人,好奇心驱使我又观看了五六集,维护的事情后面再说吧 X-D
》》》》接下来我将努力实现 Mouse Picking,然后根据个人时间考虑内容浏览面板的制作,或者是提交一波维护,因为维护已经落下很久了。
》》》》前言:
》》》》gl_VertexID 在 GLSL 中关于顶点ID的一些细节:
》》》》这一节的概述:
Cherno 为了实现类似顶点ID的效果,于是开始更改帧缓冲的操作模式。这样一来就可以更方便的实现鼠标选中,在这一节中,Cherno 只是对帧缓冲的实现过程进行了完善,使代码可以通过初始化列表标识帧缓冲,并自动的识别这些标识用来创建帧缓冲。虽然是顶点ID的前瞻操作,但实际上只是一些铺垫,不必担心看不明白。
》》》》代码的理解:
反复观看代码之后,我将所有代码分为两部分:设置结构体 和 重构帧缓冲的创建,第一部分的作用是:通过声明一些结构体来简化帧缓冲的使用方式/简化帧缓冲的设计,第二部分的作用是:通过已有的条件,动态的构建帧缓冲。
这引出了我的一些问题:
In FrameBuffer.h
》》这三个结构体有什么作用?之间有什么关系?
FramebufferTextureFormat: | 这个枚举类定义了可用的帧缓冲纹理格式 |
FramebufferTextureSpecification: | 一个使用枚举类型对象进行初始化的类,用于定义单个帧缓冲纹理的具体规格 |
FramebufferAttachmentSpecification: | 用于定义一组帧缓冲的附件 (我认为这个结构体主要是用来服务于初始化的:比如 Cherno 的代码中的使用方式) (再比如:) 由于这个 FramebufferAttachmentSpecification 的构造函数中使用的是初始化列表,所以我们可以通过上述方式对 framebufferSpecification 中的 Attachments 进行方便快捷的初始化操作。 填入的初始化列表将被储存在 FramebufferSpecification 结构体中的 Attachments 中: 并在 OpenGLFramebuffer 的构造函数中被使用:(第一个 Attachments 就是 FramebufferSpecification 结构体中的成员,第二个 Attachments 则是 FramebufferAttachmentSpecification 结构体中的成员->查看最开始的那张代码表) |
In OpenGLFrameBuffer.h
》》m_ColorAttachmentSpecification 和 m_ColorAttachments 的区别在哪里?分别起到什么作用?
前言:帧缓冲中的纹理与纹理附件的关系?帧缓中是否可以包括多个纹理?一个纹理中是否可以附加多个纹理附件?这些附件有什么作用?
一个帧缓冲绘制的场景中可以绘制一些物体,也可以绘制多个纹理,而纹理附件则是存储这些渲染结果的地方。此时如果你使用帧缓冲中的颜色附件作为纹理进行后续渲染,那这个颜色附件就可以被称为纹理附件。
在帧缓冲中每一个纹理的创建都和普通纹理的创建过程差不多,不过我们需要对这些纹理额外的附加 Attach 一些附件。一个纹理可以附加多个附件(颜色附件可以同时附加多个,但深度附件一般只能附加一个),如果你在帧缓冲对象(FBO)中使用多个颜色附件,那并不意味着你能绘制更多的物体,而是指在一次渲染过程中,你可以同时输出多个数据流。
m_ColorAttachmentSpecification 用于存储所有的纹理附件,而 m_ColorAttachments 存储的是帧缓冲中所有纹理附件的 ID 。
》》那么这个ID是怎么分配的呢?
|
|
|
|
》》这个 ID 是怎样和片段着色器中的输出变量 Color 关联起来的?也就是说纹理附件的 ID 是怎样和片段着色器中的输出变量关联起来的?
比如 Cherno 是怎样明确纹理附件的 ID:0 就指的是片段着色器中的第1个颜色(输出变量),ID:1就指的是片段着色器中的第2个颜色(输出变量)。
Eg.
通过纹理 ID 索引到纹理附件
| 首先将一组纹理创造出来,为其分配对应的ID |
| 在 BindTexture 函数中,填入对应的纹理 ID 作为参数,在明确绑定了某一个纹理的情况下,将为该纹理分配附加附件(在此基础上使用 AttachColorTexture 函数,将输出变量 Color 映射到颜色附件 0(GL_COLOR_ATTACHMENT0)) |
|
GL_COLOR_ATTACHMENT0 便是帧缓冲对象的第一个颜色附件。我们将该附件按顺序的附加到对应的纹理ID下。 |
通过纹理附件(颜色附件)索引到输出变量
现在我们已经可以通过纹理 ID 索引到想要访问的纹理附件了,可如何将 ID 与片段着色器中的颜色输出变量联系起来?
渲染目标的设置:在使用多重渲染目标(MRT)时,可以将片段着色器的输出变量映射到不同的颜色附件。
|
|
Eg.
》》》》代码上的疑问:为什么需要在附加附件之前创建纹理
为什么每附加一个附件就需要创建一次纹理?
前提: | Utils::CreateTextures 函数的作用是创建纹理对象,具体来说就是为帧缓冲区的颜色附件和深度附件分配和初始化必要的纹理资源。一个纹理可以包含多种不同的附件。 |
原因: | 因为创建纹理是渲染管线中的关键步骤,为了确保在绘制时渲染结果可以储存在合适的纹理中,我们需要创建纹理。 |
虽然一个纹理对象可以附加多个颜色附件,且只能附加一个深度附件,那为什么需要在附加深度缓冲附件之前再次新建一个纹理?
这些纹理都代表什么,为什么可以创建这么多?
颜色附件和深度附件的规范
颜色附件: | 数量:颜色附件用于存储渲染输出的颜色信息。可以有多个颜色附件,因为在许多渲染管线中,可能需要输出多个颜色通道(例如,颜色缓冲、法线缓冲、光照缓冲等)。 附件和纹理的关系:每个颜色附件可以使用不同格式的纹理,这样可以根据需求选择最合适的格式和精度。 |
深度附件: | 数量:深度附件用于存储每个像素的深度信息,用于进行深度测试以判断哪些物体在前面、哪些在后面。在一个帧缓冲对象中只能有一个深度附件,这通常是因为深度测试只需要一个深度值来进行比较。 |
创建多个纹理的原因
性能和灵活性: | 使用不同的纹理对象允许开发者根据需要选择不同的格式、分辨率和精度。例如,可能会希望使用高精度深度纹理,但对于颜色输出,可以选择较低精度的纹理。 |
资源管理: | 创建多个纹理可以更好地管理内存和资源。根据不同的渲染需求,可以动态创建和销毁纹理,而不是固定使用一个纹理。 |
多重渲染目标(MRT): | 当使用多重渲染目标(MRT)时,可以同时将多个颜色输出渲染到不同的纹理。这种灵活性可以优化渲染流程,减少多次绘制的需求。 |
在深度附件之前创建纹理的原因
指定深度格式: | 每个深度附件可能使用不同的格式(如深度16、深度24等),需要根据需要新建适当的纹理。 |
资源隔离: | 将颜色和深度缓冲分开,可以更容易地管理和调试渲染过程。 |
》》》》附件的附加代码:附加时使用不同函数的意义是什么?
两种附件中使用的函数 : glTexImage2D 和 glTexStroage2D 区别在哪里?
1. 创建纹理存储的方式
AttachColorTexture 中使用 glTexImage2D: | glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); 它适合需要动态更新纹理内容的情况,因为你可以在后续的调用中修改纹理数据,但是调用时可能会重新分配内存。 |
AttachBufferTexture 中使用 glTexStorage2D: | glTexStorage2D(GL_TEXTURE_2D, 1, format, width, height); 它更适合静态纹理,因为它只需要在创建时分配一次内存,并且不需要再进行二次的内存分配和管理。 |
适用场景:
glTexImage2D | 适合于动态纹理,例如实时渲染时需要频繁更新的纹理颜色或样式…. 。 |
优劣: | 能够灵活地上传和更新纹理数据,但可能导致性能下降。 |
glTexStorage2D | 更适合静态纹理,如用于环境映射、天空盒或其他不需要在运行时修改的纹理。 |
优劣: | 能够提供更好的性能和内存管理,但需要在创建时就确定纹理的格式和尺寸,之后不再改变。 |
》》关于纹理创建和附加操作的一点理解:
1 由于颜色附件(纹理附件)需要附加多个,所以需要创建多个纹理,并逐一对其进行 Attach 操作。
2 但是深度附件只需要添加一个,所以只创建一个纹理,并对其进行一次 Attach 操作。
》》》》以下两段代码的区别
和
的区别:
答:Size > 1 是数量从 2 开始的情况,!empty() 是数量只要不为0,也就是数量从 1 开始的情况。
》》》》glDrawBuffers 函数的作用是什么?
--------------- Preparing mouse picking (Get mouse pos and Read pixel) ---------------------------------
》》》》关于这一集,可以分为三部分:1.调整帧缓冲的使用方法 2.读取当前帧缓冲区(或指定帧缓冲区)中的像素数据 3.获取正确的窗口大小边界,并获取鼠标相对于窗口中的位置
》》》》关于读取当前帧缓冲区(或指定帧缓冲区)中的像素数据
glReadBuffer
原型: | void glReadBuffer(GLenum src); |
参数: | 这些选项指定了你希望读取的帧缓冲对象(FBO)中的缓冲区。 src(类型:GLenum):指定你希望读取的源缓冲区。这个参数的有效值有:
|
释义: | glReadBuffer 的主要作用是指定从哪个缓冲区读取数据。 这在多渲染目标(MRT, Multiple Render Targets)或使用帧缓冲对象(FBO)时非常有用,因为在这些情况下,你可能渲染到多个不同的缓冲区,并且需要从特定的缓冲区获取像素数据。 |
glReadPixels
原型: | void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels); |
参数说明: | x:读取区域左下角的 X 坐标,单位是像素。指定从帧缓冲中开始读取的位置。 y:读取区域左下角的 Y 坐标,单位是像素。指定从帧缓冲中开始读取的位置。注意,在 OpenGL 中,Y 轴的坐标是从底部到顶部增加的,所以 y 的值越大,读取的区域就越靠上。 width:指定要读取的区域的宽度(以像素为单位)。 height:指定要读取的区域的高度(以像素为单位)。 举例(width, height)
读取区域大小:指定从 (x, y) 开始的区域为 1 像素宽,1 像素高,即仅读取一个像素的数据。 实际效果:这意味着你会读取帧缓冲区中 x, y 位置处的单个像素的颜色信息。
读取区域大小:指定从 (x, y) 开始的区域为 100 像素宽,100 像素高,即读取一个 100x100 像素的矩形区域内所有像素的数据。 实际效果:这意味着你会读取从 (x, y) 开始,横向 100 个像素,纵向 100 个像素的区域内所有像素的颜色信息。 format:指定返回像素数据的格式,常见的有:
type:指定返回数据的类型。常见的类型包括:
pixels:指向存储读取像素数据的内存区域的指针。读取到的像素数据将被存储在这个内存空间中。该内存的大小应该足够容纳所有读取的像素数据。 |
释义: | glReadPixels 是 OpenGL 中用于读取当前帧缓冲区(或指定帧缓冲区)中的像素数据的函数。 它通常用于从渲染到屏幕或帧缓冲的内容中提取像素信息,可以用于后处理、截图、或用于其他需要读取像素数据的操作。 |
》》》》关于窗口和鼠标交互部分代码的理解
// 确定窗口边界(考虑标签栏)
auto viewportOffset = ImGui::GetCursorPos(); // Includes tab bar
auto windowSize = ImGui::GetWindowSize();
ImVec2 minBound = ImGui::GetWindowPos();
minBound.x += viewportOffset.x;
minBound.y += viewportOffset.y;
ImVec2 maxBound = { minBound.x + windowSize.x, minBound.y + windowSize.y };
m_ViewportBounds[0] = { minBound.x, minBound.y };
m_ViewportBounds[1] = { maxBound.x, maxBound.y };
// 确定鼠标在视口中的相对位置,并做出对应的操作
auto[mx, my] = ImGui::GetMousePos();
mx -= m_ViewportBounds[0].x;
my -= m_ViewportBounds[0].y;
glm::vec2 viewportSize = m_ViewportBounds[1] - m_ViewportBounds[0];
my = viewportSize.y - my;
int mouseX = (int)mx;
int mouseY = (int)my;
if (mouseX >= 0 && mouseY >= 0 && mouseX < (int)viewportSize.x && mouseY < (int)viewportSize.y)
{
int pixelData = m_Framebuffer->ReadPixel(1, mouseX, mouseY);
HZ_CORE_WARN("Pixel data = {0}", pixelData);
}
----- ImGui::GetCursorPos()
|
返回值:返回 ImVec2 类型变量,表示光标的 x 和 y 坐标。返回值的 x 和 y 是相对于当前窗口的左上角的坐标,单位为像素。 |
注意
ImGui::GetCursorPos() 返回的是 ImGui 光标的位置,而不是鼠标光标的位置。这里的“ImGui 光标”是一个特定的概念,与 GUI 元素的布局和绘制有关。
ImGui 光标
概念:在 ImGui 中,光标是用于管理和控制 UI 元素(如按钮、文本框、滑动条等)排列和绘制的一个指示器。它并不是真正的鼠标光标,而是指示下一个元素应该放置在何处的虚拟光标。
特性:
布局控制: | ImGui 使用即时模式的绘制方式,光标帮助管理 UI 元素的排列。每当你在 ImGui 中绘制一个元素时,光标的位置会自动更新,指向下一个可放置元素的位置。 例如,当你调用 ImGui::Button("Button") 时,按钮会被绘制在当前光标的位置。 |
位置更新/使用方式: | 每当添加一个新的 UI 元素时,光标会自动向下移动,准备好放置下一个元素。 例如,你可以通过 ImGui::GetCursorPos() 获取当前光标的位置。 |
光标的偏移: | 光标位置可以通过多种方式调整。 例如,可以使用 ImGui::SetCursorPos() 函数手动设置光标位置,或者通过调用 ImGui::NewLine() 来强制光标换行。 |
相对坐标: | GetCursorPos() 返回的是相对于当前窗口的光标位置,这意味着它会考虑到窗口的布局(如标签条、边距等),并返回一个相对位置。 |
实际应用:
当你在 ImGui 中创建界面时,通常不会直接处理鼠标光标的位置,而是依赖于 ImGui 光标自动控制 UI 元素的布局。
Eg.
ImGui::Text("Hello, World!");
ImGui::Button("Click Me!");
在这个例子中,Text 和 Button 会根据 ImGui 光标的位置自动排列。当第一个元素被绘制后,光标会向下移动,按钮会出现在文本的下方。
ImGui光标的格式
坐标格式: | ImGui 光标的位置是一个具体的坐标,表示相对于当前窗口的左上角((0, 0) 点)的位置。 这个坐标通常是以像素为单位的,并以 ImVec2 类型返回,其中包含两个浮点值:x 和 y,分别表示水平方向和垂直方向的坐标。 |
----- ImGui::GetWindowSize()
| 返回当前窗口的宽度和高度(ImVec2),便于在绘制控件时进行相应的布局。 |
----- ImGui::GetWindowPos()
| 作用:获取当前 ImGui 窗口的位置信息,通常用于确定窗口在屏幕上的位置。 坐标位置:返回当前窗口左上角相对于屏幕的坐标位置,便于进行布局和处理鼠标事件等。 |
----- ImGui::GetMousePos()
| 返回当前鼠标光标在屏幕上的坐标(全局坐标) |
《《代码理解:为何以下代码可以动态的计算出视口(窗口)的可用区域?(动态:指的是可以根据标签栏是否存在,计算出此时的窗口可用区域)
auto viewportOffset = ImGui::GetCursorPos(); // Includes tab bar
ImVec2 minBound = ImGui::GetWindowPos();
minBound.x += viewportOffset.x;
minBound.y += viewportOffset.y;
根据上述 GetWindowSize() 函数的理解,我们得知这个函数返回的是 ImGui 光标的位置。这个光标记载了 ImGui 控件的位置,当便签栏存在时,这个函数返回的值会发生变化(无标签栏时返回可用区域的左上角坐标;有标题栏的时候,会因为窗口中多绘制的标题栏控件,而返回已经发生变化的坐标)
(全局坐标 + 相对坐标 -> 符合需求的全局坐标)
由于 ImVec2 minBound = ImGui::GetWindowPos() 这里得到的是窗口在屏幕中的全局坐标,而 ImGui::GetCursorPos() 得到的是控件在窗口中的相对坐标,所以对于这个全局坐标,我们加上相对坐标,便可以计算出控件存在时的可用范围。
无标签栏时:
有标签栏时:
《《 代码理解:如何计算的相对坐标(相对坐标->指的是将鼠标的全局位置修改为相对于视口窗口的坐标,准确位置相对于视口窗口的左上角。因为对于鼠标坐标,我们都对其减去了minBound)
auto[mx, my] = ImGui::GetMousePos();
mx -= m_ViewportBounds[0].x;
my -= m_ViewportBounds[0].y;
(全局坐标 - 全局坐标 = 对于窗口的相对坐标)
计算全局鼠标坐标相对于窗口的位置,比如:假设全局鼠标坐标是 (500, 300),窗口左上角的坐标是 (450, 250)。通过计算:
mx = 500 - 450 = 50
my = 300 - 250 = 50
这表明,鼠标在窗口中的位置是 (50, 50),即距离窗口左上角50像素的位置。
》》》》一个小Bug:(白色小光标表示鼠标位置)
正确的操作:
错误的操作:当鼠标在视口中,但是鼠标位于视口靠上方时,会出现无效值。
发生这个错误的原因是:ImGui::GetWindowSize() 获取的是整个窗口的尺寸,包括边框和标题栏等。而我们需要的窗口的客户端区域尺寸不包括能边框,否则在计算鼠标相对与窗口左上角的位置坐标时,我们获取的坐标 mouseX , mouseY 会发生偏移,想要解决这个问题,就应该使用 ImGui::GetAvailableRegion() 来获取窗口尺寸。
而 m_ViewportSize 恰好是这样获取的。
》》》》疑问:
疑问1.0:为什么需要特别计算一个 viewportSize ,以此作为判断依据呢?为什么不可以按照 maxBound 来判断呢?
比如这样:
答:因为 m_ViewportBounds[1] 中存储的 maxBound 是屏幕全局中的坐标,而这里我们获取的鼠标坐标是经过处理的视口相对坐标,所以如果需要进行条件判断,则必须按照 minBound 和maxBound 计算出从[0,0] 开始的整个视口的相对坐标,并将其作为判断依据。
疑问1.1:但是ReadPixel中也填写了mouseX,MouseY这两个相对坐标,这是否正确?
正确,因为这个函数是使用在 Framebuffer 之下的,我们使用 mouseX 和 mouseY 来读取视口中的数据。虽然 mouseX,MouseY 并不是整个屏幕上的绝对坐标,但他们是窗口上的相对坐标。由于窗口完全被视口填充,且这个坐标的大小也被限制在视口的尺寸之内,所以这两个坐标可以表示视口("Viewport"这个窗口)中的位置,因此这个函数参数的使用是正常的。
疑问2:这里对于坐标进行整形转化的用意是什么?不做会怎样?
int mouseX = (int)mX; 和 int mouseY = (int)mY; | mX 和 mY 在前面是通过 ImGui::GetMousePos() 获取的鼠标位置,它们是浮动值(通常是 float 类型),表示鼠标在视口中的相对坐标。 因为图像或像素通常以整数坐标来进行处理,浮动值对它们没有直接的意义。所以通常需要将浮动型的鼠标坐标转换为整数,才能用于像素访问或图像读取等操作。 |
(int)viewportSize.x 和 (int)viewportSize.y | viewportSize.x 和 viewportSize.y 是 float 类型的值,但在进行鼠标坐标检查时,视口的宽度和高度应该是整数,这样才能与已经转换为整数的 mouseX 和 mouseY 进行比较,确保鼠标坐标在有效的范围内。 |
《《《《 Tips: You can use ( ImGui::GetForegroundDrawList()->AddRect(minBound, maxBound, IM_COL32(255, 255, 0, 255)); ) to draw the available region with colored rectangle
--------------------------------Clear texture attachments & Support multiple entity IDs--------------------------------------------
》》》》这一次我要提交两个部分:清除和多个实体ID(清除指的是将空白区域的鼠标输出改为:-1,多实体区域指的是每一个实体都将拥有一个ID,作为鼠标输出)
》》》》一些当下的理解:
当前,我们创建了一个帧缓冲对象,并且为其创建了两个颜色附件(纹理附件):RGBA8、RED_INTEGER,一个深度附件、一个模板附件。
其中颜色附件的RGBA8被用来绘制物体,RED_INTEGER被用来储存或处理实体ID。
而且由于我们使用了批渲染,所以理论上所有渲染的物体应该是一个整体,所以不用使用多个颜色附件。
在实际的代码操作中,我们会根据 FrameBuffer::Create 填入的参数顺序来分配附件索引,也就是说:
如此一来,我们可以访问不同附件插槽上的数据,如果此时我们在着色器中设置了 color2 ,并将其作为 Attachment1,就可以读取到我们在着色器中对其设定的值。(此处示例为 color2 = 50)
如果我们选择在特定情况下读取该附件的内容,并打印,才会有类似的效果。
但是只有鼠标停留在窗口中已绘制的物体上时,才可以访问到有效的 Attachment (如果鼠标停留在窗口内空白区域,此处没有绘制物体,而附件 Attachment 又恰恰是在绘制一个物体时才创建的,所以此时打印的值是无效数字 : 1036831949)
如果此时想清除空白区域的无效数字,则需要在访问所有区域之前,使用 ClearTexImage() 清空所有数据(使用 ClearTexImage() 将 Attachment0 + index 这里的数据临时更改为指定值:-1),这样一来当鼠标位于空白区域时,就会返回我们设置的值。(如果后续鼠标会位于物体上,我们只需要使用函数 ReadPixel() 重新读取 Attachment0 + index 存放的值即可。)
》》》》关于 glClearTexImage()
glClearTexImage()
作用: | 是 OpenGL 4.5 引入的一个函数,它允许你直接清除纹理(texture)对象的内容(颜色、深度、模板或其他类型的数据),而不需要绑定纹理到帧缓冲区。 这个函数提供了一种更高效的方式来清除纹理的内容,,避免了传统的方式(通过帧缓冲区绑定清除纹理)所带来的额外开销,这在图形渲染管线中尤其非常有用。 |
函数原型: | void glClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void *data); |
参数说明: | 要清除的纹理对象的名称。纹理对象在创建时通过 glGenTextures 获取的 ID。该纹理对象必须是有效的。 指定纹理的级别(level)。对于常规纹理,level 通常是 0,表示基础级别。对于多级渐远纹理(Mipmap textures),level 表示清除哪个 mipmap 级别的纹理。 指定清除时所使用的数据格式。常见的格式包括: GL_RED, GL_RG, GL_RGB, GL_RGBA 等。 GL_DEPTH_COMPONENT, GL_STENCIL_INDEX, GL_DEPTH_STENCIL 等用于深度和模板缓冲区的格式。 指定清除时所使用的数据类型。常见的数据类型包括: GL_UNSIGNED_BYTE, GL_FLOAT, GL_INT 等。 该类型应该与 format 兼容。 指向一个数据缓冲区的指针,包含用于清除纹理的值。这个值的格式和类型必须与 format 和 type 相匹配。 常见用途:data 可能是一个填充了特定清除值的数组。例如对于 format = GL_RGBA 格式,可以将其设置为一个数组(例如:{0.0f, 0.0f, 0.0f, 1.0f}),以清除纹理为透明黑色。 在此处我们将 format = GL_RED_INTEGER ,data = -1,表示我们将 GL_ATTACHMENT1(或其他位置)存储的 GL_RED_INTEGER 清除为-1
|
------------------------- Unique Entity ID for mouse picking (Mouse picking) -----------------------------
》》》》这一次提交的主要思路为:为顶点再添加一个属性 -> EntityID,这个 EntityID 会被实体默认生成的ID所填入,进而在顶点中标识某一个实体,我们也可以访问到。
具体思路是:
1. 确定新的顶点属性 layout( location = 5) in int a_EntityID
2. 根据这个新增的顶点属性,我们首先需要更改着色器中的对应语句。其次,还需要更改 VertexArray 的 SetLayout() 函数,以便我们能够正确的添加新增的顶点属性 。最后,我们还需要更改实际的绘制函数,以便我们在绘制的过程中保存对应的 entityID
3.调用更新后的绘制函数,为图像添加新的顶点属性
4.也可以通过读取出来的 EntityID 将实体信息放在 ImGui 窗口中实时查看
》》》》我们在 OpenGL 上下文中访问的顶点属性数据(比如访问片段着色器中的输出变量 color2),此时这个变量存放在哪里?我们从哪里读取到这个数据?
前提:
着色器的使用阶段: | OpenGL 渲染管线分为多个阶段,其中顶点着色器和片段着色器是关键的两个阶段。 |
着色器的作用: | 顶点着色器负责处理每个顶点的数据(如位置、颜色、纹理坐标等),而片段着色器则负责处理每个片段(即像素)的颜色和其它属性。 |
Eg.假设这里有一段着色器代码(glsl)
Vertex shader
#version 330 core
layout(location = 0) in vec3 a_Position; // 顶点位置
layout(location = 1) in int a_EntityID; // 顶点的 EntityID(来自顶点缓冲区)
out flat int v_EntityID; // 传递给片段着色器的 EntityID
void main()
{
gl_Position = vec4(a_Position, 1.0);
// 将 EntityID 从顶点着色器传递到片段着色器
v_EntityID = a_EntityID; }
Fragment shader
#version 330 core
// 接收来自顶点着色器的 EntityID
in flat int v_EntityID;
out vec4 FragColor;
void main()
{
FlagColor = v_EntityID;
}
首先让我们明确一下使用逻辑:
第一步, | 我们在绘制的时候通过函数填入了 EntityID,随后通过代码中的 SetLayout() 明确 EntityID 的属性,并将其写入顶点缓冲区中。 |
第二步, | 当顶点被写入后,在渲染管线阶段中,顶点着色器中的数据被传输给片段着色器,在片段着色器中我们也可以进行一些处理操作。 |
第三步, | 我们在实际使用时,会使用函数读取片段着色器中的 FlagColor 变量, 而 FlagColor = v_EntityID. |
》此时我的问题是,在访问过程中,片段着色器中的 FlagColor 变量存储在哪里?因为 FlagColor = v_EntityID,我还想知道 v_EntityID 此时存放在哪里?
问题一:v_EntityID 存放在哪里?
传递过程:
当 OpenGL 上下文尝试访问 v_EntityID 时,是从顶点着色器传递来的。这是具体的传递过程:
顶点缓冲区存储:每个顶点有一个对应的 EntityID,通常通过顶点属性传入(比如通过 a_EntityID)。
顶点着色器处理:顶点着色器将 EntityID 从输入的顶点数据传递给片段着色器(通过 out 变量 v_EntityID)。
片段着色器访问:片段着色器通过 in 变量 v_EntityID 获取这个值,并根据它执行不同的渲染操作。
所以,v_EntityID 实际上是存储在 GPU 的顶点缓冲区中的,并在顶点着色器和片段着色器之间传递。
问题二: FlagColor(Color2)存放在哪里?
在大多数情况下,这些输出值被存储在与当前绑定的帧缓冲关联的颜色附件中,比如片段着色器的输出通常会存储在帧缓冲的一个 颜色附件(Color Attachment)中。
OpenGL 允许你绑定多个附件位置,我们可以通过 OpenGL 提供的宏常量:GL_ATTACHMENT0,GL_ATTACHMENT1 ….. ,通过附件绑定的位置访问附件。
》》》》GLSL 中的 flat 修饰词有什么作用?
EG. | in flat int v_EntityID; |
概念: | flat 是一个修饰符,在 GLSL 中,它用于确保着色器中不同处理单元(比如不同的片段或像素)在使用此变量时不会进行插值。换句话说,使用 flat 关键字声明的变量在不同的计算单元中会保持相同的值,不进行插值,即每个计算单元直接使用传入的值。 |
兼容性: | 插值操作与 int 类型不兼容,GLSL 的插值机制默认设计用于浮点类型数据。当使用 int 类型时,插值操作并不总是适合的,因为 int 是离散的,并且它不能像浮点数那样平滑过渡,所以如果你不加 flat 修饰符,着色器会尝试对 int 进行插值,这会导致不可预测的结果。 例如,在两个顶点之间,着色器可能会尝试对整数值进行插值,这显然是无意义的,因为整数类型值无法像浮动类型那样平滑过渡。 |
实际情况说明:
在没有 flat 修饰符的情况下,当你从顶点着色器向片段着色器传递数据时,数据会被插值。而插值是为了平滑过渡而设计的(通常应用于浮点数),所以,如果你希望 v_EntityID 在片段着色器中始终保持不变(不被插值),你需要使用 flat 修饰符来阻止插值。
例如,在光栅化过程中,可能有多个片段(像素)在不同的计算单元上处理,如果没有 flat 修饰符,v_EntityID 会进行插值操作,但由于 int 类型是离散的,插值可能会导致无效的值(例如,v_EntityID 在两个顶点之间的值不会是整数,可能导致错误)。此时使用 flat 可以避免这种插值,确保每个片段获得完全相同的值。
错误注意:
1.未明确使用 flat 关键字: 对于整型的输出变量,如果没有明确使用 flat 修饰符,GLSL 会默认对 v_EntityID 进行插值。这可能会出现报错: | | ||||
2. 使用 flat 时,需要对着色器版本号进行更改: |
更改后可能会出现警告:(再次运行一次程序便不会出现警告,我猜想是因为着色器的版本号被改变了,需要重新编译一次)
|
》》》》glVertexAttribIPointer( ) 有什么意义?怎么使用?
glVertexAttribIPointer() 是 OpenGL 中的一个函数,专用于设置整数类型的顶点属性指针。
这个函数的作用是告诉 OpenGL 如何在顶点缓冲区中读取顶点属性数据,尤其是当这些数据是整数类型时。它类似于 glVertexAttribPointer(),但专门处理整数类型数据,而不涉及浮点数据。
》》》》运行之后,我注意到一个问题打印出来的 EntityID 为什么不是 0,1,2 而是 0,2,3 ?
这是因为我们在 yaml 文件中存储的实体顺序是这样的(我们不仅存储了三个物体实体,还存储了一个摄像机实体,而这个摄像机实体由于一些原因被放置在第二个实体的位置上)
》》而我们只需要在文件中调整几个实体的位置即可实现 0,1,2 的效果:
》》》》使用思路:这句代码是怎样实现对应功能的?
m_HoveredEntity = pixelData == -1 ? Entity() : Entity((entt::entity)pixelData, m_ActiveScene.get());
前提:在使用 Ctrl + O 之后,会调用一个函数 --> OpenScene(); 这个函数将会继续调用 --> serializer.Deserialize(filepath); 在 Deserializer() 中,有这两句代码:
Entity& deserializedEntity = m_Scene->CreateEntity(name); // Create a new entity in m_Scene with all default values
DeserializeEntity(entity, deserializedEntity); // Update values in this entity accroding to yaml file
也就是说, OpenScene() 函数会为我们创建实体,通过调整实体的属性,我们得以在引擎中查看文件的内容。
理解:所以在我们打开某个文件之后,画面中绘制的所有物体,其实就是我们创建的实体(这些实体在 OpenScene() 中被创建,并且调整了组件中的属性:Translate, Color ….)。如果此时我们在特定情况下,使用相同的数据再次创建一个新实体(Entity 类型对象),就可以确保新实体是对应旧实体的副本。
Eg.
Ctrl + O 打开文件,最终会运行此函数。其中高亮的代码部分会确保我们在读取文件之后,创建 YAML 文件中的实体
CreateEntity() 会创建实体,m_Registry.create() 会自动分配 entityID,这个ID是唯一的。
这时,比如在代码中,如果鼠标光标在视口范围内、且悬停在物体上方,我们就创建实体:HoveredEntity
由于我们在 DrawQuad() 这个绘制函数中,将 EntityID( EntityHandle )传入了顶点着色器,所以通过 ReadPixel() 我们可以读取对应的 EntityID。并使用这个 EntityID(pixelData)创建一个实体。
那么这个实体 和 打开YAML文件时创建的实体 有什么联系呢?为什么我们可以通过 HoveredEntity 使用 GetTagcomponent(),访问到先前实体的数据?
因为 Deserializer() 中创建的实体,是通过 EntityID( m_Registry.Create() ) 和 Scene( this 即 m_ActiveScene )创建的,这两个参数都是唯一的标识,我们可以通过这样的唯一标识访问组件。
如果此时我们又新建了一个实体,比如HoveredEntity, 并使用了相同的参数将其初始化,这样我们就拥有了先前实体的一个副本,这个副本就是 HoveredEntity。由于 HoveredEntiy 中保存着这些唯一标识,我们当然可以使用 GetComponent() 访问到对应的组件,并且组件的内容 等同于 先前实体组件中的内容。
》》》》代码中的 OpenGL 版本号管理代码,在哪里?
在代码中并没有明确表示OpenGL版本号( 比如通过 GLFWWindowHint() 设置版本号),所以GLFW 会检测你的图形卡及其驱动程序支持的 OpenGL 版本。如果没有明确指定版本,GLFW 会选择一个合适的默认版本,通常是当前系统支持的最新版本。
本机为:
》》》》我对Cherno的代码做了一点更改
在这里,我觉得满足条件判断之后,会一直调用构造函数,可能比较耗费性能。
更改之后:
不过从数据分析上看起来,虽然优化了一部分性能,但是本来好像就没有很大的性能负担。:-)
--------------------------------- Left click to select entities -----------------------------------------
》》》》ImGuizmo::IsOver( )
作用: | ImGuizmo::IsOver() 的主要作用是检查当前鼠标是否悬停在 Gizmo 上,或者说当前操作是否影响了 Gizmo。如果鼠标在某个 Gizmo 上,返回 true,否则返回 false。 通常,这个函数与其他 Gizmo 操作函数(如 ImGuizmo::Manipulate())结合使用,以便判断用户是否在进行某些变换操作。 |
原型: | bool ImGuizmo::IsOver( ImGuizmo::OPERATION operation = ImGuizmo::ALL ); |
参数: | ImGuizmo::IsOver() 需要一个 操作模式 作为参数,该参数用于指定检查的 Gizmo 类型。 ImGuizmo::OPERATION operation: 这是一个枚举类型,指定了 Gizmo 的操作类型。 包括:
|
返回值: | true:如果当前鼠标悬停在 Gizmo 上,或者用户正在对 Gizmo 进行某种操作。 false:如果鼠标不在 Gizmo 上,或者没有进行任何操作。 |
》》》》我所做的改进:
我新添了一个实体,这个实体只在 鼠标单击具体物体 / 单击某一实体的 Guizmoa 时更新实体,这样一来,尽管 HoveredEntity 会因为鼠标的位置实时发生改变,但是 UsingEntity(Entity in use) 不会轻易改变,只有鼠标操作某一物体时才会发生更改。
m_UsingEntity 逻辑更新代码:
ImGui::Text() 打印代码:
----------------------------------- Maintenace ---------------------------------------------------
》》》》后续会整合 Jul/22 2020 之后的维护
------------------------------ SPIR-V & New shader system --------------------------------------------------------
》》》》这次 Cherno 做了很多提交,所以我的笔记可能篇幅较长,但我会仔细记录。
我做了一些笔记,请认真浏览。
实际操作步骤请转到: 》》》》我将逐次的提交这些代码,并记录自己的疑虑
》》》》介绍与引入
》》》》 basic architecture layout of this episode(本集基本构架)
(截图仅供个人参考,并无侵犯版权的想法。若违反版权条款,并非本人意愿)
个人在学习过程中觉得最值得查阅的几个文档:
游戏开发者大会文档 (关于 SPRI-V 与 渲染接口 OpenGL/Vulkan 、GLSL/HLSL 之间的关系,SPIR-V 的工具及其执行流程 | https://www.neilhenning.dev/wp-content/uploads/2015/03/AnIntroductionToSPIR-V.pdf |
俄勒冈州立大学演示文档 ( SPIR-V 与 GLSL 之间的关系, SPIR-V 的实际使用方法:Win10 ) | https://web.engr.oregonstate.edu/~mjb/cs557/Handouts/VulkanGLSL.1pp.pdf |
Vulkan 官方 Github Readme 文档 ( GLSL 与 SPIR-V 之间的映射关系,以及可以在线使用的编辑器,非常好用) | https://github.com/KhronosGroup/Vulkan-Guide/blob/main/chapters/mapping_data_to_shaders.adoc 在线文档示例( Compiler Explorer ) |
大阪Khronos开发者大会(SPIR-V 语言的规范,及其意义) | https://www.lunarg.com/wp-content/uploads/2023/05/SPIRV-Osaka-MAY2023.pdf |
前 33 分钟,基本上讲述以下几点:
1.着色器将会支持 OpenGL 和 Vulkan ,故着色器中做了更改(涉及到 OpenGL 和 Vulkan 在着色器语法上的不同:比如 Uniform 的使用) | |
2.为了避免性能浪费,并高效的使用数据/统一变量,将采用 UniformBuffer 这种高级 GLSL。 (参考文献1-来自 LearnOpenGL 教程: 高级GLSL - LearnOpenGL CN ) (参考文献2-来自 Vulkan 教程: Descriptor layout and buffer - Vulkan Tutorial ) 建议阅读全文,这样理解更加深刻。 |
|
3.OpenGL 和 Vulkan 在着色器语言上的使用规范,还有不同之处。 参考文献:OpenGL教程( 材质 - LearnOpenGL CN ) 参考文献:俄勒冈州立大学演示文件《 GLSL For Vulkan 》( https://eecs.oregonstate.edu/~mjb/cs557/Handouts/VulkanGLSL.1pp.pdf ) 附录: 参考文献:Github 中文 Readme( https://github.com/zenny-chen/GLSL-for-Vulkan ) 参考文献: Vulkan 教程官网( Introduction - Vulkan Tutorial ) | 或者在 Vulkan 教程官网中搜寻( Introduction - Vulkan Tutorial )
|
4.SPIR-V 的使用思路,使用逻辑。 参考文献:SPIR-V 官网( https://www.khronos.org/api/index_2017/spir ) 参考文献:Vulkan 教程( Shader modules - Vulkan Tutorial ) 参考文献: Vulkan 指南( What is SPIR-V :: Vulkan Documentation Project ) 参考文献:俄勒冈州立大学演示文件( https://web.engr.oregonstate.edu/~mjb/cs557/Handouts/VulkanGLSL.1pp.pdf ) 参考文献:2016 年 3 月 - 游戏开发者大会 ( https://www.neilhenning.dev/wp-content/uploads/2015/03/AnIntroductionToSPIR-V.pdf ) 附件:关于 SPIR-V 也可以参考 SPIR-V 的 github 仓库: ( https://github.com/KhronosGroup/SPIRV-Guide ) | 参考接下来的笔记:( 实际使用流程: ) 或者参考( https://www.neilhenning.dev/wp-content/uploads/2015/03/AnIntroductionToSPIR-V.pdf )
|
》》》》 SPIR-V SPIR-V ?什么是 SPIR-V ? SPIR-V SPIR-V
SPIR-V 简介
SPIR-V (Standard Portable Intermediate Representation for Vulkan) 是一种低级中间表示语言(Intermediate Representation, IR),通常是由高层语言(如 GLSL 或 HLSL)编译而成,主要用于图形和计算程序的编译。( 开发者写的 GLSL 或 HLSL 代码会被编译成 SPIR-V,然后交给 Vulkan 或 OpenCL 、OpenGL等图形计算 API 来执行。)
SPIR-V 允许开发者编写更加底层的图形或计算代码,并通过它来与图形硬件交互。
实际使用流程:
OpenGL | 通常使用 GLSL(OpenGL Shading Language)来编写着色器代码 |
Vulkan | 使用 SPIR-V(Standard Portable Intermediate Representation for Vulkan)作为着色器的中间语言。 |
为什么说 SPIR-V 是中间语言?
在 Vulkan 中,着色器代码(如顶点着色器、片段着色器等)首先用高级语言(如 GLSL 或 HLSL)编写,然后通过工具(如 glslang)编译成 SPIR-V 字节码,最后通过 Vulkan API 加载并使用这些字节码。
OpenGL 与 SPIR-V的工作模式: | 在 Vulkan 出现之前,OpenGL 是主要的图形 API,GLSL 是 OpenGL 使用的着色器语言。随着 Vulkan 的推出,SPIR-V 成为了 Vulkan 着色器的中间表示,SPIR-V也被引入到 OpenGL 中。 尽管 OpenGL 一直使用 GLSL 作为着色器语言,但 OpenGL 4.5 及更高版本已经支持通过 SPIR-V 加载编译好的着色器二进制文件。 这意味着OpenGL 虽然仍旧使用 GLSL 来编写着色器,但编译过程可以将 GLSL 代码转化为 SPIR-V,之后在 OpenGL 中加载 SPIR-V 二进制代码进行执行。这一过程通过 glslang(Khronos 提供的 GLSL 编译器)实现。 |
Vulkan 与 SPIR-V 的工作模式: 参考文献:游戏开发者大会2016 ( https://www.neilhenning.dev/wp-content/uploads/2015/03/AnIntroductionToSPIR-V.pdf) | Vulkan 作为低级 API,要求所有着色器都以 SPIR-V 格式存在。由于着色器源代码通常使用高级着色器语言(如 GLSL 或 HLSL)编写,所以需要先编译成 SPIR-V 二进制格式,然后将该 SPIR-V 二进制代码上传到 GPU 进行执行。 作用:SPIR-V 使 Vulkan 可以实现跨平台的着色器支持,依靠 SPIR-V 这种中间语言,着色器能够在不同平台和硬件上正常运行。SPIR-V 规范的语言比纯文本的着色器语言(如 GLSL)更接近底层硬件,便于优化和硬件加速。 示例: |
实际使用实例:
1. GLSL 源代码编写 | 首先,编写 GLSL 源代码。这些 GLSL 代码通常包括顶点着色器、片段着色器、计算着色器等。
|
2. GLSL 编译为 SPIR-V | 将 GLSL 源代码转换为 SPIR-V 二进制格式,得到一个平台无关的二进制文件,这意味着 SPIR-V 代码可以在不同的硬件和操作系统上运行。 工具1: glslang(Khronos 提供的编译器,广泛用于将 GLSL 转换为 SPIR-V)。 编译过程: GLSL 代码通过 glslang 编译器进行语法检查和优化,并得到一个二进制文件。 工具2: 你也可以使用命令行工具 glslangValidator 来编译 GLSL 代码。 编译过程: 使用命令:glslangValidator -V shader.glsl -o shader.spv |
3. 加载 SPIR-V 到 Vulkan 或 OpenGL 中 | 3.1 在 OpenGL 中使用 SPIR-V 前情提要: 从 OpenGL 4.5 开始,OpenGL 也支持通过 SPIR-V 加载编译好的着色器二进制文件。流程与 Vulkan 类似,只不过 OpenGL 在内部做了更多的高层封装。 加载过程: 示例:
----------------------------------------------------------------------------------------------------- 3.2 在 Vulkan 中使用 SPIR-V 加载过程: 创建一个 VkShaderModule 对象,该对象包含 SPIR-V 二进制代码。 使用 SPIR-V 二进制代码来创建 Vulkan 着色器管线(例如,创建顶点着色器和片段着色器的管线)。 示例:Vulkan 使用 SPIR-V
|
4. 执行着色器程序 | 在 OpenGL 中,SPIR-V 着色器程序被链接到程序对象中,并通过调用 glUseProgram 来激活该程序,之后通过绘制调用来执行。 在 Vulkan 中, 着色器被绑定到渲染管线或计算管线中,随后可以通过绘制命令(例如 vkCmdDraw)或计算命令(例如 vkCmdDispatch)来执行。 |
》》》》上述涉及语言的纵向对比图
GLSL | |
SPIR-V SPIR-V 本身的核心是一个二进制格式,然而为了便于开发和调试,SPIR-V 也可以以类似汇编语言的文本形式表达,这种形式通常称为 SPIR-V Assembly。 它是 SPIR-V 的一种可读性较好的文本表示方式,开发者可以通过这种形式来编写、调试和优化 SPIR-V 代码,然后再将其转换为二进制格式以供图形 API 使用。 实际上,SPIR-V Assembly 代码最终还是会通过工具(如 spirv-as)转化为二进制格式,供 Vulkan 或 OpenGL 使用。 | SPIR-V SPIR-V Assembly |
OpenGL | |
Vulkan | |
》》》》具体代码更改细则
以下是更新详情(图示):
1 premake脚本更改 (and better premake scripts) | ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- |
2 py脚本 (Python scripts for retrieving dependencies) | 1. 确保在执行过程中 requests 和 fake-useragent 这两个模块已经安装。如果没有安装,它会自动使用 pip 安装它们。 1.确保所需的 Python 包已经安装。 2.检查 Vulkan SDK 是否安装,并确保 Vulkan SDK 的调试库存在。 3.改变当前工作目录到项目根目录。 4.使用 premake 工具生成 Visual Studio 2019 项目的构建文件。 DownloadFile(url, filepath) 函数的作用是从指定 URL 下载文件,并显示实时的下载进度(包括下载进度条和速度)。 YesOrNo() 函数用于与用户进行交互,获取用户的确认输入,返回布尔值表示“是”或“否”。 用于检查和安装 Vulkan SDK InstallVulkanSDK(): 下载并运行 Vulkan SDK 安装程序。 InstallVulkanPrompt(): 提示用户是否安装 Vulkan SDK。 CheckVulkanSDK(): 检查 Vulkan SDK 是否安装并且版本是否正确。 CheckVulkanSDKDebugLibs(): 检查 Vulkan SDK 的调试库是否存在,如果缺失则下载并解压。 |
3 Application 中的 ApplicationCommandLineArgs ( added command line args) | ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- |
4 Uniform Buffer 的定义以及使用,包括着色器更新 (added uniform buffers) | ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- ------ ------ ------ ------ ------ ------ ------- |
5 着色器系统更新: (New shader system) | Timer 的定义 ------ ------ ------ ------ ------ ------ ------- 着色器更新 |
6 平台工具的更新(打开或保存文件) | |
7 视口与摄像机更新: | |
》》》》我将逐次的提交这些代码,并记录自己的疑虑
》》》一:我首先使用更新并使用 py 文件下载 Vulkan SDK
首先第一步:运行 bat 脚本,通过该文件下载 Vulkan SDK。
(Vulkan.py 文件使用了 Utils.py 中的函数,当你在 Hazel\scripts 的路径下通过 Setup.py 使用 Vulkan.py 时,Vulkan.py 会将 Vulkan 默认下载到 Nut/vendor/VulkanSDK。)
》》以下是这些 py 文件的结构: | |
》》问题零
运行脚本时,请关闭代理。
》》问题一
如果将文件放在 Scripts 文件夹下,并直接通过 Setup.bat 运行 Setup.py 的话,会出现报错,表示文件路径已经不存在。---> | |
这需要提前在 vendor 创建 VulkanSDK 文件夹。 (记得修改 .py 中的下载路径,这取决于你的项目名称,还有你想下载到本机的路径) | |
》》问题二
创建好 VulkanSDK 文件夹之后,重新运行 Setup.py,脚本运行之后开始尝试运行 Vulkan installer: | |
但是随后的弹窗中提示: | |
这可能是 Vulkan.py 中存放的 VulkanSDK 下载地址不适合 64 位系统,我将其更新为 2023 年的某一版本。 附录:如果你想进入官网查看适合你系统的SDK,以下是网址 -> ( LunarXchange ) | --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- |
当前我只更新了 SDK Installer 的安装地址,但是我还没有更新随后的 debug lib.zip,这是下一个问题会出现的地方,现在先不讨论。 我们先重新运行一遍,使用更新之后的 SDK install。 于是运行后出现这样的窗口: | |
安装 vulkan SDK | 我目前没有选择任何拓展,但在安装过程中,我不是很确定这个拓展和 DebugLibs 有没有什么直接关系。就先标注一下。 (毕竟这将会占用我1G空间 bushi) 随后便得到这样的文件构架: |
》》问题三
我们发现 Cherno 另外下载了一个 Debuglib.zip,并对其进行了一些处理。 但是在1.2.198.1版本之后,lunarg 公司不再支持 debuglibs 的单独下载。现在 SDK 中的调试库通常随着 Vulkan 库一起分发,不再单独打包成一个 zip 文件。 所以现在,这些文件通常直接包含在 Vulkan SDK 的核心目录下,特别是在 lib 目录中 我们也可以从评论中窥见这一更改。(@SionGRG) | |
现在我们需要更改这个函数( CheckVulkanSDKDebugLibs )的逻辑 首先,我对这个 shaderc_sharedd.lib 的路径有点疑惑:因为我的确查找到了 shaderc_shared.lib 这个库,而不是shaderc_sharedd.lib。 现在我开始更改,不过我发现原先的逻辑是:如果没有找到调试库,就在线去下载。 但现在这些文件将会在安装 Vulkan SDK 时,同步安装在文件夹中,所以如果没有找到的话,一定是安装是出了什么问题。 我便做了以下更改:( 仅仅是口头提醒一下 :-) ) 随后我重新运行 Setup.bat,并拒绝再次安装 installer,便得到这样的结果: | --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- |
我想应该是对了。
》》》》二:现在我们已经成功安装了 Vulkan,现在则需要更新 premake 文件内容。
这是将要实现的 premake 文件构架图(以及细则)
》》》》接下来我先更新 Premake Dependencies.lua 文件(这里为预处理,实际操作步骤在后面)。
第一步,我们在项目的根目录下重新编写一个 premake 文件,这个文件主要用来索引 vendor 中的外部库(API) | |
但我发现有些问题,比如 shaderc 和 spriv_cross 的路径已经发生改变,参考 1.3.250.1 版本:这两个文件夹位于 VulkanSDK/Include 下 而且由于我没有下载某些组件,这使很多文件并不存在。(我将其标注出来) 于是我决定下载拓展(shader toolchain debug symbols),这一步通过运行 maintenancetool.exe 文件实现: 虽然下载了一个组件可以解决但部分问题,但是尽管在之后我下载了其余的所有组件, VulkanSDK/Include/Lib 这个路径都不存在(但是Vulkan/Lib 这个路径存在),且 VKLayer_utils.lib 这个文件也不存在。 | 系统变量示例: 这个组件将会解决这部分问题: 一个问题:VKLayer_utils.lib 似乎在1.3.216.0 版本中被移除了。 |
所以这是 premake 文件最新的样子:
》》》》操作步骤:
》》111 现在我们将 Nut/premake.lua 中的表单独存放在另一个文件中( Dependencies.lua)
其中包括:
》》222 在 Nut/vendor/premake 下(注意不是 Nut/Nut/vendor/ 这个路径)创建如下文件。(内容等会说明)
(链接 : 》》》》接下来谈谈 vendor/premake 文件中我们新添的两个文件:premake5.lua 和 premake_customization/solution_items )
》》333 修改 Nut/premake.lua 内容,使其包含上述三个文件
》》444 修改 Nut/Nut/premake5.lua 和 Nut/Nut-Editor/premake5.lua 文件内容
具体内容是 : | Nut-premake 文件需要包含 Vulkan 的库目录,并在对应配置下添加相关链接。 |
》》问题:
在此处我遇到一个问题,就是 Cherno 对这两个文件关闭了 staticruntime 设置。
这表示禁用静态链接运行时库,使用动态链接的运行时库。意味着程序在运行时将依赖外部的动态链接库(DLL),而不是将运行时库直接嵌入到可执行文件中。
示例:
而我印象里 Cherno 没有说明要转回使用动态库的方式,所以现在我没有将其打开。
(在之后的提交中,我修改掉了这里的代码,可以查看:》》》》对着色器系统进行修改后,需要将 Premake 中的运行时静态链接关掉:)
(顺便一提,如果需要打开的话,还需要额外进行动态链接的配置操作,具体可以回看Cherno的视频: Static Libraries and ZERO Warnings | Game Engine series )
》》》》接下来谈谈 vendor/premake 文件中我们新添的两个文件:premake5.lua 和 premake_customization/solution_items.lua
具体的 Pull&requests 记载于 #301( https://github.com/TheCherno/Hazel/pull/301 )
Premake5.lua | 定义一个工具类型的项目 Premake,并且在构建后通过 premake5 工具来重新生成或更新项目文件。
这个脚本的目的是 生成或重新生成构建项目文件(如 Visual Studio 工程文件、Makefile 等),使用的是 premake5 工具。它是一个自动化构建的过程,通常用于生成构建系统(如 Makefile 或 Visual Studio 工程文件)等。 |
solution_items.lua | 这段代码的作用是为 Visual Studio 解决方案 文件(.sln)添加一个新的部分,称为 Solution Items,并将工作区中指定的文件(通过 solution_items 命令)添加到这个部分中。 解决方案项是指那些不是属于任何特定项目的文件,例如文档、配置文件等,通常用于存储一些和整个解决方案相关但不属于某个单独项目的文件。 这添加了对 Visual Studio 解决方案项(solution items)的支持。文档、配置文件、README 或其他相关文件将可以被作为解决方案项添加到解决方案中。 |
》》》》三:Application 中的 ApplicationCommandLineArgs
( added command line args)命令行参数
》》流程与定义的概述
首先,我们位于入口点的主函数中使用了(argc, argv) 来获取命令行信息。并且将参数传入到 CreateApplication() 中,以便后续使用这些信息:
| 在入口点使用的 Nut::CreateApplication({ argc, argv }),实际上是在构造一个 ApplicationCommandLineArgs 类型的对象,并将 argc 和 argv 传递给它。 |
管线流程:
| 这里是 CreateApplication() 的定义。 CreateApplication() 中使用了NutEditor() |
| 这里是 NutEditor() 的定义。 NutEditor() 是 Application() 的子类,故 NutEditor() 的构造函数会自动先使用父类 Application() 的构造函数,我们可以通过这个特性将 args 参数传给 Application() 的构造函数,并实现一些目的。 |
| 这是父类 Application() 构造函数的新定义。 同时我们新添了一个 GetCommandLineArgs() 的函数,用于获取私有变量 m_CommandLineArgs 中存放的数据。 |
我们可以在运行时查看argv获取到的信息是什么。 | |
》》》》知识点
》》》》关于 Argc, Argv
1. argc 和 argv 的含义
定义:
在 C 和 C++ 程序中,argc 和 argv 是由编译器(如 GCC、Clang 或 Visual Studio)在程序启动时自动传递给程序的 main 函数的两个参数。用于传递命令行的输入参数。
- argc:是 argument count 的缩写,表示命令行参数的数量。它是一个整数,包含程序名和任何附加的命令行参数。
- argv:是 argument vector 的缩写,表示命令行参数的数组。它是一个字符指针数组,每个元素是一个指向命令行参数的字符串。
例如,当你使用指令运行一个程序( ./myapp input.txt --verbose )时,argc 和 argv 的内容如下:
argc = 3,因为有三个参数(程序名、input.txt 和 --verbose) | argv[0] = "./myapp",表示程序的路径。 argv[1] = "input.txt",表示第一个参数(输入文件)。 argv[2] = "--verbose",表示第二个参数(开启调试模式)。 |
运行机制:
(什么时候传递?传递什么内容?)
| argc 和 argv 是由操作系统在启动程序时根据命令行输入自动传递的,不需要手动获取。 程序中 argc 和 argv 的值取决于你启动程序时后台输入到命令行中的命令或参数内容。在不同的操作系统上,命令行参数的格式和解释规则可能会有所不同。 比如:
|
(内容什么时候被确定?是否可以被随时改变?)
| argc 和 argv 是实时的,但它们是程序启动时由操作系统从命令行提取的参数,并且在程序执行过程中保持不变。 所以一旦程序开始执行,argc 和 argv 的值就固定了,不能在程序运行过程中改变。 |
2.有没有类似 argc 和 argv 的参数?
C++ 标准库没有其他内建的类似 argc 和 argv 的机制。argc 和 argv 是 main 函数的参数,是 C++ 标准定义的,通常用于处理命令行参数。
不过,你可以使用其他自定义的数据结构来封装命令行参数,为它们提供更灵活的操作方式,
例如,在当前情况下,我们可以在 EditorLayer.cpp 中实时的获取到命令行参数信息并将其打印在控制台上: | |
或者在 EntryPoint.h 中尝试打印所有捕获的命令行参数: | |
得到这样这样的结果: | |
3. 在怎样的影响下,获取的命令行参数或发生变化?
在通常情况下,一旦项目的构架被明确(比如依赖性、文件路径等等),仅对程序进行代码上的“软”处理无法修改从命令行中获取的指令内容,因为这个内容一般是在程序启动时cmd中的内容。此处我们可以看到命令为:“ E:\VS\Nut\bin\Debug-windows-x86_64\Nut-Editor\Nut-Editor.exe”
如果想要对其进行修改,可能需要在VS的项目属性页面,进行相关修改:
4.Cherno 为什么进行这样的处理?这个新功能的意图是什么?
分析指令内容:
让我们分析获取的指令:“ E:\VS\Nut\bin\Debug-windows-x86_64\Nut-Editor\Nut-Editor.exe”,这个指令的 argc 为 1,表示只有一段连续的指令。所以 argv 是一个只有一个元素的数组 argv,argv[0] 的内容便是“ ”,而 argv[1] 自然为 null。
先决条件:
首先要明确一点,在 x64 、 Debug 的模式下,如果我们运行这个程序 (Nut-Editor) ,我们会从命令行中固定的获取到诸如:“ E:\VS\Nut\bin\Debug-windows-x86_64\Nut-Editor\Nut-Editor.exe”这样的命令如上文所说,在项目的构架被明确之后,获取到的内容一般就固定下来了。
实际使用时发生的情况:
现在 Cherno 设置了命令行参数的新功能,但其实并不是想通过在某处修改命令内容,或者实时根据命令的变化进行一些操作。而是为了在命令行中运行指令时,开启引擎并进入页面的时候,能够自动预先加载一个场景,让我们查看效果以了解详情:
这里是 Cherno 的使用场景:
(旧) 一个黄褐色头发的男人,他打开了 cmd ,想要运行 Nut-Editor 应用,于是他输入了一句指令:
通常情况下,这一段指令将直接打开引擎,但并不会在开启时加载一个场景。因为现在只有一段完整的指令,也就是说,这个条件判断不满足: | (新) 但是在新功能的加持下,如果我们在该指令之后添加了一个来自场景的目录:
此时运行指令,你将会在启动时看到一个预先加载的场景。 |
》》》》遇到问题:
在理想状态下,运行指令后,程序应该能正常打开,但实际上我遇到了一些错误。 更新了着色器系统之后,我发现问题似乎出自文件路径。我猜测是绝对路径和相对路径导致的错误。 | |
第一个错误:"Could not open file from:…." | 现在我将 Renderer2d.cpp 中的代码进行修改:(将此前的相对路径改为绝对路径) |
第二个错误:我发现报错还来自这个函数: AddFontFromFileTTF ,于是我在使用这个函数的时候,将路径改为绝对路径(虽然这会导致该应用的可移植性降低),但着实是无奈之举。 | ImGuiLayer.cpp 中: 更改前: 更改后: |
问题三:纹理加载中的路径修复 | 关于纹理的加载: (ContentBrowserPanel.cpp) (EditorLayer.cpp) |
现在,便能够通过在终端输入:“E:\VS\Nut\bin\Debug-windows-x86_64\Nut-Editor\Nut-Editor.exe E:\VS\Nut\Nut-Editor\assets\scenes\3DExample.yaml”,来启动游戏引擎,并保证启动时预先加载了一个场景。 | |
值得注意的是,由于缓存的存在,我们需要在单次文件之后,刷新项目使得 bin/Nur-Editor/Nut-Editor.exe 文件运行的结果刷新。
或者需要手动删除一些缓存文件(例如着色器缓存文件,OpenGLShader的更新中会涉及到),这样才能保证我们在终端使用指令运行游戏引擎的时候,得到最新的报错日志等信息。
TODO:
这里的调试手段就是将 相对路径 改为了 绝对路径 ,以此避免中断。但这非常影响项目的可移植性,我暂时没有想到好的解决办法,如果有人可以补充,或者此后我有了想法,我会将其合并于项目代码中。
》》》》四:添加Uniform Buffer
》》关于 Uniform Buffer 的定义:具体可以查看( 高级GLSL - LearnOpenGL CN )
建议浏览该页面之后,再查看更新的代码。
etc….
》》操作步骤
现在我们了解了 Uniform Buffer 的原理及其使用方式,现在开始更新代码:
首先 | 是设置与定义 UniformBuffer (UniformBuffer.h, UniformBuffer.cpp, OpenGLUniformBuffer.h, OpenGLUniformBuffer.cpp) |
接着 | 是修改着色器中的统一变量,将其改为统一变量块( Uniform 块) |
最后 | 需要更新实际绘制是,绑定统一变量的代码(之前是一个一个绑定,现在可以直接绑定 Uniform 块),使用时方便快捷。 |
示例: | |
运行机制: 具体可以参考 ( 高级GLSL - LearnOpenGL CN ) | |
所以在设置了 Uniform Buffer 之后,可以取消绑定着色器并绑定统一变量的操作:
在更新代码以使用 Uniform Buffer 的时候,我发现一个问题:
前提:
我们在着色器中将两个统一变量更改为统一变量块,他们分别是:"u_ViewProjection"和"u_Textures"。
这都是为了UBO的使用而做的更改,因为Uniform buffer的使用需要在着色器统一变量块与UBO之间建立一种联系:”Binding Points“ -> 绑定点。
需要做的修改:
当然,我们也需要在着色器做完更改之后,再去更新相应的代码,比如:
| 在这里,我们直接将 u_ViewProjection 作为一个 mat4 变量传递给着色器,实现统一变量的直接绑定。 |
| 现在我们先创建了UBO,然后将着色器中的统一变量块(Uniform block)通过封装好的函数 "SetData()" ,绑定 UBO 到正确的绑定点 ( Binding Point). |
疑问:
我发现 Cherno 虽然为 u_Viewprojection 进行了更新,但是在将 u_Textures 由统一变量设置为统一变量块之后,他不仅删除了之前显示绑定统一变量的代码,还没有对 u_Textures 进行类似的更新,这让我有点迷惑。
思考:
这个问题的原因这是为何呢?
其实这和 Uniform buffer obj 没有很大的关系,这仅仅与 u_Textures 的一些特性有关。具体来讲,这和 OpenGL 纹理的特性相关。
答案:
纹理是 OpenGL 中的一种特殊资源,在着色器中使用 layout(binding = 0) 声明绑定点后,你只需对纹理进行绑定操作即可(将纹理绑定到对应的纹理单元),OpenGL 会自动处理纹理与着色器变量的映射。因此,在提前声明了 layout(binding = 0) 的情况下,纹理数组不需要像 UBO 那样通过 SetIntArray 或 SetData 来更新。
分析:
1. layout(binding = 0) 的原理
layout(binding = 0) 语法在 GLSL 中告诉 OpenGL,某个 uniform 变量(例如纹理或 UBO)会与一个 绑定点(binding point)关联。这种方式是 OpenGL 中的一种标准机制,允许你将资源(如纹理、UBO)直接绑定到特定的资源绑定点,从而避免了逐个设置 uniform 值的麻烦。
具体来说:
- 对于纹理(sampler2D、samplerCube 等):当你使用 layout(binding = N) 时,着色器的该纹理变量会与 OpenGL 中的绑定点 N 关联。
- 对于 Uniform Buffer Objects (UBO):UBO 的工作方式类似,也需要通过绑定点(binding = N)来绑定到 OpenGL 中某个绑定点
2. 但为什么纹理可以直接通过 layout(binding = 0) 来绑定,而不需要额外的操作?
对于纹理数组(sampler2D u_Textures[32]),其实你并不需要像 UBO 那样来传递数据。因为纹理绑定在 OpenGL 中已经是一个非常内建的机制,你只需要使用 layout(binding = N) 来声明绑定点,而不需要手动传递纹理单元索引。就能直接将这些纹理单元与着色器中的纹理数组自动对应。
》》》》五:OpenGL Shader 更新
》》》》接下来我将对着色器系统进行相关更新。(其中包括了: Vulkan 植入,日志错误提醒的更新,着色器缓存文件的生成)
》》》关于 Timer 的使用
示例:
| { // 创建一个 Timer 对象 Hazel::Timer timer; // 第一个操作:模拟一个短时间的操作 std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟 500 毫秒的延迟 std::cout << "Time after first operation: " << timer.ElapsedMillis() << " ms\n"; // 重置计时器 timer.Reset(); // 第二个操作:模拟一个稍长的操作 std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟 1 秒的延迟 std::cout << "Time after second operation: " << timer.ElapsedMillis() << " ms\n"; } |
》》》》对着色器系统进行修改后,需要将 Premake 中的运行时静态链接关掉:
包括 Nut/premake5.lua 、 Nut-Editor/premake5.lua、Nut/vendor/yaml-cpp/premake5.lua 这三个文件中的相关代码。
》》》》着色器中的 Location 要求
SPIR-V 作为 Vulkan 的中间表示语言,需要为每个输入/输出变量分配一个 location 值(为输入和输出变量明确指定 location 属性),以便于着色器编译器正确地将这些变量与 GPU 的管线绑定。
在 OpenGL 中,某些输入/输出变量(如顶点属性、uniforms等)可以通过其他方式来绑定。而在 Vulkan 中,SPIR-V 显式要求在着色器中为所有的输入和输出变量指定唯一的 location。
比如:
》》》》我差不多是直接复制了 OpenGLShader 更新的代码,所以没有仔细查看,可能会补充关于更新的理解笔记,我也不知道。
TODO:
(着色器更新中包括了: Vulkan 植入,日志错误提醒的更新,着色器缓存文件的生成 这几点)
在我做出更改之前,如果有人补充着色器中代码更新的细则与用意,我可以将其合并进来。
》》》》六:平台工具的更新(打开或保存文件)
》》》》这里我不用做更改,因为我的代码似乎是正确的。
》》》》七:视口与摄像机更新
》》》》 这里只是新增一些判断条件,非常简单。
Anyway , 这一集的提交应该到此结束了。这期间过了很久,并不是因为这一集很难,而是因为期末事情比较多,时间比较赶紧。
现在终于提交完毕了,无论如何,请享受接下来的学习。
--------------------------------------- Content browser panel -----------------------------------
》》》》std::filesystem::relative( )
如果 s_AssetPath 是 C:\Projects\MyGame\Assets,path 是 C:\Projects\MyGame\Assets\Models\Character.obj。
那么 std::filesystem::relative(path, s_AssetPath) 会返回 Models\Character.obj,这是 path 相对于 s_AssetPath 的相对路径。
》》操作图示:
第一次循环:
---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
第二次循环:
》》》》 "/=" 运算符重载
Eg.
"m_CurrentDirectory /= path.filename();"
/= 运算符的重载
概念:
在 C++17 的 std::filesystem::path 中,/= 运算符是被重载的,用于拼接路径。其功能是将路径对象 path 中的部分与左侧的路径进行合并。
使用要求:
m_CurrentDirectory 是一个表示当前目录的路径,通常是一个 std::filesystem::path 类型的对象。path.filename() 返回的是 path 对象中的文件名部分,且其类型也是 std::filesystem::path。
示例说明:
假设
m_CurrentDirectory | C:\Projects\MyGame\Assets。 |
path | C:\Projects\MyGame\Assets\Models\Character.obj。 |
path.filename() | Character.obj。 |
那么,m_CurrentDirectory /= path.filename(); 的结果会是 m_CurrentDirectory 等于 C:\Projects\MyGame\Assets\Character.obj
》》》》ImGui::Columns(columnCount, 0, false);
ImGui::Columns()
原型:
void ImGui::Columns(int columns_count = 1, const char* id = NULL, bool border = true);
参数解释:
columns_count (类型:int,默认值:1) | 功能:指定列的数量。默认值是 1,表示只有一列。如果你想创建多个列,可以设置为大于 1 的数字。 |
id (类型:const char*,默认值:NULL) | 功能:这是一个可选的字符串,用来指定一个唯一的 ID。 如果多个列使用相同的 ID,ImGui 会为它们创建一个统一的状态。这个 ID 在 ImGui 的内部用于区分不同的列布局,但如果不需要区分,可以传入 NULL 或忽略它。 |
border (类型:bool,默认值:true) | 功能:指定是否显示列之间的边框。如果为 true,列之间会有一个分隔线。如果为 false,则没有边框,列之间没有分隔线。 |
示例: | 示例:ImGui::Columns(3) 表示创建 3 列布局。 |
示例:ImGui::Columns(3, "MyColumns"),通过指定 ID,可以在后续的操作中区分不同的列布局。 | |
示例:ImGui::Columns(3, NULL, false) 表示创建 3 列,并且不显示列间的边框。 |
》》》》一段错误代码诱发的思考:
错误的:
如果将 ImGui::ImageButton() 放在条件判断中,会导致优先判断按钮是否被单击,随后才会判断使用者是否在指定区域双击图标,这会导致鼠标双击的逻辑不能正常触发。
正确的
》》》》ImGui::TextWrapped()
概念:
ImGui::TextWrapped() 是一个用于在 ImGui 中显示文本的函数,主要特点是当文本内容超出当前窗口或控件的宽度时,会自动换行显示。
这个特性适用于显示多行文本,因为文本宽度是动态的,可以适应父容器的大小。这避免了手动计算的麻烦。
函数原型:
void ImGui::TextWrapped(const char* fmt, ...);
void ImGui::TextWrapped(const std::string& str);
参数:
fmt:一个格式化字符串,允许你使用 ImGui 的格式化语法来插入变量。例如,可以传入一个字符串,或者传入多个参数,通过 fmt 来格式化它们。
str:传入一个 std::string 对象。它会自动转化为 C 字符串并显示在界面上。
用法:
1. 基本用法: |
|
2. 与格式化字符串一起使用: 你可以通过格式化字符串来显示动态内容。例如显示文件名、错误信息等。 |
|
3. 使用 std::string: 如果你有一个 std::string 对象,也可以直接传给 TextWrapped。 |
|
》》》》DragFloat 和 SliderFloat 的区别。
ImGui::DragFloat 和 ImGui::SliderFloat 的区别
DragFloat: 既可以通过鼠标在输入框中直接滑动,也可以输入值。 | |
SliderFloat : 只能操作滑块来改变大小。 | |
--------------------- Content browser panel (Drag & drop) ----------------------------------
》》》》PushID 和 PopID 的作用是什么?PopID 是否可以放在 if 条件判断之前?
一:PushID 和 PopID 的作用
在 ImGui 中,当你渲染多个相似的控件(例如多个互动式按钮)时,它们通常会基于 ID 来管理自己的状态(如是否被点击、是否被悬停)。如果没有使用 PushID,这些控件可能会因为共享相同的 ID 而相互干扰(例如,所有的按钮都会共享同一个按下状态,或者鼠标悬停状态)。
通过 PushID 和 PopID,你确保每次循环渲染时,都为每个控件生成一个独特的 ID,这样每个文件的按钮、拖放等行为都能独立工作。
二:PopID 是否可以放在 If 判断之前?:不可以
如果在创建完控件之后就结束 ID 的作用范畴,接下来的条件判断 if(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked()) 将不再依赖于正确的 ID,而是随机的对某些按钮进行响应,这可能导致行为不一致或 UI 控件无法正常工作。
》》》》BeginDragDropTarget() 使用细则
如果手动跟进了 Cherno 的代码,我们会发现,使用 DragDrop 功能只需要两步操作:设置拖动源、设置拖动目标。
拖动源的设置 (ContentBrowserPanel.cpp) | |
拖动目标的设置 (EditorLayer.cpp) | |
》》计算const char* 类型字符串
如果有这样一个变量: const char* path = "abc/def/g"。计算其长度时,如下两种方式,一个错一个对:
Sizeof(path) | 错误:这行代码只计算了指针的大小,而不是整个字符串的大小。(指针->指的是 "abc/def/g"中首字符的内存位置,也就是 'a' 在内存中的存储位置。 |
(Strlen(path) + 1) * size(char) | 正确: strlen() 计算字符串的长度,但不包含 '\0',故加一。然后对其乘以 char 类型的大小,得到正确结果。 |
》》关于拖拽预览的绘制,还需要注意一点:
注意:在使用 BeginDragDropTarget( ) 之前,需要绘制一个有效的交互区域。
比如在视口的设置之后,我们使用了BeginDragDropTarget( ) ,你会发现在拖动文件到视口区域时,视口的可用区域会高亮,并且能够处理后续文件拖入操作。 可是如果注释掉 ImGui::Image() 这一行代码,你会发现拖动文件的功能会无响应。 这是因为 ImGui::Image 不仅显示了图像,还会自动处理它的交互区域,因此它是一个“有效”的拖放目标。 | |
如果你只绘制了一个窗口,或者在窗口中放置了Text,Child等“不可交互”的空间,可用区域高亮便不会出现。同样的,文件拖动也会不起作用。 Eg. | |
此时便需要我们创建一个可交互的区域:ImGui::Button、ImGui::Dummy 等等控件,以此来完善文件拖动的功能。 | |
》》什么是 ImGui::Dummy
概念:
ImGui::Dummy 是 ImGui 提供的一个函数,用于创建一个“占位符”或“虚拟”元素,它不会渲染任何实际的内容,但可以用来占据空间或提供一个交互区域。
主要用途: | 占位符:ImGui::Dummy 可以作为一个占位符,帮助你设置一些占用空间但不渲染任何实际内容的区域。这对于需要控制布局、调整空间或创建拖放目标区域非常有用。 控制布局:通过 ImGui::Dummy,你可以创建精确的布局区域,而不会干扰其他控件的显示。例如,当你需要创建一个特定大小的区域来接收拖放操作时,可以使用 Dummy 来占据空间。 |
语法: | void ImGui::Dummy(const ImVec2& size); |
参数: | size:指定占位符的大小,通常是一个 ImVec2(x 和 y 坐标)。这定义了 Dummy 占据的区域的大小。 |
示例: | 假设你想在 ImGui 窗口中创建一个区域,它不会显示任何内容,但你希望它占据一个特定的空间:
|
--------------------------------------- Texture Drag&Drop -------------------------------------------------------
》》》》很久没回来更新了,懒人一个。
之前把游戏引擎的视频看完了,但一直疏于更新,接下来我好好更新。(真的)
》》》》没什么要记的
-------------------------------Something you need to know in GAME ENGINE ---------------------------------
》》》》看了一会看不动了,应该也没什么代码提交,So I skip that
-------------------------------------- Play Button ----------------------------------------
》》》》5:28 ~ 15:05 修复纹理撕裂的Bug
》》》》 后面好像也没什么好记的
ImGui::GetWindowContentRegionMax() | |
函数签名: | ImVec2 ImGui::GetWindowContentRegionMax(); |
返回值: | ImVec2 类型,表示当前窗口内容区域的最大坐标(右下角的坐标)。 |
通常与ImGui::GetWindowContentRegionMin()一起使用。 |
》》》》一些 ImGuiWindowFlags_ 的定义:
》》》》GL_LINER 和 GL_NEAREST 的概念及区别:
概念: | |
对比 | |
-------------------------------------- 2D Physics ----------------------------------------
》》》》概述
2:51 ~ 9:40 修复一个无法编译的错误
9:50 ~ 13:30 将 Box2D 设置为子模块
13:50 ~16:25 修改 Premake 文件
18:30~ 25:18 Box2D 使用解释
25:20~25:58 引擎的一些小改变
26:00~56:32 设计组件和实际使用 Box2D
56:32~ 1:08:00 UI 界面的设置以及成果运行展示
1:08:20~ 1:19:45 序列化与反序列化以及成果演示
》》》》网址 Box2D(3.1.0)
官网: | Box2D |
文档: | Box2D: Overview |
Github: | https://github.com/erincatto/box2d |
非常建议在开始写代码前阅读(3.1.0):
简单演示参考-> | Box2D: Hello Box2D |
模拟时的代码参考 -> | Box2D: Simulation (包含了 ID, World, Body,Shapes,Contacts, Joints 的定义、初始化、概念等等) |
》》》》网址(2.4.1)
官网: | Box2D: Overview |
Github | https://github.com/erincatto/box2d/tree/9ebbbcd960ad424e03e5de6e66a40764c16f51bc |
》》》》开始之前
前提:
我发现有人曾经提示过版本更改的问题,Cherno 使用的是2.4.1,当前已经更新到 3.1.0。但我选择先使用 3.1.0 试试看,毕竟新的库更前卫一些,我也想尝尝鲜。
如果你想按照 Cherno 的想法来,就照他的方法做,使用 2.4.1。
如果你想保持 2.4.1 版本中的 C++ 特性,且对性能要求不敏感, 那就使用 2.4.1 。
更改和操作:
3.0.1 相较于 2.4.1 有了较大变化,文件结构发生变化。另外,由 C++ 转换为了 C。(迁移指南: Box2D: Migration Guide )
》》遇到的第一个错误:来自 Nut-Editor 的 LINK 错误,解决方式:
》》遇到的第二个错误:许多语法错误
并且所有的错误都指向一个函数:_Static_assert( )
我想这是因为没有将C语言的编译器设置为 /std:c11,因为_Static_assert( ) 是 c11 中的特性,在 C++11 中,这个函数被定义为 Static_assert( ) 。
我手动在 Box2D 的属性页中设置了 C 编译器,将其从默认(旧MSVC)修改为 ISO C11标准(/std:c11)
》》但是还有一个问题,就是我们无法将这个操作写在 premake 文件中,即无法将其脚本化。
我搜集了很多论坛和答案,但是 premake 好像无法为 msvc 提供合适的指令,也就是说没有可用的指令对 C 编译器的版本进行修改。
类似的指令有:buildoptions { "/std:c11" } 或者 buildoptions { "-std=c11"},但是这两个指令似乎只能针对 GCC/Clang 的 C 编译器,对其进行自动化更改。
这会导致一个结果,如果我选用了 Box2D 的 3.1.0 版本,为了在项目中正常使用 Box2D,则必须修改 MSVC 中的 C 编译器(以修正报错。但是我无法在 premake 中脚本化这个操作,就只能手动设置。这时,如果在外部重新使用 bat 脚本(Win-GenProjects.bat)运行或更新项目,则会导致 Box2D API 受它本身的 Premake 脚本影响,从手动设置的 /std:c11 状态退回到默认(因为 premake 中的指令无法对 msvc 进行 C 编辑器的修改,即使写下类似的代码,也相当于空白,所以只要重新调用 Box2D 的 premake5.lua,就总是会撤回 VS 中手动设置 C 编辑器的版本)
这里我提供3个解决方案:
第一(我选择的) | 退回至 Box2D 2.4.1 版本的使用,因为这个版本由C++开发而成,没有上述问题。虽然性能不如 3.1.0 好,但目前引擎还没有遇到性能瓶颈,而且在项目中植入 C++ 很容易。 |
第二 | 将 Box2D 在 MSVC 中 C 编辑器的修改,调整到独立于 premake 之外的脚本中,以实现自动化操作。 |
第三 | 对 Box2D.vcsproj 直接进行修改,在该文件中直接标明 Box2d C 编辑器的版本为 /std:c11, 这个操作可能会受到 premake 脚本重新运行的影响。(我是说可能,我也没有仔细思考) |
》》》》没想到经过一番查证和思索,到头来还是使用 Box2D 老版本。
https://github.com/JJJJJJJustin/box2d 这是我 fork 之后创建的库,其中有两个分支: main 代表最新的 Box2d, V2.4.1 代表 2.4.1 版本,你们可以使用。
》》》》我先提交序列化-反序列化部分的代码,然后再提交逻辑更新,以及UI设置。
》》》》return {}; 和 return; 的区别
return; | 用于 void 函数,表示结束函数。 |
return {}; | 用于 有返回值的函数,它返回一个 默认初始化的对象,通常会将返回值设为类型的默认值(例如,0、nullptr、空字符串等)。 |
》》》》关于前向声明的位置问题(命名空间之内与命名空间之外)
命名空间的影响:
- 处于命名空间内部:Entity 的前向声明位于 Nut 命名空间内,这意味着编译器会认为这个 Entity 类是属于 Nut 命名空间的 Entity 类。
所有在 Nut::Scene 类中使用 Entity 类型的地方,编译器都会以 Nut 命名空间下定义的 Entity 类进行条件判断。
- 移到命名空间外部:如果你将前向声明移到 Nut 命名空间外,那么 Entity 类将不再被视为 Nut 命名空间的一部分。此时,编译器将 Entity 视为全局作用域中的一个类。
任何在 Nut 命名空间内使用 Entity 的地方,将无法正确识别它是属于 Nut 命名空间的类,编译器将会在全局作用域中寻找 Entity 类的定义。
后果:
- 如果 Scene 中的 Entity 使用的是命名空间内的 Entity(即 Nut::Entity),而前向声明被移到命名空间外部,就会导致编译错误,提示找不到 Nut::Entity。
- 编译器会试图查找一个全局作用域中的 Entity 类,而实际上你可能需要的是 Nut::Entity,因此会出现命名冲突或找不到类定义的问题。
》》》》关于初始化( OnRuntimeStart() )
》》》》关于内存泄漏问题
如果 Delete 之后,不执行 m_PhysicsWorld = nullptr; 这句代码,会出现什么情况?
悬挂指针 (Dangling Pointer)
- 执行 delete m_PhysicsWorld; 会释放 m_PhysicsWorld 指向的内存,但 m_PhysicsWorld 本身仍然持有之前指向已释放内存的地址。
- 如果后续尝试使用 m_PhysicsWorld(例如访问它或再次删除它),会导致未定义的行为(通常是崩溃),因为 m_PhysicsWorld 现在是一个悬挂指针,指向已经无效的内存。
》》》》OnUpdateRuntime() 中的更新
》》》》OnRuntimeStart() 和 OnUpdateRuntime () 中代码的作用:
初始化 Box2d 世界, 并预先将所有物理属性附加到对象上 | |
允许物理模拟,并每帧都更新物体的 Transform 以进行渲染 | |
》》》》我发现 yaml 的序列化系统似乎没有将 Texture 的结果进行保存,每次进入引擎的场景之后,纹理都会被刷新掉。
------------------------------------------ UUID ------------------------------------------------------
》》》》 xhash ?
》》》》代码理解:
》解释代码:
- std::random_device s_RandomDevice;
- std::random_device 是用于生成随机数的设备(通常依赖硬件或操作系统提供的随机源),用来初始化 std::mt19937_64 引擎。
- std::mt19937_64 s_Engine(s_RandomDevice());
- s_Engine 是一个基于 std::mt19937_64 的随机数生成器。它的种子是从 s_RandomDevice 获取的一个值。
- std::uniform_int_distribution<uint64_t> s_UniformDistribution;
- s_UniformDistribution 是一个均匀分布对象,用于生成在某个范围内的随机整数。
- 当前这行代码并没有指定范围,默认情况下它生成的随机数范围是 [std::numeric_limits<uint64_t>::min(), std::numeric_limits<uint64_t>::max()],即 uint64_t 类型的最小值到最大值。
》范围设置 与 实际使用
默认情况下,s_UniformDistribution 可以生成的随机数范围是:std::numeric_limits<uint64_t>::min() ~ std::numeric_limits<uint64_t>::max()
即 0 ~ 2^64 - 1 -> (0, 18446744073709551615)
如果需要设置范围,可以这样操作:
》》》》运算符重载的定义方式:
》》》》
operator uint64_t() const { return m_UUID; }
uint64_t operator() const { return m_UUID; }
这两个有什么区别?哪一个是运算符重载?
operator uint64_t() const { return m_UUID; }
| uint64_t operator() const { return m_UUID; }
|
结论:
- 如果你的目的是将 UUID 对象转换为 uint64_t 类型,那么 第一个(operator uint64_t())更为合适。因为它是运算符重载,能够让 UUID 对象与 uint64_t 类型之间进行无缝转换。
- 如果你的目的是让 UUID 对象像函数一样被调用并返回 m_UUID,那么 第二个(uint64_t operator() const)是正确的。它实现了函数调用操作符。
》》同样的,这里也可以看到 函数调用操作符 的踪迹:
s_RD 只是对象本身,它本身并没有直接生成随机数。 | s_RD() 是对 std::random_device 对象的调用,它会生成一个随机数(通常是 unsigned int 类型),并返回。 s_RD() 实际上是调用 std::random_device 的 operator(),它返回一个随机数,并将这个数作为种子传递给 std::mt19937_64 引擎。 |
》》》》一些思考:
| Cherno的提交中,并没有为 IDComponent 提供这个自定义的构造函数。我认为这是一个会出现争议的地方。 |
| 在这个函数中,我们为 AddComponent<>() 传入了参数:uuid。 就我的理解来看,这里传入的 uuid ,将会被用来初始化 IDComponent 中的 ID,所以 IDComponent 中需要上述的构造函数。 在这里,我为 AddComponent() 填入的参数是 UUID(uuid),意为我使用了 UUID 中的构造函数: 而不是默认的构造函数,而默认的构造函数会生成随机的一段数字。 |
| 这里是 AddComponent() 的定义。 |
但从运行结果看来, Cherno 似乎并没有什么错误。不过我还没完成这一集的提交,稍后再来证明我是否正确。
》》事实证明没什么影响。
》》》》随机 ID 分发以及使用流程:
创建实体(创建时使用 CreateEntity() 函数,而不是 CreateEntityWithUUID() , 这会为新创建的实体分配一个随机 ID)
序列化文件:(我们会将默认随机分配的 ID 保存进配置文件)
反序列化文件:(读取文件中的文本数据,并将其重新用于实体组件的初始化。同时,ID 将会一直保持在文本配置文件中,实体只有在最初创建时通过 CreateEntity() 函数获得其 UUID,之后便一直存储在配置文件中,除非手动更改)
》》值得注意的是
如果你想像 Cherno 那样,先通过 Ctrl + Shift + S ,在保存文件的时候,更改所有实体的 ID 。(将实体的 ID 从之前设置的固定 ID 改为 随机的 UUID,则需要做以下更改)
先使用 UUID 的默认构造函数进行随机数分发: | |
当分发的 ID 被存储在 yaml 配置文件中之后,我们将其更改为图示。之后便可以不再更改这里的代码。 接下来只会有实体在创建之初,才会拥有一个新的 UUID(具体参考上述:》》》》随机 ID 分发以及使用流程:) 否则场景在被加载时,只会通过配置文件中的 ID 数据进行初始化,只要文件中的数据不变,实体的 ID 将一直保持。 | |
》》》》一个疑惑:这两个代码效果应该是一样的。
-------------------------- Playing and stopping Scene ----------------------------------------------
》》》》这一集一共做了三件事:区别编辑器场景和运行时场景、制作复制实体的快捷键、重新设置保存&另存为这两个功能。
》》》》Unordered_map 中,通过下标访问对应值的方式 和 通过成员函数:at() 访问有什么不同?
|
|
|
|
》》》》entt::registry 的成员函数 : replace 和 emplace_or_replace 有什么区别?
emplace_or_replace |
|
replace |
|
emplace |
|
》》》》void 类型的函数可以有返回值吗?
void 表示该函数不返回任何值。然而,void 类型的函数 可以有一个 return 语句,但是这个 return 语句 不能带有返回值。
你可以在 void 函数中使用 return; 来提前结束函数的执行,但不能像返回值类型的函数那样使用 return some_value;。
》》》》有一个问题:不能通过简单的单击“Hierarchy Panel”中的选项来选择实体,并进行复制。反而需要在单击选项之后,保持鼠标停留在视口中,才能实现从“Hierarchy Panel”直接进行复制的操作。
原因在这里:只需将 !m_ViewportHovered 改为 m_ViewportHovered,但同时,视口中的图像也会受影响(无论鼠标是否在窗口中,图像都会受鼠标的操作响应)
》》》》一些维护:
(EditorLayer.cpp)
1.防止运行时对场景进行滚轮调整,虽然运行场景(运行时摄像机)没有变化,但编辑区(编辑时摄像机)会受影响的问题:
(ToolbarPanel.cpp)
2.修复未加载场景时,单击运行按钮导致的中断(现增加了提示小窗口)
(EditorLayer.cpp)
3.解决了“运行某一场景之后单击选择实体,此时单击暂停按钮退出会导致程序崩溃” 的问题。
问题出现原因:之前更新 m_UsingEntity 是根据鼠标单击更新的,这意味着 m_UsingEntity 并不是实时刷新的。
所以每一次,如果在运行时选择了实体,此时实体属于运行时场景(ActiveScene)。而且这个场景在运行时被设置为 m_EditorScene 的副本(通过 Scene::Copy() ) 函数。此时 ActiveScene 指向 Scene::Copy(m_EditorScene) 的内存。
当我们通过按钮暂停运行时,ActiveScene 会退出”m_EditorScene 的副本“这个身份,并被设置为”m_EditorScene 的引用“,此时 ActiveScene 指向 m_EditorScene 的内存。
但是,虽然 ActiveScene 现在指向了不同的内存位置,我们用鼠标选择的实体却仍然指向”m_EditorScene 的副本“的内存位置,因为实体是在运行场景中选择的(场景开始运行时,会使用 OnScenePlay(), 其中包含:activeScene = Scene::Copy(editorScene); )。
此时的实体仍然指向”m_EditorScene 的副本“这里的内存位置,也就是 Scene::Copy() 的返回值,但是这个函数的返回值已经被销毁了,所以会造成内存泄漏。
-------------------------------------- Rendering Circles ----------------------------------------
》》》》概述
0:00 ~ 8:41 Hazel3D 中对于圆形渲染和多边形碰撞的演示
8:42 ~ 13:18 一些赘述
13:18~19:36 CircleComponent 的设置、CircleComponent UI 绘制、CircleComponent 的添加途径
19:45~35:42 渲染代码的编写
35:45~42:42 着色器的编写
42:44 ~ 44:43 渲染函数的更新
44:44~54:22 调试与演示
54:43~1:00:00 边缘检测和选中物体
1:00:04~1:01:55 圆形渲染调试
1:02:00 ~ 1:04:27 序列化文件
》》》》这一集内容虽多,但没有什么笔记可以做。
》》》》记录一个问题:(未解决)
在很久以前,我不知出于什么原因,在 DrawIndexed() 函数中,通过传入的参数 indexCount 直接运行了 glDrawElements() 这个函数,一直以来没有导致什么问题。
这个原因可能是:如果 s_QuadIndexCount 能够进入该条件判断,则表明 QuadIndexCount 是有数据的,所以不需要在 RendererCommand::DrawIndexed() 函数中额外添加一个条件判断:(判断indexCount 是否为空)
按理来说,无论我在 RendererCommand::DrawIndexed() 中使用/不使用 三元表达式来进行判断,函数渲染的结果都应该正常,但当决定重新启用这个三元表达式时,我的渲染却出现了问题。
如下:(如果鼠标位移至右上角错误区域,会导致程序崩溃:
崩溃位置:
》》》》尝试:将 Local Position 改为 vec2
Renderer2D.cpp | Glsl: |
length 函数: 用于计算一个向量的欧几里得范数(即向量的长度或模) 它的定义: | float length(vec2 v); |
》》》》TODO:理解 Circle Shader 中渲染圆形代码的运行机制。
---------------------------------- Rendering Lines -------------------------------------------------
》》》》也没啥要记的。记得不要把 GL_LINES 写成 GL_LINE, 而且不要把线条的 alpha 通道误设为 0 ,否则你看不见线条。
---------------------------------------------- Circle collider ------------------------------------------------
》》》》m_P 指的是?
m_P 指的是 m_Position,用来存储圆的位置,依此来模拟碰撞时的数据。