一、发展历史
历史上有两种选择来调用 GPU 的强大功能:OpenGL 和仅限 Windows 的 DirectX。2013 年,GPU 供应商 AMD 宣布了 Mantle 项目,旨在改进 GPU API,并提出 Direct3D(DirectX 的一部分)和 OpenGL 的替代方案。AMD 率先创建了真正的低开销 API,用于对 GPU 进行底层访问。Mantle 承诺能够生成比类似 API 多 9 倍的绘制调用(绘制到屏幕上的对象数量),并且还引入了异步命令队列,以便图形和计算工作负载可以并行运行。不幸的是,该项目在成为主流 API 之前就被终止了。
Metal 于 2014 年 6 月 2 日在全球开发者大会 (WWDC) 上宣布,最初仅在 A7 或更新的 GPU 上提供。Apple 创建了一种新语言,可以直接通过着色器函数对 GPU 进行编程。这是基于 C++11 规范的Metal着色语言(MSL)。一年后,在 WWDC 2015 上,Apple 宣布了两个 Metal 子框架:MetalKit和Metal Performance Shaders (MPS)。2018 年,MPS 作为光线追踪加速器首次亮相。
API 继续发展,以与 Apple 内部设计的新型 Apple GPU 的令人兴奋的功能配合使用。Metal 2增加了对虚拟现实 (VR)、增强现实 (AR) 和加速机器学习 (ML) 的支持,以及许多新功能,包括图像块、图块着色和线程组共享。Metal着色语言现基于 C++14 规范。
2022 年,Apple 推出了带有新框架 MetalFX 的Metal 3,用于升级解决低分辨率的方案。Metal 3 的功能还包括直接从磁盘快速加载纹理资源、用于添加或减少几何体的网格着色器以及使用 C/C++ for Metal。
为什么要使用Metal?
Metal 是一流的图形 API。这意味着 Metal 可以增强图形管道的能力,更具体地说,可以增强游戏引擎的能力,如下所示:
Unity 和虚幻引擎:当今两个领先的跨平台游戏引擎非常适合针对各种控制台、桌面和移动设备的游戏程序员。然而,这些引擎并不总是跟上 Metal 中新功能的步伐。例如,Unity 中的曲面细分被延迟了很长时间,并且仍然不支持网格着色器。如果想使用尖端的 Metal 开发,并使用 Apple 芯片的强大功能,不能总是依赖第三方引擎。
神界 - 原罪 2:Larian Studios 与 Apple 密切合作,充分利用 Metal 和 Apple GPU 硬件,将他们令人惊叹的 AAA 游戏带到了 iPad 上。这确实是一次令人惊叹的视觉体验。
The Witness:这款屡获殊荣的益智游戏有一个在 Metal 之上运行的定制引擎。通过利用 Metal,iPad 版本与桌面版本一样华丽,强烈推荐给益智游戏迷。
其他游戏:来自著名游戏,如《杀手》、《生化奇兵》、《杀出重围》 、 《四海兄弟》、 《星际争霸》、 《魔兽世界》、《堡垒之夜》 、《虚幻竞技场》 、《蝙蝠侠》,甚至是深受喜爱的《我的世界》。
但 Metal 并不局限于游戏世界。许多应用程序都受益于图像和视频处理的 GPU 加速:
- Procreate:一款用于素描、绘画和插图的应用程序。自从转换为 Metal 以来,它的运行速度比以前快了四倍。
- Pixelmator:一款基于 Metal 的应用程序,提供图像失真工具。事实上,他们能够实现由 Metal 2 提供支持的新绘画引擎和动态油漆混合技术。
- Affinity Photo:在 iPad 上可用。据开发人员 Serif 介绍,“使用 Metal 可以让用户轻松处理大型超高分辨率照片或可能具有数千层的复杂构图。”
- Metal,特别是 MPS 子框架,在机器学习和卷积神经网络 (CNN) 深度学习领域非常有用。
二、对比
1)Metal 与 SpriteKit、SceneKit 或 Unity
在开始之前,了解 Metal 与 SpriteKit、SceneKit 或 Unity 等更高级别框架的比较会很有帮助。
Metal 是一种底层 3D 图形 API,类似于 OpenGL ES,但开销更低,性能更好。它是 GPU 上方非常低的一层,这意味着,在执行任何操作(例如将精灵或 3D 模型渲染到屏幕上)时,都需要编写所有代码来执行此操作。
相反,SpriteKit、SceneKit 和 Unity 等高级游戏框架构建在 Metal 或 OpenGL ES 等较低级 3D 图形 API 之上。它们提供了通常需要在游戏中编写的大部分样板代码,例如将精灵或 3D 模型渲染到屏幕上。
如果只想制作游戏,那么大多数时候可能会使用更高级的游戏框架,例如 SpriteKit、SceneKit 或 Unity,因为这样做会更简单。
然而,仍然有两个学习 Metal 的充分理由:
将硬件推向极限:由于 Metal 的如此底层,它可以将硬件性能推向极限,并完全控制游戏的运行方式。
这是一次很棒的学习经历:学习 Metal 会教你很多关于 3D 图形、编写自己的游戏引擎以及高级游戏框架如何工作的知识。
2)Metal 与 OpenGL ES
OpenGL ES 被设计为跨平台。这意味着可以编写 C++ OpenGL ES 代码,并且大多数时候,通过一些小的修改,可以在其他平台(例如 Android)上运行它。
对于有着超过25年历史的 OpenGL 技术本身,随着现代图形技术的发展,遇到了一些问题:
- 现代 GPU 的渲染管线已经发生变化。
- 不支持多线程操作。
- 不支持异步处理。
- 较为复杂的开发语言。
随着图形学的发展,OpenGL 本身设计上存在的问题已经影响了 GPU 真正性能的发挥,因此 Apple 设计了 Metal。
为了解决这些问题,Metal 诞生了。
它为现代 GPU 设计,并面向 OpenGL 开发者。它拥有:
- 更高效的 GPU 交互,更低的 CPU 负荷。
- 支持多线程操作,以及线程间资源共享能力。
- 支持资源和同步的控制。
- 语言更符合开发者的开发习惯。
- 可逐帧调试。
三、Metal使用场景
GPU 属于一类特殊的计算,Flynn的分类术语为单指令多数据 (SIMD)。简而言之,GPU 是针对吞吐量(一个单位时间内可以处理多少数据)进行优化的处理器,而 CPU 是针对延迟(处理单个数据单位需要多长时间)进行优化。大多数程序都是串行执行的:它们接收输入,处理它,提供输出,然后重复循环。
这些周期有时会执行计算密集型任务,例如大型矩阵乘法,这将花费 CPU 大量时间进行串行处理,即使在少数内核上以多线程方式也是如此。
相比之下,GPU 拥有数百甚至数千个核心,这些核心比 CPU 核心更小、内存更少,但可以执行快速并行数学计算。
在以下情况下选择Metal:
- 希望尽可能高效地渲染 3D 模型。
- 希望的游戏具有自己独特的风格,也许具有自定义照明和阴影。
- 将执行密集的数据处理,例如每帧计算和更改屏幕上每个像素的颜色,就像处理图像和视频时一样。
- 有大型数值问题,例如科学模拟,可以将其划分为独立的子问题以并行处理。
- 需要并行处理多个大型数据集,例如在训练深度学习模型时。
四、绘制原理
工欲善其事必先利其器,如果对图形学没有一点入门理解,还是好好先看一看图形渲染的步骤,最好了解一下OpenGL的工作原理,不要因为OpenGL在苹果被废弃掉了就对其嗤之以鼻,因为这个库在苹果以外的很多地方还是被广泛应用到的,学会了图形渲染,对Metal的理解会有很大帮助。该篇章取自Learn OpenGL中文文档。
1)基本原理概括
手机包含两个不同的处理单元,CPU 和 GPU。CPU 是个多面手,并且不得不处理所有的事情,而 GPU 则可以集中来处理好一件事情,就是并行地做浮点运算。事实上,图像处理和渲染就是在将要渲染到窗口上的像素上做许许多多的浮点运算。通过有效的利用 GPU,可以成百倍甚至上千倍地提高手机上的图像渲染能力。下面的流程图显示了一个图像渲染到屏幕的流程。
通过流程图我们可以看到,在我们日常的渲染中,OpenGL/Metal已经默默地替我们承担了很多渲染的操作,如果感兴趣可以在iOS 图像渲染原理看看这些图像是怎么一步步渲染下去的。
总的来说,Metal担任的就是CPU和GPU交互的一个桥梁,他负责一个管理图形渲染的队列,在屏幕刷新一帧的时候,将队列的内容提交给GPU,以及时地渲染到屏幕上。
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
以下是图形渲染管线的每个阶段的抽象展示,也是渲染图片的一个重要步骤,相当于给一幅画勾勒出线条,再上色,三维混合(如有必要),以达到我们想要的图画效果。
2)图形渲染的根基——三角形与像素点
在图形渲染中,有个非常非常非常重要的概念——三角形,可以这样说,如果呈现在屏幕上的图像是一座美丽的布达拉宫,那么三角形就是里面的一座地基、一根根柱子。
而你所看到的前三个步骤,就是从几个点,以三角形的方式勾勒出了整个线条。而第四个步骤则把线条做成一格一格的像素点。
顶点着色器:该阶段的输入是顶点数据(Vertex Data) 数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。
形状(图元)装配:该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。
几何着色器:该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。(其实个人觉得这里应该加多个顶点才对,不然好像有点让人误解多出来的那条线是怎么来的)
光栅化阶段(Rasterization Stage):根据几何着色器的输出,把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。
3)纹理、采样与着色
到光栅化这一步,我们已经可以获取到未被上色的像素了,一个图像有了初步的一些轮廓,那么他是怎么被上色,甚至被组合形成一个三维图案的呢?片段着色器就是上色的重要一环了。
片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
那么,他的颜色从哪里来呢?程序员可以根据自己想要的颜色进行上色,即直接在片段着色器写死颜色的rgba值,比如生成一个橘色的三角形:
那如果我们想读取一张图片渲染到上面去呢?像下面一样,把罗伊斯的照片贴到屏幕上去。
这时候需要引入一个同样重要的概念:纹理。
纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
上面的概念可能有点笼统,在渲染的知识里面,你需要暂时先将一张图片看成一个一个像素点,采样器(sampler)将图片上的像素点一一采样,再映射到已经光栅化的像素点中,使其上色,最终得到一个个上色后的像素点。后文会着重介绍怎么采样纹理和给光栅化像素上色。
最后,如果涉及到3D渲染(本文暂不涉及),该阶段会检测片段的对应的深度值(z 坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
4)顶点着色器与片段着色器
前面主要给大家介绍了从0到1的渲染过程,那么本文则会着重介绍一下MSL(Metal Shader Language) 给我们提供的接口,也就是说,我们只需要着手这两个着色器的开发,其他步骤无需我们动手。
在基于Metal介绍这两个着色器之前,请大家再着重复习一下几个重要的概念:
像素:一个图像由许多许多像素组成。
顶点着色器:将原图像的3D坐标转换成适应屏幕的3D坐标,同时建立需要绘制的顶点坐标 与 需要采样的纹理坐标的映射关系。在开发中,我们需要预先设好顶点坐标与纹理坐标的映射,供系统内部光栅化处理,最后传到片段着色器中。
纹理:用于被采样器采样,给片段着色器上色的图像。在开发中,我们需要读取图像的字节,调用接口生成纹理。
片段着色器:基于顶点着色器的输出、纹理的采样结果,输出一个个着色后的像素,这些像素组成了一整个图像。在开发中,我们需要根据顶点着色器输出(光栅化处理后)的数据、纹理数据,对纹理进行采样,并输出该光栅化像素对应的rgba。(多个像素即为一张图片)
接下来将会介绍Metal如何运用上面几个概念,在屏幕上渲染出一张图片出来,如果读到后面有疑惑,不妨回头再看看这几个概念和他们的职能。
五、开始创建Metal
Metal的简要流程图如下:
设置 Metal 需要执行七个步骤才能开始渲染。需要创建一个:
1、MTLDevice
2、MTKView
3、Vertex Buffer
4、Vertex Shader
5、Fragment Shader
6、Render Pipeline
7、Command Queue
1)创建MTLDevice
首先需要获得对MTLDevice.
GPU 的主要 Metal 接口,应用程序使用它来绘制图形并并行运行计算。可以通过调用 MTLCreateSystemDefaultDevice 在运行时获取默认 MTLDevice(请参阅获取默认 GPU)。 每个 Metal Device 实例代表一个 GPU,并且是应用程序与其交互的主要起点。 使用 Metal Device 实例,可以检查 GPU 的特性和功能(请参阅设备检查)并使用其工厂方法创建辅助类型实例。
1 |
|
2)创建MTKView
用于创建、配置和显示 Metal 对象的专用视图。
1 | id <MTLDevice> device = MTLCreateSystemDefaultDevice(); // 创建device |
逐行浏览一下:
1、创建一个device。
2、利用device创建一个新的MTKView。
3、出于性能原因,Apple 鼓励设置framebufferOnly为true,除非需要从为此层生成的纹理中进行采样,或者需要在层可绘制纹理上启用计算内核。大多数时候,不需要这样做。
4、最后,将该图层添加为视图主图层的子图层。
3) 创建顶点缓冲区
Metal 中的一切都是三角形。在此应用程序中,只需绘制一个三角形,但即使是复杂的 3D 形状也可以分解为一系列三角形。
然后我们需要设置顶点数据,这里需要说明一下Metal的坐标系:
顶点坐标系是四维的(x, y, z, w),原点在画布的正中心。采用左手坐标系。
纹理坐标系是二维的(x, y),原点在图片的左上角。
在 Metal 中,默认坐标系是标准化坐标系,这意味着默认情况下看到的是一个以 (0, 0, 0.5) 为中心的 2x2x1 立方体。
如果考虑 Z = 0 平面,则 (-1, -1, 0) 为左下角,(0, 0, 0) 为中心,(1, 1, 0) 为右上角。在本教程中,想要绘制一个具有以下三个点的三角形:
必须为此创建一个缓冲区。将以下常量属性添加到的类中:
1 | static const vector_float4 quadVertices[] = |
这会在 CPU 上创建一个浮点数组。需要将此数据移动到称为MTLBuffer,生成buffer。
1 | self.vertices = [self.device newBufferWithBytes:quadVertices |
4) 创建顶点着色器
在上一节中创建的顶点将成为将编写的称为顶点着色器的小程序的输入。
顶点着色器只是一个在 GPU 上运行的小程序,用称为Metal Shading Language的类 C++ 语言编写。
每个顶点调用一次顶点着色器,其工作是获取该顶点的信息(例如位置)以及可能的其他信息(例如颜色或纹理坐标)并返回可能修改的位置和可能的其他数据。
为了简单起见,的简单顶点着色器将返回与传入位置相同的位置。
了解顶点着色器的最简单方法就是亲自查看。转到文件 ▸ 新建 ▸ 文件,选择iOS ▸ 源 ▸ Metal 文件,然后单击下一步。输入Shaders.metal作为文件名,然后单击“创建”。
注意:在 Metal 中,可以在单个 Metal 文件中包含多个着色器。如果愿意,还可以将着色器拆分到多个 Metal 文件中,因为 Metal 将从项目中包含的任何 Metal 文件加载着色器。
1 | typedef struct |
分析:
1.所有顶点着色器必须以关键字 vertex 开头。该函数必须(至少)返回顶点的最终位置。float4 可以通过指示(四个浮点数的向量)来执行此操作。然后给出顶点着色器的名称;
2.第二个参数是一个指向数组 float4(四个浮点数)的指针,即每个顶点的位置。
使用[[ … ]]语法来声明属性,可以使用这些属性来指定其他信息,例如资源位置、着色器输入和内置变量。在这里,标记此参数,[[ buffer(0) ]] 以指示从 Metal 代码发送到顶点着色器的第一个数据缓冲区将填充此参数。
3、顶点着色器还采用带有该属性的特殊参数 vertex_id,这意味着 Metal 将使用顶点数组内该特定顶点的索引来填充它。
4、在这里,根据顶点 ID 查找顶点数组内的位置并返回该位置。还可以将向量转换为 float4,这是 3D 数学所必需的。
5) 创建片元着色器
顶点着色器完成后,Metal 为屏幕上的每个片段(像素)调用另一个着色器:片元着色器。
片元着色器通过对顶点着色器的输出值进行插值来获取其输入值。例如,考虑三角形底部两个顶点之间的片段:
片元着色器的工作是返回每个片段的最终颜色。为了简单起见,将每个片段设置为蓝色。
将以下代码添加到Shaders.metal的底部:
1 | // 片元着色器 |
分析:
1.所有片元着色器必须以关键字 fragment 开头。该函数必须(至少)返回片段的最终颜色。可以在此处通过指示half4(四分量颜色值 RGBA)来执行此操作。注意,half4 比 float4 更节省 GPU 显存。
2.在这里,返回 (0, 0, 1, 1) 来表示颜色,即蓝色。
6) 创建渲染管线
现在已经创建了顶点和片元着色器,需要将它们与一些其他配置数据一起组合到一个称为渲染管道的特殊对象中。
Metal 的一大优点是着色器是预编译的,并且渲染管道配置是在首次设置后编译的。这使得一切都变得非常高效。
1 | id<MTLLibrary> defaultLibrary = [self.device newDefaultLibrary]; |
分析:
1.MTLLibrary可以通过调用 获得的对象来访问项目中包含的任何预编译着色器[device makeDefaultLibrary]。然后,可以按名称查找每个着色器。
2.可以在此处设置渲染管道配置。它包含想要使用的着色器,以及配置的像素格式 - 即要渲染到的输出缓冲区的数据格式。
3.最后,将管道配置编译为可以高效使用的管道状态。
7) 创建命令队列
需要执行的最后一个一次性设置步骤是创建一个MTLCommandQueue.
可以将其视为命令 GPU 执行的有序命令列表,一次执行一个。
1 | self.commandQueue = [self.device newCommandQueue]; |
8) 实战:渲染三角形
1 |
|
效果:
问:
如果画出一个矩形?椎体?