Metal学习(3) - 渲染一个简单的2D三角形

理解Metal渲染管道

渲染管道负责处理绘制命令并将数据写入呈现通道的目标。一个渲染管道有许多阶段,一些使用着色器编程,其他固定或可配置的行为。
简化的渲染管道的有三个主要阶段:顶点阶段光栅化阶段片段阶段顶点阶段片段阶段是可编程的,所以你可以用 Metal Shading Language(MSL) 为它们编写函数。光栅化阶段具有固定的行为。

image.png

顶点阶段 为每个顶点提供数据。当处理了足够多的顶点时,渲染管道将对图元进行栅格化,确定呈现目标中的哪些像素位于图元的边界内。片段阶段 决定将这些像素写入渲染目标的值。

数据的传递通常在三个地方:

  • 管道的输入,由应用程序提供,并传递到顶点阶段。
  • 顶点阶段的输出,它被传递到光栅化阶段。
  • 片段阶段的输入,由应用程序提供或由光栅化阶段生成。

使用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;
}
复制代码

归一化设备坐标系

image.png

使用示例

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()
    }
}
复制代码

猜你喜欢

转载自juejin.im/post/7085634116074340360