到目前为止,您已经学习了如何使用片段函数和着色器为模型添加颜色和细节。另一种选择是使用图像纹理,您将在本章中学习如何作。更具体地说,您将了解:
• UV 坐标:如何展开网格,以便可以对其应用纹理。
• 纹理化模型:如何读取片段着色器中的纹理。
• 资产目录:如何组织纹理。
• 采样器:读取 (采样) 纹理的不同方式。
• Mipmaps:多级纹理,以便纹理分辨率与显示大小匹配并占用更少的内存。
纹理和UV映射
下图显示了一个有12个顶点的房子模型,左边是线框(显示了顶点),右边是纹理映射好的模型。
注意:如果您想更仔细地了解此模型,您可以在本章的 resources/LowPolyHouse 文件夹中找到 Blender 和纹理文件。
要为模型添加纹理,您首先必须使用称为 UV 展开的过程来展平该模型。UV 展开通过展开模型来创建 UV 贴图。要展开模型,您需要使用建模应用程序标记和切割接缝。下图显示了在 Blender 中 UV 展开房屋模型并导出其 UV 贴图的结果。
请注意,屋顶和墙壁有明显的接缝。接缝使该模型可以平躺。如果您打印并剪下此 UV 贴图,则可以轻松地将其折叠回房屋中。在 Blender 中,您可以完全控制接缝以及如何切割网格。Blender 通过在这些接缝处切割网格来自动展开模型。如有必要,您还可以在 UV 展开 (UV Unwrap) 窗口中移动顶点以适合您的纹理。
现在你已经有一个扁平的贴图,你可以使用从 Blender 导出的 UV 贴图作为指南来 “绘制” 到它上面。下图显示了通过剪切真实房屋的照片创建的房屋纹理(在 Photoshop 中制作)。
请注意,纹理的边缘并不完美,并且可以看到版权信息。在地图上没有顶点的空间中,您可以添加任何您想要的内容,因为它不会显示在模型上。
然后,您将该图像导入 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 将模型 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 文件。请务必将新文件包含在目标中。将代码替换为:
import MetalKit
enum TextureController {
static var textures: [String: MTLTexture] = [:]
}
TextureController 将获取模型使用的纹理,并将它们保存在此字典中。
➤ 为 TextureController 添加新方法:
static func loadTexture(texture: MDLTexture, name: String) ->
MTLTexture? {
// 1
if let texture = textures[name] {
return texture
}
// 2
let textureLoader = MTKTextureLoader(device: Renderer.device)
// 3
let textureLoaderOptions: [MTKTextureLoader.Option: Any] =
[.origin: MTKTextureLoader.Origin.bottomLeft]
// 4
let texture = try? textureLoader.newTexture(
texture: texture,
options: textureLoaderOptions)
print("loaded texture from USD file")
// 5
textures[name] = texture
return texture
}
此方法将接收模型 I/O 纹理,并返回准备渲染的 MetalKit 纹理。
遍历代码:
1. 如果纹理已加载到纹理中,则返回该纹理。请注意,您是按名称加载纹理的,因此您的艺术家必须确保模型没有冲突的名称。
2. 使用 MetalKit 的 MTKTextureLoader 创建纹理加载器。
3. 更改纹理的原点选项,以确保纹理加载时其原点位于左下角。如果没有此选项,纹理将无法正确包裹房屋。
4. 使用提供的纹理和加载器选项创建新的 MTLTexture。出于调试目的,请打印一条消息。
5. 将纹理添加到纹理并返回它。
注意:加载纹理可能会变得复杂。当金属首次释放时,您必须使用mtlTextredScriptor指定有关图像的所有内容,例如像素格式,尺寸和用法。但是,使用Metalkit的MtkTextuReloDADER,您可以使用所提供的默认值并根据需要进行选择。
加载 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 提供语义以检索相关 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的金属功能套装表(https://papple.co/2upct8r)中找到设备的功能可用性。
3.更新片段功能
➤打开fragment.metal,并在[[stage_in]]中的vertexout之后,立即将以下新参数添加到fragment_main:
texture2d<float> baseColorTexture [[texture(BaseColor)]]
您现在可以访问GPU上的纹理。
➤用以下方式替换Fragment_Main中的所有代码
constexpr sampler textureSampler;
当您阅读或采样纹理时,您可能不会精确地降落在特定的像素上。在纹理空间中,您采样的单元被称为texels,您可以决定使用采样器处理每个Texel。您很快就会了解有关采样器的更多信息。
➤ 接下来,添加这个:
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 并确保您理解代码。
模型 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)
此代码放大了 ground plane。原始位置的平面是垂直的,因此您可以在 z 轴上将其旋转 90 度,然后在 y 轴上旋转它以匹配房屋的旋转。然后渲染地平面。
➤ 构建并运行应用程序以查看您的接地平面。
目前,地面没有纹理或颜色,但您很快就会通过从资产目录中加载纹理来解决此问题。
资源目录asset catalog
当您编写完整的游戏时,您可能会为不同的型号具有许多纹理。如果使用美元格式模型,通常将包括纹理。但是,您可能会使用不具有纹理的不同文件格式,并且组织这些纹理可能会变成劳动力密集型。另外,您还需要压缩图像,并向不同的设备发送不同尺寸和颜色域的纹理。资产目录是您将转向的地方。
顾名思义,资产目录可以持有您的所有资产,无论它们是数据,图像,纹理甚至颜色。您可能已将目录用于应用程序图标和图像。纹理与图像不同,因为GPU使用它们,因此它们在目录中具有不同的属性。要创建纹理,请在资产目录中添加一个新的纹理设置。
➤使用资产目录模板(在资源部分找到)创建一个新文件,并将其命名为纹理。请记住将其添加到目标中。
➤使用Textures.XCASSET打开,选择编辑器▸添加新资产▸AR和纹理▸纹理集(或单击面板底部的 + +,然后选择AR和纹理▸纹理集)。
➤重命名新的质地草。
➤打开本章的资源文件夹,然后将drail drail.png拖到目录中的通用插槽。
注意:请小心将图像放在纹理的通用插槽上。如果将图像拖到资产目录中,则默认情况下它们是图像而不是纹理。稍后您将无法更改任何纹理属性。
您需要向纹理控制器添加另一个方法,以便从资源目录中加载命名纹理。
➤ 打开 TextureController.swift,并向 TextureController 添加一个新方法:
static func loadTexture(name: String) -> MTLTexture? {
// 1
if let texture = textures[name] {
return texture
}
// 2
let textureLoader = MTKTextureLoader(device: Renderer.device)
let texture: MTLTexture?
texture = try? textureLoader.newTexture(
name: name,
scaleFactor: 1.0,
bundle: Bundle.main,
options: nil)
// 3
if 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 = texture
default: break
}
}
}
}
此方法加载纹理并将其分配给模型的第一个子网格。
注意:这是分配纹理的快速简便方法。它仅适用于仅使用一种材料的简单模型。如果您经常从资源目录加载子网格纹理,则应设置指向正确纹理的子网格初始化器。
最后要做的是将纹理设置在地面平面上.打开 Renderer.swift,并将地面的声明替换为:
lazy var ground: Model = {
let ground = Model(name: "ground", primitiveType: .plane)
ground.setTexture(name: "grass", type: BaseColor)
return ground
}()
在加载模型后,从资产目录中加载草地纹理并将其分配给地面平面.构建并运行应用程序以查看茂盛的绿色草地:
这看起来是个问题。草地比原始纹理要暗得多,而且被拉伸和像素化。
sRGB颜色空间
渲染的纹理看起来比原始图像要深得多,因为地面.png是SRGB纹理。 SRGB是一种标准的颜色格式,在阴极射线管监视的工作方式和人眼看到的颜色之间妥协。如下面的灰度值从0到1的示例,SRGB颜色不是线性的。人类比较暗的值更有能力辨别较轻的值。
不幸的是,在非线性空间中的颜色上进行数学并不容易。如果将颜色乘以0.5使其变暗,则SRGB的差异会随刻度而变化。
目前,您正在将草纹理加载为SRGB像素数据,并将其渲染到线性色彩空间中。因此,当您采样一个值为0.2的值时,在SRGB空间中是中间灰色时,线性空间将读为深灰色。
要大致转换颜色,您可以使用伽马2.2的倒数:
sRGBcolor = pow(linearColor, 1.0/2.2);
如果您在从片段功能返回之前在底座上使用此公式,则您的草纹理将与原始的SRGB纹理相同,但是将其纹理洗净,因为它正在加载在非SRGB颜色空间中。
解决此问题的另一种方法是更改视图的颜色像素格式。
➤打开渲染器。swift,然后在INIT(MetalView :)中找到MetalView.device =
设备。在此代码之后,添加:
metalView.colorPixelFormat = .bgra8Unorm_srgb
在这里,您可以将视图的像素格式从默认的bgra8unorm更改为在SRGB和线性空间之间转换的格式。
➤ 构建并运行应用程序。
使用 sRGB 颜色像素格式的视图
草地颜色现在好多了,但您的非 sRGB 房屋纹理被冲淡了。
➤ 撤消您刚刚输入的代码:
metalView.colorPixelFormat = .bgra8Unorm_srgb
GPU抓帧
有一种简单的方法可以找出纹理在 GPU 上的格式,还可以查看当前驻留在其中的所有其他 Metal 缓冲区:Capture GPU 工作负载工具(也称为 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可绘制:颜色附件0中编码的结果。在这种情况下,这是视图的当前绘制。稍后,您将使用多种颜色附件。
•mtkView深度:深度缓冲区。黑色更近。白色更远。 Rasterizer使用深度图。
➤控制单击纹理0,然后从弹出菜单中选择获取信息。
像素格式为rgba8unorm,而不是SRGB。
➤在调试导航器中,在第17步中单击第二个drawIndexedPrimitimives命令。再次,请单击“单击草纹理”,然后从弹出菜单中选择获取信息。
这次的像素格式是rgba8unorm_srgb。
如果您对应用程序中发生的情况不确定,则捕获GPU框架可能会引起您的注意,因为您可以检查每个渲染编码器命令和每个缓冲区。在本书中使用此策略来检查GPU上发生的事情是一个好主意。
现在,用不匹配的纹理恢复您的问题。解决此问题的另一种方法是完全不将资产目录纹理加载为SRGB。
打开纹理。xcassets,单击草纹理,在属性检查员中,将解释更改为数据:
当您的应用程序将 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 = 16
return ground
}()
现在,您正在将模型的平铺因子发送到 fragment 函数。
➤ 打开 Fragment.metal。在 fragment_main 中,将 baseColor 的声明替换为:
float3 baseColor = baseColorTexture.sample(
textureSampler,
in.uv * params.tiling).rgb;
➤构建并运行该应用程序,您会发现地面和房屋现在都正确地瓷砖了。
注意:在着色器中创建采样器并不是唯一的选择。您可以创建一个MTLSamplerState,用模型持有它,然后使用[[Sampler(n)]]属性将采样器状态发送到片段函数。
随着场景的旋转,您会发现一些分散注意力的噪音。您已经看到过度样品质地时在草地上发生了什么。但是,当您调解纹理时,您可以得到一个被称为Moiré的渲染文物,该文物发生在房屋的屋顶上。
此外,地平线上的噪音几乎看起来好像草在闪闪发光。您可以通过使用称为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 进行采样。