Metal MTKView入门案例

2020-08-23  本文已影响0人  Maji1

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)

控制器里的代码是不是很简单😊。

二、CQBgColorRender渲染对象中的操作

自定义初始化方法:

convenience init(mtkView: MTKView) {
    self.init() 
    self.device = mtkView.device
    self.commandQueue = self.device?.makeCommandQueue()
}

设置逐渐变化的颜色:

    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])
    }
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()

该代码调用的快慢跟我们在控制器中设置的帧率有关。

注意:当编码器结束之后,命令缓存区需要接受到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;
}
下面我们了解下Metal渲染管道 流程

此示例集中于管道的三个主要阶段:顶点阶段、光栅化阶段 和 片元阶段。顶点阶段和片元阶段是可编程的,可以用Metal Shading Language(MSL)为它们编写函数,也就是上面的一段代码。光栅化阶段有固定的行为,我们无法操作。

Metal Render Pipeline

顶点阶段为每个顶点提供数据。处理了足够多的顶点后,渲染管道将原始体栅格化,确定渲染目标中哪些像素位于原始体的边界内。片段阶段确定要写入这些像素的渲染目标的值。

看下我们自定义的渲染对象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()
    }

每当视图需要渲染时调的代理方法:

      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()
    }

苹果官方案例请参考:Using a Render Pipeline to Render Primitives

上一篇 下一篇

猜你喜欢

热点阅读