到目前為止,您已經學習了如何使用片段函數和著色器為模型添加顏色和細節。另一種選擇是使用圖像紋理,您將在本章中學習如何操作。更具體地說,您將了解:
? UV 坐標:如何展開網格,以便可以對其應用紋理。
? 紋理化模型:如何讀取片段著色器中的紋理。
? 資產目錄:如何組織紋理。
? 采樣器:讀取 (采樣) 紋理的不同方式。
? Mipmaps:多級紋理,以便紋理分辨率與顯示大小匹配并占用更少的內存。
紋理和UV映射
下圖顯示了一個有12個頂點的房子模型,左邊是線框(顯示了頂點),右邊是紋理映射好的模型。
?
注意:如果您想更仔細地查看該模型,您可以在本章的 resources/LowPolyHouse 文件夾中找到 Blender 和紋理文件。
要為模型添加紋理,您首先必須使用稱為 UV 展開的過程來展平該模型。UV 展開通過展開模型來創建 UV 貼圖。要展開模型,您需要使用建模應用程序標記和切割接縫。下圖顯示了在 Blender 中 UV 展開房屋模型并導出其 UV 貼圖的結果。
?
請注意,屋頂和墻壁有明顯的接縫。接縫使該模型可以展平。如果您打印并剪下此 UV 貼圖,則可以輕松地將其折疊回房屋。在 Blender 中,您可以完全控制接縫以及如何切割網格。Blender 通過在這些接縫處切割網格來自動展開模型。如有必要,您還可以在 UV 展開窗口中移動頂點以適應您的紋理。
現在你已經有一個扁平的貼圖,你可以使用從 Blender 導出的 UV 貼圖作為指南來 “繪制” 到它上面。下圖顯示了通過剪切真實房屋照片創建的房屋紋理(在 Photoshop 中制作)。
?
請注意,紋理的邊緣并不完美,并且可以看到版權信息。在映射上沒有頂點的地方,您可以添加任何內容,因為它不會顯示在模型上。
注意: 最好不要完全匹配 UV 邊緣,而是讓顏色滲出,因為有時計算機無法準確計算浮點數。
然后,您將該圖像導入 Blender 并將其分配給模型,以獲得您在上面看到的紋理房屋。
當您從 Blender 導出 UV 映射模型時,Blender 會將 UV 坐標添加到文件中。每個頂點都有一個二維坐標,用于將其放置在 2D 紋理平面上。左上角是 (0, 1),右下角是 (1, 0)。
下圖指示了一些房屋頂點,并列出了一些相對應的坐標。
??
使用坐標范圍0到1的好處是,可以換入較低或較高分辨率的紋理。如果您只是從遠處查看模型,則不需要高分辨的紋理。
這個房子很容易展開,但想象一下展開曲面可能有多復雜。下圖顯示了火車的 UV 貼圖(它仍然是一個簡單的模型):
?
當然,Photoshop 并不是為模型添加紋理的唯一解決方案。您可以使用任何圖像編輯器在平面紋理上繪畫。在過去的幾年里,其他幾個允許直接在模型上繪畫的應用程序已成為主流:
? Blender(免費)
? iPad 上的 Procreate
? Adobe 的 Substance Designer 和 Substance Painter:在 Designer 中,您可以按程序創建復雜的材質。使用 Substance Painter,您可以在模型上繪制這些材質。
? 3Dcoat.com 的 3DCoat
? Foundry的Mari
除了紋理之外,在 iPad 上使用 Blender、3DCoat 或 Nomad Sculpt,您還可以以類似于 ZBrush 的方式雕刻模型,然后重新劃分高精度模型的網格以創建低精度模型。您稍后會發現,使用這些應用程序繪制時,顏色并不是你唯一可以使用的紋理,因此擁有專門的紋理應用程序非常有價值。
開始程序
? 打開本章的入門項目,然后構建并運行應用程序。
?
該場景包含低精度房屋。片段著色器代碼與上一章中的挑戰代碼相同,添加了半球照明和不同的背景透明顏色。
其他主要變化是:
? Mesh.swift 和 Submesh.swift 將Model I/O 和 MetalKit 網格緩沖區提取到自定義頂點緩沖區和子網格組中。模型現在包含一個網格數組,而不是單個 MTKMesh。從 Metal API 中抽象出來,可以在生成不使用 Model I/O 和 MetalKit 的模型時提供更大的靈活性。請記住,這是您的引擎,因此您可以選擇如何保存網格數據。
? Primitive.swift 擴展了 Model,以便您可以輕松渲染基本形狀。該文件支持平面和球體,但您可以添加其他基本形狀。
? VertexDescriptor.swift 除了 Position 和 Normal 屬性外,還包含一個 UV 屬性。模型加載 UV 的方式與上一章中加載法線的方式相同。請注意 UV 如何使用與位置和法線不同的緩沖區。這不是必需的,但它使布局更靈活,可用于自定義生成的模型。
? Renderer.swift 將 uniform 和 params 傳遞給 Model 以執行渲染代碼。
? ShaderDefs.h 包含 VertexIn 和 VertexOut。這些結構體具有額外的 uv 屬性。vertex 函數將插值的 UV 傳遞給 fragment 函數。
在本章中,您將用紋理中的顏色替換 fragment 函數中的天空和地面顏色。最初,您將使用位于 Models 組中的 lowpoly-house.usdz 中包含的紋理。要在 fragment 函數中讀取紋理,您需要執行以下步驟:
1. 集中加載和存儲圖像紋理。
2. 在繪制模型之前,將加載的紋理傳遞給 fragment 函數。
3. 更改 fragment 函數以從紋理中讀取適當的像素。
1. 加載紋理
一個模型通常具有多個引用相同紋理的子網格。由于您不想重復加載此紋理,因此您將創建一個TextureController 來保存您的紋理。
? 創建一個名為 TextureController.swift 的新 Swift 文件。請務必將新文件包含在Target中。將代碼替換為:
import MetalKit
enum TextureController {static var textures: [String: MTLTexture] = [:]
}
TextureController 將獲取模型使用的紋理,并將它們保存在此字典中。
? 為 TextureController 添加新方法:
static func loadTexture(texture: MDLTexture, name: String) ->
MTLTexture? {
// 1if let texture = textures[name] {return texture}
// 2let textureLoader = MTKTextureLoader(device: Renderer.device)// 3let textureLoaderOptions: [MTKTextureLoader.Option: Any] =[.origin: MTKTextureLoader.Origin.bottomLeft]// 4let texture = try? textureLoader.newTexture(texture: texture,options: textureLoaderOptions)print("loaded texture from USD file")// 5textures[name] = texturereturn texture
}
?此方法將接收Model I/O 紋理,并返回準備渲染的 MetalKit 紋理。
瀏覽代碼:
1. 如果紋理已加載到紋理中,則返回該紋理。請注意,您是按名稱加載紋理的,因此您必須確保模型沒有沖突的名稱。
2. 使用 MetalKit 的 MTKTextureLoader 創建紋理加載器。
3. 更改紋理的原點選項,以確保紋理加載時其原點位于左下角。如果沒有此選項,紋理將無法正確覆蓋房屋。
4. 使用提供的紋理和加載器選項創建新的 MTLTexture。出于調試目的,請打印一條消息。
5. 將紋理添加到textures并返回它。
注意:加載紋理可能會變得復雜。當metal首次發布時,您必須使用MTLTextureDescriptor指定有關圖像的所有內容,例如像素格式,尺寸和用法。但是,使用Metalkit的MTKTextureLoader,您可以使用所提供的默認值并根據需要進行選擇。
加載 Submesh 紋理
一個模型網格的每個子網格具有不同的材料特性,例如粗糙度、基色和金屬光澤。現在,您將只關注基礎顏色紋理。在第11章“地圖和材料”中,你將看到一些其他特性。Model I/O 可以方便地加載包含所有材質和紋理的模型。您的工作是以適合您引擎的形式從加載的資產中提取它們。
? 打開 Model.swift,找到 let asset = MDLAsset....在這行之后,加上這個:
asset.loadTextures()
Model I/O 會將 MDLTextureSampler 的值添加到子網格中,以便您能夠很快加載紋理。
? 打開 Submesh.swift,然后在 Submesh 中,創建一個結構體和一個屬性來保存紋理:
struct Textures {var baseColor: MTLTexture?
}
var textures: Textures
不用擔心編譯錯誤;在初始化紋理之前,您的項目不會編譯。
MDLSubmesh 使用一個MDLMaterial 屬性保存每個子網格的材質信息。您可以為 Material 提供語義以檢索相關材質的值。例如,基色的語義是 MDLMaterialSemantic.baseColor。
? 在 Submesh.swift 的末尾,添加三個新的擴展:
// 1
private extension Submesh.Textures {init(material: MDLMaterial?) {baseColor = material?.texture(type: .baseColor)}
}
// 2
private extension MDLMaterialProperty {var textureName: String {stringValue ?? UUID().uuidString}
}
// 3
private extension MDLMaterial {func texture(type semantic: MDLMaterialSemantic) ->
MTLTexture? {if let property = property(with: semantic),property.type == .texture,let mdlTexture = property.textureSamplerValue?.texture {return TextureController.loadTexture(texture: mdlTexture,name: property.textureName)}
return nil
} }
了解這些擴展的作用:
1. 使用提供的子網格材質加載基礎顏色(漫反射)紋理。稍后,您將以相同的方式加載子網格的其他紋理。
2. MDLMaterialProperty.textureName 返回文件中的紋理名稱,如果未提供名稱,則返回唯一標識符。
3. MDLMaterial.property(with:) 在子網格的材質中查找提供的屬性。然后,檢查屬性類型是否為紋理,并將紋理加載到 TextureController.textures 中。Material 屬性也可以是 float 值,但是其中沒有可用于子網格的紋理。
? 在init(mdlSubmesh:mtkSubmesh)的底部添加:
textures = Textures(material: mdlSubmesh.material)
你初始化子網格紋理,最終消除了編譯器警告。
? 構建并運行您的應用程序以檢查一切是否正常。您的模型看起來與初始屏幕截圖中的模型相同。但是,您將在控制臺中收到一條消息:loaded texture from USD file,表明紋理加載器已成功加載房屋的紋理。?
?
2. 將加載的紋理傳遞給 Fragment函數
在后面的章節中,您將了解其他幾種紋理類型,以及如何使用不同的索引將它們發送到 fragment 函數。
? 打開 Shaders 組中的 Common.h,并添加新的枚舉來跟蹤這些紋理緩沖區索引號:
typedef enum {BaseColor = 0
} TextureIndices;
? 打開 VertexDescriptor.swift,并將以下代碼添加到文件末尾:
extension TextureIndices {var index: Int {return Int(self.rawValue)}
}
此代碼允許您使用BaseColor.index而不是Int(BaseColor.rawValue)。這是一個小的格調,但它使您的代碼更易于閱讀。
?打開Rendering.swift。這是您渲染模型的地方。
在處理子網格的代碼render(encoder:uniforms:params:)里,在注釋// set the fragment texture here:后面添加代碼:
encoder.setFragmentTexture(submesh.textures.baseColor,index: BaseColor.index)
現在,您將紋理緩沖區0中的紋理傳遞給片段函數。
?
注意:緩沖區,紋理和采樣器狀態保存在參數表中。如您所見,您可以通過索引號訪問這些內容。在iOS上,參數表中至少可以持有31個緩沖區和紋理,和16個采樣器聲明; macOS上的紋理數量增加到128。您可以在Apple的Metal功能套裝表(https://papple.co/2UpCT8r)中找到你的設備支持的功能。
3.更新片段功能
?打開fragment.metal,緊跟VertexOut in [[stage_in]]之后,將以下新參數添加到fragment_main函數:
texture2d<float> baseColorTexture [[texture(BaseColor)]]
您現在可以訪問GPU上的紋理。
?用以下代碼替換fragment_main中的所有代碼:
constexpr sampler textureSampler;
您讀取或采樣紋理時,可能不會精確地落在特定的像素上。在紋理空間中,您采樣的單元被稱為紋素,您可以決定如何使用采樣器處理每個紋素。您很快就會了解有關采樣器的更多信息。
? 接下來,添加這個:
float3 baseColor = baseColorTexture.sample(textureSampler,in.uv).rgb;
return float4(baseColor, 1);
在這里,使用從頂點函數發送的插值 UV 坐標對紋理進行采樣,并檢索 RGB 值。在 Metal Shading Language 中,您可以使用 rgb 作為 xyz 的等效項來獲取浮點元素。然后,從 fragment 函數返回紋理顏色。
? 構建并運行應用程序以查看您使用了紋理的房屋。
?
?地平面
是時候為您的場景添加一些地面了。您將使用 Model I/O 中的一種圖元來創建地平面,而不是加載 USD 模型,就像您在本書的前幾章中所做的那樣。
? 打開 Primitive.swift 并確保您理解代碼。
Model I/O 為平面或球體創建 MDLMesh,并初始化網格和子網格。請注意,您可以在加載 MDLMesh 后分配自己的頂點描述符,Model I/O 將自動重新排列網格緩沖區中的頂點屬性順序。
? 打開 Renderer.swift,并向 Renderer 添加新屬性以創建地面模型:
lazy var ground: Model = {Model(name: "ground", primitiveType: .plane)
}()
? 在 draw(in:)中,渲染房屋之后,renderEncoder.endEncoding()之前,添加:
ground.scale = 40
ground.rotation.z = Float(90).degreesToRadians
ground.rotation.y = sin(timer)
ground.render(encoder: renderEncoder,uniforms: uniforms,params: params)
此代碼放大了地平面。原始位置的平面是垂直的,因此您可以在 z 軸上將其旋轉 90 度,然后在 y 軸上旋轉它以匹配房屋的旋轉角度。然后渲染地平面。
? 構建并運行應用程序以查看您的地平面。
?
目前,地面沒有紋理或顏色,但您很快就會通過從資產目錄中加載紋理來解決此問題。
資源目錄asset catalog
當您編寫完整的游戲時,您可能會為不同的模型配備許多紋理。如果使用USD格式模型,通常將包括紋理。但是,您可能會使用不具有紋理的不同文件格式,并且組織這些紋理可能會變成勞動力密集型的工作。另外,您還需要壓縮圖像,并向不同的設備發送不同尺寸和色域的紋理。資產目錄將是您轉而使用的方式。
顧名思義,資產目錄可以持有您的所有資產,無論它們是數據,圖像,紋理甚至顏色。您可能已將目錄用于應用程序圖標和圖像。紋理與圖像不同,因為GPU使用它們,因此它們在目錄中具有不同的屬性。要創建紋理,請在資產目錄中添加一個新的紋理設置。
?使用Asset Catalog模板(在Resource部分找到)創建一個新文件,并將其命名為Textures。請記住將其添加到目標中。
?打開Textures.xcassets,選擇Editor ? Add New Asset ? AR and Textures ? Texture Set(或單擊面板底部的+,然后選擇AR and Textures ? Texture Set)。
?重命名新的紋理為grass。
?打開本章的資源文件夾,然后將ground.png拖到目錄中的Universal插槽。
注意:請小心將圖像放在紋理的Universal插槽上。如果將圖像拖到資產目錄中,則默認情況下它們是圖像而不是紋理。稍后您將無法更改任何紋理屬性。
?
您需要向紋理控制器添加另一個方法,以便從資源目錄中加載命名紋理。
? 打開 TextureController.swift,并向 TextureController 添加一個新方法:
static func loadTexture(name: String) -> MTLTexture? {// 1if let texture = textures[name] {return texture}
// 2let textureLoader = MTKTextureLoader(device: Renderer.device)let texture: MTLTexture?texture = try? textureLoader.newTexture(name: name,scaleFactor: 1.0,bundle: Bundle.main,options: nil)
// 3if texture != nil {print("loaded texture: \(name)")textures[name] = texture}return texture
}
瀏覽代碼:
1. 如果您已經加載了此名稱的紋理,請返回加載的紋理。
2. 像設置 USD 紋理加載一樣設置紋理加載器。從資產目錄中加載紋理,并指定名稱。在實際應用程序中,對于不同的分辨率比例,您將擁有不同大小的紋理。在資源目錄中,您可以根據比例以及設備和色域分配紋理。此處只有一個紋理,因此請使用 1.0 的比例因子。
3. 如果紋理加載正確,則打印出調試語句,并將其保存在紋理控制器中。
現在,您需要將此紋理分配給地平面。
? 打開 Model.swift,并將以下內容添加到文件末尾:
extension Model {func setTexture(name: String, type: TextureIndices) {if let texture = TextureController.loadTexture(name: name) {switch type {case BaseColor:meshes[0].submeshes[0].textures.baseColor = texturedefault: break} }}
}
此方法加載紋理并將其分配給模型的第一個子網格。
注意:這是分配紋理的快速簡便方法。它僅適用于僅使用一種材料的簡單模型。如果您經常從資源目錄加載子網格紋理,則應設置指向正確紋理的子網格初始化器。
最后要做的是將紋理設置到地面平面上。打開 Renderer.swift,并將地面的聲明替換為:
lazy var ground: Model = {let ground = Model(name: "ground", primitiveType: .plane)ground.setTexture(name: "grass", type: BaseColor)return ground
}()
在加載模型后,從資產目錄中加載草地紋理并將其分配給地面平面。
?構建并運行應用程序以查看茂盛的綠色草地:
?
這看起來是個問題。草地比原始紋理要暗得多,而且被拉伸和像素化。
sRGB顏色空間
渲染的紋理看起來比原始圖像要深得多,因為ground.png是SRGB紋理。 SRGB是一種標準的顏色格式,在陰極射線管顯示器的工作方式和人眼看到的顏色之間折中。如下面的灰度值從0到1的示例,SRGB顏色不是線性的。相比于深色,人類更能辨別淺色。
?
不幸的是,在非線性空間中的顏色上進行數學運算并不容易。如果將顏色乘以0.5使其變暗,則SRGB的差異會隨比率而變化。
目前,您正在將草地紋理加載為SRGB像素數據,并將其渲染到線性色彩空間中。因此,當您采樣一個0.2的值時,在SRGB空間中是中度灰色時,線性空間將讀取為深灰色。
要大致轉換顏色,您可以使用gamma 2.2的倒數:
sRGBcolor = pow(linearColor, 1.0/2.2);
如果您在從片段函數返回之前,在baseColor上使用此公式,則您的草紋理將看起來像原始的sRGB紋理,但是房子紋理會褪色,因為它正加載在非sRGB顏色空間中。
解決此問題的另一種方法是更改??視圖的顏色像素格式。
?打開Renderer.swift,然后在init(metalView:)中找到MetalView.device =
device。在此代碼之后,添加:
metalView.colorPixelFormat = .bgra8Unorm_srgb
在這里,您可以將視圖的像素格式,從默認的bgra8unorm更改為在sRGB和線性空間之間轉換的格式。
? 構建并運行應用程序。
??
草地顏色現在好多了,但您的非 sRGB 房屋紋理褪色了。
? 撤消您剛剛輸入的代碼:
metalView.colorPixelFormat = .bgra8Unorm_srgb
GPU抓幀
有一種簡單的方法可以找出紋理在 GPU 上的格式,還可以查看當前駐留在其中的所有其他 Metal 緩沖區:Capture GPU workload工具(也稱為 GPU 調試器)。
? 運行您的應用程序,然后在 Xcode 窗口底部(或調試控制臺上方,如果您已打開),單擊 M Metal 圖標,將要計數的幀數更改為 1,然后單擊彈出窗口中的捕獲:
?
此按鈕可捕獲當前 GPU 幀。在 Debug navigator (調試導航器) 的左側,您將看到 GPU 追溯:
?
注: 若要打開或關閉層次結構中的所有項,可以按住 Option 鍵點擊箭頭。
您可以看到您提供給渲染命令編碼器的所有命令,例如 setFragmentBytes 和 setRenderPipelineState。稍后,當您有多個命令編碼器時,您將看到每個命令編碼器都列出來,您可以選擇它們以查看它們通過編碼生成的操作或紋理。
? 在步驟 11 中選擇第一個 drawIndexedPrimitives。此時將顯示 Vertex 和 Fragment 資源。
?
? 雙擊每個頂點資源以查看緩沖區中的內容:
? indices:子網格索引。
? Buffer 0:頂點位置和法線數據,與 VertexIn 結構體和頂點描述符的屬性匹配。
? 緩沖區 1:UV 紋理坐標數據。
? Vertex Bytes:統一矩陣。
? Vertex Attributes:來自 VertexIn 的傳入數據,以及 VertexOut 返回來自頂點函數的數據。此資源對于查看頂點函數的計算結果尤其有用。
? vertex_main:頂點函數。當您有多個頂點函數時,這對于確保設置正確的管道狀態非常有用。
瀏覽 Fragment 資源:
? Texture 0:紋理槽 0 中的房屋紋理。
? Fragment Bytes:參數中的寬度和高度屏幕參數。
? fragment_main:片段函數。
附件:
?CAMetalLayer Drawable:顏色附件0中編碼的結果。在這種情況下,這是視圖的當前繪制。稍后,您將使用多種顏色附件。
?MTKView Depth:深度緩沖區。黑色更近。白色更遠。 光柵器使用深度圖。
?按住Control鍵,單擊Texture 0,然后從彈出菜單中選擇獲取信息。
?
像素格式為rgba8unorm,而不是SRGB。
?在調試導航器中,在第17步中單擊第二個drawIndexedPrimitimives命令。再次,按住Control鍵,單擊草地紋理,然后從彈出菜單中選擇Get Info。
這次的像素格式是rgba8unorm_srgb。
如果您對應用程序中發生的情況不確定,則捕獲GPU幀可能會引起您的注意,因為您可以檢查每個渲染編碼器命令和每個緩沖區。在本書中使用此策略來檢查GPU上發生的事情是一個好主意。
現在,回到您紋理不匹配的問題。解決此問題的另一種方法是完全不將資產目錄紋理加載為sRGB。
打開Textures.xcassets,單擊草紋理,在Attributes inspector中,將Interpretation更改為Data:
?
當您的應用程序將 sRGB 紋理加載到非 sRGB 緩沖區時,它會自動從 sRGB 空間轉換為線性空間。(有關轉換規則,請參閱 Apple 的 Metal Shading Language 文檔。)通過作為數據而不是顏色進行訪問,著色器可以將顏色數據視為線性數據。
您還會注意到,在上圖中,原點(與加載 USD 紋理不同)是 Top Left(左上)。資產目錄以不同的方式加載紋理。
? 構建并運行應用程序,紋理現在以線性顏色像素格式 bgra8Unorm 加載。您可以通過再次捕獲 GPU 工作負載來確認這一點。
?
現在,您可以處理渲染中的其他問題,從像素化的草地開始。
采樣器Samplers
在 fragment 函數中對紋理進行采樣時,使用了默認采樣器。通過更改采樣器參數,您可以決定應用程序如何讀取紋素。
地面紋理會拉伸以適應地平面,并且紋理中的每個像素都可能被多個渲染的片段使用,從而使其具有像素化的外觀。通過更改其中一個采樣器參數,您可以告訴 Metal 如何處理紋素小于分配的片段的情況。
? 打開 Fragment.metal。在 fragment_main 中,將 textureSampler 定義更改為:
constexpr sampler textureSampler(filter::linear);
此代碼指示采樣器平滑紋理。
? 構建并運行應用程序。
?
地面紋理(盡管仍然拉伸)現在是平滑的。有時,例如當您制作 Frogger 的復古游戲時,您會希望保持像素化。在這種情況下,請使用 nearest 篩選。
?
但是,在這種特殊情況下,您需要平鋪紋理。這對于采樣來說很容易。
? 更改采樣器定義, 將baseColor分配為:
constexpr sampler textureSampler(filter::linear,address::repeat);
float3 baseColor = baseColorTexture.sample(textureSampler,in.uv * 16).rgb;
此代碼將 UV 坐標乘以 16,并訪問超出允許范圍(0 到 1)的紋理。address::repeat 會更改采樣器的尋址模式,因此它將在整個平面上重復紋理 16 次。
下圖說明了平鋪值為 3 時顯示的其他地址采樣選項。您可以使用 s_address 或 t_address 分別僅更改寬度或高度坐標。
?
?? 構建并運行您的應用程序。
?
地面看起來很棒!房子...沒那么棒。著色器還平鋪了房屋紋理。為了解決這個問題,您將在模型上創建一個 tiling 屬性,并使用 params 將其發送到 fragment 函數。
? 在 Common.h 中,將此添加到 Params:
uint tiling;
? 在 Model.swift 中,在 Model 中創建一個新屬性:
var tiling: UInt32 = 1
? 打開 Rendering.swift,然后在 render(encoder:uniforms:params:)中,在 var params = fragment 之后添加以下內容:
params.tiling = tiling
? 在 Renderer.swift 中,將 ground 的聲明替換為:
lazy var ground: Model = {let ground = Model(name: "ground", primitiveType: .plane)ground.setTexture(name: "grass", type: BaseColor)ground.tiling = 16return ground
}()
現在,您正在將模型的平鋪因子發送到 fragment 函數。
? 打開 Fragment.metal。在 fragment_main 中,將 baseColor 的聲明替換為:
float3 baseColor = baseColorTexture.sample(textureSampler,in.uv * params.tiling).rgb;
?構建并運行該應用程序,您會發現地面和房屋現在都正確地平鋪了。
??
隨著場景的旋轉,您會發現一些惱人的噪點。您已經看到過度采樣時在草地上發生了什么。但是,當您向下采樣紋理時,您可以得到一個被稱為Moiré的失真,該失真發生在房屋的屋頂上。
注意:在著色器中創建采樣器并不是唯一的選擇。您可以創建一個MTLSamplerState,將采樣器和模型一起持有,然后使用[[Sampler(n)]]屬性將采樣器狀態發送到片段函數。
??
此外,地平線上的噪點使草幾乎看起來好像在閃閃發光。您可以通過正確采樣,使用調整紋理大小的mipmap來解決這些失真問題。
多級紋理Mipmaps
檢測屋頂紋理大小以及它在屏幕中顯示的大小:
?
出現這種模式是因為您采樣的紋素多于像素。理想的情況是,具有相同數量的紋素對應于像素,這意味著對象離得越遠,您需要的紋理就越小。解決方案是使用 mipmap。Mipmap 允許 GPU 比較其深度紋理上的片段,并以合適的大小對紋理進行采樣。
MIP 代表 multum in parvo — 一個拉丁短語,意思是“小而多”。
Mipmap 是按 2 的冪次逐級縮小的紋理貼圖,一直減小到 1 像素大小。如果您的紋理為 64 x 64 像素,則完整的 mipmap 集將包括:
級別 0:64 x 64,1:32 x 32,2:16 x 16,3:8 x 8,4:4 x 4,5:2 x 2,6:1 x 1。
?
在下圖中,頂部的棋盤格子紋理沒有使用mipmap。但在底部圖像中,每個片段都是從適當的 MIP 級別紋理中采樣的。
隨著棋盤格后退,有更少的噪點,圖像也會更清晰。在地平線上,您可以看到純色較小的灰色 mipmap。
?
首次加載紋理時,可以輕松自動生成這些 mipmap。
? 打開 TextureController.swift。在loadTexture(texture:name:)中,將紋理加載選項更改為:
let textureLoaderOptions: [MTKTextureLoader.Option: Any] =[.origin: MTKTextureLoader.Origin.bottomLeft,.generateMipmaps: true]
此代碼將創建 mipmap,一直到最小的像素。
還有一件事需要更改:片段著色器中的紋理采樣器。
? 打開 Fragment.metal,將以下代碼添加到 textureSampler 的構造中:
mip_filter::linear
mip_filter 的默認值為 none。但是,如果您提供 .linear 或 .nearest,則 GPU 將對正確的 mipmap 進行采樣。
? 構建并運行應用程序。
?
建筑物和地面的噪點都消失了。
使用 Capture GPU workload工具,您可以檢查 mipmap。選擇 draw 調用,然后雙擊紋理。
您可以看到所有不同大小的 mipmap 紋理。GPU 將自動加載相應的 mipmap。
資源目錄屬性
也許您感到驚訝,因為您只更改了 USD 紋理加載方法,就看到地面渲染得到了改善。地面是一個圖元平面,您可以從資產目錄中加載其紋理。
? 打開 Textures.xcassets,然后在 Attributes inspector(屬性檢查器)打開的情況下,單擊草地紋理以查看所有紋理選項。
在這里,你可以看到,默認情況下,所有 mipmap 都是自動創建的。如果將 Mipmap Levels (Mipmap 級別) 更改為 Fixed (固定),則可以選擇要創建的級別數。如果您不喜歡自動 mipmap,可以通過將它們拖動到正確的槽位,來將它們替換為您自定義的mipmap。
為正確的工作提供正確的質地
使用資源目錄可以完全控制如何交付紋理。目前,草地只有一種顏色紋理。但是,如果您支持具有不同功能的各種設備,則可能需要為每種情況提供特定的紋理。在 RAM 較少的設備上,您需要更小的圖形。
例如,以下是您可以通過檢查 Apple Watch 以及 sRGB 和 P3 顯示器的 Attributes Inspector 中的不同選項來分配各個紋理的列表。
各向異性
渲染的地面在背景中看起來有點泥濘和模糊。這是由于各向異性造成的。各向異性表面會根據您查看它們的角度而變化,當 GPU 對以傾斜角度投影的紋理進行采樣時,會導致鋸齒。
? 在 Fragment.metal 中,將以下內容添加到 textureSampler 的構造中:
max_anisotropy(8)
Metal 現在將從紋素中獲取 8 個樣本來構建片段。最多可以指定 16 個樣本以提高質量。使用盡可能少的采樣以獲得所需的顯示質量,因為采樣會減慢渲染速度。
注意:如前所述,您可以在 Model 上保留 MTLSamplerState。如果增加各向異性采樣,則可能不希望在所有模型上都這樣做,這可能是在片段著色器之外創建采樣器狀態的一個很好的理由。
? 構建并運行,您的渲染應該是無偽影的。
挑戰
將這兩個紋理添加到資產目錄中,并將 house 和 ground 的當前紋理替換為這些紋理。除了添加紋理之外,您只需按照本章所述更改模型的初始化即可。如果你有任何困難,請查看本章的挑戰文件夾。
參考
https://zhuanlan.zhihu.com/p/394059532
https://zhuanlan.zhihu.com/p/393366147