理解Metal渲染管道
渲染管道负责处理绘制命令并将数据写入呈现通道的目标。一个渲染管道有许多阶段,一些使用着色器编程,其他固定或可配置的行为。
简化的渲染管道的有三个主要阶段:顶点阶段、光栅化阶段和片段阶段。顶点阶段和片段阶段是可编程的,所以你可以用 Metal Shading Language(MSL) 为它们编写函数。光栅化阶段具有固定的行为。
顶点阶段 为每个顶点提供数据。当处理了足够多的顶点时,渲染管道将对图元进行栅格化,确定呈现目标中的哪些像素位于图元的边界内。片段阶段 决定将这些像素写入渲染目标的值。
数据的传递通常在三个地方:
- 管道的输入,由应用程序提供,并传递到顶点阶段。
- 顶点阶段的输出,它被传递到光栅化阶段。
- 片段阶段的输入,由应用程序提供或由光栅化阶段生成。
使用MSL自定义Shader
首先Metal的shader支持自定义的基于SIMD类型的自定义数据结构做为应用程序的输入数据。
但是这个存放自定义数据结构的需要是一个 .h
文件,而Swift如果想使用,就需要通过桥接文件 XXXX-Bridging-Header
需要注意,这个存放自定义数据结构的
.h
文件必须与对应的.metal
文件放在同一文件夹下面,否则.metal
中会报错找不到文件
ShaderType.h
#ifndef AZShaderType_h
#define AZShaderType_h
// 使用SIMD类型需要导入SIMD库头文件
#include <simd/simd.h>
// 定义一个枚举,存放顶点着色器传参的索引值,方便应用程序使用,比硬编码方便阅读
typedef enum ShaderVertexInputIndex
{
// 顶点参数的索引
ShaderVertexInputIndexVertices = 0,
// 画布大小参数的索引
ShaderVertexInputIndexViewportSize = 1,
} ShaderVertexInputIndex;
// 自定义的顶点数据类型
typedef struct
{
// 顶点x,y值
vector_float2 position;
// 顶点颜色 rgba
vector_float4 color;
} ShaderVertex;
#endif /* AZShaderType_h */
复制代码
Shader.metal
#include <metal_stdlib>
using namespace metal;
// 导入自定义的顶点入参数据类型
#include "ShaderType.h"
// 自定义的顶点着色器的输出
struct RasterizerData
{
// [[position]]关键字表示这是顶点数据
float4 posit [[position]];
float4 color;
};
// 顶点着色器代码 以 `vertex` 关键字表示这是一个顶点函数
// [[vertex_id]]关键字表示这个参数是顶点索引
// [[buffer(n)]]修饰符,默认情况下,Metal会在参数表中为每个参数自动分配槽位。
// 当将[[buffer(n)]]限定符添加到buffer参数时,您将显式地告诉Metal使用哪个槽。
// 明确声明槽可以让你更容易修改着色器,而不需要改变应用程序代码。
// 在共享头文件中声明两个索引的常量。
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
constant ShaderVertex *vertices [[buffer(ShaderVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(ShaderVertexInputIndexViewportSize)]])
{
// 顶点着色器中顶点坐标系是[-1,1]的
RasterizerData out;
vector_float2 pixelSpacePosition = vertices[vertexID].position.xy;
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
float ratio = viewportSize.y / viewportSize.x;
out.posit = vector_float4(0.0, 0.0, 0.0, 1.0);
out.posit.x = pixelSpacePosition.x/100.0;
out.posit.y = pixelSpacePosition.y/100.0 * ratio;
out.color = vertices[vertexID].color;
// 输出结果,会进入光栅化器,光栅化器会对顶点输出进行插值计算,生成对应像素点的数据,然后逐像素调用片段着色器
return out;
}
// 片段着色器代码 以 `fragment` 关键字表示这是一个片段函数
// [[stage_in]]关键字表示该参数是由光栅化器生成的
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
// 返回的颜色,就是当前像素的渲染颜色
return in.color;
}
复制代码
归一化设备坐标系
使用示例
ViewController.swift
import UIKit
import MetalKit
class ViewController: UIViewController {
// 用了呈现渲染内容的MTKView
lazy private var metalView: MTKView = {
let view = MTKView(frame: CGRect(x: 100, y: 100, width: 250, height: 150))
view.enableSetNeedsDisplay = true
view.device = MTLCreateSystemDefaultDevice()
view.clearColor = MTLClearColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)
return view
}()
// 负责渲染的实际对象
lazy private var render: MetalRender = {
// 把呈现view传入,配置渲染管道时需要用到
let render = MetalRender(metalView)
return render
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(metalView)
// 把呈现view与负责渲染的对象绑定
metalView.delegate = render
// 通过view frame变化方法,传入当前view的大小
render.mtkView(metalView, drawableSizeWillChange: metalView.drawableSize)
}
}
复制代码
MetalRender.swift
import UIKit
import MetalKit
// 导入SIMD库
import simd
class MetalRender: NSObject {
// 向设备传递命令的命令队列
private var commandQueue: MTLCommandQueue?
// 渲染管道状态
private var pipelineState: MTLRenderPipelineState?
// 视图的当前大小
private var viewportSize: vector_uint2 = vector_uint2(x: 1, y: 1)
// 顶点数据
private let triangleVertices: [ShaderVertex] = {
let vertex0 = ShaderVertex(position: vector_float2(x: 0, y: 0), color: vector_float4(x: 1, y: 0, z: 0, w: 1))
let vertex1 = ShaderVertex(position: vector_float2(100, 0), color: vector_float4(0, 1, 0, 1))
let vertex2 = ShaderVertex(position: vector_float2(50, 100), color: vector_float4(0, 0, 1, 1))
let array: [ShaderVertex] = [vertex0, vertex1, vertex2]
return array
}()
private override init() {
super.init()
}
convenience init(_ view: MTKView) {
self.init()
// 从view获取device
let device = view.device
// 生成命令队列
self.commandQueue = device?.makeCommandQueue()
// 加载项目中所有扩展名为.metal的着色器文件
let defaultLibrary = device?.makeDefaultLibrary()
// 顶点着色器函数
let vertexFunction = defaultLibrary?.makeFunction(name: "vertexShader")
// 片段着色器函数
let fragmentFunction = defaultLibrary?.makeFunction(name: "fragmentShader")
// 配置用于创建管道状态的管道描述符
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
// 辅助标识的String,可以在调试程序时辅助定位
pipelineStateDescriptor.label = "Simple Pipeline"
// 设置顶点着色器函数
pipelineStateDescriptor.vertexFunction = vertexFunction
// 设置片段着色器函数
pipelineStateDescriptor.fragmentFunction = fragmentFunction
// 渲染目标的像素格式。因为这个示例只有一个渲染目标,并且它是由视图提供的,所以将视图的像素格式复制到渲染管道描述符中。
pipelineStateDescriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat
// 创建渲染管道
do {
pipelineState = try device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
}catch {
print(error)
}
}
}
extension MetalRender: MTKViewDelegate {
// 每当视图改变方向或调整大小时调用
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
viewportSize.x = UInt32(size.width)
viewportSize.y = UInt32(size.height)
print("mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize)")
}
// 当视图需要渲染帧时调
func draw(in view: MTKView) {
print("draw(in view: MTKView)")
// 渲染管道要存在
guard pipelineState != nil else { return }
// 给命令队列创建buffer
let commandBuffer = commandQueue?.makeCommandBuffer()
// 辅助标识的String,可以在调试程序时辅助定位
commandBuffer?.label = "MyCommand"
// 获取当前view的renderPassDescriptor
if let renderPassDescriptor = view.currentRenderPassDescriptor {
// 创建命令编码器
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
// 辅助标识的String,可以在调试程序时辅助定位
renderEncoder?.label = "MyRenderEncoder"
// 设置视口
renderEncoder?.setViewport(MTLViewport(originX: 0, originY: 0, width: Double(viewportSize.x), height: Double(viewportSize.y), znear: -1, zfar: 1))
// 设置渲染管道
renderEncoder?.setRenderPipelineState(pipelineState!)
// 传递顶点数据
renderEncoder?.setVertexBytes(triangleVertices, length: triangleVertices.count*MemoryLayout<ShaderVertex>.size, index: Int(ShaderVertexInputIndexVertices.rawValue))
// 传递viewportSize
renderEncoder?.setVertexBytes(&viewportSize, length: MemoryLayout<vector_uint2>.size, index: Int(ShaderVertexInputIndexViewportSize.rawValue))
// 编码绘图命令 指定原语的类型、起始索引和顶点的数量
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
// 结束编码
renderEncoder?.endEncoding()
if let drawable = view.currentDrawable {
// 配置是否渲染到屏幕上
commandBuffer?.present(drawable)
}
}
// 提交命令缓冲区
commandBuffer?.commit()
}
}
复制代码