Metal入门

1,314 阅读4分钟

Metal简述

Metal是2018年苹果推出的用于取代在苹果端的业务的图形编程接口,在2018年之前使用的是基于OpenGL ES 封装的GLKit,通过Metal相关API直接操作GPU,能最大限度的利用GPU能力。因此使用Metal必须使用iPhone6s及以上机型且必须是真机(模拟器不支持GPU,模拟器使用的是CPU来模拟GPU)。

特点

  • CPU低消耗性
  • 更高效的GPU性能,Metal能更好的发挥GPU的性能
  • 提高CPU与GPU的并发性
  • 有效的资源管理

如下图介绍

图形管道

Metal的图形管道通OpenGL大体相同,如图所示

  • CPU将顶点数据传到顶点着色器
  • 顶点着色器将处理好的顶点进行图元装配
  • 进行光栅化处理
  • 将光栅化的数据传给片元着色器处理
  • 渲染到屏幕显示

Metal 命令与对象之间的关系

  • 命令缓存区(command buffer):从命令队列(Command Queue)中创建

  • 命令编码(Command encoder):将命令编码到命令缓冲区中

  • 提交命令缓冲区并将其发送到GPU

  • GPU执行命令并将结果呈现为可绘制

Metal 部分API简介

MTKView

在MetalKit中提供了一个视图类MTKView,类似于GLKit中GLKView,它是NSView(macOS中的视图类)或者UIView(iOS、tvOS中的视图类)的子类。用于处理metal绘制并显示到屏幕过程中的细节。因此使用的时候一种是将self.View强转成MTKView,当然self.View的类型需要是MTKView的类型,否则会崩溃;另一种就是直接创建MTKView。

MTLDevice

在Metal中操作的是GPU,因此必须达到GPU对象才可以操作。Metal提供了MTLDevice协议接口来操作GPU,创建MTLDevice,如下

view.device = ;MTLCreateSystemDefaultDevice();

其中view是MTKView类型

MTLCommandQueue

命令队列,与GPU交互的第一个对象,其保存了将要渲染的命令对象

MTLCommandBuffer

命令缓冲区,用来存储命令

MTLRenderCommandEncoder

表示单个渲染过程中相关联的渲染状态和渲染命令,有以下功能

  • 指定图形资源:例如缓存区和纹理对象,其中包含顶点、片元、纹理图片数据

  • 指定一个MTLRenderPipelineState对象,表示编译的渲染状态,包含顶点着色器和片元着色器的编译&链接情况

  • 指定固定功能,包括视口、三角形填充模式、剪刀矩形、深度、模板测试以及其他值

  • 绘制3D图元

Apple 建议

  • Separate Your Rendering Loop:分开渲染循环,苹果希望渲染放在一个单独的类中处理

  • Respond to View Events:即MTKDelegate,响应视图事件也要放在单独的类中处理 

  • Metal Command Objects:创建一个命令对象,即创建执行命令的GPU、与GPU交互的MTLCommandQueue对象以及MTCommandBuffer渲染缓存区

案例分析

颜色渲染案例

  • 创建MTKView

     self.mtkView = (MTKView *)self.view;
    
  • 设置device

     self.mtkView.device = MTLCreateSystemDefaultDevice();
    
  • 设置metal代理

    self.renderView = [[SFxRender alloc] initWithMetalView:self.mtkView];
        if (!self.renderView) {
            NSLog(@"render 失败");
            return;
        }
        self.mtkView.delegate = self.renderView;
        self.mtkView.preferredFramesPerSecond = 60;
    

渲染分类

  • 根据device创建命令队列

    -(instancetype)initWithMetalView:(MTKView *)mtkView {
        self = [super init];
        if (self) {
            self.device = mtkView.device;
            self.commandQueue = [self.device newCommandQueue];
        }
        return self;
    }
    
  • 使用命名队列创建命令缓冲区

    //创建命令缓冲区
        id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
        commandBuffer.label = @"myBuffer";
    
  • 获得渲染描述符

    //从视图绘制中获得渲染描述符
        MTLRenderPassDescriptor *passDescriptor = view.currentRenderPassDescriptor;
    
  • 创建命令编码器

    //创建命令编码器
            id<MTLCommandEncoder> commanderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:passDescriptor];
            commanderEncoder.label = @"myEncoder";
            //本demo只是变换颜色,所以创建命令编码器即可
            //停止编码
            [commanderEncoder endEncoding];
    
  • 显示清除的可绘制屏幕

     /*
             当编码器结束之后,命令缓存区就会接受到2个命令.
             1) present
             2) commit
             因为GPU是不会直接绘制到屏幕上,因此你不给出去指令.是不会有任何内容渲染到屏幕上.
            */
            //添加一个最后的命令来显示清除的可绘制的屏幕
            [commandBuffer presentDrawable:view.currentDrawable];
    
  • 提交绘制

    //提交绘制
        [commandBuffer commit];
    
  • 动态色值计算

    -(Colors)getRandomColor {
        static BOOL  growing = YES;
        //设置颜色通道的值[0, 1]
        static NSUInteger primaryChannel = 0;
        //颜色通道数组
        static float colorsChannel[] = {1.0, 0.0, 0.0, 1.0};
        //颜色变化的步长
        const float dynamicColorRate = 0.015;
        if (growing) {
            //获取动态信道索引 (0,1,2,3)之间变换
            NSUInteger dynamicChannelIndex = (primaryChannel + 1) % 3;
            //对应色值自增
            colorsChannel[dynamicChannelIndex] += dynamicColorRate;
            if (colorsChannel[dynamicChannelIndex] >= 1.0) {//最大是1.0
                growing = NO;
                //将颜色通道改为动态颜色通道
                primaryChannel = dynamicChannelIndex;
            }
        }else{
            NSUInteger dynamicChannelIndex = (primaryChannel + 2) % 3;
            colorsChannel[dynamicChannelIndex] -= dynamicColorRate;
            if (colorsChannel[dynamicChannelIndex] <= 0.0) {
                growing = YES;
            }
        }
        Colors color;
        color.red = colorsChannel[0];
        color.green = colorsChannel[1];
        color.blue = colorsChannel[2];
        color.alpha = colorsChannel[3];
        return color;
    }