前言

Metal是一个为肾系列量GPU量身定做的框架。名字是根据iOS平台最底层的图形处理框架命名出来的。

这套框架的两个主题:3D图形渲染以及并行计算。

给谁用

跟虚幻/Unity对比扯皮Metal的强大,潜力(略)

对比OpenGL/OpenGLES, 教程相对简单地Metal在肾平台的图形渲染优化程度做的比上述两者好。

最后下个结论在iOS系统,Metal是不二选择。

优势

最大得优势是Metal轻量级了(对比OpenGL)。无论何时你使用OpenGL在创建一个缓存或者纹理的时候,总是要进行拷贝动作避免GPU对他们进行操作。为了安全考虑大量进行资源拷贝的代价显而易见是巨大的。而Metal则不用进行这种拷贝的动作。由开发者来负责同步CPU与GPU的读写。运气还不错的是,大苹果提供了其他一些很棒的API--GCD,我们可以用这些API使同步更加容易。

Metal另外一个优势提供了GPU状态预判来避免大量的校验与编译。一般来说,如果你使用OpenGL你需要不断的设置GPU的状态,并且在画图之前需要进行状态集校验。最差得情况下要重新编译着色工厂(XD,不知道怎么翻译shader)并以此来获取新的状态。当然校验的步骤是必须的,但是Metal选择另外一种方案。//重要一句,暂放。。。(在渲染引擎初始化阶段,状态集合被提取到预校验渲染值。。。)

API

Metal很多API以协议的方式提供。原因是具体类型的Metal对象需要依赖其具体的机器型号。这也是为了适应接口编程而不是实现编程。这也意味着你不用在继承Metal类或者添加扩展以及去使用runtime的风险了。(作者很不自信啊。。。)

Metal为了速度某种程度上牺牲了安全性。举个栗子,你收到了一个指向内部缓存的空指针,这时候你的操作需要额外小心。当OpenGL这类场景出错的时候通常是黑屏。Metal的话可能是随机错误,跳屏或者崩溃都是妥妥的。

坑爹的是模拟器不支持。。。(这句话早说出来会被打吧,还有就是我是4S机器不支持硬件都不支持。。)

基础编程

首先可以到Github下个DEMO。

根据设备创建相应的UI接口

在Metal中,设备是GPU的抽象。我们可以通过MTLCreateSystemDefaultDevice方法来获取当前设备。

id<MTLDevice> device = MTLCreateSystemDefaultDevice();

注意到返回值是一个泛型类,但是遵循了MTLDevice协议。

接下来得代码片段展示创建一个Metal层并且添加了一个UIView的背景层。

CAMetalLayer *metalLayer = [CAMetalLayer layer];
metalLayer.device = device;
metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
metalLayer.frame = view.bounds;
[view.layer addSublayer:self.metalLayer];

CAMetalLayerCALayer的一个子类用来展示Metal图层缓存中的内容。我们需要告知Layer层哪一个设备我们在用,并且告知要使用的像素格式。示例中我们选择了8位BGRA格式。

库与函数

先提到Metal shaders用Metal shading语言来写。

Metal库是一堆函数集合。所有你去实现的shader方法将会被编译进默认的库。你可以如此检索到:

id<MTLLibrary> library = [device newDefaultLibrary]

我们将在构建渲染流水线状态的时候使用到这些库。

命令队列

指令提交给Metal设备通过相关得指令队列。指令队列获取指令是线程安全的并且在设备上串行执行。如下创建一个执行指令:

id<MTLLibrary> library = [device newDefaultLibrary]

构建流水线

当我们提及Metal编程中的流水线时候,我们通常是指渲染时顶点矩阵的变换。顶点shaders与区域shaders是流水线可编程的关节,但是同时还有其他会发生的事情(剪切,放大, 观察点变换)不受我们控制。后者流水线等功能组成了我们的固定防水层流水线。

为了获取该方程,我们根据加方法名字来从库中获取:

id<MTLFunction> vertexProgram = [library newFunctionWithName:@"vertex_function"];
id<MTLFunction> fragmentProgram = [library newFunctionWithName:@"fragment_function"];

我们创建了一个pipeline来配置这些方法以及像素格式设置:

MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
[pipelineStateDescriptor setVertexFunction:vertexProgram];
[pipelineStateDescriptor setFragmentFunction:fragmentProgram];
pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;

最后我们根据描述创建流水线自身的状态。(略一句)

id<MTLRenderPipelineState> pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:nil];

加载数据到缓存

我们设置好流水线后,我们需要将数据填入进去。在示例工程中,我们画一个简单地几何图形:快速转动的方体。这个方体包含了共享一条边的两个正三角形。

static float quadVertexData[] =
{
     0.5, -0.5, 0.0, 1.0,     1.0, 0.0, 0.0, 1.0,
    -0.5, -0.5, 0.0, 1.0,     0.0, 1.0, 0.0, 1.0,
    -0.5,  0.5, 0.0, 1.0,     0.0, 0.0, 1.0, 1.0,

     0.5,  0.5, 0.0, 1.0,     1.0, 1.0, 0.0, 1.0,
     0.5, -0.5, 0.0, 1.0,     1.0, 0.0, 0.0, 1.0,
    -0.5,  0.5, 0.0, 1.0,     0.0, 0.0, 1.0, 1.0,
};

每行的前四个数字代表了x,y,z,w的组成值。后四个数字代表了红色,绿色,蓝色,还有透明值的组成值。

你可能感到奇怪用四个分量来表示3D空间。第四个组成点w为了让我们在做3D变换(翻转、移位、缩放)时候做统一处理提供了一个数学计算的便捷。

为了用Metal画出顶点数据,我们需要将它放置在缓存中。缓存是一个简单无结构化被CPU与GPU共享的少量内存。

vertexBuffer = [device newBufferWithBytes:quadVertexData
                                   length:sizeof(quadVertexData)
                                  options:MTLResourceOptionCPUCacheModeDef

我们用另外一个块缓存去存储旋转后的矩阵。与提供数据更新不同,我们只需要提供足够长度的缓存空间。

uniformBuffer = [device newBufferWithLength:sizeof(Uniforms) 
                                    options:MTLResourceOptionCPUCacheModeDefault];

动画

为了在屏幕上旋转方体,我们需要变换顶点坐标成顶点着色的一部分。这需要每一帧都更新统一的缓存。为了达成这点,我们运用三角学根据当前的旋转角度来生成旋转矩阵,并将旋转矩阵拷贝到统一的缓存中。

统一的数据结构有单一的方法,该方法是一个44的矩阵保存着旋转矩阵,其类型是在苹果SIMD库定义的浮点型44矩阵。该数据类型优势是可以进行数据并行操作。

typedef struct
{
    matrix_float4x4 rotation_matrix;
} Uniforms;

为了将旋转矩阵拷贝到统一的缓存中,我们获取到其内存首地址并调用memcpy方法将矩阵拷入。

Uniforms uniforms;
uniforms.rotation_matrix = rotation_matrix_2d(rotationAngle);
void *bufferPointer = [uniformBuffer contents];
memcpy(bufferPointer, &uniforms, sizeof(Uniforms));

开始绘图

为了绘制Metal图层,我们首先要从图层获取‘可绘制的’部分。可绘制的对象管理着一套适合渲染的纹理:

id<CAMetalDrawable> drawable = [metalLayer nextDrawable];

接下来,我们创建一个渲染过程描述,该描述用来说明Metal在执行渲染前后所需要得操作。如下面代码,我们描述的一个渲染过程首先会将帧缓存清空成一个白色固体,然后执行绘制操作,最后将结果存储到帧缓存中用来展示:

MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1, 1, 1, 1);
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;

绘制调用问题

放置指令到命令队列的时候,指令必须被编码到命令缓存中。一套命令缓存含有一个或者多个指令用来被执行和被紧凑的编码成GPU识别的指令:

id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];

为了实际编码渲染指令,我们需要另外一个对象去了解怎么将我们的绘制函数调用转换成GPU语言。这个对象被成为command encoder。我们通过想指令缓存申请编码者和传递上述我们创建的渲染过程描述符来创建它。

id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];

在绘制调用即将开始之前,我们根据预编译流水线状态配置渲染指令编码以及创建好缓存,这些都是顶点着色器的参数。

[renderEncoder setRenderPipelineState:pipelineState];
[renderEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0];
[renderEncoder setVertexBuffer:uniformBuffer offset:0 atIndex:1];

为了着实执行几何画图,我们需要告知Metal我们需要画的图是啥形状,以及我们要从缓存中消费多少顶点。

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];

最后告诉编码器,我们已经处理完画图调用,结束编码:

[renderEncoder endEncoding];

展示帧缓存

现在我们的绘图指令已经编码好并准备执行,我们需要告知指令缓存来在屏幕上显示结果。为了达成这点,我们根据从Metal层获取到当前可以绘制的对象调用presentDrawable

[commandBuffer presentDrawable:drawable];

为了告知缓存已经准备就绪执行了,我们需要进行确认:

[commandBuffer commit];

大概如此。。!

Metal Shading语言

基础是C++11,限制了一些特性,添加了一些关键字。。。

实战

为了从我们得着色器里面取出顶点数据,我们顶一个数据结构用来与OC中的顶点布局数据进行交互。

typedef struct
{
    float4 position;
    float4 color;
} VertexIn;

我们也需要一个非常相近的类型用来描述要从我们顶点着色器顶点传到局部着色器得数据类型。但是,这种情况下我们必须要确定(根据使用[[position]]属性)到底是哪一个数据结构成员需要被认定为顶点位置:

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

根据顶点数据中的每个顶点都会执行一次顶点函数。它获取执行顶点列表的指针与含有旋转矩阵的统一数据的引用。第三个参数是当前操作顶点的索引。

需要注意的是属性后得顶点函数的参数已经能说明它们的用途。上述情况的缓存参数索引值与当设置渲染指定编码器缓存时候我们指定的索引值相匹配。这正是Metal怎么计算出与缓存相对应得参数。

在定点方程内,我们用顶点坐标乘以旋转矩阵。我们将变换过的坐标信息赋值给输出顶点。顶点的颜色从输入到输出采用直接拷贝。

vertex VertexOut vertex_function(device VertexIn *vertices [[buffer(0)]],
                                 constant Uniforms &uniforms [[buffer(1)]],
                                 uint vid [[vertex_id]])
{
    VertexOut out;
    out.position = uniforms.rotation_matrix * vertices[vid].position;
    out.color = vertices[vid].color;
    return out;
}

对于每一个像素来说这片段方式都被执行一次。在简单得片段方程中,我们只是简单地传递由Metal生成的插入颜色。这就是屏幕上所显示出来的像素颜色:

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

为啥不用OpenGL扩展?

大苹果是OpenGL考虑的平台,并且OpenGL一直都为iOS提供对应的扩展框架。但是从内部改变OpenGL似乎应该是一个困难的任务因为它需要兼容多平台的原因(不可能定制)。尽管OpenGL一直在前进,但是进度很慢并且效果微乎其微。

另一方面Metal是大苹果平台专属工作。尽管API使用协议看上去怪怪得,但是它用起来还是蛮不错的。Metal使用OC写的,以Fundation为基础,并使用了GCD来同步CPU和GPU。对比OpenGL它更高度抽象了GPU的流水线可以做到不用完全的重写。

Mac上的Metal?

应该还没有整好。。。(略)

总结

总结了现状是很多人还用不上Metal,但是高级码农已经开始使用并且从中获益。
如果你想完全发挥硬件的潜力,Metal可以让开发者在游戏中创造出独一无二效果,或是更加快速的并行计算,让他们(产品)更具有竞争力。


Cruise_Chan
729 声望71 粉丝

技能树点歪了...咋办