Metal(1)——Hello Metal

695 阅读5分钟

1. 认识Metal

1.1 定义

先贴一段苹果的官方说明。
Metal provides near-direct access to the graphics processing unit (GPU), enabling you to maximize the graphics and compute potential of your apps on iOS, macOS, and tvOS. Building on an approachable, low-overhead architecture with precompiled GPU shaders, fine-grained resource control, and multithreading support, Metal further evolves support for GPU-driven command creation, simplifies working with the array of Metal-capable GPUs, and lets you tap into Pro power of Mac Pro and Pro Display XDR.
大概意思就是说Metal很牛叉吧。提供了近乎可以直接访问系统GPU的能力。在图像渲染,通用计算方面,通过发挥 GPU 的特性,极大提升性能。

1.2 metal的图形管道

metal的图像显示流程如下图所示:

  1. CPU将顶点数据传到GPU
  2. 顶点着色器处理顶点数据
  3. 处理好的顶点数据进行图元装配
  4. 光栅化,得到每个像素点的颜色值
  5. 片元着色器返回最终要显示的颜色
  6. 颜色数据存入帧缓冲区,最终显示到屏幕上

1.3 Metal使用建议

1.3.1 Separate Your Rendering Loop

在我们开发Metal 程序时, 将渲染循环分为自己创建的类, 是非常有用的一种方式, 使用单独的类, 我们可以更好管理初始化Metal, 以及Metal视图委托.

//创建一个单独的Render类来处理渲染方面的逻辑 与其他的视图操作分开
render = Render.init(mtkView: view)
//进行一下必要的初始化操作
render?.mtkView(view, drawableSizeWillChange: view.drawableSize)
//代理设置为render
view.delegate = render

1.3.2 Respond to View Events

//render来实现MTKViewDelegate的代理方法
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize)
func draw(in view: MTKView)

1.3.3 Metal Command Objects

创建一个MTLCommandQueue对象,通过它来处理与GPU之间的交互。

 //Metal与GPU 交互的第一个对象
 commandQueue = device!.makeCommandQueue()

1.4 Metal对象之间的关系

  1. 命令缓存区(command buffer) 是从命令队列(command queue) 创建的
  2. 命令编码器(command encoders) 将命令编码到命令缓存区中
  3. 提交命令缓存区并将其发送到GPU
  4. GPU执⾏命令并将结果呈现为可绘制.

2 Metal 小案例

案例效果图如下:

2.1 创建MTKView

MetalKit中提供了一个视图类MTKView,类似于GLKitGLKView,用于处理Metal绘制并显示到屏幕过程中的细节。
我们直接将storyboard中的view的类型改为MTKView。使得在控制器带初始化的时候,自动为我们创建一个MTKView,供我们使用。 获取MTKView:

//1. 获取MTKView
 let view = self.view as! MTKView

2.2 为MTKView 设置MTLDevice

一个MTLDevice对象就代表这着一个GPU, 通常我们可以调用方法MTLCreateSystemDefaultDevice() 来获取代表默认的GPU单个对象。并且在创建完成后,需要判断是否获取GPU的使用权限,如果不成功,则中断渲染流程。

//2.为MTKView 设置MTLDevice(必须)
  view.device = MTLCreateSystemDefaultDevice()
  
//3.判断是否设置成功
  if view.device == nil {
     print("Metal is not supported on this device")
     return
  }

2.3 创建Render

我们听从苹果大大的建议,创建一个单独处理Metal的类——Render。通过自定义构造方法,保存mtkViewdevice,并根据device创建commandQueue,同样将commandQueue保存起来以供后续使用。

//声明变量
var device: MTLDevice?
var commandQueue: MTLCommandQueue?

//自定义构造方法
convenience init(mtkView: MTKView) {
   self.init()
        
   device = mtkView.device  
   //Metal与GPU 交互的第一个对象
   commandQueue = device!.makeCommandQueue()
 }

viewcontrollerviewDidLoad方法中创建render对象,并将render设置为mtkView的代理。

//4. 创建Render
render = Render.init(mtkView: view)
        
//5.判断render 是否创建成功
if render == nil {
    print("Renderer failed initialization")
    return
}

//6.设置MTKView 的代理(由CCRender来实现MTKView 的代理方法)
view.delegate = render

2.4 在render实现代理方法

MTKViewDelegate协议包含下面两个方法。

//当MTKView视图发生大小改变时调用
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize)

//每当视图需要渲染时调用
func draw(in view: MTKView)

根据设置好的帧率,每到特定的时间点,系统会自动调用draw方法来进行渲染。因此,我们的实现代码也写在draw方法里面。 这个小案例没有什么复杂的渲染逻辑,是简单的设置一下清屏颜色就好了。首先通过自定的方法获取一个随机颜色,然后为mtkViewclearColor属性赋值就OK了。

//1. 获取颜色值
let color = makeFancyColor()

//2. 设置view的clearColor
view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha)
注意: 以下操作步骤为固定流程,以后都要用到。

为当前渲染的每个渲染传递创建一个新的命令缓冲区。使用MTLCommandQueue创建对象并且加入到MTCommandBuffer对象中去。

//3.为当前渲染的每个渲染传递创建一个新的命令缓冲区
let commandBuffer = commandQueue?.makeCommandBuffer()
commandBuffer?.label = "MyCommand"

从视图绘制中,获得渲染描述符,类型为MTLRenderPassDescriptor。在创建MTLRenderCommandEncoder会用到

let renderPassDescriptor = view.currentRenderPassDescriptor

通过渲染描述符renderPassDescriptor创建MTLRenderCommandEncoder 对象,即命令渲染编辑器。我们可以使用MTLRenderCommandEncoder 来绘制对象,但是这个demo我们仅仅创建编码器就可以了,我们并没有让Metal去执行我们绘制的东西,这个时候表示我们的任务已经完成。

let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor )
renderEncoder?.label = "renderEncoder"

当编码器结束之后,命令缓存区就会接受到2个命令。

         1) present
         2) commit

因为GPU是不会直接绘制到屏幕上,因此你不给出去指令。是不会有任何内容渲染到屏幕上的。所以,我们最后要发出这两条指令。

//8.添加一个最后的命令来显示清除的可绘制的屏幕
commandBuffer?.present(view.currentDrawable!)
            
//9.在这里完成渲染并将命令缓冲区提交给GPU
commandBuffer?.commit()

最后附上完整demo