Metal学习笔记八:纹理

news2025/2/28 5:48:44

到目前为止,您已经学习了如何使用片段函数和着色器为模型添加颜色和细节。另一种选择是使用图像纹理,您将在本章中学习如何作。更具体地说,您将了解:
• 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 进行采样。

挑战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2307223.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Idea 和 Pycharm 快捷键

一、快捷键 二、Pycharm 中怎么切换分支 参考如下 如果在界面右下角 没有看到当前所在的分支&#xff0c;如 “Git:master” 3. 有了 4.

fody引用c++的dll合并后提示找不到

fody引用c的dll合并后提示找不到 解决方案&#xff1a; 在 FodyWeavers.xml 文件中添加配置 CreateTemporaryAssemblies‘true’ 官方文档&#xff1a;https://github.com/Fody/Costura <Weavers xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance" xsi:noN…

初识flutter1

为什么使用flutter, 说直白一点,就是移动UI框架, 个人认为优势是: 1.一套代码adnroid 和ios 可以使用, 代码好维护 2.原生用户界面 3.开源、免费的 开发工具&#xff1a; 1.下载 java 包, 根据自己的电脑配置选择 官网下载: https://www.oracle.com/java/technologies/download…

12字符函数

一、函数strchr与strrchr 注意&#xff1a; 这两个函数的功能&#xff0c;都是在指定的字符串 s 中&#xff0c;试图找到字符 c。strchr() 从左往右找&#xff0c;strrchr() 从右往左找。字符串结束标记 ‘\0’ 被认为是字符串的一部分。 图解&#xff1a; 示例代码&#xff…

QT6开发高性能企业视频会议-8 使用VSCode+Copilot AI开发

Github Copilot是Github和OpenAI推出的AI编程辅助工具&#xff0c;之前版本的Github Copilot只有简单的代码自动补全&#xff0c;根据注释生成一些代码等辅助功能。 近期Copilot有了一次大的升级&#xff0c;加入了Agent模式&#xff0c;可以实现自然语言对话讨论和最重要的&a…

矩阵的奇异值(SVD)分解和线性变换

矩阵的奇异值&#xff08;SVD&#xff09;分解和线性变换 SVD定义 奇异值分解&#xff08;Singular Value Decomposition&#xff0c;简称 SVD&#xff09;是一种重要的线性代数工具&#xff0c;能够将任意矩阵 ( A ∈ R m n \mathbf{A} \in \mathbb{R}^{m \times n} A∈Rmn…

数据库的sql语句

本篇文章主要用来收集项目开发中&#xff0c;遇到的各种sql语句的编写。 1、根据user表的role_id字段&#xff0c;查询role表。 sql语句&#xff1a;使用JOIN连接两个表 SELECT u.*,r.rolename FROM user u JOIN role r ON u.role_id r.id WHERE u.id 1; 查询结果&#xff1a…

Ubuntu2204下使用NVIDIA GeForce RTX 4090进行DeepSeek-R1-Distill-Llama-8B模型微调

Ubuntu2204下使用NVIDIA GeForce RTX 4090进行DeepSeek-R1-Distill-Llama-8B模型微调 环境准备创建Python微调环境准备数据集准备模型文件 模型微调模型预测原始模型预测微调模型预测 使用unsloth&#xff0c;可以方便地对大模型进行微调。以微调DeepSeek-R1-Distill-Llama-8B为…

Bugku CTF Crypto(二)

目录 这不是md5 贝斯家族 把猪困在猪圈里 黄道十二官&#xff08;宫&#xff09; 抄错的字符 这不是md5 描 述: 666c61677b616537333538376261353662616566357d 分 析&#xff1a;题目提示这不是md5&#xff0c;字符中出现了d&#xff0c;猜测16进制 使用…

单片机总结【GPIO/TIM/IIC/SPI/UART】

一、GPIO 1、概念 通用输入输出口&#xff1b;开发者可以根据自己的需求将其配置为输入或输出模式&#xff0c;以实现与外部设备进行数据交互、控制外部设备等功能。简单来说&#xff0c;GPIO 就像是计算机或微控制器与外部世界沟通的 “桥梁”。 2、工作模式 工作模式性质特…

Rt-thread源码剖析(1)——内核对象

前言 该系列基于rtthread-nano的内核源码&#xff0c;来研究RTOS的底层逻辑&#xff0c;本文介绍RTT的内核对象&#xff0c;对于其他RTOS来说也可供参考&#xff0c;万变不离其宗&#xff0c;大家都是互相借鉴&#xff0c;实现不会差太多。 内核对象容器 首先要明确的一点是什…

十一、大数据治理平台总体功能架构

大数据治理平台的功能架构图中心主题&#xff1a;数据治理 核心重点是建立健全大数据资产管理框架&#xff0c;确保数据质量、安全性、可访问性和合规性。 大数据治理平台总体功能架构图 关键功能领域 1.数据资产平台&#xff08;左侧&#xff09; 此部分主要关注数据资产本身…

STM32——HAL库开发笔记23(定时器4—输入捕获)(参考来源:b站铁头山羊)

定时器有四个通道&#xff0c;这些通道既可以用来作为输入&#xff0c;又可以作为输出。做输入的时候&#xff0c;可以使用定时器对外部输入的信号的时间参数进行测量&#xff1b;做输出的时候&#xff0c;可以使用定时器向外输出精确定时的方波信号。 一、输入捕获 的基本原理…

向量数据库milvus部署

官方文档 Milvus vector database documentationRun Milvus in Docker (Linux) | Milvus DocumentationMilvus vector database documentation 按部署比较简单&#xff0c;这里说一下遇到的问题 一&#xff1a;Docker Compose 方式部署 1、镜像无法拉取,(docker.io被禁) …

修改`FSL Yocto Project Community BSP`用到的u-boot源码,使其能适配百问网(100ask)的开发板

前言 在博文 https://blog.csdn.net/wenhao_ir/article/details/145547974 中,我们利用官方提供的BSP(FSL Yocto Project Community BSP)构建了写到SD卡中的完整镜像,然后启动后发现存在不少问题,首要的问题就是u-boot不能识别网卡,在这篇博文中,我们就找到FSL Yocto Pro…

(python)Arrow库使时间处理变得更简单

前言 Arrow库并不是简单的二次开发,而是在datetime的基础上进行了扩展和增强。它通过提供更简洁的API、强大的时区支持、丰富的格式化和解析功能以及人性化的显示,填补了datetime在某些功能上的空白。如果你需要更高效、更人性化的日期时间处理方式,Arrow库是一个不错的选择…

【亲测有效】百度Ueditor富文本编辑器添加插入视频、视频不显示、和插入视频后二次编辑视频标签不显示,显示成img标签,二次保存视频被替换问题,解决方案

【亲测有效】项目使用百度Ueditor富文本编辑器上传视频相关操作问题 1.百度Ueditor富文本编辑器添加插入视频、视频不显示 2.百度Ueditor富文本编辑器插入视频后二次编辑视频标签不显示&#xff0c;在编辑器内显示成img标签&#xff0c;二次保存视频被替换问题 问题1&#xff1…

二、IDE集成DeepSeek保姆级教学(使用篇)

各位看官老爷好&#xff0c;如果还没有安装DeepSeek请查阅前一篇 一、IDE集成DeepSeek保姆级教学(安装篇) 一、DeepSeek在CodeGPT中使用教学 1.1、Edit Code 编辑代码 选中代码片段 —> 右键 —> CodeGPT —> Edit Code, 输入自然语言可编辑代码&#xff0c;点击S…

四、Redis主从复制与读写分离

一、环境搭建 准备环境 IP角色192.168.10.101Master192.168.10.102Slave192.168.10.103Slave 创建配置/数据/日志目录 # 创建配置目录 mkdir -p /usr/local/redis/conf # 创建数据目录 mkdir -p /usr/local/redis/data # 创建日志目录 mkdir -p /usr/local/redis/log修改配置…

数据如何安全“过桥”?分类分级与风险评估,守护数据流通安全

信息化高速发展&#xff0c;数据已成为企业的核心资产&#xff0c;驱动着业务决策、创新与市场竞争力。随着数据开发利用不断深入&#xff0c;常态化的数据流通不仅促进了信息的快速传递与共享&#xff0c;还能帮助企业快速响应市场变化&#xff0c;把握商业机遇&#xff0c;实…