概要
本文主要总结一下Metal的基本使用,用来渲染拍照的到的图像,其中涉及到的有UIKit中的MTKView,Metal中负责渲染的几个类,使用MSL(Metal Shading Language)编写着色器,最终将图片渲染出来。这一篇先简介一下Metal的工作流程。
Metal对图像的处理流程
图像的最小单位可以认为是像素,一幅图片由若干像素组成,每个像素在图片中都有一个位置,也就是该像素的坐标。二维下也就是(x,y),三维下也就是(x,y,x)。所以Metal对图像处理,其实就是对每个像素的处理。
Metal的处理流程简化可以表示成以下这张图,从最左侧的Vertices是提供的初始的顶点数据,经过Vertex function处理
和OpenGL中类似,Metal中的Vertex stage可以理解为OpenGL中的顶点处理阶段,主要来进行坐标转换,Vertex function就是此阶段实现的方法,同样,fragment stage对应到片段处理阶段,主要计算点的颜色值Fragment function就是此阶段实现的方法。所以Vertex fucntion和fragment function也就对应上了OpenGL中的顶点着色器和片段着色器。只不过编写的语言使用的是Metal的MSL,不再是glsl。
在顶点处理阶段,有几个坐标需要先明确一下,首先是在使用UIKit布局的时候,通常是以左上角为(0,0),而右下角是(1,1);同时,在使用纹理的时候,对应的纹理也有一个自己的坐标系,在Metal中叫做标准纹理坐标,如下图:
左上角为(0,0),右下角为(1,1);Metal最终进行绘制的时候,使用的另一种坐标系,就是标准化设备坐标,和上面的就不太一样,如下图:
在只有2D情况下,中心点是(0,0),左下角是(-1,-1),右上角是(1,1),在3D情况下,z就是表示距离屏幕的距离。结合上面的标准化纹理坐标,如果要将纹理布满整个Metal的绘制范围,那么就需要将四个角的坐标对应上,也就是:
纹理坐标:(0,0) 设备坐标:(-1,1)
纹理坐标:(1,0) 设备坐标:(1,1)
纹理坐标:(0,1) 设备坐标:(-1,-1)
纹理坐标:(1,1) 设备坐标:(1,-1)
这样,就能将纹理中布满整个渲染空间范围,而这个,也就是vertex function主要要做的事情,我举的这个例子是最简单的,实际情况中肯定会很复杂,而且vertex function计算出来的坐标,有时候超过-1~1的这个区间,而这个时候,中间的光栅化器(Rasterization)就发挥作用了。
光栅化器要做的就是,将vertex function得到的所有坐标以标准化设备坐标为边界进行选取,超出标准化设备坐标的点,一律去掉,并且将范围内的点转换成屏幕上对应的像素。将这些有效像素传递到下一个阶段。
fragment function接下来登场了,这里面就是将光栅化器筛选出来的有效像素进行上色,当然,具体怎么上色,这个就需要根据实际情况来看,这也就是我们实现自己上色逻辑的重要部分。
关于vertex function和fragment function的例子,放到下一章实践的时候再看。从纹理坐标到设备坐标,这个主要是针对图像的渲染。试想一个场景,我在一个MTKView上点击了一个点,或者用手指画了一条线,如何使用Metal来绘制呢?我也是突然想到的,以后实现之后再看吧。上面的部分是Metal渲染的概念部分,下面看下Metal中具体实现这个过程,涉及到的一些东西。
Metal实现绘制的组件
MTLDevice
Metal使用GPU来进行绘制,这个MTLDevice就代表了给当前绘制分配的设备,官方文档中说到这个就相当于一个GPU。这个device主要提供了一些方法,主要有三个方面
- 查询设备状态
- 创建设备相关的绘制资源,比如缓冲区,纹理等等
- 执行渲染的命令
下面是一些代码例子,这几个后面都会介绍:
let defaultLibrary = self.renderDevice.makeDefaultLibrary()
let pipelineState = try! self.renderDevice.makeRenderPipelineState(descriptor: pipelineStateDesc)
let vertices = self.renderDevice.makeBuffer(bytes: vertexArr!,
length: MemoryLayout<AVImageVertex>.size * 6,
options: .storageModeShared)
MTLCommandQueue、MTLCommandBuffer、MTLCommandEncoder
这三个放到了一起,因为它们配合起来完成实际的绘制过程。首先commandQueue是由上面的device创建,从名字能看出来,是一个队列,那么这个队列里面是什么呢?就是commandBuffer。若干commandBuffer的顺序由这个commandQueue来组织,然后依次执行。
然后是commandBuffer,是个啥呢?它的主要作用是创建commandEncoder,以及在commandEncoder工作完成之后,将所有渲染命令提交,进入commandQueue中。
那么commandEncoder是干什么的呢?它的主要就是决定了绘制的具体操作,包括需要的顶点资源,绘制的方式(点、线、三角形)等等,具体的绘制都由它管。commandBuffer支持三种commandEncoder。官方文档中特别提到一点,在一个commandBuffer中,某一个时刻只能有一个commandEncoder处于激活状态,这一点我暂时还没有什么体会,后面使用中再慢慢了解吧。我的例子里面只使用了负责图形绘制的MTLRenderCommandEncoder,对其他感兴趣的可以自行去了解。
然后用一张官方的大图来看下这个绘制的结构:
这个图还是比较清晰的,我会面会根据代码来一步一步地对应上去。 图中有些地方没有介绍到,后面涉及到的我会特别提到
MTKView
这个是将Metal的工作和UI框架相联系的一个东西,一方面继承自UIView,参与UIKit中的工作,一方面实现了Metal的协议,参与Metal绘制的逻辑工作,还是挺重要的,其实用起来比较方便。使用这个类需要实现一个协议:MTKViewDelegate,必须实现其中的两个方法
- func mtkView(MTKView, drawableSizeWillChange: CGSize),这个方法主要是在布局发生改变,需要重新调整View大小的时候,会被调用,传入的drawableSizeWillChange就是新的尺寸;
- func draw(in: MTKView) 这个方法中通常就是具体的绘制操作,也就是前面提到的使用commandQueue、commandBuffer以及commandEncoder的地方。
结束
以上主要从Metal绘制流程,绘制的功能组件以及UIKit中的MTKView这三个方面简介使用Metal的几个主要部分,接下来,后来的会从我自己的使用过程,来具体看看Metal绘制的流程。
如果有不对的地方,欢迎大家指正,相互学习,共同进步吧。