是什么讓一個好游戲更好玩?漂亮的圖像!就像《神界:原罪2》,《暗黑破壞神3》以及《巫師3》等大作一樣,需要一個強大的程序團隊以及3D美術團隊強強合作。你在屏幕中看到正是3D模型使用自定義渲染繪制的結果。就像上一章你繪制的紅色球體那樣,只是效果更為豐富和先進而已。
在本節,我們會熟悉3D模型,我們會學習如何創建它們,它們是由什么組成,以及如何用不同的顏色和風格來渲染它們。
什么是3D模型?
3D模型是由頂點組成的。每個頂點都是3D空間的一個點,由 x, y, z 值組成。
就像你在前一章看到的,你會發送這些頂點數據到GPU來渲染它們。
打開本章的starter的playground。這個playground包含兩頁,Render and Export 3D Model 以及 Import Train。它也包含了USDZ格式的train模型,如果你看不到這些東西的話,你可能需要使用右上角圖標來顯示項目導航。
要顯示文件擴展名,請打開 Xcode 設置,然后在 General 選項卡上,選擇 File Extensions: Show All。
從項目導航那里,選擇Render and Export 3D Model。它包含了第一章的代碼,"Hello Metal!" 檢查它在playground實時視圖中顯示的球體。當前球體是顯示為一個平坦的實心紅色實體。
要查看每個三角形的邊,您可以使用線框渲染模型。
為了使用線框渲染,在renderEncoder.setVertexBuffer(...)后添加如下代碼:
renderEncoder.setTriangleFillMode(.lines)
此代碼指示 GPU 渲染線條而不是實心三角形。
運行playground:
這里有點視覺錯覺。它可能看起來不像,但 GPU 正在渲染直線。球體邊緣看起來彎曲的原因是 GPU 渲染的三角形數量。如果渲染的三角形較少,則曲線模型往往看起來有些“塊狀”。
你現在可以真正看到球體的 3D 性質。模型的三角形在水平方向上均勻分布,但由于您在二維屏幕上查看,因此它們在球體邊緣的位置比中間的三角形小。
在 Blender 或 Maya 等 3D 應用程序中,您通常會作點、線條和面。點是頂點;線(也稱為邊)是頂點之間的線;而面是三角形平面區域。
頂點通常按三角形排列,因為 GPU 硬件專門用于處理它們。GPU 的核心指令希望看到一個三角形。在所有可能的形狀中,為什么是三角形?
? 三角形是二維中可繪制的任何多邊形中頂點最少的。
? 無論以何種方式移動三角形的點,這三個點將始終位于同一平面上。
? 從任何頂點開始分割三角形時,它總是變成兩個三角形。
在 3D 應用程序中建模時,通常使用四邊形(四點多邊形)。四邊形與細分或平滑算法配合得很好。當您使用 Model I/O 框架導入模型時,Model I/O 會將這些四邊形轉換為三角形。
用Blender創建模型
為了創建3D模型,你需要一個好用的3D建模程序。市面上有很多建模軟件,從免費的到昂貴的。對于免費中最好用的3D建模軟件是Blender(作者使用的版本是 v.3.6)。也有很多建模專家使用Blender,如果你了解Cheetah3D、Maya或Houdini等其他軟件的話,你會發現Blender用起來也差不多。
下載并安裝Blender,通過 https://www.blender.org 網址。運行Blender,點擊空白區域關閉啟動tips界面,然后你會看到一個操作界面,如下圖:
如果你的界面看起來不一樣的話,可以點擊Edit Menu -> Preferences. 點擊左下角的漢堡菜單,選擇Load Factory Settings,并且點擊Load Factory Preferences。點擊Save Preferences保存設置。
如果你希望創建你自己的模型,最好的開始是參照Blender說的教程,在地址。
這個教程會教你怎么做一個蘑菇,你可以后續在你的playerground中把這個蘑菇給渲染出來,本章后面的挑戰中,有這個題目。
3D文件格式
.obj: 由Wavefront科技公司開發,流行過一段時間;基本上所有3D建模程序都支持導入和導出.obj文件,你可以使用和obj同文件的.mtl文件來指定材質(包括紋理以及表面屬性等),不過這個文件不支持動畫。
.glTF:由Khronos開發出來--就是那個管理Vulkan和OpenGL的機構--這個格式相對比較新,并且仍然在開發中。因為它的靈活性,所以它有強大的社區支持,它也支持動畫模型。
.blend:是Blender建模軟件用的格式。
.fbx:Autodesk私有格式,由于Autodesk公司有多個強大建模軟件的原因,這個格式使用很廣泛,并且支持動畫,但是缺點是, 它是私有的,并且沒有單一標準。
.usd:Universal Scene Description是一個由Pixar開發的開源格式,完整文檔在https://openusd.org。USD 文件可以引用許多模型和文件,因此團隊中的每個人都可以處理場景的單獨部分。USD 文件可以具有多個不同的擴展名。.usd 可以是 ASCII 或二進制。.usda 是人類可讀的 ASCII。.usdz 是一個 USD 存檔文件,其中包含模型或場景所需的一切。Apple 將 USDZ 格式用于其 AR 模型。
OBJ 文件僅包含單個模型,而 glTF 和 USD 文件是整個場景的容器,包括模型、動畫、相機和燈光。
在本書中,您將主要使用 USD 格式。
您可以使用 Apple 的 Reality Converter 將 3D 文件轉換為 USDZ。Apple 還提供了用于驗證和檢查 USDZ 文件 (https:// apple.co/3gykNcI) 的工具,以及示例 USDZ 文件庫 (https://apple.co/ 3iJzMBW)。
導出到Blender
現在你已經安裝好Blender了,是時候導出你在playground中創建的模型到Blender中了。
仍然在Render and Export 3D Model頁,到playground的靠前的代碼,你創建模型的地方,修改如下代碼:
let mdlMesh = MDLMesh(shereWithExtent: [0.75, 0.75, 0.75],segments:[100, 100],inwardNormals: false,geomeryType: .triangles,allocator: allocator)// 修改為如下代碼,即改為創建圓錐體,而不是球體
let mdlMesh = MDLMesh(coneWithExtent: [1, 1, 1],segments:[10, 10],inwardNormals: false,cap: true,geomeryType: .triangles,allocator: allocator)
這將會創建圓錐體,而不是球體。運行playground,你將會看到線框的圓錐體。
這個將會是你通過Model I/O 來導出的模型。
在Finder中,在Documents目錄,創建一個新的文件夾,命名為Shared Playground Data。這個是你保存playgrounds的文件的地方,確保你命名準確。
注意:全局常量playgroundSharedDataDirectory會持有這個文件夾的名字。
為了導出這個圓錐體,增加如下代碼,在創建mesh的后面:
// begin export code// 在Model I/O 的場景的最上層是一個MDLAsset,
// 你可以增加子對象,比如Mesh,相機,燈光等
// 到asset并且構建一個完整場景層次
let asset = MDLAsset()
asset.add(mdlMesh)// 檢測Model I/O 是否能導出一個 .usda文件格式
// 你可以選擇.usd或.usdz,但是選擇ASCII文本可以方便檢查文件內容
let fileExtension = "usda"
guard MDLAsset.canExportFileExtension(fileExtension) else {fatalError("Can't export a .\(fileExtension) format")
}// 導出圓錐體到Shared Playground Data的文件夾中
do {let url = playgroundSharedDataDirectory.appendingPathComponent("primitive.\(fileExtension)")try asset.export(to: url)
} catch {fatalError("Error \(error.localizedDescription)")
}
// end export code
運行playground即可導出圓錐體對象了。
usd文件格式
在Finder,導航到Documents -> Shared Playground Data. 我們看到playground已經導出這個文件:primitive.usda。
使用一個文本編輯器,打開primitive.usda。
以下是描述具有四個角頂點的平面基元的示例 USD 文件。圓錐體 USD 文件看起來相似,只是它具有更多的頂點數據。
#usda 1.0
(defaultPrim = "plane"endTimeCode = 0startTimeCode = 0timeCodesPerSecond = 60upAxis = "Y" )
def Mesh "plane"
{uniform bool doubleSided = 0float3[] extent = [(0, -0.5, -0.5), (0, 0.5, 0.5)]int[] faceVertexCounts = [3, 3]int[] faceVertexIndices = [3, 2, 1, 3, 1, 0]normal3f[] normals = [(-1, 0, 0), (-1, 0, 0), (-1, 0, 0),
(-1, 0, 0)]point3f[] points = [(0, 0.5, 0.5), (0, -0.5, 0.5), (-0,
-0.5, -0.5), (0, 0.5, -0.5)]float2[] primvars:Texture_uv = [(1, 1), (0, 1), (0, 0), (1,
0)] (interpolation = "vertex")
}
該文件首先描述大體功能,例如動畫計時和向上的方向。然后,該文件將描述網格。
以下是平面 USD 的詳細解釋:
? extent:網格的大小。創建圓錐體時,在 coneWithExtent 中,您在所有軸上都指定了 1。最小頂點位置值為 -0.5,最大值為 0.5。
? faceVertexCounts:此平面由兩個三角形組成,每個三角形有三個頂點。您的圓錐體將有許多三角形。
? faceVertexIndices:此平面有四個頂點,每個角一個。索引順序是這些頂點的渲染順序。要組成兩個三角形,您需要渲染六個頂點。
? normals:表面法線。法線是與平面正交、指向外面的向量。稍后將閱讀有關法線的更多信息。
? points:每個頂點的位置。該平面有四個頂點。您的圓錐體將具有許多頂點。
? Texture_uv:UV 坐標確定頂點在 2D 紋理上的位置。紋理上的坐標稱為 uv 坐標,而不是 xy 坐標,但它們的工作原理相同。您的圓錐體不使用這些 UV 坐標,因為您尚未對其應用任何自定義紋理。
導入圓錐
現在準備導入圓錐體到Blender中,按如下步驟:
1,打開Blender
2,選擇File -> New -> General
3,選中初始的立方體
4,按X鍵刪除那個立方體,并確認
現在你的Blender已經清空好了。
選擇 File -> Import -> Universal Scene Description (.usd*),并在Documents/Shared Playground Data的文件夾中選中primitive.usda文件。
這樣圓錐體就導入到Blender中了:
左鍵點擊這個圓錐體以選中它,并按Tab鍵,讓Blender切換到編輯模式,讓你能看到組成圓錐體的頂點和三角形。
在編輯模式中,你可以移動頂點位置,以及添加新頂點來創造更復雜的3D模型。
使用playground,我們已經可以創建,渲染和導出3D模型了。在本章的后面,我們將渲染一個更為復雜的模型,它有多個材質組。
材質
材質描述 3D 渲染器應如何為頂點著色。例如,頂點應該光滑有光澤嗎?粉紅色?反射?
材料特性可以包括:
? diffuse:表面的基本顏色。
? metallic:描述表面是否為金屬。
? roughness:描述表面的粗糙程度。如果表面的粗糙度為 0,則它是完全平坦且有光澤的。
材質組
在Blender中,打開train.blend。它就在本章的資源目錄下。它是tran.usdz的源編輯文件。左鍵點擊這個模型來選中它,然后按Tab鍵進入編輯模式。
不同于普通的灰色圓錐,火車模型有幾種顏色。這些顏色被定義在材料組中 - 每種顏色一組。在 Blender 屏幕的右側,您將看到屬性面板,其中材料上下文已選定(這是垂直圖標列表底部的圖標),該模型中的材料列表在頂部。
選中Body,然后點擊下面的“Select”,分配到Body紋理組的頂點都會高亮起來:
注意如何將頂點分為不同的組或材料。這種分離使選擇Blender中的各個部分變得更加容易,并使您能夠分配不同的顏色。
回到Xcode中,在項目導航那里,打開Import Train的playground頁面。
在playground的資源文件夾,我們可以看到train.usdz文件。在窗口上拖動,以便將視圖攝像頭繞著模型移動。
在Import Train中,移除你創建圓錐體的代碼:
let mdlMesh = MDLMesh(coneWithExtent: [1, 1, 1],segments:[10, 10],inwardNormals: false,cap: true,geomeryType: .triangles,allocator: allocator)
不用擔心,你的代碼暫時編譯不過,直到你完本節。我們現在要加載train模型,使用如下代碼:
guard let assetURL = Bundle.main.url(forResource: "train",withExtension: "usdz") else {fatalError()
}
這會設置好USD文件的URL地址。
頂點描述
Metal創建對象時,都會使用一個描述來填充信息。你在上一章看到創建管線狀態對象時也使用過描述。在加載模型之前,我們需要通過創建一個頂點描述,以便告訴Metal如何安排頂點和其他數據。
接下來的圖表描述模型頂點數據的輸入緩沖,它共有兩個頂點,每個頂點包含了位置,法線以及紋理坐標屬性。頂點描述讓Metal知道怎么解析頂點數據。
在剛才設置模型URL代碼后,添加如下代碼:
// 創建一個頂點描述,用來配置所有頂點屬性。
// 一般頂點屬性會包含位置,法線,和紋理坐標等數據。
// 不過這里只需要位置。每個頂點最多可以包含31種屬性。
let vertexDescriptor = MTLVertexDescriptor()// 屬性0:位置,它是一個由3個float組成的數據
vertexDescriptor.attributes[0].format = .float3// 屬性0在緩存中的偏移為0,是第一個屬性
vertexDescriptor.attributes[0].offset = 0// 當你發送頂點數據到GPU時,你通過一個MTLBuffer發送它,
// 并且會用一個索引來標識這個緩沖。有31個有效緩沖,
// Metal會用一個緩沖參數表來記錄它們。使用0號緩沖,
// 以便頂點著色器可以匹配輸入到緩沖0的頂點數據
vertexDescriptor.attributes[0].bufferIndex = 0
緊接著是如下代碼:
// 指定buffer0的頂點數據步幅值。步幅值是每個頂點占用字節個數。
// 這里我們每個頂點只有位置信息,所以占用了3個float3的尺寸,float3等同于swift中的SIMD3<Float>。
// 使用緩沖區布局索引和步幅格式,您可以設置引用具有不同布局的多個 MTLBuffer 的復雜頂點描述符。您可以選擇交錯位置、法線和紋理坐標;或者,您可以先布置包含所有位置數據的緩沖區,然后再包含其他數據。
vertexDescriptor.layouts[0].stride = MemoryLayout<SIMD3<Float>>.stride// 通過metal的頂點描述,創建一個Model I/O專用的頂點描述
// Model I/O 需要的格式略有不同的頂點描述符,因此您可以從 Metal 頂點描述符創建新的Model I/O 描述符。如果您有Model I/O 描述符并且需要一個 Metal 描述符,MTKMetalVertexDescriptorFromModelIO() 提供了一個解決方案
let meshDescriptor =MTKModelIOVertexDescriptorFromMetal(vertexDescriptor)// 把字符串名“position”分配給屬性0,這讓Model I/O 知道這個屬性代表位置信息。
// 法線和紋理坐標數據也可用,但使用此頂點描述符,您告訴 Model I/O 您對加載這些屬性不感興趣。
(meshDescriptor.attributes[0] as! MDLVertexAttribute).name =
MDLVertexAttributePosition
接下來是如下代碼:
// 通過URL,頂點描述,以及內存分配器讀取到資源。
let asset = MDLAsset(url: assetURL,vertexDescriptor: meshDescriptor,bufferAllocator: allocator)
// 先只取得第一個子mesh
let mdlMesh = asset.childObjects(of: MDLMesh.self).first as! MDLMesh
此代碼使用 URL、頂點描述符和內存分配器讀取asset。然后,您讀入asset中的第一個 Model I/O 網格緩沖區。一些更復雜的對象將具有多個網格,但您稍后會處理這個問題。
現在您已經加載了模型頂點信息,代碼的其余部分將相同,您的 Playground 將從新的 mdlMesh 變量加載網格。
運行Playground查看線框渲染的train:
耶!真酷,不過它們好像有點太高了,我們想辦法把它拉低一些,接近地面。
Metal坐標系統
所有模型都有一個原點。原點是mesh放在3D場景的定位點,火車的原點是[0, 0, 0],在模型的中下方。我們在Blender編輯這個模型時,這個點在場景的正中間。
Metal的NDC(歸一化設備坐標)系是一個2個單元寬,2個單元高,1個單元深的盒子,其中X是右/左,Y是上/下,Z是 進/出 屏幕。
歸一化,意味著調整到標準數值范圍。在屏幕中,比如你的窗口像素坐標是0到375,但是Metal的NDC坐標系不關心屏幕的實際尺寸,它的X的坐標總是從-1.0到1.0。在第四章,“3D變換”,我們會學習在不同坐標系之間的變換。因為火車的原點[0, 0, 0]在它的中下部,所以,它的中下部位于在NDC屏幕的中間。導致輪胎看起來有點高。
GPU 根據頂點函數的輸出呈現頂點位置。您的playground目前包含一個非常簡單的頂點函數,該函數返回傳遞給它的頂點位置。
在playground中定位到let shader = """...""",
shader 是一個文本字符串,其中包含 Metal 庫加載和編譯的著色器函數代碼。通過更改此字符串中的 vertex_in.position,您可以更改每個頂點的渲染位置。
在著色器文本字符串中,將返回 vertex_in.position;更改為:
float4 position = vertex_in.position;
position.y -= 1.0;
return position;
請小心地完全按照所示方式添加此代碼,在每行的末尾添加分號。由于代碼包含在字符串中,因此編譯器無法識別錯誤。
在這里,您從渲染的每個頂點的 y 位置減去 1.0。y 軸上的 NDC -1.0 位于屏幕底部。如果您還不太了解發生了什么,請不要擔心,因為您將在第 4 章 “頂點函數”中重新討論這個主題。
運行playground,輪胎現在已經顯示到了屏幕的底部了。
?
現在車輪已經固定,你準備好解決失蹤火車的情況了!
子網格submesh
到現在,你的模型只顯示了一個紋理組,也就是一個子mesh。下圖中是一個平面,有4個頂點,以及兩個材質組。
當 Model I/O 加載這個平面時,它會把4個頂點放到一個MTLBuffer中,接下來的圖顯示兩個子網格的緩沖是如何索引它的頂點數據的。
第一個子網格緩沖區保存淺色三角形 ACD 的頂點索引。這些索引指向頂點 0、2 和 3。第二個子網格緩沖區保存暗三角形 ADB 的索引。子網格在子網格緩沖區開始的位置也有一個偏移。索引可以保存在 uint16 或 uint32 中。第二個子網格緩沖區的偏移量將是 uint 類型大小的三倍。
環繞順序
頂點順序(也稱為環繞順序)在這里很重要。該平面的頂點順序是逆時針。通過逆時針環繞順序,以逆時針順序定義的三角形正面面向您,而以順時針順序環繞的三角形背對您。在后面的章節,我們將會理解渲染管線,并且會看到GPU是怎么剔除背面的三角形的,以便節省處理時間。
渲染子mesh
目前,我們只渲染了第一個子mesh。我們的火車模型有多個材質組,因此它有多個子mesh,我們需要用一個循環來渲染它們。
在playground代碼靠后位置,修改:
guard let submesh = mesh.submeshes.first else {fatalError()
}
renderEncoder.drawIndexedPrimitives(type: .triangle,indexCount: submesh.indexCount,indexType: submesh.indexType,indexBuffer: submesh.indexBuffer.buffer,indexBufferOffset: 0)
為:
for submesh in mesh.submeshes {renderEncoder.drawIndexedPrimitives(type: .triangle,indexCount: submesh.indexCount,indexType: submesh.indexType,indexBuffer: submesh.indexBuffer.buffer,indexBufferOffset: submesh.indexBuffer.offset)
}
這個循環,會遍歷所有子mesh,并調用draw call。mesh和子mesh都在MTLBuffers中,子mesh持有它使用的網格中頂點的索引清單。
運行playground,然后你的火車就能完全渲染出來了,除了材質顏色,你將會在第7章“紋理映射與材質”中學習如何著色。
祝賀!你已經渲染了3D模型了。暫時不用管你只能渲染它到一個類2D的效果,并且沒有顯示材質顏色。到下一章,你會知道渲染更多的知識,并且再后續章節,你會知道如何把它渲染得更為3D一些。
挑戰
如果你要參加一個有趣的挑戰,請完成 Blender 教程來制作蘑菇 (https://bit.ly/3gwKiel),然后將你在 Blender 中制作的內容導出到 .usdz 文件。如果要跳過建模,可以在本章的 resources 目錄中找到 mushroom.usdz 文件。
? 將 mushroom.usdz 導入 playground 并渲染它。
如果您使用自己建模的蘑菇,您可能會發現蘑菇是側躺的。Blender 使用 Z 軸向上,而你的 Playground 期望 Y 軸向上。在導出為 USD 之前,您應該將模型在 Z 軸上旋轉 180°,在 X 軸上旋轉 270°。然后,您必須在 Blender 中應用所有變換,然后才能使用菜單選項 Object ? Apply ? All Transforms 導出。resources 目錄中的 mushroom.usdz 已經旋轉。
如果你遇到困難,完成的 Playground 位于本章的挑戰目錄中。
參考
https://zhuanlan.zhihu.com/p/384962760