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.rhkb.cn/news/25470.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringSecurity的核心过滤器-CsrfFilter

Spring Security除了认证授权外功能外,还提供了安全防护功能。本文我们来介绍下SpringSecurity中是如何阻止CSRF攻击的。 一、什么是CSRF攻击 跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF…

《当齐天大圣踏入3A游戏世界:黑神话·悟空的破壁传奇》:此文为AI自动生成

国产 3A 游戏的破晓之光 2024 年 8 月 20 日,这一天注定被铭记在中国游戏发展的史册上。国产首款 3A 游戏《黑神话・悟空》震撼上线,犹如一颗重磅炸弹,在全球游戏市场掀起了惊涛骇浪。仅仅上线 3 小时,其同时在线人数便突破了 140 万,一举打破 Steam 纯单机游戏最高在线纪…

rust 前端npm依赖工具rsup升级日志

rsup是使用 rust 编写的一个前端 npm 依赖包管理工具&#xff0c;可以获取到项目中依赖包的最新版本信息&#xff0c;并通过 web 服务的形式提供查看、升级操作等一一系列操作。 在前一篇文章中&#xff0c;记录初始的功能设计&#xff0c;自己的想法实现过程。在自己的使用过…

【备赛】点亮LED

LED部分的原理图 led前面有锁存器&#xff0c;这是为了防止led会受到lcd的干扰&#xff08;lcd也需要用到这些引脚&#xff09;。 每次想要对led操作&#xff0c;就需要先打开锁存器&#xff0c;再执行操作&#xff0c;最后关闭锁存器。 这里需要注意的是&#xff0c;引脚配置…

CSS 使用white-space属性换行

一、white-space属性的常见值 * 原本格式&#xff1a; 1、white-space:normal 默认值&#xff0c;空格和换行符会被忽略过滤掉&#xff1b;宽度不够时文本会自动换行 * 宽度足够时&#xff0c;normal 处理后的格式 * 宽度不够时&#xff0c; normal 处理后的格式 2、white-spa…

electron-builder打包时github包下载失败【解决办法】

各位朋友们&#xff0c;在使用electron开发时&#xff0c;选择了electron-builder作为编译打包工具时&#xff0c;是否经常遇到无法从github上下载依赖包问题&#xff0c;如下报错&#xff1a; Get "https://github.com/electron/electron/releases/download/v6.1.12/ele…

【WSL2】 Ubuntu20.04 GUI图形化界面 VcXsrv ROS noetic Vscode 主机代理 配置

【WSL2】 Ubuntu20.04 GUI图形化界面 VcXsrv ROS noetic Vscode 主机代理 配置 前言整体思路安装 WSL2Windows 环境升级为 WIN11 专业版启用window子系统及虚拟化 安装WSL2通过 Windows 命令提示符安装 WSL安装所需的 Linux 发行版&#xff08;如 Ubuntu 20.04&#xff09;查看…

2025学年安徽省职业院校技能大赛 “信息安全管理与评估”赛项 比赛样题任务书

2024-2025 学年广东省职业院校技能大赛 “信息安全管理与评估”赛项 技能测试试卷&#xff08;五&#xff09; 第一部分&#xff1a;网络平台搭建与设备安全防护任务书第二部分&#xff1a;网络安全事件响应、数字取证调查、应用程序安全任务书任务1 &#xff1a;内存取证&…

数据库导出

MySQL数据库 使用命令行导出 导出整个数据库&#xff1a;在命令行中输入mysqldump -u用户名 -p密码 数据库名 > 导出文件路径/文件名.sql。例如mysqldump -uroot -p123456 mydb > /home/user/mydb_backup.sql&#xff0c;回车后输入密码即可将名为mydb的数据库导出为SQL…

OpenCV给图像添加噪声

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 如果你已经有了一张干净的图像&#xff0c;并希望通过编程方式向其添加噪声&#xff0c;可以使用 OpenCV 来实现这一点。以下是一个简单的例子&a…

OSPF BIT 类型说明

注&#xff1a;本文为 “OSPF BIT 类型 | LSA 类型 ” 相关文章合辑。 机翻&#xff0c;未校。 15 OSPF BIT Types Explained 15 种 OSPF BIT 类型说明 Rashmi Bhardwaj Distribution of routing information within a single autonomous system in larger networks is per…

Linux网络之传输层协议(UDP,TCP协议)

目录 重新认识端口号 端口号划分 netstat pidof UDP协议 UDP的特点 面向数据报 UDP的缓冲区 全双工和半双工 TCP协议 TCP的特点 TCP报头分析 源端口&#xff0c;目标端口&#xff0c;数据偏移(报文首部长度) 序号 确认号 窗口 6个标志位 ACK SYN …

Spring Boot 热部署

文章目录 一&#xff0c;Spring Boot热部署概述二&#xff0c;对项目HelloWorld01进行热部署 1、添加开发工具依赖2、热部署配置3、热部署测试 一&#xff0c;Spring Boot热部署概述 在开发过程中&#xff0c;通常会对一段业务代码不断地修改测试&#xff0c;在修改之后往往…

【前端基础】Day 3 CSS-2

目录 1. Emmet语法 1.1 快速生成HTML结构语法 1.2 快速生成CSS样式语法 2. CSS的复合选择器 2.1 后代选择器 2.2 子选择器 2.3 并集选择器 2.4 伪类选择器 2.4.1 链接伪类选择器 2.4.2 focus伪类选择器 2.5 复合选择器总结 3. CSS的元素显示模式 3.1 什么是元素显示…

使用vscode导出Markdown的PDF无法显示数学公式的问题

我的硬件环境是M2的MacBook air&#xff0c;在vscode中使用了Markdown PDF来导出md文件对应的PDF。但不管导出html还是PDF文件&#xff0c;数学公式都是显示的源代码。 我看了许多教程&#xff0c;给的是这个方法&#xff1a;在md文件对应的html文件中加上以下代码&#xff1a…

去耦电容的作用详解

在霍尔元件的实际应用过程中&#xff0c;经常会用到去耦电容。去耦电容是电路中装设在元件的电源端的电容&#xff0c;其作用详解如下&#xff1a; 一、基本概念 去耦电容&#xff0c;也称退耦电容&#xff0c;是把输出信号的干扰作为滤除对象。它通常安装在集成电路&#xf…

[原创]openwebui解决searxng通过接口请求不成功问题

openwebui 对接 searxng 时 无法查询到联网信息&#xff0c;使用bing搜索&#xff0c;每次返回json是正常的 神秘代码&#xff1a; http://172.30.254.200:8080/search?q北京市天气&formatjson&languagezh&time_range&safesearch0&languagezh&locale…

【JavaSE-1】初识Java

1、Java 是什么? Java 是一种优秀的程序设计语言,人类和计算机之间的交流可以借助 Java 这种语言来进行交流,就像人与人之间可以用中文、英语,日语等进行交流一样。 Java 和 JavaScript 两者有关系吗? 一点都没有关系!!! 前端内容:HTML CSS JS,称为网页三剑客 2、JDK 下…

C++知识整理day10——多态(多态的定义和实现、虚函数重写/覆盖、override和final关键字、纯虚函数和抽象类、多态的原理)

文章目录 1.多态的概念2.多态的定义和实现2.1 多态的构成条件2.2 多态必须具备的两个条件&#xff08;很重要&#xff09;2.3 虚函数2.4 虚函数的重写/覆盖2.5 协议&#xff08;了解即可&#xff09;2.6 析构函数的重写2.6 override和final关键字2.7 重载/重写/隐藏的对比 3.纯…

BladeX框架接口请求跨域

前端使用代理请求接口&#xff0c;接口可以正常访问。如果换全路径请求就跨域。 除了后端要配置跨域 还需要修改配置文件对OPTIONS请求的限制