Metal學習筆記八:紋理

到目前為止,您已經學習了如何使用片段函數和著色器為模型添加顏色和細節。另一種選擇是使用圖像紋理,您將在本章中學習如何操作。更具體地說,您將了解:
? 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

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

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

相關文章

Dify使用和入門

第一步&#xff1a;了解 Dify 在開始之前&#xff0c;先簡單了解一下 Dify 是什么&#xff1a; Dify 是一個開源的 LLM 應用開發平臺&#xff0c;專注于幫助開發者快速構建生產級的生成式 AI 應用。它支持知識庫集成、RAG&#xff08;檢索增強生成&#xff09;技術、復雜工作…

threeJS——安裝以及三要素

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 前言一、安裝二、三要素1.場景1.1創建場景1.2向場景添加元素1.3場景屬性 2.相機2.1相機特點2.2正交相機2.3空間布局2.4小姐操作 3.渲染器 總結 前言 本章簡單介紹前…

畢業項目推薦:基于yolov8/yolo11的野生菌菇檢測識別系統(python+卷積神經網絡)

文章目錄 概要一、整體資源介紹技術要點功能展示&#xff1a;功能1 支持單張圖片識別功能2 支持遍歷文件夾識別功能3 支持識別視頻文件功能4 支持攝像頭識別功能5 支持結果文件導出&#xff08;xls格式&#xff09;功能6 支持切換檢測到的目標查看 二、數據集三、算法介紹1. YO…

【精華】為什么class在前端開發中不常用?

為什么class在前端開發中不常用&#xff1f; js是一種基于原型的語言。它的對象繼承是通過 原型鏈&#xff08;prototype chain&#xff09;實現的&#xff0c;每個對象都有一個 proto 屬性指向它的原型。&#xff08;大多數傳統面向對象語言&#xff08;如 Java、C、Python、…

【六祎 - Note】SQL備忘錄;DDL,DML,DQL,DCL

SQL備忘錄 from to : 點擊訪問源地址

阿里云物聯網獲取設備屬性api接口:QueryDevicePropertyData

阿里云物聯網接口&#xff1a;QueryDevicePropertyData 說明&#xff1a;調用該接口查詢指定設備或數字孿生節點&#xff0c;在指定時間段內&#xff0c;單個屬性的數據 比如提取上傳到物聯網的溫度數據 api文檔&#xff1a;QueryDevicePropertyData_物聯網平臺_API文檔-阿里…

需求和開發模型

文章目錄 什么是需求&#xff1f;用戶需求軟件需求用戶需求和軟件需求的不同 開發模型什么是“模型”&#xff1f;軟件的生命周期常見的開發模型瀑布模型&#xff08;Waterfall Model&#xff09;螺旋模型增量模型、迭代模型敏捷模型 測試模型V 模型W 模型&#xff08;雙 V 模型…

21-發糖果

n 個孩子站成一排。給你一個整數數組 ratings 表示每個孩子的評分。 你需要按照以下要求&#xff0c;給這些孩子分發糖果&#xff1a; 每個孩子至少分配到 1 個糖果。 相鄰兩個孩子評分更高的孩子會獲得更多的糖果。 請你給每個孩子分發糖果&#xff0c;計算并返回需要準備的 最…

sql深入學習

文章目錄 前言知識學習注釋的兩種形式字符型注入萬能密碼 布爾盲注報錯注入堆疊注入時間盲注二次注入 小技巧 前言 這次學習建立在對數據庫有基本的認識&#xff0c;了解基礎的增刪改查語句&#xff0c;數字型注入和字符型注入的基礎上&#xff0c;進一步深入學習知識&#xf…

利用three.js在Vue項目中展示重構的stl模型文件

一、目的 為了在前端頁面展示3d打印機打印過程 二、前期準備 完整模型的stl文件和模型切割成的n個stl文件 models文件夾下的文件就是切割后的stl文件 三、代碼 <template><div ref"threeContainer" class"three-container"></div><…

【Eureka 緩存機制】

今天簡單介紹一下Eureka server 的緩存機制吧?????? 一、先來個小劇場&#xff1a;服務發現的"拖延癥" 想象你是個外賣小哥&#xff08;客戶端&#xff09;&#xff0c;每次接單都要打電話問調度中心&#xff08;Eureka Server&#xff09;&#xff1a;“現在…

Python--內置模塊和開發規范(下)

2. 開發規范 2.1 單文件應用 文件結構示例 # 文件注釋 import os import jsonDB_PATH "data.json" # 常量放頂部def load_data():"""函數注釋&#xff1a;加載數據"""if os.path.exists(DB_PATH):with open(DB_PATH, "r"…

go設計模式

劉&#xff1a;https://www.bilibili.com/video/BV1kG411g7h4 https://www.bilibili.com/video/BV1jyreYKE8z 1. 單例模式 2. 簡單工廠模式 代碼邏輯&#xff1a; 原始&#xff1a;業務邏輯層 —> 基礎類模塊工廠&#xff1a;業務邏輯層 —> 工廠模塊 —> 基礎類模塊…

搭建數字化生態平臺公司:痛點與蚓鏈解決方案

在數字技術突飛猛進的當下&#xff0c;數字化生態平臺成為眾多企業實現創新發展、拓展業務版圖的 “秘密工具”。今天&#xff0c;咱們就一起來聊聊搭建這類平臺的公司&#xff0c;看看它們有啥獨特之處&#xff0c;又面臨哪些難題。 一、面臨的痛點 &#xff08;一&#xff0…

標記符號“<”和“>”符號被稱為“尖括號”或“角括號”

你提到的“<”和“>”符號被稱為“尖括號”或“角括號”。它們常用于編程語言中表示類型參數&#xff08;如泛型&#xff09;、HTML標簽&#xff08;如<div>&#xff09;、數學中的不等式&#xff08;如< 5&#xff09;等。 好的&#xff0c;我來用通俗的方式解…

云平臺DeepSeek滿血版:引領AI推理革新,開啟智慧新時代

引言&#xff1a;人工智能的未來——云平臺的卓越突破 在當今科技飛速發展的時代&#xff0c;人工智能&#xff08;AI&#xff09;技術正深刻地改變著我們生活與工作方式的方方面面。作為AI領域的創新者與領航者&#xff0c;云平臺始終走在技術前沿&#xff0c;憑借無窮的熱情…

自然語言處理:文本規范化

介紹 大家好&#xff01;很高興又能在這兒和大家分享自然語言處理相關的知識了。在上一篇發布于自然語言處理&#xff1a;初識自然語言處理-CSDN博客為大家初步介紹了自然語言處理的基本概念。而這次&#xff0c;我將進一步深入這個領域&#xff0c;和大家聊聊自然語言處理中一…

HTTP非流式請求 vs HTTP流式請求

文章目錄 HTTP 非流式請求 vs 流式請求一、核心區別 服務端代碼示例&#xff08;Node.js/Express&#xff09;非流式請求處理流式請求處理 客戶端請求示例非流式請求&#xff08;瀏覽器fetch&#xff09;流式請求處理&#xff08;瀏覽器fetch&#xff09; Python客戶端示例&…

C語言機試編程題

編寫版本&#xff1a;vc2022 1.求最大/小值 #include<stdio.h> int main(){int a[50],n;int max, min;printf("請輸入您要輸入幾個數");scanf_s("%d", &n);printf("請輸入您要比較的%d個數\n",n);for (int i 0; i<n; i) {scanf_…

c++ 多個.cpp文件運行

目錄 方法 1&#xff1a;將其他文件中的 main 改為普通函數 方法 2&#xff1a;使用頭文件組織代碼 方法 3&#xff1a;條件編譯&#xff08;僅用于調試或特殊需求&#xff09; 方法 4&#xff1a;創建類或命名空間管理邏輯 在一個C項目中&#xff0c;多個.cpp文件不能同…