1. 程式人生 > >3D引擎資料結構與glTF(3): Mesh

3D引擎資料結構與glTF(3): Mesh

在上一篇文章中我們介紹了場景中物件之間的層次關係,接下來我們就探討單個物體在三維空間中的表示。

要渲染三維空間中的物體,首先就要在三維空間中表示物體的造型。目前圖形學中最常用的方法叫做“邊界表示法( Boundary Representation )”,即用一組多邊形或者曲面來定義物體的邊界,它們可以區分了空間中哪些部分是物體的內部,哪些是物體的外部。其中使用多邊形的方式更通用一些,構造一個物體邊界的一組多邊形就被稱為“ Mesh ”。 在這裡我們先討論靜態 mesh,也就是自身不發生形變的 mesh ,glTF 也支援蒙皮和骨骼動畫,我們後面再講。

也許現在三角形太流行了,大家可能不習慣了使用多邊形。實際上,現在建模軟體還在大量使用多邊形,最後這些模型匯入 3D 引擎的時候會再進行三角形化( triangularization )。三角形的流行是在 GPU 興起之後的事兒,因為三角形更有利於硬體去實現和優化。在早期的 3D 遊戲中,例如 Quake 使用軟體渲染,它使用凸多邊形。在 OpenGL 早期的 API 版本中也有對多邊形的支援: glBegin() 支援 GL_POLYGON 模式,而在3.0以後版本的 OpenGL API 中已經不再支援了,下面我們討論的 Mesh 也就使用三角形模式了。

Mesh 的資料組織

OpenGL 或者其他圖形 API 都普遍支援下面這三種三角形 Mesh 的拓撲結構:

  • Triangle List:每三個頂點組成一個三角形
  • Triangle Strip:每增加一個頂點,和前面的兩個頂點組成一個三角形(共享前面兩個頂點)
  • Triangle Fan:每增加兩個頂點,與第0個頂點組成一個三角形(共享第0個頂點)

OpenGL 支援兩種向 GPU 提交 Mesh 資料的方式:

  • DrawArrays 方式
    把 Mesh 中的三角形的所有頂點資料,按照上面那三種方式之一排列形成一個頂點陣列,就可以讓 GPU 去繪製所有這些三角形了。

  • DrawElements 方式
    在一個 Mesh 中很多三角形是互相連線在一起的,具體的說,兩個三角形會共用一條邊,那麼這條邊上的兩個頂點則可以共享。為了共享頂點,就需要引入“頂點索引”。(做實時渲染的人最摳門了,各種省:joy:)。把所有的頂點放在一個數組中,然後另外使用一個 index buffer 通過陣列下標來引用頂點陣列中的頂點資料。把 Mesh 所需的三角形,使用 index buffer 按照前面說的三種拓撲結構列舉出來,就可以提交到 GPU 去繪製了。
    通過 index buffer 可以有效的減少 Mesh 的記憶體佔用,並且能夠提升 GPU 的繪製效率,詳見Vertex Cache 優化

    小節,對於複雜的模型來說,這是一種應該優先使用的方式。

頂點緩衝的兩種記憶體佈局

這裡寫圖片描述

上圖來自蘋果開發者網站

一個頂點可能有多項資料,在 OpenGL API 中稱之為 Attribute ,這些資料的排列有兩方式,如上圖所示。這兩種方式都可以被 OpenGL 系列 API 所支援,但它們的效率上卻有所差別:
1. struct of arrays
如上圖左側所示的資料結構,我們把頂點位置、法線等各自定義一個結構體(一般也就是 vec2、vec3、vec4 啦)的陣列提交給 GPU。
2. array of structs
假想我們把每個頂點都定義一個結構體:

struct MyVertex {
vec3 pos;
vec4 normal;
vec2 uv;
}

把這個結構體陣列提交給 GPU。

第二種方式的 Vertex Buffer 在記憶體佈局上是連續的,GPU 訪問起來更高效。 應該儘量使用第二種方式,除非你需要單獨計算更新某個頂點屬性,而其他頂點屬性保持不變。另外,Vertex Buffer 保持4位元組對齊,也有助於提高 GPU 的記憶體訪問效率。

Vertex Cache 優化

GPU 在渲染每個三角形時,三角形的每個頂點都要進行計算,也就是我們通過 Vertex Shader 進行的處理。當我們使用 DrawElements 的方式繪製 Mesh 時,GPU 會利用內部的一個很小的 Cache (一般是12到24個頂點,根據硬體實現會不同) 來儲存頂點處理結果,通過 index 來判斷是否命中 Cache,從而避免頂點的重複計算,提升效率。如果使用 DrawArrays 的話, Vertex Cache 是無法起作用的。

通過對 Mesh 進行預處理,可以更好的利用 Vertex Cache 。GPU 一般使用 LRU(least recently used)策略處理 cache 替換,所以這個預處理的優化,主要就是將 Mesh 中的三角形進行排序,使得每個進入渲染管線的頂點 Index 儘量接近前面的。

頂點的法線

有一個小問題你有沒有想過:頂點是一個點,它哪裡來的什麼法線?法線是垂直於一個平面的單位向量啊!一個點怎麼會有法線?

我想所謂的“頂點的法線”是因為渲染計算需要,才被我們“捏造”出來的一項資料,其主要目的是希望 Shading 產生出儘量平滑的視覺效果。從這個角度說,某些情況下頂點法線也是有道理的,例如,當我們用一組三角形去近似一個曲面的時候,某個頂點的法線就應該是曲面上這一點的法線;而對於一般性的三角形集合來說,可以想象成共享這個頂點的所有三角形的“面法線”的某種平均。

一般情況下,頂點的法線應該由建模工具提供。在建模工具中提供了一些工具(例如光滑組等),幫助藝術家們調整法線的計算方式,產生他們想要的結果。有些時候,在模型匯入3D引擎的時候也可以重新計演算法線。

glTF 中的 Mesh 資料組織

    "meshes": [
        {
            "primitives": [
                {
                    "attributes": {
                        "NORMAL": 1,
                        "POSITION": 2
                    },
                    "indices": 0,
                    "mode": 4,
                    "material": 0
                }
            ],
            "name": "Mesh"
        }
    ]

上面這一段 JSON,就是 glTF 檔案中一個典型的 meshes 欄位,它是一個數組。陣列的每個成員是一個 mesh 物件,每個 mesh 物件有兩個欄位:name 和 primitives。primitives 又是一個數組,陣列的每個成員是一個 primitive 物件,這裡的 primitive 在有的引擎中也叫做 sub mesh 或者 mesh section。

primitive 是實際儲存幾何體資料的地方:
* attributes
來指定頂點資料,前面我們提到過 GL 中頂點的資料項叫做 attribute;
其中每個 attribute 的值都是一個數字,例如:”NORMAL”: 1,那麼這個值“1”的含義是什麼呢?它是用來索引二進位制資料結構的,具體來講是索引“accessor”。glTF 是一個面向實時渲染的內容格式標準,它使用一個二進位制檔案來儲存 mesh 的頂點、Index 這些資料,大致結構如下圖中橙色的部分。glTF 中的二進位制資料處理方式,具體的我們在下一文中再講。:smile:
* indices
用來指定三角形的頂點索引資料,這個專案可以沒有,則表示這是一個 non-indexed mesh;其值同樣也是引用 accessor 的;
* mode
用來指定繪製的方式,其值是 OpenGL 中定義的常量,具體包括:
* 0 POINTS
* 1 LINES
* 2 LINE_LOOP
* 3 LINE_STRIP
* 4 TRIANGLES
* 5 TRIANGLE_STRIP
* 6 TRIANGLE_FAN
* material 用來指定這個 primitive 所使用的材質;它的值是一個數字,用來索引 glTF 中的 “materials” 陣列。具體的,在後續講到材質的時候,我們再詳細講。

具體如下圖中的藍色部分所示。

這裡寫圖片描述

在場景節點中引用 Mesh

"nodes" : [
    {
      "mesh" : 0,
      "translation" : [ 1.0, 0.0, 0.0 ]
    }
  ]

在前面一節講 glTF 的 Scene Graph 資料的時候,我們講到 glTF 的 JSON 資料中有一個 “nodes” 陣列用來儲存整個 Scene Graph,陣列中的每個元素是 Scene Graph 中的一個節點。場景節點可以有一個 “mesh” 欄位,其值是一個數字,用來索引 “meshes” 陣列中的 mesh 物件。根據這個欄位,渲染程式就可以找到相應的 mesh ,併為這個節點新增 mesh 渲染元件。節點中的這個 mesh 欄位,基本上對應 Unity 的 MeshFilter + MeshRenderer 兩個元件的功能( MeshFilter 指定渲染哪個 mesh 物件,MeshRenderer實現渲染功能 )。

原文地址