当前位置: 首页 > news >正文

Metal 学习笔记四:顶点函数

到目前为止,您已经完成了 3D 模型和图形管道。现在,是时候看看 Metal 中两个可编程阶段中的第一个阶段,即顶点阶段,更具体地说,是顶点函数。

着色器函数

定义着色器函数时,可以为其指定一个属性。您将在本书中学到这些属性:
• vertex:顶点函数:计算顶点的位置。
• fragment: 片段函数: 计算片段的颜色。
• kernel:内核功能:用于通用的并行计算,例如图像处理。

在本章中,您将只关注 vertex 函数。在第 7 章 “片段函数”中,您将探索如何控制每个片段的颜色。在第 16 章 “GPU 计算编程” 中,您将了解如何使用具有多个线程的并行编程来写入缓冲区和纹理。

到目前为止,您应该已经熟悉顶点描述符,以及如何使用它们来描述如何从加载的 3D 模型中排列顶点属性。回顾一下:

• MDLVertexDescriptor:使用Model I/O 顶点描述符读取 USD 文件。Model I/O 会创建缓冲区,缓冲区会按我们所需的布局,存放属性值,例如位置、法线和纹理坐标。

• MTLVertexDescriptor:在创建管线状态时使用 Metal 顶点描述符。GPU 顶点函数使用 [[stage_in]] 属性将传入数据与管线状态中的顶点描述符进行匹配。

在学习本章时,您将在不使用顶点描述符情况下,构建自己的顶点网格并将顶点发送到 GPU。您将学习如何在顶点函数中控制这些顶点,然后升级到使用顶点描述符。在此过程中,您将看到如何使用 Model I/O 导入网格,从而为您完成许多繁重的工作。

开始项目

➤ 打开本章的初始项目。
此 SwiftUI 项目包含一个简化的 Renderer,以便您可以添加自己的网格,并且着色器函数返回固定的值,因此您可以构建它们。您尚未进行任何绘图,因此在运行应用程序时看不到任何内容。

渲染一个四边形

您可以使用两个三角形创建一个四边形。每个三角形有 3 个顶点,总共有 6 个顶点。

➤ 创建一个名为 Quad.swift 的新 Swift 文件。

➤ 将现有代码替换为:

import MetalKit
struct Vertex {var x: Floatvar y: Floatvar z: Float
}
struct Quad {var vertices: [Vertex] = [Vertex(x: -1, y:  1, z: 0),Vertex(x:  1, y: -1, z: 0),Vertex(x: -1, y: -1, z: 0),Vertex(x: -1, y:  1, z: 0),Vertex(x:  1, y:  1, z: 0),Vertex(x:  1, y: -1, z: 0)
] }
// triangle 1
// triangle 2

您可以创建一个结构体来组成一个具有 x、y 和 z 值的顶点。在这里,顶点的环绕顺序 (顶点顺序) 是顺时针方向的,这很重要。

➤ 向 Quad 添加新的顶点缓冲区property并初始化它:

let vertexBuffer: MTLBuffer
init(device: MTLDevice, scale: Float = 1) {vertices = vertices.map {Vertex(x: $0.x * scale, y: $0.y * scale, z: $0.z * scale)}guard let vertexBuffer = device.makeBuffer(bytes: &vertices,length: MemoryLayout<Vertex>.stride * vertices.count,options: []) else {fatalError("Unable to create quad vertex buffer")
}self.vertexBuffer = vertexBuffer
}

 使用此代码,您可以使用顶点数组初始化 Metal 缓冲区。将每个顶点乘以 scale,这样就可以在初始化期间设置四边形的大小。

➤ 打开 Renderer.swift,并为 quad 网格添加一个新property:

 lazy var quad: Quad = {Quad(device: Self.device, scale: 0.8)
}()

在这里,您将使用 Renderer 的设备初始化 quad。您必须延迟初始化 quad,因为在运行 init(metalView:) 之前,device 不会初始化。您还可以调整四边形的大小,以便可以清楚地看到它。

注意:如果您将比例保持在默认的1.0下,四边形将覆盖整个屏幕。覆盖屏幕对于全屏绘图很有用,因为您只能在渲染几何图形的区域绘制片段。

在 draw(in:) 中,在 // do drawing here 之后,添加:

renderEncoder.setVertexBuffer(quad.vertexBuffer,offset: 0,index: 0)

您在渲染命令编码器上创建一个命令,将顶点缓冲区在缓冲区参数表中的索引设置为 0。

➤ 添加 draw 调用:

renderEncoder.drawPrimitives(type: .triangle,vertexStart: 0,vertexCount: quad.vertices.count)

 在这里,您将绘制四边形的六个顶点。

➤ 打开 Shaders.metal。
➤ 将 vertex 函数替换为:

vertex float4 vertex_main(constant float3 *vertices [[buffer(0)]],uint vertexID [[vertex_id]])
{float4 position = float4(vertices[vertexID], 1);return position;
}

此代码存在错误,您将很快观察并修复该错误。

GPU 为每个顶点执行顶点函数。在绘制调用中,您指定了有6个顶点。因此,顶点函数将执行六次。

将指针传递到 vertex 函数时,必须指定地址空间,constant 或 device。constant 经过优化,可在多个顶点函数上并行访问同一变量。device 最适合通过并行函数访问缓冲区的不同部分,例如使用交错顶点和颜色数据的缓冲区时。

[[vertex_id]] 是一个属性限定符,它为您提供当前顶点。您可以将其用作访问vertices 持有数组的入口。

您可能会注意到,您正在向 GPU 发送一个缓冲区,其中填充了一个 Vertexs 数组,该数组由 3 个 Float 组成。在顶点函数中,您读取的缓冲区与 float3 数组相同,从而导致显示错误。

尽管您可能会获得不同的渲染,但顶点位于错误的位置,因为 float3 类型比具有三个 Float 类型成员的 Vertex 占用更多的内存。Float 长 4 字节,Vertex 长 12 字节。SIMD float3 类型是被填充、字节对齐的,占用与 float4 类型相同的内存,即 16 字节。将此参数更改为 packed_float3 将修复错误,因为 packed_float3占用 12 个字节。

注意:您可以在https://apple.co/2UT993x查看Metal着色语言规范中类型的大小。

在 vertex 函数中,将第一个参数中的 float3 改为 packed_float3。

编译并运行。

四边形现在显示正确了。或者,您可以将 Float 数组顶点定义为 simd_float3 数组。在这种情况下,您将在顶点函数中使用 float3,因为这两种类型都需要 16 个字节。但是,每个顶点发送 16 个字节的效率略低于每个顶点发送 12 个字节的效率。

计算位置

Metal不但支持绚丽的色彩,也支持快速平滑的动画。在下一步,我们会让我们的四边形上下移动。为了做到这个,我们需要一个计时器,每帧都更新四边形的位置。顶点shader函数就是我们更新顶点位置的地方,我们会发送计时器数据到GPU。

在Renderer的头部,添加如下属性:

var timer: Float = 0

然后在draw(in:), 在这一行前面

renderEncoder.setRenderPipelineState(pipelineState)

添加如下代码:

// 1
timer += 0.05
var currentTime = sin(timer)
// 2
renderEncoder.setVertexBytes(&currentTime,length: MemoryLayout<Float>.stride,index: 11)

1,每帧都更新计时器,如果你希望你的四边形上下移动,你需要使用一个在-1和1之间的值,使用sin()函数是一个很好的限制值在-1到1之间的方法。你可以通过更改每帧中给timer增加的值,来更改动画的速度。

2,如果你发送少量的数据(小于4kb)给GPU,setVertexBytes(_:length:index:)是一个创建MTLBuffer的较好选择。这里你将currentTime设置给缓冲参数表中索引为11的缓冲区。为顶点属性(例如顶点位置)保留缓冲区 1 到 10 有助于记住哪些缓冲区保存哪些数据。

在Shader.metal,把vertex_main函数改成这样:

vertex float4 vertex_main(constant packed_float3 *vertices [[buffer(0)]],constant float &timer [[buffer(11)]],uint vertexID [[vertex_id]])
{float4 position = float4(vertices[vertexID], 1);position.y += timer;return position;
}

您在缓冲区 11 中以浮点数的形式接收单个值timer。您将 timer 值添加到position.y,并从函数返回新位置。

在下一章中,您将开始学习如何使用矩阵乘法将顶点投影到 3D 空间中。但是,您并不总是需要矩阵乘法来移动顶点;在这里,您可以使用简单的加法来实现沿着Y 轴平移。

➤ 构建并运行应用程序,您将看到一个可爱的动画四边形。

更高效的渲染

目前,您正在使用 6 个顶点来渲染两个三角形。

在这些顶点中,0 和 3 位于同一位置,1 和 5 也是如此。如果您渲染具有数千个甚至数百万个顶点的网格,则尽可能减少重复是非常重要的。您可以使用索引渲染来实现。

仅为不同的顶点位置创建结构体,然后使用 indices 获取顶点的正确位置。

➤ 打开 Quad.swift,并将顶点重命名为 oldVertices。

➤ 将以下结构添加到 Quad:

var vertices: [Vertex] = [Vertex(x: -1, y:  1, z: 0),Vertex(x:  1, y:  1, z: 0),Vertex(x: -1, y: -1, z: 0),Vertex(x:  1, y: -1, z: 0)
]
var indices: [UInt16] = [0, 3, 2,
0, 1, 3 ]

vertices 现在以任意顺序保存四边形的唯一四个点。indices 以正确的顶点顺序保存每个顶点的索引。请参阅 oldVertices 以确保您的索引正确无误。 

➤ 添加新的 Metal 缓冲区来保存索引:

let indexBuffer: MTLBuffer

在 init(device:scale:) 的末尾,添加:

guard let indexBuffer = device.makeBuffer(bytes: &indices,length: MemoryLayout<UInt16>.stride * indices.count,options: []) else {fatalError("Unable to create quad index buffer")
}
self.indexBuffer = indexBuffer

 创建索引缓冲区的方式与创建顶点缓冲区的方式相同。

➤ 打开 Renderer.swift,在 draw(in:) 中,在 draw 调用之前,添加:

renderEncoder.setVertexBuffer(quad.indexBuffer,offset: 0,index: 1)

在这里,您将索引缓冲区发送到 GPU。

➤ 将 draw 调用更改为: 

renderEncoder.drawPrimitives(type: .triangle,vertexStart: 0,vertexCount: quad.indices.count)

使用索引计数来表示要渲染的顶点数;而不是顶点计数。

➤ 打开 Shaders.metal,并将顶点函数更改为: 

vertex float4 vertex_main(constant packed_float3 *vertices [[buffer(0)]],constant ushort *indices [[buffer(1)]],constant float &timer [[buffer(11)]],uint vertexID [[vertex_id]])
{ushort index = indices[vertexID];float4 position = float4(vertices[index], 1);return position;
}

此处,vertexID 是缓冲区中的索引,该缓冲区保存了四边形的索引。使用索引缓冲区中的值,在顶点缓冲区中正确索引顶点。 

➤ 构建并运行。

当然,你的四边形的位置与以前相同,但现在你向 GPU 发送的数据更少。

从数组中的条目数量来看,您实际上似乎在发送更多数据 — 但事实并非如此!oldVertices 的内存占用为 72 字节,而 vertices + indices 的内存占用为 60 字节。

顶点描述器

使用索引渲染顶点时,可以使用更高效的绘制调用。但是,您首先需要在管道中设置顶点描述符。
始终使用顶点描述符是一个好主意,因为大多数情况下,您不仅会向 GPU 发送位置属性。您还可能发送法线、纹理坐标和颜色等属性。当可以布置自己的顶点数据时,您可以更好地控制引擎处理模型网格的方式。

➤ 创建一个名为 VertexDescriptor.swift 的新 Swift 文件。

➤ 将代码替换为:

import MetalKit
extension MTLVertexDescriptor {static var defaultLayout: MTLVertexDescriptor {let vertexDescriptor = MTLVertexDescriptor()vertexDescriptor.attributes[0].format = .float3vertexDescriptor.attributes[0].offset = 0vertexDescriptor.attributes[0].bufferIndex = 0let stride = MemoryLayout<Vertex>.stridevertexDescriptor.layouts[0].stride = stridereturn vertexDescriptor} 
}

在这里,您将设置一个只有一个属性的顶点布局。该属性描述每个顶点的位置。

顶点描述符包含一个属性数组attributes和一个缓冲区布局数组layouts。

• attributes:对于每个属性,你需要指定类型格式,以及第一个(属性值)距离缓冲区开始位置的偏移量(以字节为单位)。您还可以指定存储该属性的缓冲区的索引。
• buffer layout:你需要指定每个缓冲区中所有属性组合的步幅长度。这里可能会让人感到困惑,因为你正在使用下标0 来索引layouts和attributes数组,但layouts下标0 对应于attributes数组使用到的 bufferIndex 0(也就是索引为0的缓冲区)。

注意: stride 描述了每个实例之间的字节数。由于内部填充和字节对齐,此值可能与 size 不同。有关大小、步幅和对齐的精彩解释,请查看 Greg Heo 的文章,网址为https://bit.ly/2V3gBJl.

对于 GPU,vertexBuffer 现在如下所示:

➤ 打开 Renderer.swift,并在 init(metalView:) 中找到创建管道状态的位置。

➤ 在 do {} 中创建管道状态之前,将以下代码添加到管道状态描述符中:

 pipelineDescriptor.vertexDescriptor =MTLVertexDescriptor.defaultLayout

GPU 现在期望顶点按描述符描述的格式存放。

➤ 在 draw(in:) 中,删除: 

renderEncoder.setVertexBuffer(quad.indexBuffer,offset: 0,index: 1)

您将在 draw 调用中包含索引缓冲区。

➤ 将 draw 调用更改为:

renderEncoder.drawIndexedPrimitives(type: .triangle,indexCount: quad.indices.count,indexType: .uint16,indexBuffer: quad.indexBuffer,indexBufferOffset: 0)

此绘图调用期望索引缓冲区使用 UInt16,这就是你在 Quad 中描述 indices 数组的方式。你没有显式(例如调用setVertexBuffer方法)地将 quad.indexBuffer 发送到 GPU,因为这个 draw 调用会为你做这件事。
➤ 打开 Shaders.metal。
➤ 将 vertex 函数替换为:

vertex float4 vertex_main(float4 position [[attribute(0)]] [[stage_in]],constant float &timer [[buffer(11)]])
{return position;
}

 你在Swift 端的布局做了所有繁重的工作,所以顶点函数的大小大大减小了。

您可以使用 [[stage_in]] 属性描述每个逐顶点的输入。GPU 现在查看管道状态的顶点描述符。
[[attribute(0)]] 是顶点描述符中描述位置的属性。即使您将原始顶点数据定义为包含三个浮点数的顶点类型,也可以在此处将位置定义为 float4。GPU 可以进行转换。
值得注意的是,当 GPU 将 w 信息添加到 xyz 位置时,它会添加为1.0。正如您将在以下章节中看到的那样,这个 w 值在栅格化过程中非常重要。
GPU 现在拥有计算每个顶点位置所需的所有信息。
➤ 构建并运行应用程序以确保一切仍然有效。生成的渲染将与以前相同。

添加另一个顶点属性

您可能永远不会只有一个属性,因此让我们为每个顶点添加一个 color 属性。
您可以选择是使用两个缓冲区,或者在每个顶点位置之间交错存放颜色。如果您选择交错,您将设置一个结构来保存位置和颜色。但是,在此示例中,添加新的颜色缓冲区以匹配每个顶点会更容易。

➤ 打开 Quad.swift,并添加新数组:

var colors: [simd_float3] = [[1, 0, 0], // red[0, 1, 0], // green[0, 0, 1], // blue[1, 1, 0]  // yellow
]

现在,您有四种 RGB 颜色来对应这四个顶点。

➤ 创建一个新的缓冲区属性: 

let colorBuffer: MTLBuffer

➤ 在 init(device:scale:) 的末尾添加: 

guard let colorBuffer = device.makeBuffer(bytes: &colors,length: MemoryLayout<simd_float3>.stride * colors.count,options: []) else {fatalError("Unable to create quad color buffer")}
self.colorBuffer = colorBuffer

初始化 colorBuffer 的方式与前两个缓冲区相同。
➤ 打开 Renderer.swift,然后在 draw(in:) 中,在 draw 调用之前添加: 

renderEncoder.setVertexBuffer(quad.colorBuffer,offset: 0,index: 1)

使用缓冲区索引 1 将颜色缓冲区发送到 GPU,该索引必须与顶点描述符layouts数组中的下标匹配。
➤ 打开 VertexDescriptor.swift,并在返回之前将以下代码添加到 defaultLayout:

vertexDescriptor.attributes[1].format = .float3
vertexDescriptor.attributes[1].offset = 0
vertexDescriptor.attributes[1].bufferIndex = 1
vertexDescriptor.layouts[1].stride =MemoryLayout<simd_float3>.stride

在这里,您将描述索引为1的缓冲区中颜色缓冲区的布局。

 ➤ 打开 Shaders.metal。
➤ 您只能在一个参数上使用 [[stage_in]],因此在 Vertex 函数之前创建一个新结构体:

struct VertexIn {float4 position [[attribute(0)]];float4 color [[attribute(1)]];
};

 ➤ 将 vertex 函数更改为:

vertex float4 vertex_main(VertexIn in [[stage_in]],constant float &timer [[buffer(11)]])
{return in.position;
}

这段代码仍然简短明了。GPU 知道如何从缓冲区中检索位置和颜色,因为结构中的 [[attribute(n)]] 限定符会查看管道状态的顶点描述符。

➤ 构建并运行以确保您的蓝色四边形仍然渲染。

fragment 函数确定每个渲染片段的颜色。您需要将顶点的颜色传递给 fragment 函数。您将在第 7 章 “片段函数” 中了解有关 fragment 函数的更多信息。
➤ 仍在 Shaders.metal 中,在 vertex 函数之前添加以下结构: 

 struct VertexOut {float4 position [[position]];float4 color;
};

现在,您不仅可以从 vertex 函数返回 position,还可以返回 position 和 color。您可以指定 [[position]]属性,让 GPU 知道此结构中的哪个属性是 position。
➤ 将 vertex 函数替换为: 

vertex VertexOut vertex_main(VertexIn in [[stage_in]],constant float &timer [[buffer(11)]]) {VertexOut out {.position = in.position,.color = in.color};
return out; }

 现在,您返回 VertexOut 而不是 float4。

➤ 将片段函数改为:

 fragment float4 fragment_main(VertexOut in [[stage_in]]) {return in.color;
}

[[stage_in]] 属性指示 GPU 应从顶点函数获取 VertexOut 输出,并将其与栅格化片段匹配。在这里,您将返回顶点颜色。请记住第 3 章 “渲染管道” 中,每个片段的输入都会进行插值。

➤ 构建并运行应用程序,您将看到以美丽的颜色渲染的四边形。 

 

渲染成点状

您可以使用点和线渲染,而不是三角形。

➤ 打开 Renderer.swift,然后在 draw(in:) 中更改

 renderEncoder.drawIndexedPrimitives(type: .triangle,

为:

 renderEncoder.drawIndexedPrimitives(type: .point,

如果您现在构建并运行,GPU 将使用点渲染,但它不知道要使用什么大小的点,因此它会在各种大小上闪烁。要解决此问题,您需要从 vertex 函数返回数据时,同时返回点尺寸。
➤ 打开 Shaders.metal,并将此属性添加到 VertexOut: 

float pointSize [[point_size]];

[[point_size]] 属性将告诉 GPU 要使用什么尺寸的点。

➤ 在 vertex_main 中,将 out 的初始化替换为: 

VertexOut out {.position = in.position,.color = in.color,.pointSize = 30
};

 在这里,您将设置点的大小为30。
➤ 构建并运行以查看使用顶点颜色渲染的点:

挑战

到目前为止,您通过数组缓冲区,已将顶点位置发送到GPU。但这并不完全必要。GPU 需要知道的是要绘制多少个顶点。您的挑战是删除顶点和索引缓冲区,并在一个圆圈中绘制 50 个点。以下是您需要采取的步骤的概述,以及一些帮助您入门的代码:
1. 在 Renderer 中,从管道中删除顶点描述符。
2. 替换 Renderer 中的 draw 调用,使其不使用索引,而是绘制 50个顶点。
3. 在 draw(in:) 中,删除所有 setVertexBuffer 命令。
4. GPU 需要知道总点数,因此请以与缓冲区 0 中的 timer 相同的方式发送此值。
5. 将 vertex 函数替换为:

vertex VertexOut vertex_main(constant uint &count [[buffer(0)]],constant float &timer [[buffer(11)]],uint vertexID [[vertex_id]])
{float radius = 0.8;float pi = 3.14159;float current = float(vertexID) / float(count);float2 position;position.x = radius * cos(2 * pi * current);position.y = radius * sin(2 * pi * current);VertexOut out {.position = float4(position, 0, 1),.color = float4(1, 0, 0, 1),.pointSize = 20
};
return out; }

请记住,这是一个练习,可帮助您了解如何在 GPU 上定位点,而无需在 Swift 端保存任何等效数据。所以,不要太担心数学。您可以使用当前的vertexID的正弦和余弦来绘制圆周围的点。

请注意,GPU 上没有 pi 的内置值。

您将看到 50 个点被绘制成一个圆圈。


尝试通过将 timer 添加到 current 来为点添加动画。
如果你有任何困难,你可以在本章的项目挑战目录中找到解决方案。 

参考

https://zhuanlan.zhihu.com/p/385638027


http://www.mrgr.cn/news/92327.html

相关文章:

  • Python学习第十七天之PyTorch保姆级安装
  • TCPDF 任意文件读取漏洞:隐藏在 PDF 生成背后的危险
  • (0)阿里云大模型ACP-考试回忆
  • day7作业
  • kubernetes-完美下载
  • 2025-02-25 学习记录--C/C++-用C语言实现删除字符串中的子串
  • Redis存储​⑫​哨兵Sentinel_高可用实现方案
  • 智慧交通之信号控制【绿波带】
  • 大厂都在用的前端换肤方案:多品牌适配 + 夜间模式全解析
  • java.lang.IllegalStateException: dbType not support : null, url null
  • 本地开发用ASP.NET Core Web API项目创建及测试
  • low rank decomposition如何用于矩阵的分解
  • Flutter - 基础Widget
  • 2024最新版鸿蒙纯血原生应用开发教程文档丨学习ArkTS语言-基本语法
  • AI绘画软件Stable Diffusion详解教程(2):Windows系统本地化部署操作方法(专业版)
  • OkHttp、Retrofit、RxJava:一文讲清楚
  • 单目摄像头物体深度计算基础原理
  • SQL进阶实战技巧:汽车转向次数分析 | 真实场景案例
  • Android Realm数据库使用与集成指南
  • 外发抄板SCH与PCB检查系列