前言
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学习过程和体会心得,还有很多理解不透彻的地方,希望多多加油!