在OpenGL
中有GLKit
,苹果供我们封装好了GLKView
使用,笔者在之前的文章 OpenGL ES立方体贴图 中有使用过GLKView
。在Metal
中有MetalKit
,苹果封装了MTKView
让我们使用。
案例一:背景色逐渐变化
一、我们首先要导入import MetalKit
,看下控制器代码:
self.mtkView = MTKView()
self.mtkView.device = MTLCreateSystemDefaultDevice()
if self.mtkView.device == nil {
debugPrint("Device is nil!")
return
}
self.mtkView.preferredFramesPerSecond = 60
self.render = CQBgColorRender(mtkView: self.mtkView)
self.mtkView.delegate = self.render
self.view.addSubview(self.mtkView)
- 初始化
MTKView
。 - 设置
device
,MTLCreateSystemDefaultDevice()
设置该属性会使系统切换到高功率GPU。 -
preferredFramesPerSecond
帧率,默认每秒60帧。指定时间来调用drawInMTKView
方法,视图需要渲染时调用。 -
CQBgColorRender
自定义的渲染对象。苹果建议:Separate Your Rendering Loop
,所以这里我们自定义了渲染对象,将渲染操作跟其它事件隔离。 -
mtkView.delegate
将代理设置为我们的渲染对象render
。
控制器里的代码是不是很简单😊。
二、CQBgColorRender
渲染对象中的操作
自定义初始化方法:
convenience init(mtkView: MTKView) {
self.init()
self.device = mtkView.device
self.commandQueue = self.device?.makeCommandQueue()
}
- 所有应用程序需要与
GPU
交互的第一个对象是MTLCommandQueue
。
设置逐渐变化的颜色:
fileprivate func makeFancyColor() -> Color {
if growing {
//动态信道索引 (1,2,3,0)通道间切换
let dynamicChannelIndex = (primaryChannel + 1) % 3
colorChannels[dynamicChannelIndex] += DynamicColorRate
if(colorChannels[dynamicChannelIndex] >= 1.0) {
growing = false
//将颜色通道修改为动态颜色通道
primaryChannel = dynamicChannelIndex
}
} else {
//获取动态颜色通道
let dynamicChannelIndex = (primaryChannel + 2) % 3
colorChannels[dynamicChannelIndex] -= DynamicColorRate
if(colorChannels[dynamicChannelIndex] <= 0.0) {
growing = true
}
}
return Color(red: colorChannels[0], green: colorChannels[1], blue: colorChannels[2], alpha: colorChannels[3])
}
- Color是我们自定义的结构体
struct Color {
let red, green, blue, alpha : Double
}
还有MTKView
的两个代理方法:
当MTKView视图发生大小改变时调用:
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
这个代理方法在该案例中并没有添加操作。
每当视图需要渲染时调用:
let color = self.makeFancyColor()
view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha)
guard let commandBuffer = self.commandQueue?.makeCommandBuffer() else {
debugPrint("Make CommandBuffe failed!")
return
}
commandBuffer.label = "MyCommand"
guard let renderPassDescriptor = view.currentRenderPassDescriptor else {
debugPrint("Get current render pass descriptor failed!")
return
}
//通过渲染描述符renderPassDescriptor创建MTLRenderCommandEncoder 对象
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
debugPrint("Make render command encoder failed!")
return
}
renderCommandEncoder.label = "MyRenderCommandEncoder"
renderCommandEncoder.endEncoding()
guard let drawable = view.currentDrawable else {
debugPrint("Get current drawable failed!")
return
}
commandBuffer.present(drawable)
commandBuffer.commit()
该代码调用的快慢跟我们在控制器中设置的帧率有关。
-
self.makeFancyColor()
:获取颜色值,每次调用获取的颜色都是变化的。 -
clearColor
:用于生成currentRenderPassDescriptor
(渲染过程描述符)的清除颜色值。 -
MTLCommandBuffer
:使用MTLCommandQueue
创建对象并且加入到MTCommandBuffer
对象中去。为当前绘制的每个渲染过程创建一个新的命令缓冲区。 -
currentRenderPassDescriptor
:从当前可绘制的纹理和视图的深度depth
、模具stencil
、采样缓冲区sample buffers
和clear values
生成的渲染过程描述符。 -
makeRenderCommandEncoder
:渲染命令编码器。 -
endEncoding
:结束编码。 -
present()
:添加一个最后的命令来显示清除的可绘制的屏幕。 -
commit()
:在这里完成渲染并将命令缓冲区提交给GPU。
注意:当编码器结束之后,命令缓存区需要接受到2个命令 present()
、commit()
。因为GPU是不会直接绘制到屏幕上,因此你不给出去指令是不会有任何内容渲染到屏幕上。
案例二:绘制三角形

该示例为每个顶点提供位置和颜色,渲染管道使用该数据渲染三角形,在为三角形顶点指定的颜色之间插值颜色值。

下面我们来看下代码,控制器中的代码跟上面的案例基本上一样。
本案例需要用到metal
文件,首先看下metal
文件代码:
#include <metal_stdlib>
using namespace metal;//命名空间metal
typedef struct {
vector_float4 position;
vector_float4 color;
} VertexInput;
typedef struct {
//处理空间的顶点信息
float4 clipSpacePosition [[position]];
float4 color;
} VertexOut;
vertex VertexOut vertexShader(uint vertexID [[vertex_id]],
constant VertexInput *input [[buffer(0)]]) {
VertexOut out;
//1.执行坐标系转换,将生成的顶点剪辑空间写入到返回值中。
out.clipSpacePosition = input[vertexID].position;
//2.将顶点颜色值传递给返回值。
out.color = input[vertexID].color;
return out;
}
fragment float4 fragmentShader(VertexOut in [[stage_in]]) {
return in.color;
}
-
vertex
表示是顶点函数。VertexOut
我们自定义的返回值类型。vertexShader
函数名。
uint vertexID [[vertex_id]]
:uint
参数类型,vertexID
参数名可自定义,vertex_id
顶点id,苹果定义的不能改。 -
fragment
表示是片元函数。float4
返回值类型。fragmentShader
片元函数的函数名。
VertexOut in [[stage_in]]
:VertexOut
参数类型,跟顶点函数的返回值类型保持一致。in
:参数名,可修改。stage_in
苹果定义不能改。
下面我们了解下Metal
渲染管道 流程
此示例集中于管道的三个主要阶段:顶点阶段、光栅化阶段 和 片元阶段。顶点阶段和片元阶段是可编程的,可以用Metal Shading Language(MSL)
为它们编写函数,也就是上面的一段代码。光栅化阶段有固定的行为,我们无法操作。

顶点阶段为每个顶点提供数据。处理了足够多的顶点后,渲染管道将原始体栅格化,确定渲染目标中哪些像素位于原始体的边界内。片段阶段确定要写入这些像素的渲染目标的值。
看下我们自定义的渲染对象CQTriangleRender
中的代码,首先看下初始化的代码:
convenience init(mtkView: MTKView) {
self.init()
self.device = mtkView.device//1.
//2.加载着色器文件
let library = self.device?.makeDefaultLibrary()
let vertexFunction = library?.makeFunction(name: "vertexShader")
let fragmentFunction = library?.makeFunction(name: "fragmentShader")
//3.创建渲染管线描述符
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
renderPipelineDescriptor.label = "Simple Pipeline"
//可编程函数,处理渲染过程中的各个顶点
renderPipelineDescriptor.vertexFunction = vertexFunction
//可编程函数,用于处理渲染过程中各个片段/片元
renderPipelineDescriptor.fragmentFunction = fragmentFunction
//一组存储颜色数据的组件
renderPipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
//4.创建渲染管线状态对象
do {
self.pipelineState = try self.device?.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
} catch {
debugPrint("pipelineState error")
}
//5.
self.commandQueue = self.device?.makeCommandQueue()
}
- 1.获取设备
MTLDevice
- 2.这里需要加载着色器文件
"vertexShader"
和"fragmentShader"
需要跟metal
文件中的顶点函数和片元函数名保持一致。 - 3.创建渲染管线描述符
MTLRenderPipelineDescriptor ()
。 - 4.创建渲染管线状态对象
MTLRenderPipelineState
。 - 5.创建命令队列
MTLCommandQueue
。
每当视图需要渲染时调的代理方法:
func draw(in view: MTKView) {
//1.创建命令缓冲区
guard let commandBuffer = self.commandQueue?.makeCommandBuffer() else {
debugPrint("Make command buffer failed!")
return
}
commandBuffer.label = "MyCommand"
//2.获取当前渲染过程描述符
guard let renderPassDescriptor = view.currentRenderPassDescriptor else {
debugPrint("Get current render pass descriptor failed!")
return
}
//3.创建命令编码器
guard let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
debugPrint("Make command encoder failed!")
return
}
commandEncoder.label = "MyRenderCommandEncoder"
//4.设置当前渲染管道状态对象
commandEncoder.setRenderPipelineState(self.pipelineState!)
//5.设置顶点、颜色数据
let vertexBufferSize = MemoryLayout<Float>.size * self.vertexArrayData.count
commandEncoder.setVertexBytes(vertexArrayData, length:vertexBufferSize, index: 0)
//6.绘制
commandEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
//7.表示该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离
commandEncoder.endEncoding()
//8.一旦框架缓冲区完成,使用当前可绘制的进度表
commandBuffer.present(view.currentDrawable!)
//9.完成渲染并将命令缓冲区推送到GPU
commandBuffer.commit()
}
-
func setVertexBytes(_ bytes: UnsafeRawPointer, length: Int, index: Int)
:从应用程序代码中发送数据给Metal
顶点着色函数,顶点数据 和 颜色数据
bytes
:指向要传递给着色器的内存的指针
length
:我们想要传递的数据的内存大小
index
:一个整数索引,它对应于我们的vertexShader
函数中的缓冲区属性限定符的索引。 -
func drawPrimitives(type primitiveType: MTLPrimitiveType, vertexStart: Int, vertexCount: Int)
:在不使用索引列表的情况下,绘制图元。类型:点、线段、线环、三角形、三角形伞。
primitiveType
:绘制图形组装的基元类型。
vertexStart
:从哪个位置数据开始绘制,一般为0
vertexCount
:每个图元的顶点个数,绘制的图型顶点数量
网友评论