美文网首页
Metal MTKView入门案例

Metal MTKView入门案例

作者: Maji1 | 来源:发表于2020-08-23 19:08 被阅读0次

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
  • 设置deviceMTLCreateSystemDefaultDevice()设置该属性会使系统切换到高功率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 buffersclear 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)为它们编写函数,也就是上面的一段代码。光栅化阶段有固定的行为,我们无法操作。

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()
    }
  • 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:每个图元的顶点个数,绘制的图型顶点数量

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

相关文章

  • Metal MTKView入门案例

    在OpenGL中有GLKit,苹果供我们封装好了GLKView使用,笔者在之前的文章 OpenGL ES立方体贴图...

  • Metal入门002-Metal相关API

    Metal入门001-初识Metal 1. MTKView MTKView官方文档 在MetalKit中提供了一个...

  • Metal 绘制网格

    之前写过入门案例 Metal MTKView入门案例 请参考。 我们先来看下本案例的实现效果图: 首先说明一下该案...

  • 渲染代码书写指北之Metal

    渲染代码书写指北之Metal 使用MTKView设置MTKView的deviceMTLCreateSystemDe...

  • Metal 1

    Metal 1 MetalKit MTKView Metal 渲染管道 1.VertexData 通过CPU也就是...

  • Metal入门003-案例:渲染背景色

    Metal入门001-初识MetalMetal入门002-Metal相关API 本文通过渲染背景色这个小案例,展现...

  • Metal入门案例详解

    案例一:随机改变背景色 需要使用的框架:MetalKit其中的MTKViewDelegate 协议中有2个方法. ...

  • Metal入门教程(四)灰度计算

    前言 Metal入门教程(一)图片绘制Metal入门教程(二)三维变换Metal入门教程(三)摄像头采集渲染 前面...

  • Metal入门教程总结

    前言 Metal入门教程(一)图片绘制Metal入门教程(二)三维变换Metal入门教程(三)摄像头采集渲染Met...

  • Metal入门教程(七)天空盒全景

    前言 Metal入门教程(一)图片绘制Metal入门教程(二)三维变换Metal入门教程(三)摄像头采集渲染Met...

网友评论

      本文标题:Metal MTKView入门案例

      本文链接:https://www.haomeiwen.com/subject/qhgejktx.html