做电商到底如何赚钱,极限优化wordpress,sem和seo有什么区别,成都公司注册地址托管我自己维护引擎的github地址在这里#xff0c;里面加了不少注释#xff0c;有需要的可以看看
参考视频链接在这里 Scene类重构
参考#xff1a;《InsideUE4》GamePlay架构#xff08;二#xff09;Level和World
目前我的Scene类基本只是给entt的封装#xff0c;提供了…我自己维护引擎的github地址在这里里面加了不少注释有需要的可以看看
参考视频链接在这里 Scene类重构
参考《InsideUE4》GamePlay架构二Level和World
目前我的Scene类基本只是给entt的封装提供了一些GameObject和Component的相关函数其Update函数没有被调用相关API也很缺乏大概是这样
namespace Hazel
{class GameObject;class Scene{public:Scene();~Scene();void Update();void OnViewportResized(uint32_t width, uint32_t height);GameObject CreateGameObjectInScene(const std::shared_ptrScene ps, const std::string name Default Name);...}
}现在的窗口其实只有Viewport没有GameView窗口。为了完善Scene的相关功能可以先来看看别的游戏引擎里的设计Unity里会有SceneView和GameView两个窗口一个负责绘制场景一个负责代表GamePlay的实际窗口而UE里的Viewport和GameView则共用一个窗口按F8 eject可以切换SceneView和GameView
游戏引擎里允许同时加载多个场景(Scene)那么逻辑上讲需要三个内容
Scene Manager应该是作为全局单例存在负责加载和卸载场景场景文件作为文本文件(或可以被解析的二进制文件)存放了场景信息程序加载时的Scene每个Scene由序列化好的场景文件实例化而来
对应到各个引擎里有不同的叫法UE里把World认作Scene Manager、场景文件叫xxxx.mapLevel就是实例化的Scene而Unity早期用Application代表Scene Manager(后面改到了Scene Manager里)场景文件叫xxxx.unityScene就是实例化的Scene。
比如UE里的代码
// UWorld里存了Level数组
class ENGINE_API UWorld final : public UObject, public FNetworkNotify
{/** Array of levels currently in this world. Not serialized to disk to avoid hard references. */UPROPERTY(Transient)TArrayTObjectPtrclass ULevel Levels;/** Level collection. ULevels are referenced by FName (Package name) to avoid serialized references. Also contains offsets in world units */UPROPERTY(Transient)TArrayTObjectPtrULevelStreaming StreamingLevels;...
}// ULevel里存了Actor数组
UCLASS(MinimalAPI)
class ULevel : public UObject, public IInterface_AssetUserData, public ITextureStreamingContainer
{...// Level里的Actor和待GC的Actor数组TArrayAActor* Actors;TArrayAActor* ActorsForGC;// 唯一的LevelScriptActor指针UPROPERTY(NonTransactional)TObjectPtrclass ALevelScriptActor LevelScriptActor;
}大概逻辑是Update这些Level(或Scene)从而Tick里面的Actor和ActorComponent。所以这里把Scene类重构成如下所示
namespace Hazel
{class GameObject;class Scene{public:Scene();~Scene();void Begin();void Pause();void Stop();void Update(const float deltaTime);void Clear();void OnViewportResized(uint32_t width, uint32_t height);void ClearAllGameObjectsInScene();...}
}PS这里我并没有像Cherno一样区分了RuntimeUpdate和EditorUpdate因为感觉别的游戏引擎里都没这么区分而且Editor下应该也调用Update函数才对 2D PHYSICS!
主要是实现简单的物理引擎做了以下事情
Fork GitHub上的比较有名的2D物理引擎(erincatto/box2d)作为submodule加入到引擎里创建2D用的Rigidbody组件目前只支持BoxScene类里添加一个box2d的manager对象的指针具体的Rigidbody分为Static和Dynamic类型还有Kinematic?Render模块、物理模块与脚本模块的顺序问题 关于Box2D
参考Box2D —— A 2D physics engine for games From the game engine’s point of view, a physics engine is just a system for procedural animation. 这是个C写的库还挺牛的Unity和Cocos都用到了它为了避免与其他的库命名冲突里面的类型名都以b2作为前缀这里举个简单的Demo代码
// 1. 创建世界, b2World is the physics hub that manages memory, objects, and simulation.
b2Vec2 gravity(0.0f, -10.0f);
b2World world(gravity);// 创建世界时需要设置重力加速度// 2. 创建一个长方形代表地面, 默认为static类型
b2BodyDef groundBodyDef;// 先创建引用
groundBodyDef.position.Set(0.0f, -10.0f);
b2Body* groundBody world.CreateBody(groundBodyDef);// 再创建实体// 给地面添加Box Collider, 这里的0.0f指的是?
b2PolygonShape groundBox;
groundBox.SetAsBox(50.0f, 10.0f);
groundBody-CreateFixture(groundBox, 0.0f);// 3. 创建一个dynamic类型的rigidbody
b2BodyDef bodyDef;
bodyDef.type b2_dynamicBody;
bodyDef.position.Set(0.0f, 4.0f);
b2Body* body world.CreateBody(bodyDef);// 添加Box Collider, 添加一些物理参数
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(1.0f, 1.0f);b2FixtureDef fixtureDef;
fixtureDef.shape dynamicBox;
fixtureDef.density 1.0f;
fixtureDef.friction 0.3f;
body-CreateFixture(fixtureDef);// 5. 开启世界的更新, 根据前面的设置, 世界开始后动态box会坠落到静态的地面上, 不断bounce
for (int32 i 0; i 60; i)
{// 调用物理世界的更新world.Step(timeStep, velocityIterations, positionIterations);// 打印更新后的Transformb2Vec2 position body-GetPosition();float angle body-GetAngle();printf(%4.2f %4.2f %4.2f\n, position.x, position.y, angle);
}// 6. Clean up
// world离开作用域时, 会自动释放相关的所有memory顺便提一下这里面有个概念叫fixture具体有shape、restitution、friction和density四个主要属性并不是固定装置的意思而是类似于Unity里的Collider的概念。另外注意一点这里需要先填写好BodyRef再调用CreateBody代码如下所示
// 注意, 调用CreateBody之前要把b2BodyDef里的参数都填好// 正确写法
m_BodyDef.position.Set(x, y);
m_BodyDef.type Rigidbody2DTypeToB2BodyType(type);
m_Body world-CreateBody(m_BodyDef);// 错误写法
m_BodyDef.position.Set(x, y);
m_Body world-CreateBody(m_BodyDef);
m_BodyDef.type Rigidbody2DTypeToB2BodyType(type);这里做的事情主要是封装比较简单
创建Rigidbody2DComponent类对应类对象的UI绘制允许在Inspector上Add此Component可以编辑相关属性Rigidbody2DComponent类相关属性与场景对应的YAML文件之间的序列化与反序列化Scene里利用b2World更新物理场景更新完后更新带Rigidbody2DComponent的GameObject的Transform(感觉这个过程应该在渲染Update之前) Universally Unique Identifiers (UUID/GUID)
思考一个问题在进入PlayMode再退出之后怎么回到原本的场景
最暴力的做法重新load一下场景文件
资产的GUID不能用path因为它们的路径是会改变的而且存字符串性能也不好也不适合用那种从0开始的每创建一次就1的global id这是因为不同的人可能同时在创建新文件这样做在资产合并之后会有GUID冲突所以这里决定用random hash就跟git commit hash一样杜绝重名的GUID
比如git commit hash为2b621d3e7eee447057bb974fab80aad4193e5389这里面有40个16进制的数字每个数字有4个bit代表半个字节一共就是20个字节的hash值git还有个short hash也就是这里的前7位2b621d3一般16^7这么大范围的数字作为hash就足够了这里的引擎没有那么大的体量选择使用8个字节来存hash也就是2 ^ 64 16 ^ 16对应类型为uint64_t
具体步骤为
随机数生成64位的hash即HashFunction虽然各个Platform有提供自己的算法这里还是统一使用std的random库创建UUID类static涉及到thread safe但GUID的创建应该只会在main thread里进行设置static set避免冲突把UUID实装到GameObject(视频里叫Entity)上 std提供的随机数生成算法
只是记录下写法
#include random
#include iostreamstatic std::random_device s_RandomDevice;
static std::mt19937_64 s_Engine(s_RandomDevice());
static std::uniform_int_distributionuint64_t s_UniformDistribution;int main()
{for (size_t i 0; i 10; i){uint64_t rab s_UniformDistribution(s_Engine);std::cout rab std::endl;}std::cin.get();
}print为
16594018988857477878
3025682643124843386
10762836072128041903
...Playing and Stopping Scenes (and Resetting)
为了在进入和退出Play Mode后还原场景本身这里我认为有两种比较实用的做法
区分Editor Scene和Runtime Scene(准确的说是PlayMode Scene)Editor下编辑的是Editor Scene在点击Play进入Play Mode时拷贝一份Editor Scene作为Runtime Scene。会有一个Scene的指针代表CurrentActiveScenePlayMode下指向Runtime SceneEditor下指向Editor Scene在Editor下编辑Scene时创建一个Cache作为保存Scene的文本文件。当点击Play后开始执行Runtime的逻辑Stop Scene回到Editor状态下后再Load一次Cache的Scene文件即可当Save Scene时该Cache文件可以直接覆盖原本的Scene文件
第二种方法是我自己想的感觉写起来比较方便但是涉及到了频繁的IO如果场景变复杂了可能会很卡所以还是选择第一种做法也是Cherno的做法感觉挺复杂的提供了Scene、Component和GameObject的Copy操作然后在点击Play时复制出Runtime用的Scene
注意在复制Scene时只复制Scene的初始状态(即需要被序列化的部分)因为实际游戏里的Scene可能会非常复杂里面可能会有很多脚本Spawn出来的对象所以只记录初始状态让其自己去Tick是比较合理的 那么怎么复制一个场景呢这里的需求应该是
复制的Runtime Scene的数据与Editor Scene完全相同GameObject的GUID也完全相同Runtime Scene下不管怎么鼓捣都不会影响Editor下的对象这意味着两个类的数据成员都是独有的不共享
目前Scene的数据成员有
entt::registry m_Registryuint32_t m_ViewportWidth 0, m_ViewportHeight 0;b2World* m_PhysicsWorld nullptr;
要从Editor Scene复制出一个新的Runtime下随便折腾的Scene还不能影响Editor Scene的内容相对于要对原本的Scene做一个Deep Copy的操作。所以肯定是不能直接Copy registry和b2World对象的新的registry会影响Editor Scene所以这里的Copy Scene的做法跟反序列化一个场景很像无非反序列化是从文本里读取信息而这里是从原场景里读取信息。
这样一看思路就比较清晰了
设计CopyComponent函数设计CopyGameObject函数里面会调用各个Component的CopyComponent函数设计CopyScene函数里面调用CopyGameObject函数 Copy Component
很久没写这块的代码了先来回顾下Component相关代码
// 目前的Component基类基本就是简单成了这个样子
class Component
{// 记录的是此Component对应的Instance在Scene(register)里的Id, 是ECS系统里的唯一标识// 派生类复制的时候应该走的是CopyCtoruint32_t InstanceId 0;
};class GameObject
{
public:templateclass T, class... ArgsT AddComponent(Args ...args){auto com m_SceneRegistry.emplaceT(m_InstanceId, std::forwardArgs(args)...);com.InstanceId (uint32_t)m_InstanceId;return com;}private:entt::entity m_InstanceId;// entt::entity就是std::uint32_tentt::registry m_SceneRegistry;std::shared_ptrUUID m_ID;
}有了这个就好说了CopyComponent的时候任务交给Component类的复制构造函数即可模板函数如下
// 用于把src里entity的某种Component复制到dst的entity上
templatetypename Component
static void CopyComponent(entt::registry dst, entt::registry src)
{// 获取所有带有Component的entity数组auto view src.viewComponent();for (auto e : view){// 保证Component对应的Entity()的ID与原来的相同即可auto component src.getComponent(e);dst.emplace_or_replaceComponent(component.InstanceId, component);}
}Copy Scene
代码如下
// 大体是new一个scene, 基于原本的UUID, new出每个Entity, 再逐一为Component调用Copy操作
// 为复制得到的Entity添加Component
std::shard_ptrScene Scene::Copy(std::shard_ptrScene other)
{std::shard_ptrScene newScene CreateRefScene();newScene-m_ViewportWidth other-m_ViewportWidth;newScene-m_ViewportHeight other-m_ViewportHeight;auto srcSceneRegistry other-m_Registry;auto dstSceneRegistry newScene-m_Registry;// 创建一个临时map, 存储新创建的带UUID的Entitystd::unordered_mapUUID, entt::entity enttMap;// Create entities in new sceneauto idView srcSceneRegistry.viewIDComponent();for (auto e : idView){UUID uuid srcSceneRegistry.getIDComponent(e).ID;const auto name srcSceneRegistry.getTagComponent(e).Tag;Entity newEntity newScene-CreateEntityWithUUID(uuid, name);enttMap[uuid] (entt::entity)newEntity;}// Copy components (except IDComponent and TagComponent)CopyComponentTransformComponent(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponentSpriteRendererComponent(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponentCameraComponent(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponentNativeScriptComponent(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponentRigidbody2DComponent(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponentBoxCollider2DComponent(dstSceneRegistry, srcSceneRegistry, enttMap);return newScene;
}去掉Component之间的继承关系
参考Iterating through components with common base class or via ducktype
在Copy Scene时遇到了一个操作我想给所有的Component添加Awake函数(类似于Unity里的Awake和UE里的BeginPlay函数)这个函数只在Play Mode下被调用。目前我的类继承逻辑是这样的
class Component
{
public:Component() default;virtual ~Component() default;virtual void Awake() {}uint32_t InstanceId 0;
};class CameraComponent : public Component...
class Rigidbody2D : public Component...
class Transform : public Component...现在我需要在进入PlayMode时调用所有Component的Awake函数我是这么写的
auto view m_Registry.viewComponent();
for (auto entity : view)
{Component* com ref view.getComponent(entity);com-Awake();
}实际代码里发现返回的view数组为空这意味着entt的view函数只能返回派生类类型的对象这么写就不会有问题
auto view m_Registry.viewCameraComponent();看了下相关论坛的解释发现entt是不希望Component之间存在继承关系的因为这样会有悖ECS的设计理念它对于Component有两个要求
每个带有数据的Component都应该有单独的final type每个Component里的数据应该存的是value而不是指针或引用
这样是为了Cache Friendly每个相同的Component类的Data都放到同一个内存池了当遍历场景里的同一种Component时会只在同一片内存区域上操作Component不存指针和引用也是为了避免内存访问的跳转同时这里的虚函数调用也会给CPU造成额外的性能消耗(Another thing your CPU really likes, is predictable code to execute. It tries to predict and prepare upcoming code. It also has some local space for instructions as well)所以理想的entt代码应该是这样的
Struct TextDrawable {
sf::Text text;
};struct SpriteDrawable {
sf::Sprite sprite;
};//and you simple iterate them separately:
registry.view().each([renderTarget](const TextDrawable rDrawable){
renderTarget.draw(rDrawable);
});registry.view().each([renderTarget](const SpriteDrawable rDrawable){
renderTarget.draw(rDrawable);
});看了下Cherno写的确实没用到继承关系类声明都写到了一个Component.h里方便后续遍历
顺便看了下俩常用游戏引擎里的Component设计发现它们还是存在着继承关系的比如
// 虽然UObject里没有任何数据, 都是接口, 但这里的UActorComponent里面是有数据的
class UMovementComponent : public UActorComponent : (public UObject, public IInterface_AssetUserData)这也正说明了俩引擎是EC架构不是严格的ECS架构因为相同的Component的Data不是完全存放到一起去的
暂时还没想好要不要这么改后面再说吧 Using C# Scripting with the Entity Component System
前两课基本都是搭建基础建设使得在引擎脚本层的C#和引擎本身的C之间的api可以相互调用这节课主要是为了在引擎里的应用比如这两个操作
C暴露创建GameObject的接口用户可以从C#里调用它来创建GameObject添加ScriptComponent在C#里使用WASD键位来实现场景里Quad的移动
我的理解是要做的方法跟Unity是类似的。添加按钮点击的时候出现一个TextBox然后输入会创建对应的类在项目里然后C里会定义去查询C#里的特殊名字的函数为其调用mono的internal call函数应该会有一个集合把这些函数都存起来再在C引擎特定loop的地方调用这些函数 Finishing The Pull Request
enum class enum class有个不太好的地方它不可以直接参与位运算需要重载它的相关运算符而enum可以其实可以用下面这种写法用enum模拟enum class Hazel未来想要完全从C#里调用C的代码而不是像Unity这样要一个个接口暴露出来这么麻烦。因为Unity为了闭源是把C代码提供成dll的而Hazel的C#是直接有C的源码工程的 往Transform组件里加Cache数据并不好一方面是加内存占用更重要的是一堆Transform数组会在Cache里造成Cache miss因为带来了很多无效数据 Rendering Circles in a Game Engine
有几种取巧的办法
绘制正多边形近似圆使用圆形贴图模拟这种情况下其实绘制的是quad
一般游戏里如果是Debug用的东西比如Gizmos那么可以用多边形模拟圆但是Runtime用的东西比如角色吃的甜甜圈还是比较适合直接绘制出真正的圆
这里主要讲的是渲染Circles其实不需要具体的geometry可以通过Shader算法来实现其实挺简单的就是利用UV坐标做文章绘制一个圆时需要知道它的圆心对应的UV坐标然后再给定一个半径对应的UV坐标长度R即可那么每次绘制像素时如果其UV坐标到中心UV坐标的长度小于R即绘制点即可
这里可以使用Shader Toy帮助快速看效果如下图所示用左下角的00点为圆心绘制的圆裁剪后就剩四分之一部分 由于UV坐标都在[0,1]区间但是这里的屏幕长宽比不同所以要根据ratio调整一下UV坐标这里把U坐标按比例增大再改一下圆心坐标为屏幕中心的(0.5, 0.5)即可 附录
raw pointer转换成smart pointers的问题
参考Creating shared_ptr from raw pointer
首先回顾一下相关语法对于raw pointer转换成shared_ptr时写法为
classA* raw_ptr new classA;// 一定要记住, 后续不能再使用raw_ptr// 如果是声明加定义shared_ptr
shared_ptrclassA my_ptr(raw_ptr);// 这个过程会析构classA么// 如果只是给shared_ptr赋值
my_ptr.reset(raw_ptr);// 这个过程会析构classA么顺便说一句这里的三行代码只有第三行会调用classA的析构函数因为reset函数会把原本的对象析构再重新赋值不过此时的raw_ptr已经被析构了再去给my_ptr赋值的话此时的my_ptr里面会存一个野指针总之raw pointer转换成shared_ptr不涉及对象的析构过程但是shared_ptr.reset赋值方法会调用原本存储对象的析构函数
上面的写法其实并不好这样写更好
// 避免创建一个指针变量, 让其他代码使用, 这样从根本上避免了raw pointer的问题
shared_ptrclassA my_ptr(new classA);raw pointer转换成unique_ptr的情况也是类似的
classA* raw_ptr new classA;
std::unique_ptrclassA my_ptr(raw_ptr);classA* raw_ptr2 new classA;
my_ptr.reset(raw_ptr2);至于weak_ptr它不能直接通过raw pointer转过来因为weak_ptr必须基于shared_ptr或其他的weak_ptr参考Creating weak_ptr from raw pointer Intrusive Reference Counting
参考invasive vs non-invasive ref-counted pointers in C Intrusive reference counters are atomic integers embedded inside data object which tell how many times in the program the object is being used. As soon as the reference counter reaches value 0 , the object is deleted When counter is stored inside body class, it is called intrusive reference counting and when the counter is stored external to the body class it is known as non-intrusive reference counting. 引用计数写在对象类内的叫做intrusive(或Invasive) reference counting否则为non intrusive reference counting 关于MSAA(抗锯齿)
跟Cherno上的课没有太大关系纯粹是因为我看到屏幕里绘制的图形存在锯齿看上去很不舒服所以决定开的抗锯齿。
这个过程比较复杂踩了很多坑就不多说了记录下几个重点
ImGui::Image需要输出Textuer2D类型的id而不支持Texture2DMultiSample应该单独写一个framebuffer专门用于Resolve MSAA的framebuffer输出的MSAA的Texture Attachment和Render ObjectOpenGL里同一个framebuffer不可以输出不同类型的Texture Attachment或Render Object要么都是MSAA要么都不是MSAA学习使用Render Doc可以很好的排查这些问题
最后的效果差异如下图所示 解决Z Depth的Bug
参考Depth testing 参考Framebuffers 参考Advanced GLSL
目前遇到了一个以为比较棘手的Bug渲染的深度测试功能完全失效了仔细研究了下发现它是按照物体的渲染顺序来的即第一个出现在Hierarchy里的永远最后被画永远不会被其他物体所遮挡。
为了确认我到底是哪里出了问题我决定先把Depth数据绘制出来把Shader改成了如下所示
void main()
{// 原本的不管out_color texture(u_Texture[v_TexIndex], v_TexCoord * v_TilingFactor) * v_Color;out_InstanceId v_InstanceId;out_color vec4(vec3(gl_FragCoord.z,gl_FragCoord.z,gl_FragCoord.z), 1.0);
}这里的gl_FragCoord是OPENGL在fs里提供的内置变量其xy值代表片元的屏幕坐标z值代表绘制的primitive对应片元的深度值(注意并不是Depth buffer里对应位置的深度值)其值所在的区间为[0, Infinity)由于绘制的时候z值超过1的都是白色了所以我拉的比较近如下图所示 可以看到较近的颜色较暗然而较远的颜色是白的这说明我的Depth值应该是没问题的
仔细查了下发现是framebuffer没有添加depth attachment的缘故因为我是把场景用fbo渲染出一张贴图的对应的depth attachment也应该加上MSAA和正常的fbo绘制写法稍有不同如下所示
// 正常FBO的写法
// create a renderbuffer object for depth and stencil attachment (we wont be sampling these)
unsigned int rbo;
glGenRenderbuffers(1, rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, spec.width, spec.height); // use a single renderbuffer object for both a depth AND stencil buffer.
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); // now actually attach it
m_RboAttachmentIndices.push_back(rbo);// MSAA的FBO的写法
unsigned int rbo;
glGenRenderbuffers(1, rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, spec.width, spec.height); // use a single renderbuffer object for both a depth AND stencil buffer.
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); // now actually attach it关于ImDrawList::AddCallback() AddCallback() just registers a function and parameter that your imgui renderer loop will call. The order within a same window/drawlist are preserved, so if you add 1000 triangles and then add a callback, then 1000 triangles again, your renderer will see them in that order. AddCallback其实就是传入一个DrawCall的function指针随后ImGui会在对应的位置调用此函数 When should I set GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER?
参考https://learnopengl.com/Getting-started/Textures
这里回顾一下贴图在OpenGL里的参数设置原本绘制的Viewport贴图的代码为
{...GLuint textureId;glGenTextures(1, textureId);glBindTexture(GL_TEXTURE_2D, textureId);// R32I应该是代表32位interger, 意思是这32位都只存一个integerglTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, spec.width, spec.height, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_NEAREST);// 下面这两行代码是必须的, 否则会黑屏glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}前面两行比较简单属于Texture Wrapping是当UV坐标超过[0, 1]范围时应该如何Mapping UV坐标的问题可以用Map也可以用Clamp。但是这里的GL_TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER就不太理解了看了下文档这两行属于Texture Filtering处理的是当输入贴图大小与输出的贴图大小不匹配时的选项这里分为好几种情况
输入贴图的长和宽均大于输出贴图的长和宽输入贴图的长和宽均小于输出贴图的长和宽输入贴图和输出贴图各有一个尺寸更长(不太清楚这种情况下的计算)
这里的逻辑是这里会根据屏幕上要绘制的像素点的中心坐标得到相对于屏幕的XY值(在[0, 1]区间)即为采样贴图的UV坐标由于贴图上也是一个个的Texel所以采样的时候可以直接选择Texel点(GL_LINEAR)或者根据周围的四个Texel点进行双线性插值一下(GL_NEAREST)如下图所示 至于这里的GL_TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER其实是把这种GL_LINEAR和GL_NEAREST的使用情况更细分一下而已
GL_TEXTURE_MIN_FILTER意味着输入贴图尺寸大于输出的贴图尺寸这意味着贴图变小GL_TEXTURE_MAX_FILTER意味着输入贴图尺寸小于输出的贴图尺寸这意味着贴图变大
有比较好的思路是在贴图变大时使用线性效果而贴图变小时使用插值效果
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);回到这个问题本身为啥没有设置这两行会黑屏我猜是因为framebuffer的贴图尺寸会改变必须设置这个的原因吧查了下这两行对于任何Texture2D贴图来说应该都是必要的Texture Wraping不是必要的 For standard OpenGL textures, the filtering state is part of the texture, and must be defined when the texture is created. 这里还有个问题就是MultiSample Texture如何设置Texture Wraping和Filtering参考Creating Multisample Texture Correctly
得到的结论是由于MultiSample Texture就是带了多个Sampler的Texture比如它会对一个Texel的四个角进行采样然后融合所以它本身就是GL_NEAREST类型的参数相当于
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);由于所有的MultiSample Texture都是这么设置的所以OpenGL就直接省略让人设置的环节了如果像下面这么写
glGenTextures(1,_texture_id);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE,_texture_id);
glTexParameterf(GL_TEXTURE_2D_MULTISAMPLE,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D_MULTISAMPLE,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexImage2DMultisample(...);应该会报错 GL_INVALID_ENUM error generated. multisample texture targets doesn’t support sampler state glDrawBuffers函数复习
之前代码里调用过这个函数现在给忘了咋用了这里复习一下代码如下
OpenGLFramebuffer::OpenGLFramebuffer(const FramebufferSpecification spec) : Framebuffer(spec.width, spec.height)
{glGenFramebuffers(1, m_FramebufferId);glBindFramebuffer(GL_FRAMEBUFFER, m_FramebufferId);// 创建俩Color Attachment ...HAZEL_CORE_ASSERT((bool)(glCheckFramebufferStatus(GL_FRAMEBUFFER) GL_FRAMEBUFFER_COMPLETE), Framebuffer incomplete);// 目前每个Camera只output两张贴图, 第一张代表Viewport里的贴图, 第二张代表InstanceID贴图const GLenum buffers[]{ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };glDrawBuffers(2, buffers);
}问题是
glDrawBuffers是不是DrawCall如果是的话为啥会在fbo的ctor里调用而不是在loop里被调用glDrawBuffers的用法
参考https://www.reddit.com/r/opengl/comments/11sz3yf/does_gldrawbuffer_permanently_alter_the_bound/ 参考https://stackoverflow.com/questions/51030120/concept-what-is-the-use-of-gldrawbuffer-and-gldrawbuffers glDrawBuffers specifies a list of color buffers to be drawn into. 看了下这个函数应该不是DrawCall只是负责开启和关闭FBO上的Color Attachment而已开启后可供shader写入 glDrawElementsBaseVertex报Access violation reading location错
glDrawElementsBaseVertex与glDrawElements差不多可以先回顾下glDrawElements函数
// 函数签名
// indices: Specifies a byte offset (cast to a pointer type) into the buffer bound to GL_ELEMENT_ARRAY_BUFFER to start reading indices from.
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void * indices);glClearColor(0.1f, 0.1f, 0.1f, 1);
glClear(GL_COLOR_BUFFER_BIT);
// DrawCall调用, 在调用此函数前, 需要绑定Index Buffer
glDrawElements(GL_TRIANGLES, m_QuadVertexArray-GetIndexBuffer()-GetCount(), GL_UNSIGNED_INT, nullptr);glDrawElementsBaseVertex的签名如下相较于glDrawElements只多了一个int参数
void glDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, void *indices, GLint basevertex);这里都需要传入的指针是代表的indices数组里的指针比如说我有六个顶点和一个顶点数组
float positions[]
{-0.5f, -1.0f, 0.0f,-1.5f, 1.0f, 0.0f,-2.5f, -1.0f, 0.0f,2.5f, -1.0f, 0.0f,1.5f, 1.0f, 0.0f,0.5f, -1.0f, 0.0f
};uint8 indices[]
{0, 1, 2,3, 4, 5
};通过传入合适的指针可以只绘制部分顶点比如
// 直接绘制第二个三角形
glDrawElements(/* mode */ GL_TRIANGLES,/* count */ 3,/* type */ GL_UNSIGNED_BYTE,/* offset */ (void*)( sizeof( uint8 ) * 3 ) );PDB文件
参考https://learn.microsoft.com/en-us/visualstudio/debugger/specify-symbol-dot-pdb-and-source-files-in-the-visual-studio-debugger?viewvs-2022
PDB全程program database文件后缀为.pdb也叫symbol files它负责处理下面二者之间的映射
源代码里的identifier或statement编译后的APP里的identifiers或instruction 正是因为此特性有了PDB文件的存在才能把debugger和源码link到一起从而实现基于代码的调试 查看lib里的object文件依赖的pdb路径
参考https://stackoverflow.com/questions/25843883/how-to-remove-warning-lnk4099-pdb-lib-pdb-was-not-found
先把lib文件用7Zip解压然后找到对应的object文件然后打开VS对应的Developer Command Prompt For VS2022cd到对应目录执行以下命令输出到AAA.txt文件即可
C:\dev\scaler\center\agent\thirdparty\libcurl\win\libdumpbin /section:.debug$T /rawdata rc2_cbc.obj AAA.txt打开AAA文件即可看到里面依赖的PDB文件的路径如下图所示 解决所有的Warnings
目前分为以下几种Warning会仔细分析后面几个复杂点的Warning
类型转换提示的loss of data主要是把uint32_t转成指针类型在64位机器上会把32位的整型转成64位的整型会给警告返回对象引用的函数返回了临时变量比如函数GameObject Func(){ return GameObject(); }warning LNK4006 second definition ignoredwarning LNK4099: PDB ‘’ was not found with ‘Hazel.lib(sgen-fin-weak-hash.obj)’ or at ‘’; linking object as if no debug info warning LNK4006 关于Warning LNK4006详细信息为
Ws2_32.lib(WS2_32.dll) : warning LNK4006: __NULL_IMPORT_DESCRIPTOR already defined in opengl32.lib(OPENGL32.dll); second definition ignored
Winmm.lib(WINMM.dll) : warning LNK4006: __NULL_IMPORT_DESCRIPTOR already defined in opengl32.lib(OPENGL32.dll); second definition ignored这里的Ws2_32.lib是系统在C:\Program Files (x86)\Windows Kits下环境自带的文件基于如何判断lib文件是static lib还是import lib我用解压软件打开看了一下里面一堆dll文件说明它是import lib比如对应的ws2_32.dll会出现在C:\Windows\System32文件夹下。关于这个警告我看了下Linker warnings LNK4006 and LNK4221和Stop Warning: __NULL_IMPORT_DESCRIPTOR这两篇文章得到的结论大概是
由于我现在的HazelEditor依赖于Hazel项目前者会build出一个exe(HazelEditor.exe)后者会build出一个静态lib文件(Hazel.lib)HazelEditor.exe只依赖Hazel.lib而后者又为了支持Mono依赖了Ws2_32.dll和Winmm.dll这些系统的库。重点就在于我这个static lib的Hazel项目是不应该依赖一个dll的这样当我最终build出exe时会有两份相同的Ws2_32.dll一份来自于依赖的Hazel.lib(应该是静态lib会拷贝所有内容的缘故)另一份来自HazelEditor.exe这样就会重复了(也可能是Hazel.lib里多个静态lib都依赖于这个Ws2_32.dll的缘故)。
我把Hazel对应Project对这些系统lib的依赖挪到HazelEditor项目就可以了。 warning LNK4099 提示为
warning LNK4099: PDB was not found with Hazel.lib(sgen-fin-weak-hash.obj) or at ; linking object as if no debug info意思是找不到对应的PDB文件如下图所示 关于PDB(Program database)文件就不多说了它是在Debug Configuration下build出Binary时会附带的文件它包含了源码的变量和指令与实际执行的APP里变量和指令的mapping。这里的Hazel.lib作为静态库文件里面其实包含了一堆object文件如下图所示是我用解压文件对它进行操作时的样子 解压后能在对应的文件里看到我很多类编译出的.obj文件如下图所示 仔细看了了对应lib里报错的object文件依赖的pdb路径发现本机上确实没有原本build出来的PDB文件了(因为我改动了盘的位置)要解决这个问题应该不难做法感觉比较多
要么考虑直接更改object里依赖的PDB路径考虑更新lib库本机上是OK的把PDB文件也提交上去然后依赖时使用相对路径这样每台电脑上都可以写代码禁用此类warning毕竟也不会去debug这些Mono的库 fatal error LNK1127: library is corrupt
用自己的笔记本电脑打开项目的时候报了这个错
1C:\Program Files (x86)\Windows Kits\10\lib\10.0.22000.0\um\x64\opengl32.lib : fatal error LNK1127: library is corrupt本来是用VS2022打开的项目但是我笔记本电脑上下载错了版本导致VS2022专业版试用过期了所以我换成了VS2017重新Build出来sln后打开编译就有这个错了
看了下这个库文件属于Windows SDK里提供的库文件在我对应项目的Properties-Librarian-General的Additional Dependencies里设置了对应的依赖 看了下本机的同名文件路径发现确实有好几个版本177开头的应该是VS2017用的SDK220开头的则是VS2022用的SDK 发现右键点击对应项目点击Retarget Projects降级到VS2017对应的SDK就可以了之前使用Premake5.exe重新创建sln的过程居然没改SDK的版本需要这里手动改下 UE与EnTT的关系
参考Upcoming ECS in UE5 (Mass) 参考EnTT and Unreal Engine
其实没啥关系UE本身是EC架构的引擎跟Unity一样近些年Unity提出了ECS对应的DOTS系统旨在把游戏转成ECS架构UE5也提出了类似的理念即Mass系统这个系统目前不知道有没有完全成型但是在此之前如果想要在UE的项目里自行实现ECS架构很多人会选择使用EnTT库
这里介绍下在UE里使用EnTT的方法截至UE4.25其C版本是C14但是EnTT最低需要使用C17的版本为了在项目里用它需要手动升级项目里C的版本需要在对应项目的Build.cs里加上
using UnrealBuildTool;public class MyProject : ModuleRules
{public MyProject(ReadOnlyTargetRules Target) : base(Target){PCHUsage PCHUsageMode.NoSharedPCHs;PrivatePCHHeaderFile PCH filename.h;CppStandard CppStandardVersion.Cpp17;PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore });PrivateDependencyModuleNames.AddRange(new string[] { });}
}最后再为EnTT创建单独的第三方库Module加进来即可