前言:
今天要做個有hierarchy概念的UI。建議讀者具備基本C++與OpenGL概念。
這是上個學期的作業,從基本OpenGL渲染、UI、GPU instancing、Shadow到光追都有實作,看看我能寫多少吧XD
成品
什麼是OpenGL?
OpenGL是一個跨平台的圖形庫,由Silicon Graphics Inc.(SGI)於1992年發布。它是一個開放、靈活的圖形處理框架。現在已經發展到了OpenGL 4.x的版本。
跨平台性:OpenGL是一個跨平台的API。
硬體獨立性:它與特定的硬體無關。
開源:OpenGL是一個開源項目,這意味著任何人都可以查看其源代碼並對其進行修改和擴展,從而使得它更加適應不同的應用需求。
建立基本Window
以下是個繪製基本window的程式。
載入模型 (ASSIMP)
ASSIMP 是一個開源的3D模型資產導入庫,它能夠讀取和處理來自各種3D模型文件格式的數據,包括但不限於OBJ、FBX、STL、Collada等。它提供了一個統一的界面,讓開發人員可以輕鬆地導入不同格式的模型數據,而無需擔心文件格式的細節。
- 多格式支持:ASSIMP 支持許多主流和次流的3D模型文件格式。這使得開發人員可以使用單個庫來處理來自各種來源的模型數據,而無需依賴於多個庫或工具。
- 統一的數據結構:無論輸入的模型是來自於哪種格式,ASSIMP 都將其轉換為一個統一的數據結構,這使得開發人員可以使用相同的代碼來處理不同格式的模型數據。
- 輕鬆擴展:ASSIMP 的設計使得它易於擴展以支持新的3D模型格式。這意味著開發人員可以根據需要添加對新格式的支持,而無需修改現有的代碼。
- 高性能:ASSIMP 被設計為高效處理大型模型和場景。它使用了各種優化技術,包括多線程處理和內存管理,以確保快速而穩定的性能。
Assimp會將讀取的模型當成一個Scene,其下包含數個Mesh、Material等參數。
aiScene內容:
- mAnimations: The array of animations
- mCameras: The array of cameras
- mFlags: Any combination of the AI_SCENE_FLAGS_XXX flags
- mLights: The array of light sources
- mMaterials: The array of materials
- mMeshes: The array of meshes
- mNumAnimations: The number of animations in the scene
- mNumCameras: The number of cameras in the scene
- mNumLights: The number of light sources in the scene
- mNumMaterials: The number of materials in the scene
- mNumMeshes: The number of meshes in the scene
- mNumTextures: The number of textures embedded into the file
- mRootNode: The root node of the hierarchy
- mTextures: The array of embedded textures
Mesh程式大綱
- Mesh 類的構造函數:
- 第一個構造函數初始化了 Mesh 類的基礎成員,其中調用了 Component 類的構造函數。
- 第二個構造函數在初始化時加載模型,並在加載成功後打印加載成功的消息。
- 第三個構造函數同樣加載模型,並初始化了與模型相關的着色器(Shader)。
- Mesh 類的析構函數:
- 釋放了 materials 向量中的資源,即釋放了所有材質的內存。
- Render 函數:
- 該函數用於渲染模型,首先設置光照相關的Uniform變量,然後遍歷模型的所有入口(m_Entries),繪製每個入口所對應的網格。
- 在渲染每個網格之前,綁定相關的頂點數據和索引數據,並啟用頂點屬性。
- 然後根據每個材質設置相應的模型矩陣和 Uniform 變量,最後調用材質的渲染函數,以及使用 glDrawElements 函數進行繪製。
- Do 函數:
- copy 函數:
- 這個函數用於複製 Mesh 對象,並返回一個新的 Mesh 對象,其內容與原始對象相同。 主要用於複製參數。
- 其他輔助函數:
- init_ui_content 函數用於初始化用戶界面的內容。
- LoadModel 函數用於加載模型文件。
- InitMaterials 函數用於初始化材質。
- InitMesh 函數用於初始化網格。
- Clear 函數用於清理資源。
- 其他一些輔助函數則用於初始化網格和計算切線等。
讀取Scene
在Mesh腳本中,使用const aiScene* pScene = Importer.ReadFile載入Scene資料,接著在後續解析Material, Mesh資訊。
解析Mesh
遍歷模型的每個頂點,並將其屬性(位置、法線、紋理坐標和切線)轉換為頂點結構(Vertex),然後將其添加到頂點向量(Vertices)中。
for 循環遍歷模型的每個頂點:
- paiMesh->mNumVertices 是模型中頂點的總數,這個循環遍歷所有頂點。
- i 是遍歷的索引,從0開始直到總頂點數減1。
從模型的頂點數據中獲取位置、法線、紋理坐標和切線:
- pPos 是指向頂點位置的指針,通過 paiMesh->mVertices[i] 獲取。
- pNormal 是指向頂點法線的指針,如果模型有法線數據,則通過 paiMesh->mNormals[i] 獲取;否則使用默認值 &Zero3D。
- pTexCoord 是指向頂點紋理坐標的指針,如果模型有紋理坐標數據,則通過 paiMesh->mTextureCoords[0][i] 獲取;否則使用默認值 &Zero3D。
- tangent 是指向頂點切線的指針,如果模型有切線數據,則通過 paiMesh->mTangents[i] 獲取;否則使用默認值 &Zero3D。
創建頂點結構:
- 使用上述獲取的位置、法線、紋理坐標和切線數據,創建了一個 Vertex 對象 v。
- Vertex 對象的構造函數使用這些數據來初始化頂點的位置、紋理坐標和法線。
將頂點添加到頂點向量中:
- Vertices.push_back(v) 將創建的頂點結構添加到頂點向量中,以便後續使用。
總的來說,這段程式碼的目的是將模型的每個頂點的位置、法線、紋理坐標和切線轉換為一個 Vertex 對象,並將其添加到一個頂點向量中,以便後續在渲染過程中使用。
切線
需要注意的是Assimp 不會替我們計算切線。切線是一個在處理3D模型時非常重要的屬性,它用於紋理貼圖的正確顯示、光照計算以及其他一些效果中。
通常,切線(tangent)和副切線(bitangent)是在建模軟件中生成的,並隨著模型一起儲存在文件中。這意味著當你使用Assimp加載模型時,如果模型文件中包含了切線數據,Assimp會將其讀取並存儲在內存中。如果模型文件不包含切線數據,則Assimp將不會自動計算切線,而是將其設置為默認值(通常是0或其他相應的值)。
在某些情況下,如果你的模型文件不包含切線數據,你可能需要手動計算切線。這可以通過在加載模型後遍歷其頂點並根據模型的幾何信息計算切線,然後將其存儲在內存中。這種情況下,你需要自己編寫代碼來實現這一功能,而不是依賴於Assimp庫。
這邊附上計算tangent的程式。 (取自網路)
Hierarchy Window
Hierarchy
設計邏輯:
架構基本上是直接抄Unity Component的概念,每個Gameobject初始化時會自動bind 一個transform component,其他如Mesh , Light 等等component... 都能掛在GameObject物件上,並在hierarchy的Main Logic呼叫。
Component有準備個UiableComponent類別,提供binding時候自動掛載自定義的UI到hierarchy上。 而Draw UI的指令是以reference lambda的方式在初始化時追加,所以不限制UI的種類以及處理方式。
另外framebuffer類別也做了類似Unity的Blit功能,Blit單張圖片或指定shader去blit,或追加附數張貼圖。
(原本shadow的更新想掛在hierarchy的Do_Before_Frame執行,但因為學期末沒時間了,只好全塞在main裡面)
Imgui
ImGui(即“Dear ImGui”,之前稱為ImGui)是一個輕量級的圖形用戶界面(GUI)庫,用於創建應用程序的用戶界面。
建立樹狀UI
每個UI都需要用Begin() , End()函數包起來,以下建立了一個TreeNode架構的UI視窗。
- void create_window(): 定義了一個名為create_window的函數,該函數用於創建一個ImGui視窗。
- Begin("herrrr"): 開始了一個名為"herrrr"的ImGui視窗。這個函數開始了一個GUI範圍,其中的所有後續函數調用都將構建在這個範圍內。
- PushID(0): 將一個ID推送到ImGui堆棧中。這個ID通常用於確保在同一個範圍內使用多個具有相同名稱的元素時不會產生衝突。在這個示例中,ID為0。
- bool tree_node_open = TreeNodeEx("hire", ...):
- TreeNodeEx()函數用於創建一個可展開的樹節點。
- "hire"是樹節點的標題。
- ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_FramePadding | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth是一系列標誌,用於定義樹節點的行為和外觀。在這個例子中,它表示樹節點默認是打開的,具有框架填充,點擊箭頭可以打開/關閉節點,並且節點橫跨可用寬度。
- PopID(): 將之前推送到堆棧中的ID彈出。這確保我們不會在後續的函數調用中意外使用相同的ID。
- if (tree_node_open) { TreePop(); }: 檢查樹節點是否是打開狀態,如果是,則使用TreePop()函數將樹節點彈出。這確保在樹節點被打開時,End()之前的代碼將被執行。
- End(): 結束ImGui視窗。這個函數標記了GUI範圍的結束,並且在這之後的任何ImGui函數調用將不再影響到這個視窗。
Bind Property
將參數顯示在UI,以動態更新。
Position , Rotation ,Scale 拉霸
定義繪製UI命令:
- 首先,定義了幾個 lambda 函數,分別用於繪製UI元素。這些函數通過 ImGui 的函數來創建標題和輸入框,分別用於顯示位置、旋轉和縮放屬性。
- title_text 函數用於顯示標題 "Transform"。
- pos_inp、rot_inp 和 scale_inp 函數分別用於創建輸入框,讓用戶輸入位置、旋轉和縮放的值。
- 這些 lambda 函數被添加到 TransformObject 對象的 add_draw_item() 方法中,以便稍後呼叫。
將以上定義的繪製UI命令添加到 TransformObject 物件中:
- 使用 add_draw_item() 方法將上述 lambda 函數添加到 TransformObject 物件中,以便稍後呼叫繪製UI。
建立下拉選單,使其能選擇場上物件當成父物件
定義選擇父物件的下拉菜單:
- 獲取了一個遊戲物件列表,用於在下拉菜單中顯示所有可選的父物件。
- 創建了一個下拉菜單,標題為 "Set Parent"。
- 使用 ImGui::BeginCombo() 開始下拉菜單的創建,並使用 ImGui::EndCombo() 結束。
- 這裡遍歷了遊戲物件列表,並使用 ImGui::Selectable() 創建下拉菜單的每一個選項。
- 如果用戶選擇了某個選項,則將該選項的名稱設置為當前選擇的項目,並且調用 set_transform_parent() 方法將該選項對應的遊戲物件設置為當前選擇物件的父物件。
* 目前這裡字串顯示會有問題,更新後字串的位置改變,導致名稱變成"???",但功能正常。
新增Recursive Node
ImGui::GetMainViewport():獲取主視窗,即整個ImGui的顯示區域。
ImGui::SetNextWindowPos(viewport->WorkPos):設置下一個視窗的位置為主視口的工作區位置,即螢幕左上角。
Begin("Object List"):開始一個名為"Object List"的ImGui視窗。
迴圈遍歷所有遊戲物件 objs:
- 如果遊戲物件的父轉換為空指針,表示該物件是根物件,則調用 recursive_add_node 函數添加根節點。
- 呼叫 recursive_add_node 函數,將根物件的名稱、指針和子物件列表傳遞給它。
End():結束視窗。
這段程式碼通過遞歸添加樹節點,創建了一個階層視窗,用於顯示遊戲物件的階層結構。隨著遊戲物件的增加或修改,視窗將動態更新以反映最新的階層結構。
成果:
(畫質很破是因為gif壓縮)
使用AI協助寫文,希望看的習慣
。