-
不喜欢游戏的人也可以做引擎,比如 cherno
-
引擎的作用主要是有两点:
- 将数据可视化
- 交互
-
当然有些引擎的功能也包含有制作数据文件,称之为资产 assets
-
不做窗口类的应用栈,可能要花一年才能做一个能实际使用的应用,只需要在游戏引擎中创建一个像 ImGUI 之类的关卡编辑器
类似于着色编译器
目录
- 设计
- 开发
- 属性配置
- EntryPoint 入口
- Logging Library 日志记录
- premake 构建系统
- 事件系统
- 事件系统(下)_实现
- 预编译头
- 窗口抽象和 GLFW
- 窗口事件
- Layer
- imgui
- imgui 事件
- P17 Github 和 Hazel 仓库
- P18 Github, Build
- P19 Input 轮询
- P20 Key And Mouse Codes 按键和鼠标代码
- P21 Math 数学
- P22 ImGui Docking and Viewports 停靠和视口
- P23-P25
- P27 Rendering Context
- P29 OpenGL Shaders
- P30 Renderer API Abstraction
- P31 Vertex Buffer Layouts
- P32 Vertex Array
- P33 Render Flow And Submission
- P34-35 CAMERAS and How They Work
- P36 Moving to Sandbox
- P37 TimeStep
- P38 Material Systems
- P40 Shader Abstraction and Uniforms
- P41 Refs, Scopes and Smart Pointers
- P42 Textures
- P43 Blending 混合
- P44 Shader Asset Files 着色器资产文件
- P45 Shader Library
- P46 How to Build a 2D Renderer
- P47 Cameras Controllers
- 技巧
- 报错
- 找不到头文件
- Sandbox.exe 无法找到入口
- inconsistent dll linkage 不一致的 dll 链接
- error LNK2001: 无法解析的外部符号
- error LNK2019: 无法解析的外部符号 & fatal error LNK1120: 1 个无法解析的外部命令
- error LNK2019: 无法解析的外部符号,函数 main 中引用了该符号
- error LNK2019: 无法解析的外部符号 __imp_realloc,函数 defaultReallocate 中引用了该符号
- error LNK2019: 无法解析的外部符号 "public: __cdecl Hazel::Layer::Layer(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)"
- error LNK2019 无法解析的外部符号 "__declspec(dllimport) bool __cdecl ImGui::Begin(char const *,bool *,int)"
- Layer.obj : error LNK2001: 无法解析的外部符号 "public: virtual void __cdecl Hazel::Layer::OnAttach(void)" (?OnAttach@Layer@Hazel@@UEAAXXZ)
- git push origin main报错 连接不到 443
- git 拉取超时
- error C2597: 对非静态成员的非法引用
- error C2039: "stdout_color_mt": 不是 "spdlog" 的成员
- error C2040: 间接寻址级别不同
- 弹窗报错:Sandbox.exe 无法找到入口
- No Premake script (premake5.lua) found!
- Error: [string "src/base/api.lua"]:606: bad argument #2 to 'deferredjoin' (string expected, got table)
- error MSB3191: 无法创建目录,因为同名文件或目录已存在。
- error MSB3073
- error C2065: “DC”: 未声明的标识符
- error C3646: “BindTexture”: 未知重写说明符
- 弹窗报错:Did you call ImGui_ImplOpenGL3_Init()
- 弹窗报错: DisplaySize() 顺序问题
- Warning: imgui.h(74,1): warning C4005: “IMGUI_VERSION”: 宏重定义
- 弹窗报错: g.IO.KeyMap[n] >=-1 && g.IO.KeyMap[n] <512:
- 提醒:overriding '/MTd' with '/MDd'
- 弹窗报错:RuntimeError
- 错误 C2338 Formatting of non-void pointers is disallowed
- 错误 C2143 语法错误: 缺少“;”(在“&”的前面)
- 错误 C2512 “Hazel::BufferLayout”: 没有合适的默认构造函数可用
- 报错:对象含有与成员函数不兼容的类型限定符
- 着色器编译错误
- 报错:对象含有与成员函数 "BufferLayout::GetElements"不兼容的类型限定符,对象类型是 const Hazel::BufferLayout
- 将 <>& 类型的引用绑定到 const <> 类型的初始化设定项时,限定符被丢弃
- LNK2019 无法解析的外部符号
- 编译报错
- LNK2019 无法解析的外部符号
- b站视频下的笔记
- 参考链接
设计
-
EntryPorint:控制了什么,
-
Apllication Layer
整个窗口
处理应用的生命周期和事件,比如运行循环,什么能保持应用运行并渲染帧,什么能够推动时间发展,什么能够执行游戏想要执行的所有代码,什么是事件,窗口调整大小或者关闭,输入事件,鼠标和键盘。需要一种方法来运行游戏或引擎,作为一个实际的应用运行在任意平台 -
Window Layer
渲染的目标地,输入事件将来自哪里,整个应用就是这个窗口,和平台支持和渲染接口支持有关- 输入
- 事件:事件管理器
- 处理输入
- 广播功能:订阅事件并获得通知
-
Render
引擎最重要占比最大的一部分,很多引擎教程都是从渲染器开始。但是自己做一个引擎会从调试器开始,其实当其他部分都完成的时候,渲染器的构建将会变得非常简单
通常是每帧一次
-
Render API (OpenGL 是最简单的接口,跨平台)
最后 Hazel 将会支持多种渲染接口,如 Vulcan(在实现一些东西的时候会比 OpenGL 更高效)
-
Debugger Support
-
Scripting Language
不用一直写 c++
-
Memory Systems
内存对于性能而言很重要,比如分配内存的时候CPU的耗时
自定义分配器和内存跟踪
-
Entity-Compoennt-System(ECS)
实体-组件系统,模块化游戏制作过程,能够包含特定的组件 -
Physics
-
File I/O, VFS(virtual file system)
-
Build System
能够把现有的 3D 模型或者资产自动导入引擎变成优化后的资产格式
能够在 photoshop 更改后事实热更新到引擎内
开发
-
视频是 VS2017,视频中讲到过,但是 github 文档说 VS2019 也可以
-
git clone https://github.com/xieyouchen/Hazel Git
这一句指令是把 Hazel 项目拷贝到 Git 空文件夹(如果没有创建 Git 空文件夹会自动创建),把 Hazel 里面的所有文件(包括隐藏文件)拷贝到本地的 Git 文件夹。如果只是 git clone … 那么会在指定位置先创建一个 Hazel 文件夹,再 clone 下 github 内所有资源
为什么不先 clone 到空文件夹,然后再创建 VS 解决方案?其实是一样的,都需要手动迁移一下文件内容,因为 git clone 克隆下来的文件夹和 vs 创建解决方案时创建的文件夹应该处于同一文件层级 -
使用动态链接,将引擎和制作的可执行文件 .exe 动态链接,可以自定义加载或者卸载某些部分,会存在很多依赖项链接到引擎的 engine.dll,再将 engine.dll 链接到游戏
c++系列有个视频介绍过动态链接和静态链接的区别,以及动态库和静态库,总的来说就是静态库会臃肿
属性配置
cherno 有个视频是在 window 设置 c++ 的视频,建立 c++ VS 项目的最好方式,会介绍配置
-
通用 -> 属性管理器 -> 活动解决方案平台,直接把 x86 删掉,因为 32 位的系统很少人用了
-
同理把下面的,项目上下文 -> 平台下拉框,把 Win32 删掉
-
通用 -> 配置类型,.exe 可执行文件改成 .dll 动态链接文件
-
这个 engine.dll 文件将会输出到解决方案目录下的 bin 文件夹,通过文件的类型存储到对应文件夹,文件夹名字为 “Release/Debug - 平台(x64)”,再放到 Hazel 文件夹下面
-
中间目录就是 VS 创建的所有文件(比如 obj 文件)将会输出到的地方,所以最好还是使用一个区别于输出目录 bin 的文件夹 bin-int,这样你完全可以把 bin-int 给删掉,因为我们自己实现的部分全部在 bin 文件夹
-
在解决方案内(注意不是 Hazel )创建一个游戏 Sandbox 使用 Hazel,重复一遍 Hazel 的配置项设置,区别在于 Sandbox 是一个可执行文件 .exe,并设置为启动项目
这里有个小技巧,当打开某个项目的属性面板时,直接双击别的项目,也会打开该项目的属性面板
-
使用 vscode 打开 sln 文件,把 Sandbox 改成第一项,这样别人从 github 克隆下来打开解决方案时,也能以 Sandbox 为启动项目
本地的启动项目为 Sandbox 被记录在 .vs 文件中,.vs 文件一般不会上传到 github
-
给 Sandbox 添加 Hazel 的引用,打开 Sandbox 属性 -> 链接器 -> 命令行 -> Debug 配置,和视频中的不一样,我这里没有 Hazel.lib 文件
但是在解决方案面板中可以看到是有的
这里解释为什么我们设置生成的文件是 .dll,但视频中链接的确是 .lib 文件,因为 VS 编译 dll 文件时,会生成 lib 文件,包含了 dll 中所有函数
-
折叠 Hazel 所有的文件,创建我们需要的 src 文件夹,Sandbox 同理。这个显示所有文件的功能好神奇,要么显示 src 文件夹,要么显示初始化的所有文件夹
-
这个时候可以看到 bin 路径下已经有了 dll 文件,需要说明的是,实际运行代码的是 dll 文件,lib 文件会被链接到 Sandbox 中,这样就知道有哪些函数可以用以及这些函数在 dll 文件的什么地方
-
Sandbox 中引入命名空间后,执行 Hazel::Print(),编译不会出错,但直接执行会导致 dll 文件缺失,这种问题怎么解决?其实有自动化的方法,也可以直接把 dll 文件复制到 Sandbox.exe 同目录下
EntryPoint 入口
除非是静态链接库,其他都有入口,包括动态链接库
-
将析构方法 ~Appliaction() 变为虚函数,因为这个类会被(Sandbox)继承
- 支持多态
- 确保基类指针创建的派生类对象调用对应的析构函数时,正确释放内存
-
因为 __declspec(dllexport) 只在 Win 中被使用,所以定义一个宏限定 Win 使用,这个宏需要在解决方案的配置项内设置:c/c++ -> 预处理器 -> 预处理器定义
HZ_PLATFORM_WINDOWS;HZ_BUILD_DLL
。并且需要注意的是,这里应该设置为 Debug 配置时候定义宏, 映射到 __declspec(dllexport),而 Release 没有,映射宏为 __declspec(dllimport) -
为什么 Hazel::CreateApplication() 要在客户端定义,并且定义在类 Sandbox 的外面,而不是直接定义为 public 内?
-
extern 是怎么找到外部的 Hazel::CreateApplication() 的?这里的 extern 只是告诉该文件后文出现 CreateApplicaiton() 的地方,该函数是外部定义的
-
目前 EntryPoint.h 文件中有标红,但是执行成功,这个不用管吗?extern 的作用就是把其后的所有内容在项目中找到相似的实现并引用
很神奇的是,在 EntryPoint.h 的 main 函数测试部分添加 printf(“Hello”) (需要在 Hazel.h 中添加 stdio.h 头文件),执行成功再把测试部分删除后,不标红了 -
使用 git 不一定用 git bash,可以直接再资源管理器里面用 cmd
-
alt + d
是资源管理器的搜索栏快捷键 -
.gitignore 中 # 表示注释,.vs/ 可以无视 .vs 文件夹,*.user 可以无视 .user 后缀的文件 (.user 无法无视 Hazel.vcxproj.user 文件)
-
git reset . 退回到上一步
-
我的 github 默认主分支是 main,不是 master
-
git add . 和 git add * 的区别是什么
-
cherno 用的是 git push origin master,但是我的 github 默认主分支是 main,所以我使用 git push origin main
Logging Library 日志记录
记录所有的信息,比如启动信息,系统的信息,文件是否被打开,着色器是否成功编译,引擎正在做哪些事情
错误是红色,警告是黄色,普通信息是绿色,不重要信息是灰色
重点是格式化数据,比如有时候要打印对象,有时候打印数字,字符串,并且参数可能不定
-
使用
git submodule add https://github.com/gabime/spdlog Hazel/vendor/spdlog
将外部依赖库 spdlog 作为子模块导入到主仓库 Hazel/vendor/spdlog 文件夹下,可以保证主仓库和子模块仓库的独立性 -
因为 Logging 需要在所有项目中都能使用,所以把 spdlog 第三方依赖的 include 文件夹路径名添加到 “所有配置” 的附加包目录中去
-
解决方案中左上角显示 “挂起的编辑” 是什么意思
-
inline 什么作用?内联函数,用于降低程序的运行时间,编译期间把函数体复制到函数调用处,减少函数调用时候入栈出栈的开销,适合函数不超过10行 (相比于宏,多了类型检查等好处)
函数比较短,比如说就一行,或者直接实现在 .h 文件的,就可以使用内联函数 inline func() -
Log.cpp 调用的 s_CoreLogger 和 s_ClientLogger 是 Log.h 文件内的 static 标注的变量吗?是的,在 .cpp 文件相当于声明有这个变量,动态链接库会帮忙链接两个变量 (ctrl + 左键能跳转到 static 位置)
-
只是修改了头文件,就不需要更换 dll 文件,那么 dll 文件应该是只包含了 .cpp 文件
premake 构建系统
修改项目操作:
- 修改完代码后保存文件
- 到工程文件根目录,双击 GenerateProject.bat 脚本
- 进入到 VS,会弹窗口,直接全部重新加载
- 按 F5 即可正常运行
-
kind “SharedLib” 表示项目是 dll,在 premake 中,共享库和动态库相同
-
** 表示递归搜索文件
-
cfg: configuration 配置
-
files 表示要包含的所有的文件列表
-
include 用于包含头文件,相当于附加包含目录
-
filter 过滤器,只考虑 windows 的情况,
-
cppdialect 设置语言版本
-
staticruntime, 连接运行时库有关,我们希望静态连接
-
systemVersion win SDK版本
-
postbuildcommands 从配置中复制构建目标的相对路径
-
根据最新的 premake5 的文档修改的代码,两种方式都可以
-- 编译好后移动Hazel.dll文件到Sandbox文件夹下 postbuildcommands{ ("{COPY} %{cfg.buildtarget.relpath} ../bin/".. outputdir .."/Sandbox/") --("{COPY} %{cfg.buildtarget.relpath} \"../bin/".. outputdir .."/Sandbox/\"") }
-
如何运行 premake.lua ?
-
从官网下载下来的 premake 文件夹有很多 dll 文件,但是如果删了之后好像也能运行
这样也可以
事件系统
- 可以处理窗口事件,比如:关闭窗口、调整窗口大小、鼠标键盘输入事件
- 如果要做一个渲染器,没有事件系统也没问题。但是对于一个引擎来讲,是可视化创作工具,需要很多设计功能,鼠标键盘就能操作,比如一个可以通过鼠标键盘或其他工具移动的相机。
- 自定义事件是立即处理事件,没有缓冲事件
缓冲事件:键盘 A 一直按,第一个A输出后,停顿一下再一直输出 - 事件都是阻塞事件,意味着没有被缓冲,事件没有被延迟。只要发生,比如鼠标被点击,那么整个应用会停止,优先调度(get dispatched)点击事件。
区别于更加复杂也更优秀的过程:先从事件中获取信息,再推送到某个队列或某个缓冲区,这会产生延迟的现象,直到真正处理该事件。例如像传递、或者通过某事件总线之类的方式传递。
事件系统(下)_实现
目的是让 Application 创建一个窗口 Window,并且 Window 不会知道 Application 的信息,所以 Application 在创建窗口的时候需要创建一个 callback。
https://blog.csdn.net/alexhu2010q/article/details/106942099
-
整个工程文件的入口是 EntryPoint.h,里面直接有 main 函数的实现,应该是属性的配置中设置为该文件为入口
-
之前是写了 Event.h 文件,现在直接跟着帖子继续下去
-
c++ 中 class 后面跟着两个变量名,分别表示什么意思,比如:class HAZEL_API Application {}
宏 HAZEL_API 在我的工程中被映射为 __declspec(dllexport),表示要将 Application 导入 dll 文件内,其他文件可以链接到该类。这个关键字可以保证类能够在多文件内共享
-
创建一个 Events 文件夹,包含有 Event.h、KeyEvent.h、MouseEvent.h、Application.h 头文件,其中以 Event.h 为主,是整个事件系统的主要文件,其他三个作为不同的事件独立
-
需要确定的是事件系统的接口是啥?如果要自己创建一个事件,需要做什么?
-
包含的如 standard.h 这样的原生 c++ 文件应该包含在预编译头文件中,甚至于应该包含在 Core.h 文件中
-
事件系统是阻塞型事件,前面说过,不像是从事件中获取信息,将其推送到某个队列缓冲区,没有事件总线一样的东西进行传递
- Event:作为事件的基类,最基本的接口有:获取该事件的类型GetEventType(); 获取该事件的名字 GetName();同时还可以有一个 ToString() 函数方便打印一些消息
- EventType: 将事件的类型写成 enum 形式,当需要在运行时候获取到事件的类型名字时,就不用使用 dynamic_cast 之类的函数
- EventCategory: 事件的类型 enum 形式用于过滤掉某些事件,比如说:Application 类会接受所有的事件,但是只关心键盘事件或鼠标事件
EventType 关注的是某个具体的鼠标事件,EventCategory 表示一种类型,某些时候需要所有的鼠标类型表述
也是一个位字段,BIT(),这个函数是宏定义在 Core.h,#define BIT(x) (1 << x)
,左移1位,相当于 2 x 2^x 2x
目的:让它不局限于 0 1 2 3 4,因为一个事件可以分成多个分类,比如:键盘、鼠标和鼠标按钮事件都是输入事件 EventCategoryInput,而鼠标按钮事件又是 EventCategoryMouse 事件。创建一个位字段,就可以设置多个位,简单取掩码,看看什么样的分类和事件,或者一个事件属于什么分类
那为什么 EventType 没有写 - m_Handler: 表示这个事件是否被处理,调度事件去处理不同的层时,不想让事件进一步传递。比如:屏幕上有一个按钮,点击了鼠标,鼠标落在按钮的范围内,事件就被处理。这个时候游戏世界并没有收到点击事件,因为按钮已经被处理过了,m_Handler 就是说明按钮是否被处理
- IsInCategory() 是非常简单的工具函数,判断事件是否在给定的 EventCategory 中,用于快速过滤某些事件,只需要对实际分类按位于运算,返回这个 bool 值即可
- GetName() 暂时设置为空:一般不会去检索一个事件名称
- ToString() 只是用于调试,纯虚函数必须实现,默认返回事件的名称,如果要打印更多信息,直接在函数内添加即可
KeyEvent: 重点
- KeyPressedEvent(int keycode, int repeatCount) : KeyEvent(keycode), m_repeatCount(repeatCount) {} 这句话里面的 KeyEvent(keycode) 什么意思,是给哪个变量初始化?是不是调用 KeyEvent 构造函数之后,也会生成一个 m_KeyCode 变量
- KeyEvent 的构造函数被放在 protected 内,说明其他类不能构造,只有派生类可以
- 对于构造函数中的 repeatCount 是有必要的,防止有时候按下一个键,会触发两次按键事件
- 宏定义 #define const char* GetName(x) { return #x; },就是将 x 转化为字符串字面量并返回
- GetStaticType() 获取静态类型,有需求是在运行时候能够知道是什么类型,那么不想先实例化一个 KeyPressedEvent 之后,再调用 GetEventType(),所以获取类型最好作为静态函数
- 在 EventDispatcher 类中使用,作为调度器,需要检查所有的待处理事件,只有当待处理事件满足目标事件类型,才应该放到待执行列表中等待执行
using EventFn = std::function<bool(T&)>;
这句话,function() 的作用是啥- function() 是一个模板类,可调用一切可调用的实体(lambda 函数、函数指针、函数对象等)
- 这句话内,EventFn 是一个类型别名,定义了一个 function 对象
- function 对象接受以 T& 参数,返回 bool 值的一个函数
- 这里的 T 是模板类型
- 这里的 function() 能作为一个接口,存储任何返回 bool 值并接受一个 T& 类型引用作为参数的函数。这样调用者就不用关心函数的实现细节,直接调用。
- function 在回调机制、事件驱动编程和泛型编程中作用很大
- m_Event.m_Handled = func(*(T*)&m_Event); 这句意思是无论 func 是什么函数,只要返回值是 bool,参数是 T& 类型,那么这个函数在此处都会被调用
- 更新、渲染、时钟函数都有对应的事件类,可以以事件的形式传播
- 重载的输出流符号作用于日志库,能更方便调用事件的 ToString(),记录事件
HZ_TRACE(e);
Application.cpp 文件中运行这句话,因为已经重载过了输出流符号- 必须添加头文件
#include "spdlog/fmt/ostr.h"
spd 的输出流操作符,这样就可以自定义输出类型
预编译头
就是一个头文件,源文件添加后可以避免某些头文件被反复编译
- hapch.cpp 本来是不需要的,但是 VS 会自动生成
- 在 premake.lua 中添加 pchheader “hzpch.h” 和 pchsource “Hazel/src/hzpch.cpp”,后者只有 VS 需要,其他 IDE 会无视。其中,前面那句效果相当于把所有包含了 hzpch.h 的文件属性中预编译头设置为 “使用”,而后面那句效果相当于把 hzpch.cpp 文件属性中的预编译头设置为 “创建”
- 再次 Build 工程后,项目配置会提醒是否使用 pch
- 之后再所有的 .cpp 文件中包含 hzpch.h (实际上没有使用的 cpp 文件也需要添加),为什么是 cpp 文件,不应该 .h 文件添加吗
- 需要有一个 hzpch.cpp 文件,告诉编译器把 hzpch.h 预编译头文件的内容全部编译完 (因为 VS 的编译系统以一个个文件为单位,当一个文件发生修改,那么所有文件都会被重新编译,所以把一些不太变动的文件放在预编译头文件中,告诉编译器提前编译好,之后就不会改变,提升编译效率)
窗口抽象和 GLFW
不用原生的 Win32 API,使用 glfw 创建窗口
git submodule add git@github.com:TheCherno/glfw.git Hazel/vendor/GLFW
- Hazel工程根目录下的 premake.lua 中添加内容
这里可以注意到 include “GLFW”,拷贝工程 GLFW 下的 premake.lua 内容,最后变成 Project GLFW,和 Project Hazel,Project Sandbox 同级,即解决方案中会出现新的一个项目 GLFW-- 包含相对解决方案的目录 includeDir = {} includeDir["GLFW"] = "Hazel/vendor/GLFW/include" -- 下面这个 include 相当于把 glfw 库中的 premake.lua 内容拷贝到此处 include "Hazel/vendor/GLFW" project "Hazel" --Hazel项目 (...省略原本就有的内容) -- 包含目录 includedirs{ "%{prj.name}/src", "%{prj.name}/vendor/spdlog/include", "%{IncludeDir.GLFW}" } -- Hazel链接glfw项目 links { "GLFW", "opengl32.lib" } filter "system:windows" defines{ "HZ_PLATFORM_WINDOWS", "HZ_ENABLE_ASSERTS" } (...省略原本就有的内容)
- Window.h 中,
virtual ~Window() {}
// 和 virtual ~Window(); 有什么区别 - WindowsWindow 归类到 Platform 文件夹中,表示 Windows 平台中的窗口,父类是 Window
WindowsWindow(const WindowProps& props);
// 看样子是会调用父类的构造函数?virtual ~WindowsWindow();
这里的虚构函数就直接用了分号结束,为什么不用 {}void OnUpdate() override;
// 会更新 glfw,缓冲区会轮询输入事件,应该按帧运行,从 Application 中调用- 其中,WindowData 结构体作为一个数据集合,将被传给 glfw,问题是和 WindowProps 的区别是什么?
WindowsWindow.cpp
-
s_GLFWInitialized 变量是干嘛用的:表明 glfw 上下文环境是否初始化
-
为什么 Init() 内容要单独封装,不能直接写在构造函数中吗?如果是为了可读性,构造函数除了初始化还会有其他操作吗?讲道理构造函数和初始化是等价的
-
又是之前那个问题,为什么要单独封装一个 WindowData 结构
-
HZ_CORE_INFO("Creating Window {0} ({1}, {2})", props.Title, props.Width, props.Height);
这个地方用 props. 比 m_Data 好,因为整个函数都是基于 props 进行的,可以统一修改参数名完成逻辑改变 -
如果 glfw 不初始化,那么执行: 是的,因为 glfw 执行需要先初始化上下文环境
-
#define HZ_ASSERT(x, ...) {if(!(x)) { HZ_ERROR("Assertion Failed: {0}", __VA_ARGS__); __debugbreak(); } }
这里的 x 为什么要括号,有什么意外情况需要注意吗? -
… 是参数包,由 __VA_ARGS__ 展开
-
__debugbreak() 是debug模式下的断点
-
VS 解决方案两种模式呈现不一样,为什么 Platform 文件夹不会显式出来?为什么 GenerateProject.bat 后 WindowsWindow.h 等后面创建的一系列文件都消失
其实没有消失,只不过重新生成工程后,筛选器会被重置。需要区分文件模式和观察模式的区别
文件模式创建的是文件夹,会直接修改工程文件的相对目录
观察模式创建的是筛选器,不会修改文件的相对目录,只是辅助分类
在文件模式下完成创建文件夹,则 GenerateProject.bat 后不变 -
这个 glfwInit() 是 GLFW 库的内置函数
-
NULL 和 nullptr 的区别?
-
取名字 m_Window 为什么要大写 W,还有 m_Data 也是?
-
“设置窗口关联的用户数据指针…” 是什么意思,怎么理解?为什么这个 m_Data 能够直接传入到 glfw 的内置函数中,m_Data 的结构 glfw 不知道,如何去使用?
-
轮询事件和交换缓冲?
其他修改
- Application.h 中的 Window* m_Window 和 WindowsWindow.h 内的 GLFWwindow* m_Window 区分开:前者是自己自定义的窗口类(使用基类是作为接口,后续的衍生子类都能使用这个接口);后者是 GLFW 库的内置类
m_Window = std::make_unique<WindowsWindow>(new WindowProps());
m_Window = std::unique_ptr<Window>(Window::Create()); // 创建窗口
两者的区别- make_unique 和 unique_ptr 的区别
- 使用 Window 而不是 WindowsWindow?还是统一接口吗?那为什么 Window::Create() 会自己调用 WindowsWindow?如果同时有 MacWindow::Create(),他会调用哪个?
- 为什么参数是 Window::Create(),这会返回一个 Window*,这是什么写法?unique_ptr<>() 本身就会创建一个指针啊,为什么要传入一个指针?因为 unique_ptr 可以接受一个指针,转化为智能指针类型
glClearColor(1, 0, 1, 1); glClear(GL_COLOR_BUFFER_BIT);
这两句效果是把窗口变成粉色,如果把这个函数放到 while 外面,那么窗口只能短暂保持粉色。可以看出 OpenGL 会重新渲染每一帧的画面。- 在 glfw 工程下的 premake.lua 中,第50行位置添加
buildoptions "/MT"
这一操作等价于 c/c++ -> 代码生成 -> 运行库,选择多线程调式(/MTD)
什么是运行库?
具体内容看这个帖子最下面https://blog.csdn.net/qq_34060370/article/details/131157538
成功运行
疑惑
- 为什么这里的 EntryPoint.h 启动文件中,可以直接调用 CreateApplication(),这个函数并没有被定义为 static?
- 为什么CreateApplication() 被调用后自己找到了 Application 的子类 Sandbox 的函数 CreateApplication()?
窗口事件
- 改变了窗口的大小(或关闭窗口)等事件,这种叫做窗口事件
- 这个窗口是通过 GLFW 实现的,对于窗口事件的执行也将由 GLFW 库内置的窗口函数实现。目前无法做出关闭窗口等操作,是因为我们并没有给该窗口打开该权限。
- 第二个要做的事情是,当我们打开了相关的窗口事件权限,GLFW 对窗口完成操作的同时,需要告诉 Hazel 内核,“嘿,我已经完成了窗口事件,接下来你需要做什么吗?”
- GLFW 执行完窗口事件回头告诉 Hazel 内核的操作就叫回调
总的来说,要做两件事:设置窗口事件+设置窗口事件的回调函数
-
设置 Hazel 自己的回调函数 OnEvent(Event&),当发生窗口事件 Event后,由窗口(回过头告诉 Applicaiton窗口事件已完成)回调这个 Application::OnEvent() 回调函数。
-
为了简便,这里的 OnEvent() 回调函数逻辑设置为打印刚执行完的 Event 窗口事件
-
m_Window->SetEventCallback(OnEvent) 用于绑定窗口和回调函数的接口,告诉窗口,回调函数是自定义的 OnEvent()
其实可以把每个窗口事件的回调逻辑写在对应的函数里,但是当窗口事件的回调逻辑都相同时,最好就把该逻辑封装为一个函数,此处就是 OnEvent()
-
至于这个 bind() 干嘛用的,后面再说?
-
此后回到 WindowsWindow::Init(),在窗口初始化函数中,需要打开 glfw 的窗口事件,允许其发生,同时设置窗口事件的回调函数
-
以改变窗口大小为例,设置glfw的窗口大小改变的回调函数,默认允许glfw执行窗口事件。此处的回调函数逻辑设置为空,效果就是可以自由改变glfw窗口大小,但是并不会在Hazel终端打印事件信息
glfwSetWindowSizeCallback(m_Window, [](GLFWwindow* window, int width, int height) {});
-
在 glfw 回调函数中,需要调用自定义的 OnEvent() 回调函数,同时更新窗口的最新状态到 WindowData
-
最后 m_Data->OnEvent() 进行窗口事件的回调 (在 Application 中创建窗口之后就进行了回调函数的绑定)
-
(还是不理解 WindowData 和 WindowProps 之间的区别,如果直接把 WindowProps 直接作为用户数据,和 GLFWwindow 绑定在一起,也能实现类似的功能。难道WindowProps 的作用是存储窗口的初始化数据?初始化数据有存储的必要吗?
-
EventDispatcher 本质是个 if 语句,判定如果是窗口关闭事件,那么执行。至于为什么要用调度器来控制判断窗口是否关闭?
-
WindowCloseEvent event;
和WindowCloseEvent event();
的区别
小实验
目前已经创建了一个窗口,如何同时创建第二个窗口?
(其实很简单,只是帮助回顾窗口的创建过程,理清 Application、Window、回调函数之间的关系)
回顾第一个窗口的创建流程:
- Application::Application():
tmp_Win = std::unique_ptr<Window>(Window::Create(WindowProps("Win2", 200, 300)));
- 设置窗口回调函数:
tmp_Win-> SetEventCallback(BIND_EVENT_FN(Application::OnEvent));
- 调用窗口的更新响应函数,每一帧监听窗口的状态是否发生改变:
tmp_Win->OnUpdate();
没了)) 效果如图
非常有趣的是,glClearColor() 函数只作用于一个窗口,另一个窗口并没有产生效果,这和 OpenGL 的工作原理有关系。
在 WindowsWindow::Init() 中,glfwInit() 完成了 OpenGL 环境的初始化。
之后 glfwCreateWindow() 创建一个 OpenGL 窗口,
并基于此窗口设置上下文,设置此窗口的用户数据指针(包含了自定义事件回调函数)
再依次设置各种需要的回调函数(也默认启用了对应的 OpenGL 窗口事件)
表面上看,窗口1和窗口2的建立都分别初始化,分别设置上下文,用户数据指针。
只需要在 Application::Run() 的 while() 中,调用相同的函数即可实现多窗口分别设置不同的颜色
但是结果就是只能渲染一个窗口,因为 OpenGL 默认创建的是共享上下文环境,如果想要实现多上下文,需要额外设置。
另外,可以尝试不写2/3,看看报错反应。
可以看到产生窗口的同时,会报错指针问题,因为只创建了窗口,并没有设置回调函数,也没有设置窗口更新的响应函数。这会导致在 WindowsWindow::Init() 中执行 glfw 窗口回调函数的时候,找不到 m_Data->OnEvent(),m_Data->OnEvent()=nullptr 只完成了初始化,并没有指向实质内容。
步骤3的 .OnUpdate() 同理
Layer
处理窗口中的层级内容,一个窗口内会有很多内容,这些内容存在重叠部分,类似 PS 中的多个图层的遮挡。
- 从渲染的角度,应该先渲染最底层(有部分内容会被覆盖),再渲染上层(会覆盖其他层的内容)。
- 从事件处理的角度,应该执行最上层的事件(前端称之为冒泡事件),e.g. 大按钮上有个小按钮,点击小按钮的时候只触发小按钮的响应事件,该点击操作不会传递到大按钮。
- 为什么 Layer.h 内使用
virtual void Attach();
就可以,但是 WindowsWindow.h 内,使用virtual void Shutdown();
不行?此时报错:WindowsWindow.obj : error LNK2019: 无法解析的外部符号 “public: virtual __cdecl Hazel::WindowsWindow::~WindowsWindow(void)” (??1WindowsWindow@Hazel@@UEAA@XZ),函数 “public: virtual void * __cdecl Hazel::WindowsWindow::`scalar deleting destructor’(unsigned int)” (??_GWindowsWindow@Hazel@@UEAAPEAXI@Z) 中引用了该符号
1>…\bin\Debug-windows-x86_64\Hazel\Hazel.dll : fatal error LNK1120: 1 个无法解析的外部命令 - 突然忘了 HZ_INFO(“{0}”, event); 这条指令是如何把事件相关信息打印出来的
- 为什么 Layer.h 中的 m_DebugName 设置为 protected
// 在指定位置之前插入一个新层,返回插入新层的位置 (那位置似乎没变?) m_LayerInsert = m_Layers.emplace(m_LayerInsert, layer);
那最后的效果会变成先插入的层为覆盖层?因为是一直往前面插入- PopLayer() 中的,m_LayerInsert–; 指向 Begin 什么意思
- 为什么 PopOverlay() 中并不需要 insert–,两者区别是什么
- 为什么 Layer.h 所有虚函数都写成了空,而不是
virtual void OnUpdate() = 0
结果如图,可以看到层级的 Update() 渲染得非常快,连续的拖动鼠标事件频率也比不上
imgui
- ImGuiLayer.cpp 中的 .OnAttach() 这个函数什么意思,什么时候触发,看内容是获得键盘输入,映射到 glfw 内的按键输入
static bool show = true;
为什么这里的 show 要用 static- 既然 Application 是单例模式,只有一个,为什么还要去实现一个子类 SandBox
- .h 文件中声明的变量放到 .cpp 文件中继续声明,两个还是同一个吗
Application* Application::s_Instance = nullptr;
#include "Hazel/ImGui/ImGuiLayer.h"
为什么这个头文件引用放在 Applicaiton.h 内
imgui 事件
- 怎么通过这种错误得知,应该引用头文件
- Application.OnEvent() 在哪里被调用?将会在 WindowsWindow.cpp 中 glfwSetWindowCloseCallback() 等回调函数中被调用
由data.EventCallback(event);
函数指针调用
这个 EventCallback() 会在WindowsWindow::SetEventCallback(function<void(Event&))
中被设置
所以只需要看有多少 SetEventCallback(),就能看出绑定了多少 OnEvent()
唯一出现的是 Application.cpp 中
m_Window->SetEventCallback(std::bind(&Application::OnEvent, this, std::placeholders::_1));
这个 Application::OnEvent() 作用是检测 OnWindowClose() 窗口是否关闭
整个调用逻辑:Application.WindowsWindow.SetEventCallback() 设置事件回调函数,使得Application.WindowsWindow.mData.EventCallback = Application::OnEvent(Event&)
在 WindowsWindow.Init() 中设置了 glfwSetWindowCloseCallback() 等回调函数
当 Apllicaiton.Run() 中通过 m_Window->Update() 捕获到了 WindowsWindow.cpp 中设置好的 glfw 的窗口事件,则会触发 WindowsWindow 内设置的回调函数,并执行Application.WindowsWindow.mData.EventCallback
即 Application::OnEvent(Event&)
其实那些 glfw 的回调函数都是 OnEvent(),是进入到 OnEvent,通过事件调度器 EvetDispatche 判断执行哪个函数
那么什么时候 Layer.OnEvent() 被执行呢
LayerStack 是在 Run() 中通过 layer.Update() 进行活动
所以 glfw 回调函数调用 OnEvent 的时候,除了执行 Application::OnEvent(),还会执行 Layer::OnEvent() 吗?不会
所以怎么让 Layer::OnEvent() 也被执行
每个 Layer 都有自己的 OnEvent(),如何让一个 WindowsWindow.LayerStack 内部的所有 Layer.OnEvent() 在 glfw 回调函数的时候都能依次传递执行呢
可以看到 Application.Run 中执行了 for 循环 Update()
-
同理,Layer 的 .OnEvent() 是在哪里被调用的?
为什么说 Application 把事件传给 ImGuiLayer 层? -
bind() 和 placeholders::_1 什么意思? bind() 封装函数为一个新的可调用对象,placeholders::_1 表示新的可调用对象的第一个参数,placeholders::_2 表示第二个参数
https://blog.csdn.net/maybcsdn/article/details/122915302
P17 Github 和 Hazel 仓库
- 解决 warning:overriding ‘/MTd’ with ‘/MDd’,看下面的"报错" 部分
- 修改了宏,简单提了一下,没啥操作,说之后一起讲
P18 Github, Build
-
解决方案内的相关依赖放到 Dependencies 文件夹内,但只是解决方案里面的文件夹,方便观看,实际上在文件目录里搜 Dependencies 搜不到
group "Dependencies" include "Hazel/vendor/imgui" include "Hazel/vendor/GLFW" include "Hazel/vendor/Glad" group ""
-
修改了premake.lua,解决了之间的 Sandbox 编译错误 error MSB3073,下方的 “报错” 部分也有 说明
-- 编译好后移动Hazel.dll文件到Sandbox文件夹下 postbuildcommands{ -- ("{COPY} %{cfg.buildtarget.relpath} ../bin/".. outputdir .."/Sandbox/") ("{COPY} %{cfg.buildtarget.relpath} \"../bin/".. outputdir .."/Sandbox/\"") }
P19 Input 轮询
不同于事件系统的被动通知 by 事件回调,轮询是主动查询输入状态
有时候对输入处理时候需要检测当前的输入状态,比如当两个键同时按下会发生其他的事件,这个过程叫做轮询(可以理解为轮流询问检查各个按键状态)
封装 glfw 的查询键状态函数到 Input 类
类支持全局访问,故 static
多态以支持多平台,故 virtual
如果是直接 static+virtual,会失败
virtual 是通过类实例的虚函数指针实现多态,static 通过类名调用函数,不通过类实例,此时 virtual func 调用无法找到正确的函数(基类的static virtual / 派生类的 static override)
正确方法:在基类中创建一个静态单例,如 Application
单例对象设置为 private,static get() 设置为 public,防止外部更改这个单例对象
单例对象还要把拷贝构造函数和赋值重载函数设置为 delete
单例模式的作用?静态单例模式?是否就是前面说的可以实现 static virtual 的作用
期间报错
Application.obj : error LNK2001: 无法解析的外部符号 “private: static class Hazel::Input * Hazel::Input:😒_Instance” (?s_Instance@Input@Hazel@@0PEAV12@EA)
解决方法见下方
P20 Key And Mouse Codes 按键和鼠标代码
和 glfw3 解耦,需要设计自己的 API,之后如果要换 API,直接将自己的 API 和新的 API 做一个转换就行
P21 Math 数学
引入 OpenGL 的数学库 glm(opengl math),使用 SIMD 指令加速(需要内存对齐等要求),SIMD 和汇编器编译器有关
语法基于 OpenGL,但其实没什么关系,其语言风格和 GLSL(一种着色器语言) 十分相似
(一般每个项目组会有自己的数学库,满足最基本的要求的同时可以最大限度提升性能)
实际上由模板组成,可以改变内存的顺序
P22 ImGui Docking and Viewports 停靠和视口
当前通过一个 EntryPoint.h -> main() 作为启动入口(那 Sandbox 作为启动项目有什么作用?)
之后是 Log 初始化,使得 HZ_CORE_…() 打印方法前面会有 Hazel,HZ_…()打印方法前面会有 App
之后 CreateApplication() 创建一个 Sandbox 类实例:
对于基类 Application 而言,两件事:
一个是创建了一个 WindowsWindow 窗口,即粉色窗口。
在 WindowsWindow 初始化中绑定了大部分的回调函数,
使得在粉色窗口的一些 glfw 事件被接收后会转换为 Hazel 事件 (只是转换不执行)
而 WindowsWindow.Update() 在窗口更新的过程中负责轮询,接收 glfw 事件
另一个是使 WindowsWindow 绑定了 Application 的 OnEvent() 事件回调函数
作为 WindowsWindow 层封装好但未执行的 Hazel 事件传递给 Application 层
因为一个输入事件不知道作用于哪个子功能,
比如:不知道用户的鼠标点击事件是作用于调试功能 / 场景视图功能 / 资产视图等,
所以需要根据功能窗口的前后,依次选择功能是否响应该输入事件,
对于 Application 而言,所有功能都被存储在 LayerStack 中
故在 Application.OnEvent() 中,
通过一个 for 循环,从后往前依次将输入
并执行循环 Run()
目前的粉色窗口内已经实现了 ImGui 窗口的创建和拖拽,但是缺少一个窗口布局的功能,本节采样 ImGui 的功能完成
之前的实现通过复制 ImGui 内的代码到自己的代码文件中进行一部分的修改实现功能,坏处在于当 ImGui 更新代码文件后,无法直接应用于 Hazel,需要二次开发,故本节删除原先的复制代码,通过引用源文件的形式复用 ImGui 原生功能
目前对于 Hazel 而言:
1. Window 窗口(粉色窗口)负责整个上下文的渲染和事件处理,Layer 负责的是一个功能模块,ImGui 窗口是最小的一级单位,只是负责创建并渲染出一个功能的可视化窗口(区别于 Window 窗口)
2. 可以认为 Window 窗口只有一个
3. Layer 层可以有多个,因为可能有多种功能,如:调试工具、场景视图、资产视图
4. 每个 Lyaer 层(功能模块) 可以有多个 ImGui 窗口,比如在调试工具中,可能需要创建一个 Help 窗口、一个 Log 窗口等
现在的 ImGuiLayer 由 Sandbox 添加,但更好的做法是由 Hazel 的 Runtime(运行时) 自动添加,即 ImGuiLayer 应该是 Hazel 的一部分
Application 中为什么要把 m_ImGuiLayer 定义在 Application.h 中
这个应该是在 layerStack 内,那应该是在某个函数内定义,然后 PushOverlay 才对
:ImGuiLayer 应该由 Application 的运行态自动添加,而不是在 Sandbox 客户端内
ImGui 层的渲染应该由层本身控制,所以应该把 ImGuiLayerRender 的前后过程封装为 Begin() End()
然后由客户端自己编写 Render() 的主体内容
因为 vector.iterator 会在 vecotr 的内存位置改变的时候失效(内存的位置为什么会发生改变呢?)
所以不能使用,应该使用 insertIndex 代替
P23-P25
定义 dll 的好处是热更新,更新代码后只需要重新编译 dll,让所有的测试用例不用重新编译就可以使用最新的引擎代码,使得客户端的链接更容易
缺点:dll 的链接容易出问题
同时,exe 文件链接 dll 后启动速度慢
lib 就是把所有相关库都放到 exe 里面
引擎代码如果更新完了就可以不用 dll
P26 静态库和无警告
需要把引擎改为静态库,因为之后不会有很大改变
需要热更新的是一些游戏内容
P27 Rendering Context
- 顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO - 为什么 OpenGLContext.h 中可以直接使用 struct GLFWwindow; 那不是应该引用头文件 #include “glfw.h”
- 为什么这篇文章说不应该把"三角形"的代码放在 Window 类里,https://blog.csdn.net/alexhu2010q/article/details/111030313
- 有 spdlog 的报错,解决方法如下
错误C2338 Formatting of non-void pointers is disallowed. Hazel D:\Project\Hazel\Hazel\vendor\spdlog\include\spdlog\fmt\bundled\core.h 1751
改为
HZ_CORE_INFO("OpenGL info:");
HZ_CORE_INFO(" Vendor: {0}", (const char*)glGetString(GL_VENDOR));//打印厂商
HZ_CORE_INFO(" Renderer: {0}", (const char*)glGetString(GL_RENDERER));
HZ_CORE_INFO(" Version: {0}",(const char*)glGetString(GL_VERSION));
P29 OpenGL Shaders
-
前面没有写 shader 为什么也能渲染出一个三角形?这里的 shader 和前面的程序又是什么关系?
-
vertexSrc 负责布局顶点,作为顶点着色器;fragmentSrc 负责绘制颜色,作为片段/像素着色器 (那么没有这两个着色器的时候,如何修改?只能通过 vertices 修改坐标,但是不知道怎么修改颜色。那么 vertices 就是确定3d模型的顶点相对位置,而顶点着色器是将整个模型进行全局变换)
- 前面的顶点、索引缓冲区存储了必要的数据,着色器用于顶点变换+像素着色
-
(const GLchar*) 为什么要把这个去掉
-
为什么不直接删除着色器程序,只是 detach
// Always detach shaders after a successful link.
glDetachShader(program, vertexShader);
glDetachShader(program, fragmentShader);
}Shader::~Shader()
{
glDeleteShader(m_RendererID);
} -
layout(location=0) 表示数组的偏移量为0,和 glVertexAttributePtr(0) 第一个参数 0 对应,即顶点缓冲区内的位置
-
需要注意的是,out 和 in 的变量名要一致,否则无法找到对应的变量,如此处顶点着色器的输出是 v_Position,那么片段着色器的输入就是 v_Position
std::string vertexSrc = R"(
#version 330 corelayout(location = 0) in vec3 a_Position; out vec4 v_Position; void main() { gl_Position = vec4(a_Position, 1.0); v_Position = gl_Position; } )"; std::string fragmentSrc = R"( #version 330 core layout(location = 0) out vec4 color; in vec4 v_Position; void main() { color = v_Position; } )";
-
c++ 写多行字符串,可以直接 string = R"()"; 之后括号里就可以直接换行,不用额外的换行符
-
使用 .reset() 代替 make_unique(),纯粹是代码更简单
-
最后在 loop 里,相当于先 Bind() 程序,再 Bind 顶点数组对象 VAO
效果
P30 Renderer API Abstraction
-
为什么 Bind() 要加上 const;
-
不同平台的视觉效果是否会不同,所以要使用 OpenGL 和 DirectX 的着色器语言去实现着色器,当适配多个平台时,需要抽象接口出来,利用多态,针对当前的平台 Create 上下文
-
如果编译所有的(DirectX / OpenGL),那就不能用 ifdef,因为两个接口不能共用一个着色器类(比如,在着色器中的每个函数如Bind 里面检测,这一段使用DirectX / OpenGL)
通常,可以分成多个文件,如果在运行时实现,需要个基本的抽象类(纯虚类),比如 Shader 有 OpenGLShader / DirectxShader
利用多态,Create 类型 Shader 时候,可以根据当前使用的图形 API 决定。这样,客户端只能看到 Shader,并不需要关心是什么类型的 Shader
-
Buffer 的衍生类 VertexBuffer 和 IndexBuffer 其实可以直接放在一个 Buffer.h 文件中,这样就不用额外多出一些文件。
对于这两个文件,数据成员都是空的,只有一个字节,所以其实没有必要构造函数,不过需要一个虚拟的析构函数,因为当基类的指针指向衍圣类如 Animal* a = new Cat(); 这个时候 delete a; 就需要先执行派生类的析构函数,再执行基类的析构函数;如果没有析构函数不为虚,那么只会析构基类,造成内存泄露
-
为什么这里的虚析构函数不直接 =0, 而是直接花括号 {}
-
内置的引用技术系统?22:31,Buffer.h 中 static VertexBuffer* Create() 这里面在调用的时候会用一个顶点数组,vertice
解决方法是静态函数,可以接收各种参数 -
无符号的32位整型,可能会直接用 int32 indices,其实 16 位整型是之后可以做的优化之一,否则索引很快就用完了。但是当想要快速实现,32int 就够了
-
glGenBuffer 和 glCreateBuffer 的区别
-
这三行代码发生了什么?我之前点进去过,看到很多的命名空间定义就被吓退了
-
RendererAPI 中的 None = 0,适用在无头文件中运行,或者说只是不想,比如说不需要 Renderer 的单元测试,或者在构建机器上运行,或者没有想测试的显卡
-
为什么 Render.cpp .h 中就那么几行代码,还要额外放在单独的文件中?为什么 GetAPI() 要设置成 inline 形式,只是因为代码的逻辑比较简单吗?API = OpenGL 这个初始化可不可以直接放在 .h 作为默认值?而不是放在 Render.cpp 文件中?
为了接口一致,都是 Create,不用 Gen
那么 glGenBuffers(1, &m_VertexBuffer); 和 glCreateBuffers() 函数的区别是什么? -
他这里的 Buffer.cpp 里面并没有真的实现 Create(),所以我干脆不创建,直接在 .h 中函数后面添加 {}
-
这个报错加上 static 就可以解决,为什么
P31 Vertex Buffer Layouts
顶点缓冲布局
- VAO 是 VBO 的容器,一个VAO里面有很多 VBO,每个VBO存储不同类型的顶点数据,如位置、法线、纹理坐标等,VAO存储了顶点处理的状态信息(渲染需要的所有状态
https://www.haroldserrano.com/blog/what-are-vertex-array-bindings-in-opengl
),如是否绑定元素缓冲对象EBO
VAO:简化了设置顶点数据的过程,使得切换不同的顶点数据更高效,只要绑定了VAO,顶点会自动应用
VBO:实际存储顶点数据的地方
这个代码转化过程解释得很好,来自 https://blog.csdn.net/alexhu2010q/article/details/111030313
m_VertexBuffer = std::unique_ptr<VertexBuffer>(VertexBuffer::Create(vertices, sizeof(vertices)));
m_VertexBuffer->Bind();
// 原本是这么写的
glGenVertexArrays(1, &m_VertexArray);
glBindVertexArray(m_VertexArray);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// 现在想改成这么写,核心就是创建一个layout, 然后设置给顶点Buffer
BufferLayout layout = {
// vertexBuffer里每七个浮点数是一组,前面三个数是Position,后面四个是Color
{ ShaderDataType::Float3, "a_Position" },// 这里的a_Position就是Shader里输入的参数名字
{ ShaderDataType::Float4, "a_Color" }
};
m_VertexBuffer->SetLayout(layout);
element.Normalized = false; 可以直接写在类的成员属性声明中吗?所以写在构造函数的初始化列表里已经是最优雅的写法了吗?
const_iterator
为什么最后是添加 const_iterator? 有什么区别
virtual const BufferLayout& GetLayout() const = 0;
virtual void SetLayout(const BufferLayout& layout) = 0;
const = 0 和 =0 的区别
为什么 OpenGLBuffer.h 里面的 BufferLayout 不使用指针,而是直接的对象
结果
P32 Vertex Array
-
顶点缓冲区和索引缓冲区有真正的顶点数据和索引数据,而顶点数组里面只是顶点的布局,里面是顶点缓冲区和索引缓冲区的索引
-
为什么 Add_() 不需要 const = 0,常量成员函数,该函数不能修改类成员,这里是 Add_() 类型函数,明显作为类对象的添加型函数,不能有 const
-
一般只有一个 IndexBuffer,所以 SetIndexBuffer() 就够了
-
为什么不直接使用 Bind(),因为不想让 OpenGL的调用和自己的调用混合使用,如果说 Bind() 逻辑很复杂,那么这是必要的。
而且,在未来,可能会添加需要绑定的内容,通知添加解绑的内容,为了跟踪或者分析目的。
所以Bind 的作用并不是真的实际绑定,就像游戏引擎的底层调用一样。这是 api 内部的东西,不希望这个 Bind() 不必要地分支到其他函数去。如果真的需要这样的一个绑定接口,可能会取名字 AddVertexAndIndexBuffer()。这里的绑定逻辑很简单,不需要把这个逻辑转移到其他函数,这会让代码变得更复杂。 -
绑定缓冲区的时候就要调用 VertexBuffer.Bind(),因为这在处理一个实际的 api 对象,不要做类似于 glBindVertexBuffer() 的动作,然后再 VertexBuffer-> GetRenderID()
-
这里的 layout 作用域为什么要去掉,为什么 vertexBuffer->GetLayout() 就会变成空
-
VertexArray 类里面的 m_RendererID 是代替了哪部分,之前似乎没有看到过需要绑定 VertexArray
原先的 VertexArray 相关处理只有这两行
glGenVertexArrays(1, &m_VertexArray);
glBindVertexArray(m_VertexArray);
那么这里的 VertexArray 怎么起作用的,讲道理就是 VAO,本来是 VAO 设置 layout,但前面已经通过 VertexBuffer 设置好了 layout
同时,在 m_VertexArray->SetIndexBuffer(m_IndexBuffer) 时候,也会触发 m_IndexBuffer.Bind(),但是当没有 m_VertexArray 类对象的时候,实际上也没有调用 IndexBuffer.Bind() 的逻辑,这是为什么?哦,m_VertexArray 的绑定原本放在了 Application::Run() 的 while 循环里面
P33 Render Flow And Submission
为什么 RendererAPI::API RendererAPI::s_API = RendererAPI::API::OpenGL
为什么要添加等号左边的命名空间
-
简单的函数逻辑都可以使用内联 inline,比如 Get() 和 Set()
-
RenderCommand.cpp 中的
RendererAPI* RenderCommand::s_RendererAPI = new OpenGLRenderAPI;
这里的 new 为什么没有添加括号 () 表示实现 -
需要知道 Renderer.h / RendererAPI.h / RenderCommand.h 的区别
-
为什么 RendererCommand.h 里面的前两个函数要设置为 inline static
-
为什么 Renderer.cpp 里面会报错,添加了 static,特别是 Renderer.h 文件中实际上是声明了 static void BeginScene()
P34-35 CAMERAS and How They Work
MVP: model 模型矩阵 | View 视图矩阵 | Projection 投影矩阵
一个世界空间的顶点坐标计算到屏幕坐标:gl_Position = projection * view * model * v_pos(一般指Mesh上的坐标)
(column major 列主序,比如 glm;如果是行主序,比如 DirectX,那么就是 v_pos * model * view * projection)
相机系统本质就是视图矩阵+投影矩阵,因为对于世界中每个物体,这两个矩阵都相同,所以这两个矩阵应该作为Camera的成员。实际渲染的时候,相机在世界中心(相机坐标系)比较方便对世界物体处理。所以一般需要将相机坐标 (x,y,z) 的逆作为视图 View 矩阵,对世界物体变换,进入相机坐标系。而 model 属于物体,每个物体在自己的局部坐标系下的变换是不一样的
视图矩阵 View 有两种计算方法:
- 对摄像机的位置、旋转角组成的变换矩阵 transform 取逆后就是 view
- 用欧拉角 + LookAt 计算观察矩阵
https://learnopengl-cn.github.io/01%20Getting%20started/09%20Camera/
GameLoop 中有一个 BeginScene(),是 Renderer 的静态函数,负责更新相机的更新、灯光等设置,即 BeginScene() 需要接收 Camera 作为参数,两种做法:
- 引用传参
- 值传参
一般涉及类的传参都是第一种,因为 BeginScene() 会放到 RendererCommandQueue 中,考虑到多线程渲染的时候,Camera 不能保证不变,所以引用传参会比较危险,比如说:主程改变了 Camera 的 Pos,那么对于其他并行线程而言,就会矛盾。(这里 const& 也不行,这只能保证在 RendererCommandQueue 中无法修改 Camera 的信息,不代表主程不可以改变 Camera 信息)
摄像机移动后,应该 projection * view 在 CPU 上计算,再传入 GLSL
而不是直接将新的 projection 和 view 直接传入 GPU,那么每个物体都要执行一遍 projection * view
作业2
自己设计实现一个 Camera 系统
-
为什么这个时候不提示,而
这个时候会提示 glm::ortho()
这里让我疑惑的是为什么 ViewMatrix 是先 translate * rotation 再取逆
screen_pos = T * R * v_pos,讲道理是这样,对 model 的顶点坐标 v_pos 先旋转再平移,变成屏幕坐标
如果是 T * R * screen_pos = v_pos,取逆后
s
c
r
e
e
n
p
o
s
=
(
T
∗
R
)
−
1
∗
v
p
o
s
screen_{pos} = (T*R)^{-1} * v_{pos}
screenpos=(T∗R)−1∗vpos
从头推导,相机在世界坐标系,相机的平移和旋转矩阵被定为 T 和 R
将物体转换到相机坐标系,一般是先平移,再旋转,再平移
(放弃,这种不知道怎么理解,还是 LookAt 矩阵 + 旋转矩阵好理解)
很牛,效果,直接在创建 Camera 的时候,指定上下左右为 2,就能让原先屏幕内的内容等比例缩小一半,原因在于正交投影函数 glm::Ortho(),
为什么相机(-1.6, 1.6, -0.9, 0.9) 就是正方形的屏幕
为什么要额外添加一个 SceneData,而不是直接添加一个 Camera 或者 ViewProjectionMatrix
什么原因导致平移相机到 {0.5, 0.5}
再旋转45°后变成这样
旋转是有问题的,为什么视角斜了
debug
既然是旋转的问题,那么先不改变相机的位置
会发现屏幕没有内容
进而改变旋转角度为 0.5°,看看发生了什么,同时把断点标上
发现了 m_Position 没有初始化
解决后发现旋转还是有问题
这个问题需要自己解决
这是因为相机的比例问题,相机屏幕改成 1.6-0.9 即可变成正方形,视角也不会那么奇怪
如果我要添加一个按键 ESC 退出按钮,应该怎么添加
Sandbox::OnUpdate() 里面可以判断输入按键是哪个
而 Application::OnEvent() 可以调度窗口关闭事件
为什么 color 通过 Shader 传递,而不是通过 Submit,而 transform 是 Submit
讲道理 color 和 transform 都应该调用 Shaer-> 传递才对
P36 Moving to Sandbox
为什么 Camera 从 Application 去掉,因为它属于场景,而不是属于 Hazel 内核
Sandbox 内的 OnEvent(event)
使用 event 构造 dispatcher 事件调度器
那么每次有个事件发生,不是都要构造一个调度器?
为什么 dispatcher 不用引用
void OnEvent(Hazel::Event& event) override {
Hazel::EventDispatcher dispatcher(event);
}
问题:为什么这里需要 bind()
class ExamlerLayer {
// 事件系统
void OnEvent(Hazel::Event& event) override {
Hazel::EventDispatcher dispatcher(event);
dispatcher.Dispatch<Hazel::KeyPressedEvent>(HZ_BIND_EVENT_FN(ExampleLayer::OnKeyPressedEvent));
}
bool OnKeyPressedEvent(Hazel::KeyPressedEvent& event) {
}
}
bind 作用是将一个成员函数绑定到一个对象实例,返回一个可调用对象
这里是将 OnKeyPressedEvent() 绑定到 Examplerlayer 类实例上,并返回一个可调用对象 std::function<bool(KeyPressedEvent&)>
- 虽然 OnKeyPressedEvent() 是 ExamplerLayer 类的成员函数,但是并不是知道属于哪个对象实例,调用者不知道调用的是哪个实例的 OnKeyPressedEvent()
- 最终的效果就是让 EventDispatcher 能够调用 bool OnKeyPressedEvent(KeyPressedEvent&),仅此而已
从设计的目的出发,bool OnKeyPressedEvent() 的返回值表示是否发生了阻塞
m_Event.m_Handled = func(*(T*)&m_Event); // 此处进行了 OnXXXEvent() 回调函数的调用
在 Application::OnEvent() 会检查每一层 Layer,并调用事件 (*--it)->OnEvent(e);
void Application::OnEvent(Event& e)
{
//HZ_ERROR("Application OnEvent");
// 通过事件调度器判断是否为窗口关闭事件
EventDispatcher dispatcher(e);
//dispatcher.Dispathcer<WindowCloseEvent>(std::bind(&Application::OnWindowClose, this, std::placeholders::_1));
dispatcher.Dispatch<WindowCloseEvent>(BIND_EVENT_FN(OnWindowClose));
for (auto it = m_LayerStack.end(); it != m_LayerStack.begin();) {
(*--it)->OnEvent(e);
if (e.m_Handled) {
break;
}
}
}
当一个事件在一个层 Layer 中被处理,那么事件就会标记为 m_Handled = true; 并跳出循环;停止传播到更多层
举个例子:
当用户界面有个一个文本输入框,当你同时按下 wasd 时候,你其实只是想要这几个字母,而不是想要光标上下左右移动,这时候就需要标记事件已经处理并停止传播
需要使用 DeltaTime 结合帧率计算相机的移动速度
事实上应该轮询在 OnUpdate() 里面去判断是否移动,不封装为一个事件
如果是事件,那么不支持相机的连续移动(因为我没有做支持连续按键事件),这种移动性的事件,适合用于浏览菜单。即使是支持连续按键事件,也不够流畅,对于游戏而言
打开 v-sync,意味着应用运行速度是 fps = 60
如果有人 144Hz 的显示器,每秒144帧,这样相机移动得更快,这都需要考虑
目前简化为简单的速度统一为 0.1f
class HAZEL_API KeyPressedEvent : public KeyEvent
{
public:
KeyPressedEvent(int keycode, int repeatCount)
: KeyEvent(keycode), m_repeatCount(repeatCount) {}
inline int GetRepeatCount() const { return m_repeatCount; }
std::string ToString() const override {
std::stringstream ss;
ss << "KeyPressedEvent: " << m_KeyCode << " (" << m_repeatCount << " repeats)";
return ss.str();
}
EVENT_CLASS_TYPE(KeyPressed);
private:
int m_repeatCount;
};
为什么 KeyPressedEvent.GetEventType() 可以调用成功
为什么 if(event.GetKeyCode() == HZ_KEY_LEFT) 可以调用成功
而且 KeyPressedEvent 的构造函数需要 keycode,那么这个事件是在哪里被初始化的?
(其实在写这个代码的时候不需要管哪里初始化,因为参数 event 就是一个 KeyPressedEvent,已经完成了初始化,即有 keycode 这个属性,至于哪里初始化的,应该是写完这个代码之后再去考虑,这就是面向对象的一个好处,逻辑解耦,写完一个再思考另一个)
P37 TimeStep
为什么 TimeStep 是类,而不是struct
当有更多的东西要添加到平台而不仅仅是时间,就可以把 glfwGetTime() 抽象出来
因为不希望在 Hazel::Application 内有第三方库的东西
TimeStep timestep = time - m_LastFrameTime;
会调用 TimeStep 的 float 构造函数,隐式转换的过程
TimeStep ts 作为参数,不需要引用传递,因为只是一个 float,其实也可以直接用 float,但是封装为类可以有一些成员函数,调用的时候具有可读性
JAVA创建新对象在堆上
难道 C++ 是在栈上?
C# 创建结构体在栈上
问题
解决不了,不知道为什么会斜着的视角
Application() 内 m_Window->SetEventCallback(BIND_EVENT_FN(OnEvent));
设置 Window 的回调函数是 Application::OnEvent(),Window 在什么地方回调这个 OnEvent ?
在 WindowsWindow 里面的各种回调函数 glfwXXX() 内执行
而 Application::OnEvent(event) 会循环 LayerStack 内的层,看是否可以层是否执行 event 事件
为什么 class HAZEL_API Application的 HAZEL_API 不再需要
float time = (float)glfwGetTime();
和 float time = glfwGetTime();
的区别
P38 Material Systems
- 真正的材质系统会将着色器变成批处理,即先渲染红色的方块,再渲染蓝色的方块
- 为什么是用着色器 m_FlatC olorShader 构造一个材质类 Material
为什么第一个被渲染为蓝色
for (int j = 0; j < 20; j++) {
for (int i = 0; i < 20; i++) {
glm::vec3 pos = { i * 0.11f + 0.5f, j * 0.11f + 0.3f, 0.0f };
glm::mat4 transform = glm::translate(glm::mat4(1.0f), pos) * scale;
if (j % 2 == 0)
m_BlueShader->UploadUniformFloat4("u_Color", {0.8f, 0.2f, 0.3f, 1.0f});
else
m_BlueShader->UploadUniformFloat4("u_Color", { 0.2f, 0.3f, 0.8f, 1.0f });
Hazel::Renderer::Submit(m_BlueShader, m_SquareVAO, transform);
}
}
偶数会出现这种情况,奇数不会
如果把所有渲染的部分放到 ExampleLayer 的构造函数内,会出现闪烁的情况,同时左下角方块没有颜色,在断点调试的时候也会出现这种情况
如果只渲染一个方块,没有颜色,这是为什么
离谱了,三角形可以着色,但是方块不行
为什么明明是 m_BlueShader->UploadUniformFloat4(color)
但是 m_BlueShader 不能成功渲染方块为蓝色,
而 m_Shader 没有上传颜色,却可以成功渲染
打印出 Submit() 时候的 m_RendererID,确实是 m_BlueShader 的
P40 Shader Abstraction and Uniforms
为什么 Shader.h 要删掉所有的上传统一变量的函数,作为 OpenGLShader.h 的专属函数
因为 UplaodUniformFloat4 和 …_Matrix4() 是 glShader 的函数,而不是真的 Hazel 的函数?
如果要使用OpenGLShader->UploadUniformFloat4(),可以将 Shader 动态转换为 OpenGLShader
std::dynamic_pointer_cast<OpenGLShader>(shader)->UploadUniformFloat4()
VertexArray 也是抽象接口,不能支持 new VertexArray 吗?
还真是,这样做的好处是什么呢?总之这肯定是一种抽象的方式
Hazel 自己的库引用头文件的时候最好在最上面
OpenGL 缓存统一变量的方式还可以使用哈希表进行,key-value存储之后全局可以用
OnImGuiRender() 调用前后需要有 Begin() 和 End()
但是在 Application::Run() 里面其实已经有了 Begin() 和 End()
那我在 Sandbox 里面的 ExampleLayer::ImGuiRender() 还需要 Begin() 和 End() 吗?是不是不需要也可以
P41 Refs, Scopes and Smart Pointers
如果一开始就不设计好,后面容易崩溃
创建一个着色器,所以共享指针在堆上构建
出于两个目的封装在共享指针内
控制对象的生命周期,将所有权绑定到 Sandbox,保证当应用程序或者作用域崩溃的时候,对象也会被销毁
使用 using 是为了智能指针可以和模板一起使用
using 除了意味着当下在使用,还能实际定义为模板
还有一个好处就是,之后如果要扩展 Ref 或者 Scope
直接把这两个封装为一个类就好
template<typename T>
using Scope = std::unique_ptr<T>;
template<typename T>
using Ref = std::shared_ptr<T>;
为什么 unique_ptr 换成 make_unique 就不可以
明确仅限于 Hazel 的,可以直接使用 Hazel::Ref,因为将来会有资产管理器,需要以更细的精确方式,而像 Log 这种,直接使用智能指针即可
为什么改成了 Scope<>(),报错就消失了
case API::OpenGL: return Scope<OpenGLRendererAPI>();
读取访问权限冲突 RenderCommand
弹窗报错
如果把 Scope<> 换成原始指针,就不会报这个错,为什么换了就访问权限冲突呢
可以看到 Cherno 里面的 RendererAPI 仍然是原始指针,其实并没有使用智能指针,为什么改了之后就没有权限了
P42 Textures
最开始的尝试,这里是 a_TexCoord,左下角是 (0,0) 对应黑色
右方 (1, 0) 对应 r=1, g=0 为红色
上方 (0, 1) 对应 r = 0, g = 1 为绿色
右上角为 (1, 1),对应 r = 1, g = 1 为黄色
Texture 作为一个纯虚抽象类,Texture2D 和 VetexArray 很像,也是一个抽象类,会有一个静态创建函数 static Create()
构建这个泛型基类的原因是对于获取纹理的宽高以及Bind() 对于纹理是2d还是3d都无所谓
virtual ~Texture() = default(); 是什么意思
不需要覆盖任何 Texture 类的函数,可以直接在子类中实现
不过如何有任何特定于 2D纹理的函数,比方说不适应于3D纹理,那么就要在 Texture2D 中声明
目前还没有,所以声明一个 static Create(path) 即可
之后会经常扩展这个接口,因为可能会需要在内存中直接创建纹理(一般纹理是直接从外面的图片导入),比如纯色纹理,或者纹理工厂 texture factory(网格、渐变、纯色、五颜六色的网格、纹理脉动 pulsates等纹理,对实际的测试很有用)
另外就是可能会遇到引擎生成的错误纹理,比如说纹理无法正常加载,资产丢失(表现为洋红色),而不是直接黑屏崩溃
为什么Texture.h 要引用 “Hazel/Core.h”
这里的 static Create() 不使用 inline 吗
Texture.cpp 的 static Create() 为什么一定要把 static 去掉
一般可能只是想输出法线,而不是全色渲染3D模型
OpenGLTexture2D 创建了一个私有成员 m_Path,为了在运行时候不需要太多的东西
将其用于调试版本和引擎的开发版本很有用,因为可能会希望热重载纹理,即纹理的重新创建、保存、导出,然后希望让引擎了解文件已经更改,再加载,所以我们会希望保留一个这样的路径,不过这个路径是否放在 OpenGLTexture2D 这个实际纹理中是要想想的
比如说,其实可以使用一个资产管理器来处理热重载,通过维护一个路径,映射路径到实际纹理,比如实例
目前还没有资产管理器,所以直接保存在 OpenGLTexture2D
目前的实际任务是加载图像,有现成的库可以使用
但实际上也可以不使用库,直接加载图像,编写自己的解码器
从技术上讲,作为引擎资产,不会依赖任何现有的图像格式,一般是制作自己的纹理格式,通过自己的构建管道完成构建,在PS保存一张纹理为 png 等,然后加载到构建管道,构建为Hazel纹理文件,那就是引擎运行时加载的内容
stb_image.cpp 文件是做什么作用的,只是为了激活 stb_image 吗
不需要处理 mipmaps 什么意思
glBindTextureUnit(0, m_RendererID) 是什么意思,绑定纹理单元
插槽,意思是绑定到哪个纹理单元,因为可以绑定多个纹理,当渲染更复杂的东西时候
sampler2D 是一个整数,作为采样的纹理槽,因为把纹理绑定在 slot0
所以在 UploadUniformInt(“u_Texture”, 0) 用于映射
在创建着色器的时候可以弄清楚这些插槽
比如说,有四个纹理,把插槽分给每一个,第一个统一变量就是 slot0,之后是 是slot1, slot2, slot3
如果想要渲染特定的纹理,再绑定到统一变量的名称上,实际上就是从着色器中获取插槽的位置,这样就不用手动去Set Get 纹理
这部分自动内容也会在之后做材质系统的时候体现出来
Texture() 为什么不会返回一个指针
原始纹理为什么会垂直翻转
因为 stb_image 是从上到下读取纹理
而 OpenGL 是从下到上渲染纹理,所以要有一个反转的过程
另一个是模糊了很多,在VSCode里打开,放大是 900%,并没有线性过滤
所以需要修改渲染的过滤类型,变得清晰
glTextureParameteri(m_RendererID, GL_TEXTURE_MAG_FLITER, GL_NEAREST);
即可,每个屏幕像素应该捕捉纹理上最近的像素,而不是线性插值,这会导致图像模糊
不过线性过滤杜宇类似的图像很有用,但不适合这种双色的棋盘格
滤波:线性滤波(缩小)、最近滤波(放大)、各向同性滤波
P43 Blending 混合
为什么只需要
m_ChernoLogoTexture.Create()
m_ChernoLogoTexture->Bind()
Submit()
这三行就会修改纹理
第一句是创建一个纹理,构造函数调用 glCreateTexture()
使用 alpha 加载纹理?看不到透明度
RGBA, sRGB?
得到的纹理如果呈现破损的状态,那么说明没有做好混合,纹理的颜色通道可能支持 rgb,对于透明度的 alpha 可能被错误读取为 rgb 某个通道,取决于像素的顺序。
这会导致混乱甚至崩溃,但是这里因为提供了 rgba 的数据,多于需要的 rgb 数据,所以不会溢出缓冲区或类似的操作
混合的启用应该是在窗口创建完成时并创建渲染器的时候,需要一系列操作进行初始化,这个时候要启用混合,还有些其他功能也是在这个时候启用,比如深度测试、获取GPU的设备信息等,能提供什么级别的抗锯齿支持,支持什么级别的各向异性过滤、支持的最大纹理尺寸?,我们需要了解渲染设备的实际能力,这能尽量避免崩溃,也能在未来能支持更多的GPU和设备
这里可以讨论一下 Renderer,RenderCommand,RendererAPI 的架构设计
这里并没有把 OpenGL 直接放在渲染器中,多了 4 层抽象
主要是想要以完全与平台或特定的接口无关的方式来完成所有任务
这种渲染接口的初始化需要涉及实际的渲染接口,比如说,在渲染 Vulkan,不希望涉及 OpenGL,这部分代码甚至不会被编译,但是仍然可以正常运行,这样就可以使得一套引擎代码,可以在 Windows 上运行 DirectX,也可以在 Mac 上运行 Metal
现在是嵌套调用,Renderer->Init() 调用 RendererCommand->Init(),再调用 OpenGLRendererAPI::Init()
之所以有 RendererCommand 的原因,可以静态调用这些命令,不用实现某个接口,而且后面会在不同线程上运行这个 Renderer,为了保证这种运行方式,我们需要可以把命令发送到 RendererCommandQueue
P44 Shader Asset Files 着色器资产文件
- 目前的着色器代码直接写在客户端 Sandbox,当着色器内容多了将会占主要比例,不符合设计原则,所以要拆出来,让引擎能够识别着色器文件,自行编译、链接、热更新
- 使用 fstream 读取着色器文件代码,加载到 Shader 内
std::ifstream in(filepath, std::ios::in | std::ios::binary);
表示的是读取文本内容,不做处理,因为有时候win可能会把文本内容的 \r\n 变成 \n,binary 表示的是读取完整文本(即对应的完整二进制数据),并不影响之后结果是 string 还是不可见的 01 串(二进制文件)- TextureFiles(写在一个文件内) -> ShaderFiles(变成原本写在Sandbox内样子) -> Compile :: 这里的 Compile 在之前其实也有,就是在 OpenGLShader(versrc, fragsrc) 构造函数内
我这里对 Cherno 原本的代码做了改变,它的命名和计算过程不太优雅
特别是对于分离 Shader 代码部分
我这里少了两个变量,更加直观
nextShaderLinePos: 用于区分 vertex / fragment Shader 的开始位置 (包括标记 #type …)
typeLineEndPos: 用于找到 OpenGL 不需要的标记
type_begin: 表示 “vertex” / “fragment” 字符串的开始位置
shaderLinePos: 表示 shader 代码(无 type 标记)
std::unordered_map<GLenum, std::string> OpenGLShader::SplitShaderFromSourceFile(const std::string source)
{
std::unordered_map<GLenum, std::string> shaderSources;
std::string typeToken = "#type";
size_t typeTokenLen = typeToken.size();
size_t typeLinePos = source.find(typeToken, 0); // 有token的
size_t nextShaderLinePos = typeLinePos;
while (nextShaderLinePos != std::string::npos) {
size_t typeLineEndPos = source.find_first_of("\n", nextShaderLinePos);
HZ_CORE_ASSERT(typeLineEndPos != std::string::npos, "Read shader file failed."); // 这里是默认文件的每一行以换行为结尾
//if (currLineEndPos == std::string::npos) {
// currLineEndPos = source.size();
//} // 这样其实可以保证最后一行如果不是换行/回车结尾,也没有关系
size_t type_begin = nextShaderLinePos + typeTokenLen + 1; // 从 vertex / fragment / pixel 开始
nextShaderLinePos = source.find(typeToken, typeLineEndPos); // 包含 type 的整个 shader 模块
std::string type = source.substr(type_begin, typeLineEndPos - type_begin);
HZ_CORE_ASSERT(ShaderTypeFromString(type), "Invalid shader type.");
size_t shaderLinePos = source.find_first_not_of("\r\n", typeLineEndPos);
shaderSources[ShaderTypeFromString(type)] =
nextShaderLinePos != std::string::npos
? source.substr(shaderLinePos, nextShaderLinePos - shaderLinePos)
: source.substr(shaderLinePos);
}
return shaderSources;
}
OpenGLShader 流程
GLuint shader = GLCreateShader(GL_VETEX_SHADER) // 创建 GPU 上着色器
glShaderSource(shader, src="") // 绑定 src 着色器程序
glCompileShader(shader) // 编译着色器程序
glAttachShader(program, shader) // 建立 program 和着色器程序联系,program 表示GPU上可执行的程序
glLinkShader(program) // 链接着色器程序到 program
glDetachShader(program, shader) // 分离 program 和着色器
头文件引用
我们的目的是 Sandbox 只引用 Hazel 项目,Hazel 内部使用的 glad / glfw 等三方库 Sanbox 不可见
所以在 premake 中我们设置了 Sandbox 只引用 Hazel,没有应用 gald/glfw,但是引用了 glm(运算库)
当前情况下,Sandbox 需要引用 OpenGLShader.h
而 OpenGLShader.h 如果引用了 glfw/glfw.h 或 glad/glad.h (因为 OpenGLShader.h 中使用了 GLenum),会报错找不到头文件 glfw.h / glad.h
这时候可以直接在 OpenGLShader.h 中使用 typedef unsigned int GLenum
绕开 glad.h 头文件的引用
另一方面,为什么 OpenGLShader.cpp 引用却没事
因为 cpp 文件被编译为 obj 文件(里面已经包含了 glad.h 内容),Sandbox 只引用 .h,不包含 cpp 文件
Sandbox 通过.h 文件调用 cpp 文件
P45 Shader Library
- 将 vector 内容移动到 array,堆内存改用栈内存
- 我不能转换,会报错
std::ifstream in(path, std::ios::in, std::ios::binary);
Unknown shader type. 应该是文件内容读取错误
- 使用 Shader Library 主要是复用 shader 文件,存储在 cpu 中时直接使用,不用再创建;另一方面,之后可能会用到很多 Shader,不能都放在 Sandbox 的成员属性处,之后可以演变为 Renderer::GetShaderLibrary()
- void Add(const std::string& name, const Ref& shader); 相对于
void Add(const std::string& name, const Ref<Shader> shader);
优势在于不会产生新的引用,不会调用shared_ptr构造函数
vecotr.reserve() / .resize()
- .reserve() 只要空间,不要对象,使得 push_back() 正常使用
- .resize() 既要空间,也要对象。最后导致 glShadersIDs.push_back(id),会变成有 3 个元素的 vector,前面两个元素一直为 0,这不是我们希望的
.reset 为什么报错
要换成 = 为什么
unordered_map<string, …>
需要注意如果函数是 const,不能使用 map[“name”] 的操作,因为对于 map[] 而言,如果没有找到 name 关键字,会修改容器,而函数添加 const 意味不能修改容器
可以改为使用 .at()
Hazel::Ref<Hazel::Shader> ShaderLibrary::Get(const std::string& name) const
{
HZ_CORE_ASSERT(Exists(name), "The shader name has not found.");
return m_Shaders.at(name);
}
P46 How to Build a 2D Renderer
这一章不涉及代码,主要是探讨 2D 渲染器将会包含的内容
- 2D batch Render: 批处理渲染
- Texture Atlas: 纹理集,多个纹理组成大纹理
- Sprite sheet: 精灵图(动画),如果像素较低,直接当成纹理集就可以;像素高使用关键帧技术,类似视频编码解码
- UI 布局,根据分辨率变化
- 后期处理,粒子动态效果,爆炸,背景模糊
- 交互:脚本系统 / ECS 系统
P47 Cameras Controllers
技巧
Git
- 使用 git 不一定用 git bash,可以直接再资源管理器里面用 cmd
alt + d
是资源管理器的搜索栏快捷键- .gitignore 中 # 表示注释,.vs/ 可以无视 .vs 文件夹,*.user 可以无视 .user 后缀的文件 (.user 无法无视 Hazel.vcxproj.user 文件),! 表示不会忽略某文件,
但是要注意一旦父文件被忽略,!子文件是无效的 - git reset . 退回到上一步
- 我的 github 默认主分支是 main,不是 master
- git add . 和 git add * 的区别是什么? git add *和git add .之间的主要区别在于它们对于已删除文件的处理。git add *命令会添加所有的文件,包括新建的、修改过的和已删除的文件,而git add .命令只添加新建的和修改过的文件,不包括已删除的文件
- cherno 用的是 git push origin master,但是我的 github 默认主分支是 main,所以我使用 git push origin main
- 如何回退到上一版本:
git reset --hard HEAD^
- 撤回回退:
git reflog
查看历史命令,要回退到哪个版本
git reset --hard 版本号
- 撤回 commit 和 add .:
git reset (--mixed) HEAD^
,括号里的可有可无 - 撤销 git add . :
git reset .
即可 - github 上的版本如何回滚,网页版是没有回滚操作的,只有 Github For Win 客户端有,如果想在网页端操作,可以先按照这个教程
https://blog.csdn.net/weixin_44259720/article/details/112608864
创建一个新的分支,之后把默认分支换成新的分支,同时改名成功,并且把不想要的分支都删掉。这种操作可以在新分支的 Activity 中见到操作记录,但已经是比较好的解决方案。 - 所以必须在 push to github 之前查阅将会出现的变化,仔细核对,否则推送到 github 后非常麻烦。或者可以fetch到一个地方之后,再推送到 main 版本
- 需要知道 push 哪些文件,是必须的,哪些文件不是必须的,尽量保证 github 仓库中文件的干净程度?可以新建一个分支,先推送试试,如果clone下来发现无法正常运行,可以再尝试?
- 首先 bin/ 和 bin-int/ 是编译的结果,可以不 push 到 github;.sln 文件也不需要;.vcxproj 等文件也不需要
- .gitignore 只能忽视原来没有被跟踪的文件,如果要解除跟踪,使用
git rm --cached <file>
。比如这里需要git rm --cached *.sln
这样能把 .sln 文件给解除跟踪 - 如何修改 git commit 信息,git commit --amend,之后进入vim模式修改,保存后,git push -f,会把新的 commit 信息更改到 github
- git status 可以看哪里做了修改;git diff 可以看具体是哪里不一样
提交规范
feat: 新功能、新特性
fix: 修改 bug
perf: 更改代码,以提高性能(在不影响代码内部行为的前提下,对程序性能进行优化)
refactor: 代码重构(重构,在不影响代码内部行为、功能下的代码修改)
docs: 文档修改
style: 代码格式修改, 注意不是 css 修改(例如分号修改)
test: 测试用例新增、修改
build: 影响项目构建或依赖项修改
git fetch
git branch tmp 创建 tmp 分支
git checkout tmp 切换到 tmp 分支
git branch 查看所有分支
git branch -d tmp 删除 tmp 分支
git fetch -p 更新远程跟踪分支(-prune 表示会删除远程仓库没有的本地分支)
git branch -rv 查看远程跟踪分支的版本
git merge 合并版本
更新远程跟踪分支,即 origin/main,通过 git branch -rv 可以查看所有的远程跟踪分支
通过 git branch -vv 可以看到本地仓库分支对应的是哪个远程跟踪分支
当 github 上(远程仓库)中添加了一个新的分支 dev 后,通过 git remote update 更新(git fetch -rv 也可以,还能在本地仓库删除远程仓库已经删除的分支,推荐使用),本地仓库会添加一个新的远程跟踪分支,而本地仓库通过 origin/HEAD 来控制本地仓库引用的远程仓库分支
通过 git diff origin/main 可以对比当前仓库和远程跟踪分支的差异
需要注意的是,如果没有 git fetch,那么可能当前的远程跟踪分支不是最新的版本
这里本地仓库实际是落后远程仓库,红色表示本地仓库相对于远程仓库的少掉的那部分
绿色表示本地仓库相较于远程仓库多出来的部分
VS
- alt + 鼠标左键,竖向选中代码,用于删除代码,或者统一缩进
- 断点不命中,把这个勾取消
- 如何新建一个文件夹,而不是筛选器:显示所有文件后再新建文件夹
- Visual Assistant:拓展,搜索 visual assist 即可
- 横向(水平)滚动
下载 vsix 插件https://marketplace.visualstudio.com/items?itemName=drewnoakes.SideScroller&ssr=false#overview
下载指导
https://blog.csdn.net/feinifi/article/details/107123479
其他
- alt+d 文件资源管理器中快捷定位到路径栏,cmd 可以打开 cmd 窗口
报错
找不到头文件
为什么引用头文件说找不到?改成双引号就行 “Test.h”
第二种方法,想要引用头文件可以用尖括号 <>,可以进入属性配置面板:c/c++ -> 常规 -> 附加包含目录输入 $(SolutionDir)Hazel\src
<> 头文件会去系统目录寻找,“” 先找项目目录,再找系统目录
Sandbox.exe 无法找到入口
因为 dll 文件没有更新,去把最新的 dll 文件更新下就行,最新的 dll 文件需要看引擎的 dll 文件输出到哪里,从配置属性"常规"里面可以看到
报错窗口乱码怎么解决,同时报错的窗口英文字母也是乱码怎么解决
inconsistent dll linkage 不一致的 dll 链接
可能是 dll 链接拼写错误
“配置属性面板 -> c/c++ -> 预处理器”
看看里面的宏有没有拼写错误导致 dll 链接失败
error LNK2001: 无法解析的外部符号
1>Application.obj : error LNK2001: 无法解析的外部符号 “private: static class Hazel::Input * Hazel::Input:😒_Instance” (?s_Instance@Input@Hazel@@0PEAV12@EA)
1>WindowsInput.obj : error LNK2001: 无法解析的外部符号 “private: static class Hazel::Input * Hazel::Input:😒_Instance” (?s_Instance@Input@Hazel@@0PEAV12@EA)
1>…\bin\Debug-windows-x86_64\Hazel\Hazel.dll : fatal error LNK1120: 1 个无法解析的外部命令
这里是因为 private: static Input* s_Instance;
没有完成初始化,访问空指针报错
只需要在派生类 WindowsInput.cpp 中进行初始化即可 Input* Input::s_Instance = new WindowsInput();
error LNK2019: 无法解析的外部符号 & fatal error LNK1120: 1 个无法解析的外部命令
SandboxApp.obj : error LNK2019: 无法解析的外部符号 "class Hazel::Application * __cdecl Hazel::CreateApplication(void)" (?CreateApplication@Hazel@@YAPEAVApplication@1@XZ),函数 main 中引用了该符号
这里是 main 函数中引用的外部引用 extern CreateApplication() 无法解析
extern Hazel::Application* Hazel::CreateApplication();
查看实现 CreateApplication 的文件 Sandbox.cpp 中,发现未添加命名空间
改为 Hazel::Application* Hazel::CreateApplication()
即可
再次遇到 LNK2019 错误:
error LNK2019: 无法解析的外部符号 "public: virtual __cdecl Hazel::WindowsWindow::~WindowsWindow(void)"
error LNK2001: 无法解析的外部符号 "private: virtual void __cdecl Hazel::WindowsWindow::Shutdown(void)"
也是类似的错误,在 .h 文件声明,但是并没有在 .cpp 文件中实现,两种解决方法
- 把头文件的
virtual ~WindowsWindow();
改成virtual ~WindowsWindow() {}
- 在 .cpp 文件中添加
WindowsWindow::~WindowsWindow() {}
这里是选择前者,因为函数内容为空,个人习惯不增加无谓的跳转,增加可读性(但是这里是虚函数,不知道这种直接把函数实现写在 .h 文件会不会不好)
error LNK2019: 无法解析的外部符号,函数 main 中引用了该符号
error LNK2019: 无法解析的外部符号 "public: static void __cdecl Hazel::Log::Init(void)" (?Init@Log@Hazel@@SAXXZ),函数 main 中引用了该符号
这里仍然是 main 函数内的链接问题,这里的 Hazel::Log::Init() 函数识别不到
- 检查 Log 类定义的时候是否添加命名空间
- 检查 Log 定义的时候有没有添加 __declspec(dllexport),必须添加字段表示将类导入 dll
我犯了第二个错误
更改为class HAZEL_API Log
即可(宏 HAZEL_API 在我的工程中被映射为 __declspec(dllexport))
error LNK2019: 无法解析的外部符号 __imp_realloc,函数 defaultReallocate 中引用了该符号
很大一段报错,可以看到是 GLFW 库的链接问题,我是直接把 Cherno 仓库 glfw 复制过来报错,参考一个文章需要在第 50 行位置添加一行解决问题
GLFW.lib(init.obj) : error LNK2019: 无法解析的外部符号 __imp_realloc,函数 defaultReallocate 中引用了该符号
1>GLFW.lib(window.obj) : error LNK2019: 无法解析的外部符号 __imp_strncpy,函数 glfwWindowHintString 中引用了该符号
1>GLFW.lib(input.obj) : error LNK2001: 无法解析的外部符号 __imp_strncpy
1>GLFW.lib(monitor.obj) : error LNK2001: 无法解析的外部符号 __imp_strncpy
1>GLFW.lib(win32_joystick.obj) : error LNK2001: 无法解析的外部符号 __imp_strncpy
1>GLFW.lib(osmesa_context.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(monitor.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(vulkan.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(wgl_context.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(egl_context.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(window.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(context.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(input.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
1>GLFW.lib(win32_thread.obj) : error LNK2001: 无法解析的外部符号 __imp__wassert
error LNK2019: 无法解析的外部符号 “public: __cdecl Hazel::Layer::Layer(class std::basic_string<char,struct std::char_traits,class std::allocator > const &)”
LayerStack 类中使用的 Layer 类没有写构造函数,可以看到报错后面有 constructor
LayerStack.obj : error LNK2019: 无法解析的外部符号 “public: __cdecl Hazel::Layer::Layer(class std::basic_string<char,struct std::char_traits,class std::allocator > const &)” (??0Layer@Hazel@@QEAA@AEBV? b a s i c s t r i n g @ D U ? basic_string@DU? basicstring@DU?char_traits@D@std@@V?$allocator@D@2@@std@@@Z),函数 “public: void __cdecl Hazel::Layer::`default constructor closure’(void)” (??_FLayer@Hazel@@QEAAXXZ) 中引用了该符号
error LNK2019 无法解析的外部符号 “__declspec(dllimport) bool __cdecl ImGui::Begin(char const *,bool *,int)”
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 LNK2019 无法解析的外部符号 “__declspec(dllimport) bool __cdecl ImGui::Begin(char const *,bool *,int)” (_imp?Begin@ImGui@@YA_NPEBDPEA_NH@Z),函数 “public: virtual void __cdecl ExampleLayer::OnImGuiRender(void)” (?OnImGuiRender@ExampleLayer@@UEAAXXZ) 中引用了该符号 Sandbox D:\Project\Hazel\Sandbox\SandboxApp.obj 1
ImGui::Begin() 函数无法找到原函数,后面的信息表示在 SandboxApp.cpp 中 ExampleLayer类的 OnImGuiRender()
这是因为 Hazel 链接了 ImGui,Sandbox 链接了 Hazel
Sandbox 是无法通过间接关系直接使用 ImGui 的
只需要在 ImGui 的 premake.lua 中,导出 IMGUI_API 即可(因为 ImGui::Begin()等函数前面都用 IMGUI_API 标识)
defines
{
“IMGUI_API=__declspec(dllexport)”
}
Layer.obj : error LNK2001: 无法解析的外部符号 “public: virtual void __cdecl Hazel::Layer::OnAttach(void)” (?OnAttach@Layer@Hazel@@UEAAXXZ)
.h 文件中只有声明没有在 .cpp 文件中实现
即使是 virtual 函数,要么是空函数,要么 = 0
所以改 virtual void OnUpdate();
为 virtual void OnUpdate() {}
Layer.obj : error LNK2001: 无法解析的外部符号 “public: virtual void __cdecl Hazel::Layer::OnAttach(void)” (?OnAttach@Layer@Hazel@@UEAAXXZ)
git push origin main报错 连接不到 443
Failed to connec t to github.com port 443 after 21083 ms: Couldn't connect to server
首先看下 clash pro 的请求日志 Logs,发现都能正常访问
于是重新 git push,弹出窗口要验证码,选择网页验证,输入身份验证器中的号码即可
git 拉取超时
git submodule add
拉取超时
因为拉取的包太大了
有两种错误提示
error: RPC failed; curl 18 HTTP/2 stream 5 was reset09.00 KiB/s
- fetch-pack: unexpected disconnect while reading sideband packet
把命令改成
git submodule add --depth 1 https://github.com/gabime/spdlog Hazel/vendor/spdlog
添加关键字 ‘–depth 1’,表示只拉取最新一次更新
error C2597: 对非静态成员的非法引用
error C2597: 对非静态成员“Hazel::Log::s_CoreLogger”的非法引用
那就是说明 s_CoreLogger 是非静态成员,但是用成了静态成员,比如说 static 函数的返回值是 s_CoreLogger&,那么就出现错误,static 函数必须返回 static 成员
error C2039: “stdout_color_mt”: 不是 “spdlog” 的成员
spdlog::stdout_color_mt 这么些报错
是因为没有包含定义 stdout_color_mt 的头文件,这个头文件定义 stdout_color_mt 应该也是定义在 spdlog 这个命名空间内
error C2040: 间接寻址级别不同
error C2040: “s_CoreLogger”:“std::shared_ptr<spdlog::logger>”与“std::shared_ptr<spdlog::logger> &”的间接寻址级别不同
在 Log.cpp 中声明了 Log.h 的变量,但是没有保持两个变量相同内容,.h 中是引用,.cpp 中是变量
这里不要被 .h 文件上面的 public 函数误导,那是公开给别的对象的函数,返回的是引用
弹窗报错:Sandbox.exe 无法找到入口
无法定位程序输入点 Hazel::Log::Init() ,在动态链接库上
还是 dll 链接的问题,我这里是没有更新 dll 文件
No Premake script (premake5.lua) found!
必须命名 premake 文件为 premake5.lua
注意把当前 vs 打开的 premake.lua 文件关闭,重命名之后再打开,否则在 vs 修改的记录按 ctr + s 后,vs 会保存一个 premake.lua 文件到原位置,premake5.lua 的内容不会改变
Error: [string “src/base/api.lua”]:606: bad argument #2 to ‘deferredjoin’ (string expected, got table)
error MSB3191: 无法创建目录,因为同名文件或目录已存在。
error MSB3191: 无法创建目录“..\bin\Debug-windows-x86_64\Sandbox\”。无法创建“D:\Projects\Hazel\Hazel\bin\Debug-windows-x86_64\Sandbox”,因为同名文件或目录已存在。
把 {COPY} 改成 {COPYFILE} 不知道为啥报错
-- 编译好后移动Hazel.dll文件到Sandbox文件夹下
postbuildcommands{
("{COPYFILE} %{cfg.buildtarget.relpath} ../bin/" .. outputdir .. "/Sandbox")
}
https://github.com/TheCherno/Hazel/issues/623
复制这个答主的就好了,真他妈高血压
error MSB3073
error MSB3073: 命令“IF EXIST ..\bin\Debug-windows-x86_64\Hazel\Hazel.dll\ (xcopy /Q /E /Y /I ..\bin\Debug-windows-x86_64\Hazel\Hazel.dll ..\bin\Debug-windows-x86_64\Sandbox > nul) ELSE (xcopy /Q /Y /I ..\bin\Debug-windows-x86_64\Hazel\Hazel.dll ..\bin\Debug-windows-x86_64\Sandbox > nul)
再点一次 F5 就可以了
把 Hazel 的 dll 文件复制到 Sandbox 里面发现可以,其实就是一开始检测目录发现是空文件,如果再按一次 F5 再次运行时候,就能检测到存在的文件
https://github.com/TheCherno/Hazel/issues/9
或者,按照 P18 视频中的讲解,修改 premake.lua 即可
-- 编译好后移动Hazel.dll文件到Sandbox文件夹下
postbuildcommands{
-- ("{COPY} %{cfg.buildtarget.relpath} ../bin/".. outputdir .."/Sandbox/")
("{COPY} %{cfg.buildtarget.relpath} \"../bin/".. outputdir .."/Sandbox/\"")
}
error MSB3073: 命令“copy /B /Y ..\bin\Debug-windows-x86_64\Hazel\Hazel.dll "\bin\Debug-windows-x86_64\Sandbox"
继续报错,不是引号的问题,如果在 cmd 中是可以复制成功的
现在我想知道,执行 copy 的这条语句是在哪里
error MSB3073: 命令“copy /B /Y "..\..\bin\Debug-windows-x86_64\Hazel\Hazel.dll" "..\..\bin\Debug-windows-x86_64\Sandbox\"
退回两层去找 bin,可能是 Hazel/src,但如果是这里,这句命令可以执行成功(cmd 中可以)
error C2065: “DC”: 未声明的标识符
报错的地方加上 window->
最好加上一个 window 非空判断,不然还会报错,即
https://github.com/ocornut/imgui/discussions/6195
error C3646: “BindTexture”: 未知重写说明符
\imgui\backends\imgui_impl_opengl3_loader.h(471,43): error C3646: “BindTexture”: 未知重写说明符
这个报错有很多种可能,
头文件循环引用有可能、
类的声明实例化顺序颠倒等等
这里是 ImGuiBuild.cpp 内引用
#include "backends/imgui_impl_opengl3.cpp"
后于 #include "backends/imgui_impl_glfw.cpp"
导致
两者引用顺序倒一下即可
而且这个文件是不能删除的,如果删除了报错
1>ImGuiLayer.obj : error LNK2019: 无法解析的外部符号 "bool __cdecl ImGui_ImplOpenGL3_Init(char const *)" (?ImGui_ImplOpenGL3_Init@@YA_NPEBD@Z),函数 "public: virtual void __cdecl Hazel::ImGuiLayer::OnAttach(void)" (?OnAttach@ImGuiLayer@Hazel@@UEAAXXZ) 中引用了该符号
弹窗报错:Did you call ImGui_ImplOpenGL3_Init()
这个报错原因是没有调用 CreateContext() ?
好奇怪
推测是 PushLayer() 没有 layer->OnAttach() ?为什么
弹窗报错: DisplaySize() 顺序问题
需要调整创建帧在创建窗口(调整窗口大小)之后,即
ImGuiIO& io = ImGui::GetIO();
Application& app = Application::Get();
io.DisplaySize = ImVec2(app.GetWindow().GetWidth(), app.GetWindow().GetHeight());
// 需要创建窗口后才执行下面
ImGui_ImplOpenGL3_NewFrame();
ImGui::NewFrame();
Warning: imgui.h(74,1): warning C4005: “IMGUI_VERSION”: 宏重定义
双击这个提示,滚轮上滑,把 #pragma once 上方的宏定义注释掉
弹窗报错: g.IO.KeyMap[n] >=-1 && g.IO.KeyMap[n] <512:
右键 Shift 出现,键位映射超出范围
很奇怪,按下右 shift 后输出的值是 -1
但是可以看到宏映射是 345
提醒:overriding ‘/MTd’ with ‘/MDd’
之所以提醒,是因为 Hazel 属性 -> C/C++ -> 命令行中的 /MDd 会替换代码生成中的运行库 “多线程调试(/MTd)”
弹窗报错:RuntimeError
这里是 vector.emplace() 之后迭代器 iterator 超出范围
实际上是 vector 在内存中的位置改变后,迭代器不会跟着变化,将失效
所以这个时候需要使用 insertIndex 代替迭代器
错误 C2338 Formatting of non-void pointers is disallowed
错误C2338 Formatting of non-void pointers is disallowed. Hazel D:\Project\Hazel\Hazel\vendor\spdlog\include\spdlog\fmt\bundled\core.h 1751
改为
HZ_CORE_INFO("OpenGL info:");
HZ_CORE_INFO(" Vendor: {0}", (const char*)glGetString(GL_VENDOR));//打印厂商
HZ_CORE_INFO(" Renderer: {0}", (const char*)glGetString(GL_RENDERER));
HZ_CORE_INFO(" Version: {0}",(const char*)glGetString(GL_VERSION));
https://github.com/TheCherno/Hazel/issues/621
错误 C2143 语法错误: 缺少“;”(在“&”的前面)
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C2143 语法错误: 缺少“;”(在“&”的前面) Hazel D:\Project\Hazel\Hazel\src\Hazel\Renderer\Buffer.h 14
报错位置是 VertexBuffer 内的函数 virtual const BufferLayout& GetLayout() const = 0;
类的定义顺序不对,应该把 BufferLayout 定义在 VertexBuffer 类的定义之前
错误 C2512 “Hazel::BufferLayout”: 没有合适的默认构造函数可用
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C2512 “Hazel::BufferLayout”: 没有合适的默认构造函数可用 Hazel D:\Project\Hazel\Hazel\src\Platform\OpenGL\OpenGLBuffer.cpp 9
报错位置是 OpenGLVertexBuffer::OpenGLVertexBuffer(float* vertices, uint32_t size) {} 构造函数
这里是因为 BufferLayerout 类是 OpenGLVertexBuffer 的成员,当 OpenGLVertexBuffer 初始化的时候,会先调用 BufferLayout 的默认构造函数
报错:对象含有与成员函数不兼容的类型限定符
需要将 beign() 和 end() 添加 const 关键字
std::vector::const_iterator begin() const { return m_Elements.begin(); }
std::vector::const_iterator end() const { return m_Elements.end(); }
同样的,layout.GetStride() 也会报相同的错误
只需要 inline uint32_t GetStride() const { return m_Stride; }
添加上 const 即可
着色器编译错误
“Vertex shader compilation failed”
这大概率是着色器程序写错了,检查后发现是
out v_Color;
改成 out vec4 v_Color; 即可
报错:对象含有与成员函数 "BufferLayout::GetElements"不兼容的类型限定符,对象类型是 const Hazel::BufferLayout
这里的 BufferLayout 是一个 const 常量对象,而 GetElements() 是一个非常量成员函数,常量对象不能调用非常量成员函数
解决方法:GetElements() const 变成常量成员函数即可
将 <>& 类型的引用绑定到 const <> 类型的初始化设定项时,限定符被丢弃
inline virtual std::vector<std::shared_ptr>& GetVertexBuffer() const override { return m_VertexBuffers; };
std::vector<std::shared_ptr> m_VertexBuffers;
GetVertexBuffer() 函数试图返回一个非 const 引用,而这个函数被声明为 const 成员函数。
所以 const GetVertexBuffer() const {} 即可
LNK2019 无法解析的外部符号
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 LNK2019 无法解析的外部符号 “public: static void __cdecl Hazel::Renderer::BeginScene(void)” (?BeginScene@Renderer@Hazel@@SAXXZ),函数 “public: void __cdecl Hazel::Application::Run(void)” (?Run@Application@Hazel@@QEAAXXZ) 中引用了该符号 Sandbox D:\Project\Hazel\Sandbox\Hazel.lib(Application.obj) 1
定位到 BeginScene() 处,发现是 Renderer.cpp 文件中定义的时候没有指明是哪个类的函数,修改为
void Renderer::BeginScene() 即可
编译报错
ExamplerLayer 类中的成员函数 OnEvent() 内初始化并调用 EventDispatch 报错
没有与参数列表匹配的函数模板 Dispatch(),即 Dispatch() 的参数有问题
提示参数类型:std::_Binder() 里面是一个 void 函数指针
当前,我们可以看到传入的参数是 bind(void(KeyPressedEvent&))
跳转到 EventDispatcher 类中,可以看到
using EventFn = std::function<bool(T&)>;
bool Dispatch(EventFn<T> func)
那么138行需要的参数应该是 Dispatch(std::bind(bool(KeyPressedEvent&)))
即OnKeyPressedEvent() 的参数应该是 KeyPressedEvent&,返回值是 bool 类型
LNK2019 无法解析的外部符号
错误 LNK2019 无法解析的外部符号 “public: virtual __cdecl Hazel::Shader::~Shader(void)” (??1Shader@Hazel@@UEAA@XZ),函数 “int
public: __cdecl Hazel::OpenGLShader::OpenGLShader(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)'::
1’::dtor$0” (?dtor 0 @ ? 0 ? ? ? 0 O p e n G L S h a d e r @ H a z e l @ @ Q E A A @ A E B V ? 0@?0???0OpenGLShader@Hazel@@QEAA@AEBV? 0@?0???0OpenGLShader@Hazel@@QEAA@AEBV?basic_string@DU? c h a r t r a i t s @ D @ s t d @ @ V ? char_traits@D@std@@V? chartraits@D@std@@V?allocator@D@2@@std@@0@Z@4HA) 中引用了该符号 Sandbox C:\Project\Hazel\Sandbox\Hazel.lib(OpenGLShader.obj) 1
这里是提示 ~Shader(void) 析构函数提示问题,后面紧跟派生类 OpenGLShader 的构造函数,那估计就是提示 ~Shader(void) 析构函数没有定义,给一个空的函数体在 Shader.cpp 即可
b站视频下的笔记
p11如果报错:无法解析的外部符号__imp_xxxxx 可以尝试在属性->C/C+±>代码生成->运行库中。将其从多线程调试(/MTd)切换到多线程DLL(/MD)或多线程调试DLL(/MDd)就可以了。
p13在视频中没有提到的一个变化是使m_Handled布尔在事件类公共,并将其名称更改为Handled
p15 如果拉取imgui模块生成不成功,可看这篇文章:Hazel引擎自我学习历程-imgui层错误解决_imgui 报错c2065 “dc”:-CSDN博客
p15 对于最新的 ImGui 版本,Cherno 复制的文件位于 imgui 的backends文件夹中,另外,要将 glad 与 imgui 一起使用,会有一个 #include“imgui_impl_opengl3_loader.h”,你需要将整个块,包括 #define 和 #endif 更改为:#define IMGUI_IMPL_OPENGL_LOADER_CUSTOM #include <glad/glad.h> #endif不需要 imgui_impl_opengl3_loader.h 文件,因为我们已经在使用 glad。
p16 (我使用的imgui 1.88版本)如果点击窗口按钮后异常则尝试在imgui.cpp第3839行将for循环那行替换为 :for (int i = 0; window != NULL && i < window->DC.Layouts.Data.Size; i++)
p22 1:新版的imgui_impl_opengl3.cpp与imgui_impl_glfw.cpp在backends/文件夹下
2:新版需要替换IMGUI_IMPL_OPENGL_LOADER_GLAD 为IMGUI_IMPL_OPENGL_LOADER_CUSTOM,并且加上#include “glad/glad.h”,再删除掉Platform/OpenGL/下的两个自己定义的文件
p76 新版entity已经不支持has<>函数,需要替换为all_of<>或any_of<>
p80 如果屏幕中什么图形都不渲染,需将SceneCamera.h类的m_AspectRatio改为1.0f
p80 如果拖动视口但屏幕中的图像仍会发生形变,则需将EditorLayer.cpp的OnImGuiRender函数中的
ImVec2 viewportPanelSize = ImGui::GetContentRegionAvail();
if (m_ViewportSize != ((glm::vec2)&viewportPanelSize) && viewportPanelSize.x > 0 && viewportPanelSize.y > 0)
{
m_Framebuffer->Resize( (uint32_t)viewportPanelSize.x, (uint32_t)viewportPanelSize.y );
m_ViewportSize = { viewportPanelSize.x, viewportPanelSize.y };
m_CameraController.OnResize( viewportPanelSize.x, viewportPanelSize.y );
}
替换为
无法解析的外部符号 __imp_无法解析的外部符号__imp_下无法解ImVec2 viewportPanelSize = ImGui::GetContentRegionAvail();
if (m_ViewportSize != *((glm::vec2*)&viewportPanelSize) && viewportPanelSize.x > 0 && viewportPanelSize.y > 0)
{
m_Framebuffer->Resize( (uint32_t)viewportPanelSize.x, (uint32_t)viewportPanelSize.y );
m_ViewportSize = { viewportPanelSize.x, viewportPanelSize.y };
m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
m_CameraController.OnResize( viewportPanelSize.x, viewportPanelSize.y );
}
p81 : 如果报未定义类型“Hazel::ScriptableEntity”的错误,则需在Scene.cpp文件夹包含头文件#include “ScriptableEntity.h”
p83: .each函数若无法使用,则替换为.viewentt::entity().each
p88 : 新版替换为ImGui::BeginPopupContextWindow(0, 1 | ImGuiPopupFlags_NoOpenOverItems)
p90 : 1:新版m_Scene->m_Registry.each([&](auto entityID) 替换为 m_Scene->m_Registry.viewentt::entity().each([&](auto entityID)
2:新版需要在#include <yaml-cpp/yaml.h>上面添加#define YAML_CPP_STATIC_DEFINE
并且将yaml-cpp的premakefile文件中的所有staticruntime都切换为on,再运行脚本就好了
析的外部符号 __imp_无法解析的外部符号 __imp_无法解析的外部符号 _imp
参考链接
- 中:拆分成好几篇
https://blog.csdn.net/qq_34060370/category_12203331_3.html
- 易:一篇完整文章
https://blog.csdn.net/hijackedbycsdn/article/details/131762317#:~:text=%E6%88%91%E6%89%93%E5%BC%80%20Sandbo
- 难:(易会引用这篇)一篇文章有多个章节
https://blog.csdn.net/alexhu2010q/article/details/107688039
https://blog.csdn.net/alexhu2010q/article/details/111030313
https://blog.csdn.net/alexhu2010q/category_10165311.html?spm=1001.2014.3001.5482
- Hazel 早期 commit 的链接,可以直接体跳转,不然就要一页一页翻,300的提交))
https://github.com/TheCherno/Hazel/commits/master/?author=TheCherno&after=1feb70572fa87fa1c4ba784a2cfeada5b4a500db+139