Hazel引擎學習(十二)

我自己維護引擎的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_ptr<Scene>& 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.map,Level就是實例化的Scene;而Unity早期用Application代表Scene Manager(后面改到了Scene Manager里),場景文件叫xxxx.unity,Scene就是實例化的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)TArray<TObjectPtr<class ULevel>>						Levels;/** Level collection. ULevels are referenced by FName (Package name) to avoid serialized references. Also contains offsets in world units */UPROPERTY(Transient)TArray<TObjectPtr<ULevelStreaming>> StreamingLevels;...
}// ULevel里存了Actor數組
UCLASS(MinimalAPI)
class ULevel : public UObject, public IInterface_AssetUserData, public ITextureStreamingContainer
{...// Level里的Actor和待GC的Actor數組TArray<AActor*> Actors;TArray<AActor*> ActorsForGC;// 唯一的LevelScriptActor指針UPROPERTY(NonTransactional)TObjectPtr<class 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組件,目前只支持Box
  • Scene類里添加一個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 <iostream>static std::random_device s_RandomDevice;
static std::mt19937_64 s_Engine(s_RandomDevice());
static std::uniform_int_distribution<uint64_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的指針代表CurrentActiveScene,PlayMode下指向Runtime Scene,Editor下指向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_Registry
  • uint32_t m_ViewportWidth = 0, m_ViewportHeight = 0;
  • b2World* m_PhysicsWorld = nullptr;

要從Editor Scene復制出一個新的Runtime下隨便折騰的Scene,還不能影響Editor Scene的內容,相對于要對原本的Scene做一個Deep Copy的操作。所以肯定是不能直接Copy registryb2World對象的,新的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:template<class T, class... Args>T& AddComponent(Args&& ...args){auto& com = m_SceneRegistry.emplace<T>(m_InstanceId, std::forward<Args>(args)...);com.InstanceId = (uint32_t)m_InstanceId;return com;}private:entt::entity m_InstanceId;// entt::entity就是std::uint32_tentt::registry m_SceneRegistry;std::shared_ptr<UUID> m_ID;
}

有了這個就好說了,CopyComponent的時候,任務交給Component類的復制構造函數即可,模板函數如下:

// 用于把src里entity的某種Component復制到dst的entity上
template<typename Component>
static void CopyComponent(entt::registry& dst, entt::registry& src)
{// 獲取所有帶有Component的entity數組auto view = src.view<Component>();for (auto e : view){// 保證Component對應的Entity()的ID與原來的相同即可auto& component = src.get<Component>(e);dst.emplace_or_replace<Component>(component.InstanceId, component);}
}

Copy Scene

代碼如下:

// 大體是new一個scene, 基于原本的UUID, new出每個Entity, 再逐一為Component調用Copy操作
// 為復制得到的Entity添加Component
std::shard_ptr<Scene> Scene::Copy(std::shard_ptr<Scene> other)
{std::shard_ptr<Scene> newScene = CreateRef<Scene>();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_map<UUID, entt::entity> enttMap;// Create entities in new sceneauto idView = srcSceneRegistry.view<IDComponent>();for (auto e : idView){UUID uuid = srcSceneRegistry.get<IDComponent>(e).ID;const auto& name = srcSceneRegistry.get<TagComponent>(e).Tag;Entity newEntity = newScene->CreateEntityWithUUID(uuid, name);enttMap[uuid] = (entt::entity)newEntity;}// Copy components (except IDComponent and TagComponent)CopyComponent<TransformComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponent<SpriteRendererComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponent<CameraComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponent<NativeScriptComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponent<Rigidbody2DComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);CopyComponent<BoxCollider2DComponent>(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.view<Component>();
for (auto& entity : view)
{Component* com ref = view.get<Component>(entity);com->Awake();
}

實際代碼里,發現返回的view數組為空,這意味著entt的view函數只能返回派生類類型的對象,這么寫就不會有問題:

auto& view = m_Registry.view<CameraComponent>();

看了下相關論壇的解釋,發現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幫助快速看效果,如下圖所示,用左下角的(0,0)點為圓心,繪制的圓,裁剪后就剩四分之一部分:
在這里插入圖片描述
由于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_ptr<classA> 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_ptr<classA> my_ptr(new classA);

raw pointer轉換成unique_ptr的情況,也是類似的:

classA* raw_ptr = new classA;
std::unique_ptr<classA> 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 Object
  • OpenGL里同一個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 won't 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_FILTERGL_TEXTURE_MAG_FILTER就不太理解了,看了下文檔,這兩行屬于Texture Filtering,處理的是當輸入貼圖大小與輸出的貼圖大小不匹配時的選項,這里分為好幾種情況:

  • 輸入貼圖的長和寬均大于輸出貼圖的長和寬
  • 輸入貼圖的長和寬均小于輸出貼圖的長和寬
  • 輸入貼圖和輸出貼圖各有一個尺寸更長(不太清楚這種情況下的計算)

這里的邏輯是,這里會根據屏幕上要繪制的像素點的中心坐標,得到相對于屏幕的XY值(在[0, 1]區間),即為采樣貼圖的UV坐標,由于貼圖上也是一個個的Texel,所以采樣的時候可以直接選擇Texel點(GL_LINEAR),或者根據周圍的四個Texel點進行雙線性插值一下(GL_NEAREST),如下圖所示:
在這里插入圖片描述
至于這里的GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER,其實是把這種GL_LINEARGL_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錯

glDrawElementsBaseVertexglDrawElements差不多,可以先回顧下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?view=vs-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 VS2022,cd到對應目錄,執行以下命令輸出到AAA.txt文件即可

C:\dev\scaler\center\agent\thirdparty\libcurl\win\lib>dumpbin /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 ignored
  • warning 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.dllWinmm.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

用自己的筆記本電腦打開項目的時候報了這個錯:

1>C:\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用的SDK,220開頭的則是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++版本是C++14,但是EnTT最低需要使用C++17的版本,為了在項目里用它,需要手動升級項目里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加進來即可

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/211113.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/211113.shtml
英文地址,請注明出處:http://en.pswp.cn/news/211113.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

工業4.0分類:數字化轉型的多維度

引言 工業4.0代表著制造業的數字化革命&#xff0c;它將制造過程帶入了數字時代。然而&#xff0c;工業4.0并不是一個單一的概念&#xff0c;而是一個多維度的范疇&#xff0c;包括不同的技術、應用領域、企業規模和實施方式。但在這一多維度的概念中&#xff0c;低代碼技術正…

如何優雅地使用Mybatis逆向工程生成類

文/朱季謙 1.環境&#xff1a;SpringBoot 2.在pom.xml文件里引入相關依賴&#xff1a; 1 <plugin>2 <groupId>org.mybatis.generator</groupId>3 <artifactId>mybatis-generator-maven-plugin</artifactId>4 <version>1.3.6<…

《三十》模塊化打包構建工具 Rollup

19的2小時06分鐘 Rollup 是一個 JavaScript 的模塊化打包工具&#xff0c;可以幫助編譯微小的代碼到龐大的復雜的代碼中&#xff08;例如一個庫或者一個應用程序&#xff09;。 Rollup 和 Webpack 的區別&#xff1a; Rollup 也是一個模塊化的打包工具&#xff0c;但是它主要…

排序:非遞歸的快排

目錄 非遞歸的快排&#xff1a; 代碼分析&#xff1a; 代碼演示&#xff1a; 非遞歸的快排&#xff1a; 眾所周知&#xff0c;遞歸變成非遞歸&#xff0c;而如果還想具有遞歸的功能&#xff0c;那么遞歸的那部分則需要變成循環來實現。 而再我們的排序中&#xff0c;我們可…

深入理解asyncio:異步編程的基礎用法

引言&#xff1a; 隨著計算機硬件的不斷發展&#xff0c;對于異步編程的需求也越來越強烈。Python中的asyncio模塊為開發者提供了一種強大而靈活的異步編程方式。本文將介紹asyncio的基礎用法&#xff0c;包括async/await/run語句的使用、多個協程的并發執行、以及在協程中進行…

Let和Var的區別

一&#xff1a;區別 Let不能重復聲明&#xff0c;且必須先聲明再調用&#xff1b; 但也可以只聲明不賦值&#xff0c;默認賦值undefined&#xff1b; 二&#xff1a;實例 let x 10; let x 20; // 這里將會報錯&#xff0c;因為 x 已經被聲明過了 console.log(y); let b …

Azure Machine Learning - 使用 Azure OpenAI 服務生成圖像

在瀏覽器/Python中使用 Azure OpenAI 生成圖像&#xff0c;圖像生成 API 根據文本提示創建圖像。 關注TechLead&#xff0c;分享AI全維度知識。作者擁有10年互聯網服務架構、AI產品研發經驗、團隊管理經驗&#xff0c;同濟本復旦碩&#xff0c;復旦機器人智能實驗室成員&#x…

【動態規劃】【廣度優先】LeetCode2258:逃離火災

作者推薦 本文涉及的基礎知識點 二分查找算法合集 動態規劃 二分查找 題目 給你一個下標從 0 開始大小為 m x n 的二維整數數組 grid &#xff0c;它表示一個網格圖。每個格子為下面 3 個值之一&#xff1a; 0 表示草地。 1 表示著火的格子。 2 表示一座墻&#xff0c;你跟…

pytorch:YOLOV1的pytorch實現

pytorch&#xff1a;YOLOV1的pytorch實現 注&#xff1a;本篇僅為學習記錄、學習筆記&#xff0c;請謹慎參考&#xff0c;如果有錯誤請評論指出。 參考&#xff1a; 動手學習深度學習pytorch版——從零開始實現YOLOv1 目標檢測模型YOLO-V1損失函數詳解 3.1 YOLO系列理論合集(Y…

Redis對象類型檢測與命令多態

一. 命令類型 Redis中操作鍵的命令可以分為兩類。 一種命令可以對任意類型的鍵執行&#xff0c;比如說DEL&#xff0c;EXPIRE&#xff0c;RENAME&#xff0c;TYPE&#xff0c;OBJECT命令等。 舉個例子&#xff1a; #字符串鍵 127.0.0.1:6379> set msg "hello world&…

第76講:MySQL數據庫中常用的命令行工具的基本使用

文章目錄 1.mysql客戶端命令工具2.mysqladmin管理數據庫的客戶端工具3.mysqlbinlog查看數據庫中的二進制日志4.mysqlshow統計數據庫中的信息5.mysqldump數據庫備份工具6.mysqllimport還原備份的數據7.source命令還原SQL類型的備份文件 MySQL數據庫提供了很多的命令行工具&#…

python 畫條形圖(柱狀圖)

目錄 前言 基礎介紹 月度開支的條形圖 前言 條形圖&#xff08;bar chart&#xff09;&#xff0c;也稱為柱狀圖&#xff0c;是一種以長方形的長度為變量的統計圖表&#xff0c;長方形的長度與它所對應的變量數值呈一定比例。 當使用 Python 畫條形圖時&#xff0c;通常會使…

python代碼:如何控制一個exe程序只能執行一次

import ctypes import sys def is_program_running(): # 創建互斥體 mutex_name "Global\\MonitorClientMutex" h_mutex ctypes.windll.kernel32.CreateMutexW(None, False, mutex_name) # 檢查互斥體是否已經存在 if ctypes.windll.kernel32.Get…

Centos7.9安裝谷歌【解決依賴問題】

安裝過程 mkdir /home/app cd /home/app wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpmyum install -y redhat-lsb-core-4.0-7.el6.centos.x86_64 yum install -y libX11-devel --nogpg yum install -y cmake gcc gcc-c gtk-devel gim…

vscode 編譯運行c++ 記錄

一、打開文件夾&#xff0c;新建或打開一個cpp文件 二、ctrl shift p 進入 c/c配置 進行 IntelliSense 配置。主要是選擇編譯器、 c標準&#xff0c; 設置頭文件路徑等&#xff0c;配置好后會生成 c_cpp_properties.json&#xff1b; 二、編譯運行&#xff1a; 1、選中ma…

zabbix 通過 odbc 監控 mssql

1、環境 操作系統&#xff1a;龍蜥os 8.0 zabbix&#xff1a;6.0 mssql&#xff1a;2012 2、安裝odbc 注意&#xff1a;需要在zabbix server 或者 zabbix proxy 安裝 odbc驅動程序 dnf -y install unixODBC unixODBC-devel3、安裝mssql驅動程序 注意&#xff1a;我最開始嘗試…

Tomcat管理功能使用

前言 Tomcat管理功能用于對Tomcat自身以及部署在Tomcat上的應用進行管理的web應用。在默認情況下是處于禁用狀態的。如果需要開啟這個功能&#xff0c;需要配置管理用戶&#xff0c;即配置tomcat-users.xml文件。 &#xff01;&#xff01;&#xff01;注意&#xff1a;測試功…

react 學習筆記 李立超老師 | (學習中~)

文章目錄 react學習筆記01入門概述React 基礎案例HelloWorld三個API介紹 JSXJSX 解構數組 創建react項目(手動)創建React項目(自動) | create-react-app事件處理React中的CSS樣式內聯樣式 | 內聯樣式中使用state (不建議使用)外部樣式表 | CSS Module React組件函數式組件和類組…

【數據結構和算法】反轉字符串中的單詞

其他系列文章導航 Java基礎合集數據結構與算法合集 設計模式合集 多線程合集 分布式合集 ES合集 文章目錄 其他系列文章導航 文章目錄 前言 一、題目描述 二、題解 2.1 方法一&#xff1a;雙指針 2.2 方法二&#xff1a;分割 倒序 三、代碼 3.1 方法一&#xff1a;雙…

不同品牌的手機如何投屏到蘋果MacBook?例如小米、華為怎樣投屏比較好?

習慣使用apple全家桶的人當然知道蘋果手機或iPad可以直接用airplay投屏到MacBook。 但工作和生活的多個場合里&#xff0c;并不是所有人都喜歡用同一品牌的設備&#xff0c;如果同事或同學其他品牌的手機需要投屏到MacBook&#xff0c;有什么方法可以快捷實現&#xff1f; 首先…