metal初探

前言

metal是iOS底层图形渲染技术,它是利用GPU进行渲染,它允许我们程序员直接操作GPU绘制,所以相比UIKit层面,它更底层,效率更高。它跟OpenGL是一个层面的,OpenGL是跨平台的,metal虽说只支持iOS平台,但是它的效率确是OpenGL的十倍,作为iOS开发者,有必要学习一下。
metal、OpenGL、UiKit、CGGraphites之间的关系如图:
[图片上传中...(image.png-bc22ca-1573183714631-0)]

让我们来看一下metal框架结构图
[图片上传中...(image.png-f20d8b-1573183280420-0)]

理论基础学习

我们先来了解一下metal中几个属性:

  • MTLDevice:获取渲染的GPU硬件设备,这个是metal渲染的基础,必须要有GPU,所以该属性不能为空,模拟器获取不到该设备,所以metal不支持模拟器(iOS13开始,metal开始支持模拟器)。
    iOS 可以通过下面方法获取GPU:

MTLCreateSystemDefaultDevice()

  • MTLCommandQueue:命令队列,负责管理所有提交给GPU渲染的Buffer的顺序,该对象有MTLDevice生成,是个单例,可以通过以下方式获得:

device?.makeCommandQueue()

  • MTLBuffer:每一个由程序员提交的每一个指令块,他是metal编程的基本单元,每一个指令块应该明确告知二进制数据bytes,内存长度。创建API:

device?.makeBuffer(bytes: vertex_data, length: data_size, options: [])

  • MTLRenderPassDescriptor:渲染描述符。渲染描述符是描述本次GPU要渲染的顶点函数和片段函数。
    什么是顶点函数?
    比如说我们用metal绘制一个三角形,那么三角形的三个顶点的坐标就是顶点函数所要描述的。
    片段函数:
    三角形3个顶点之间如何过渡,过渡的颜色,由片段函数负责。
    需要了解的是:顶点函数和片段函数都是由Shader语言编写
Shader语言

Shader语言是metal的着色器语言,着色器语言不同于一般的编程语言,它不擅长逻辑运算,它只擅长数学计算,比如矩阵操作。因为Shader语言是面向GPU的,GPU内存很小,所以Shader语言不支持逻辑运算符语法,比如if for循环统统不支持,支持矩阵运算,基本内置函数等。
了解了Shader后,我们继续
我们刚才说了,渲染描述符对象怎么有两个重要属性必须要赋值,还有一个属性也要赋值:colorAttachments,这个是设置过渡颜色的。所以渲染描述符生成和重要属性赋值的代码如下:

let library = device?.makeDefaultLibrary()
let vertext_func = library?.makeFunction(name: "vertex_func")
let frag_func = library?.makeFunction(name: "fragment_func")
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertext_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .bgra8Unorm

  • MTLRenderPipelineState:渲染管道状态。这个即是上一个渲染描述符得到,代码如下:

try device?.makeRenderPipelineState(descriptor: rpld)

该对象,应该全局保存,因为它需要每次draw函数获取渲染数据就需要该对象,可以看出,该对象里面包含了所以GPU要渲染的东西了。

  • MTLRenderCommandEncoder:渲染命令编码器。这个是将渲染管道状态和顶点函数提交给draw函数,是最后阶段。

commandEncoder?.setRenderPipelineState(rps!)
commandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
commandEncoder?.setVertexBuffer(uniform_buffer, offset: 0, index: 1)

实战演练

我们要绘制一个颜色渐变的三角形,最后让这个三角形沿着z轴旋转。好了,不多说了,直接看演示效果图:
[图片上传中...(image.png-9ce581-1573196630965-0)]

我们新建一个view,继承MTKView。
首先,我们获取device对象:

required init(coder: NSCoder) {
        super.init(coder: coder)
        device = MTLCreateSystemDefaultDevice()
        render()
    }

init(frame : CGRect) {
        super.init(frame: frame, device: MTLCreateSystemDefaultDevice())
        render()
    }

然后,我们要绘制一个三角形,顶点函数应该是这样:

func createBuffer() {
        let vertex_data = [Vertex(position: [-1.0, -1.0, 0, 1.0], color: [1, 0, 0, 1]),
                           Vertex(position: [ 1.0, -1.0, 0, 1.0], color: [0, 1, 0, 1]),
                           Vertex(position: [   0,  0.5, 0, 1.0], color: [0, 0, 1, 1])]
        let data_size = vertex_data.count * MemoryLayout.size
        vertexBuffer = device?.makeBuffer(bytes: vertex_data, length: data_size, options: [])
    }

顶点规则是,建立以屏幕中心为左边原定,这里我们每个点是4维结构,便于和3D模型数据统一哈,当然,我们这个例子是二维的,所以只要设置前2个数据,后面两个可以固定为0和1.

color,也是4维,分别对应着r、g、b、alpha

data_size标识每个顶点数据结构的内存大小。

这样,我们就创建了三角形的三个顶点buffer了。

然后我们看完整的render函数:

func render() {
        commandQueue = device?.makeCommandQueue()
//        vertexData = [-1.0, -1.0, 0, 1.0,
//                       1.0, -1.0, 0, 1.0,
//                         0,  0.5, 0, 1.0]
//        let data_size = vertexData!.count * MemoryLayout.size
//        vertexBuffer = device?.makeBuffer(bytes: vertexData!, length: data_size, options: [])
        createBuffer()
        let library = device?.makeDefaultLibrary()
        let vertext_func = library?.makeFunction(name: "vertex_func")
        let frag_func = library?.makeFunction(name: "fragment_func")
        let rpld = MTLRenderPipelineDescriptor()
        rpld.vertexFunction = vertext_func
        rpld.fragmentFunction = frag_func
        rpld.colorAttachments[0].pixelFormat = .bgra8Unorm
        do{
            rps = try device?.makeRenderPipelineState(descriptor: rpld)
        }catch let error {
            fatalError("\(error)")
        }
    }

我们首先得到bufferQueue,然后创建buffer数据,
然后设置顶点着色器和片段着色器,合并得到渲染管道描述符,最后包装成渲染管道状态对象rps。

我们保持了这个rps, 给谁用呢?

对了,就是,都是给draw函数做准备的

我们知道,draw()每一帧运行一次,每次运行,我们把rps传进去,这样cpu就会把渲染状态对象提交到GPU去渲染。

所以,下面,我们看draw()函数:

override func draw(_ rect: CGRect) {
        if let drawable = currentDrawable ,
           let rpd = currentRenderPassDescriptor {
            rpd.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0.5, blue: 0.5, alpha: 1)
            rpd.colorAttachments[0].loadAction = .clear
            let commandBuffer = commandQueue?.makeCommandBuffer()
            let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: rpd)
            commandEncoder?.setRenderPipelineState(rps!)
            commandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
            commandEncoder?.setVertexBuffer(uniform_buffer, offset: 0, index: 1)
            update()
            commandEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
            commandEncoder?.endEncoding()
            commandBuffer?.present(drawable)
            commandBuffer?.commit()
        }
    }

这里主要是将上一步包装的rps交给encoder, 然后encoder提交给GPU。这样,一个简单的metal编程过程结束了。
还有个问题,如何进行缩放、平移呢?
这就要了解数学矩阵基础知识了。在数学矩阵中,平移矩阵是这样的:
[图片上传中...(image.png-398fc6-1573198339721-0)]

缩放矩阵:
[图片上传中...(image.png-a44394-1573198366436-0)]

旋转矩阵:
[图片上传中...(image.png-4511e8-1573198487096-0)]

所以,我们新建一个Matrix结构体:

struct Matrix {
    
    var m: [Float]

    init() {
        m = [1, 0, 0, 0,
             0, 1, 0, 0,
             0, 0, 1, 0,
             0, 0, 0, 1
        ]
    }

那么,上面三种变换,用矩阵表示:

func translationMatrix(_ matrix: Matrix, _ position: SIMD3) -> Matrix {
        var mutate = matrix
        mutate.m[12] = position.x
        mutate.m[13] = position.y
        mutate.m[14] = position.z
        return mutate
    }

    func scalingMatrix(_ matrix: Matrix, _ scale: Float) -> Matrix {
        var mutate = matrix
        mutate.m[0] = scale
        mutate.m[5] = scale
        mutate.m[10] = scale
        mutate.m[15] = 1.0
        return mutate
    }

    func rotationMatrix(_ matrix: Matrix, _ rot: SIMD3) -> Matrix {
        var mutate = matrix
        mutate.m[0] = cos(rot.y) * cos(rot.z)
        mutate.m[4] = cos(rot.z) * sin(rot.x) * sin(rot.y) - cos(rot.x) * sin(rot.z)
        mutate.m[8] = cos(rot.x) * cos(rot.z) * sin(rot.y) + sin(rot.x) * sin(rot.z)
        mutate.m[1] = cos(rot.y) * sin(rot.z)
        mutate.m[5] = cos(rot.x) * cos(rot.z) + sin(rot.x) * sin(rot.y) * sin(rot.z)
        mutate.m[9] = -cos(rot.z) * sin(rot.x) + cos(rot.x) * sin(rot.y) * sin(rot.z)
        mutate.m[2] = -sin(rot.y)
        mutate.m[6] = cos(rot.y) * sin(rot.x)
        mutate.m[10] = cos(rot.x) * cos(rot.y)
        mutate.m[15] = 1.0
        return mutate
    }

好,现在矩阵变换写好了,如何在metal中使用呢?
那么,我们上面矩阵是4x4的矩阵,所以,我们要创建buffer ,大小应该是16个顶点的大小:

func createUniformBuffer() {
        let data_length = 16 * MemoryLayout.size
        uniform_buffer = device?.makeBuffer(length: data_length, options: [])
        let bufferPointer = uniform_buffer?.contents()
        memcpy(bufferPointer, Matrix().modelMatrix(Matrix()).m, data_length)
    }

我们应该把这个方法插入到上面的render()中。

最后我们在draw()方法中,设置变化buffer:

commandEncoder?.setVertexBuffer(uniform_buffer, offset: 0, index: 1)

这里index为1,因为之前0的位置是三角形顶点buffer.

最后,为了让图形转起来,我们只要设置一个定时器,每一帧增加0.01的角度:

func update() {
        time += 0.01
        let length = 16 * MemoryLayout.size
        let bufferPointer = uniform_buffer?.contents()
        memcpy(bufferPointer, Matrix().rotateMatrixDlta(Matrix(), time).m, length)
    }

最后,应该把这个update()函数放到draw()方法里面,因为draw()是没一帧执行一次,这样图形就转起来了。

以上,就是我这两天metal学习过程和体会心得,还有很多理解不透彻的地方,希望多多加油!

你可能感兴趣的:(metal初探)