OpenGL ES

十三、Metal - 初探

2021-10-06  本文已影响0人  iOS之文一

音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总

Metal是苹果在2018年推出用于取代在苹果端的业务的图形编程接口,通过Metal相关API直接操作GPU,能最大限度的利用GPU能力,以后会慢慢的使用Metal取代OpenGL ES,因此有必要了解学习。

主要学习:

  1. Metal介绍
  2. 常用API
  3. Metal渲染流程
  4. Metal加载图片案例

1、Metal介绍

1.1 特点

  1. 低CPU开销
    1. 用来减少或消除许多CPU端性能瓶颈
    2. 使用Metal的API时,可以突破CPU的低性能瓶颈
  2. 最佳GPU性能
    1. Metal能在GPU上发挥最大的性能
    2. 比OpenGL ES有更高的性能
  3. 最大限度的提高CPU/GPU的并发性
  4. 有效的资源管理
    1. Metal使用简单而强大的与资源对象的接口
    2. 管理这些接口可以有效的减少内存消耗和增加访问速度

1.2 图形管道

图形管道与OpenGL ES中一样

示意图:

图形管道.png

过程:

  1. 在CPU程序将顶点数据传递到顶点着色器
  2. 在顶点着色器进行图形变换
  3. 进行图元装配
  4. 光栅化
  5. 片元着色器进行色值处理
  6. 存储到帧缓存区中

1.3 苹果给的开发建议

1. 分开渲染循环(Separate Your Rendering Loop):

  1. 项目中对于Metal的处理放在额外的工具类,不要与项目中其他的代码混在一起
  2. 在我们开发Metal程序时,将渲染循环分为自己创建的类,是非常有用的一种方式,
  3. 使用单独的类,我们可以更好管理初始化Metal,以及Metal视图委托
  4. 也就是不要把渲染代码放在ViewConroller

2. 响应视图的事件(Respond to View Events):

需要两个代理方法:
进行重绘:

窗口大小变化或重新布局:

3. 创建命令对象(Metal Command Objects):

图示:


创建命令对象.png
  1. 命令缓存区是通过命令队列创建的
  2. 命令编码器(command encoders)将命令编码到命令缓存区中
  3. 提交命令缓存区并将其发送到GPU
  4. GPU执行命令并将结果呈现为可绘制

2、常用API

3、渲染流程

流程:

  1. 获取驱动的Metal设备,也就是GPU
  2. 通过Metal设备创建命令队列
  3. 通过命令队列创建命令缓存区
  4. 通过视图创建渲染描述符
  5. 通过命令缓存区和渲染描述符创建命令编码器
  6. 通过命令编码对象进行绘制
  7. 编码结束后命令缓存区接收到present指令表示将缓存区内容渲染到屏幕上
  8. commit将命令缓存区提交到GPU,提交之后前面写的那些命令才可以真正执行

代码实现:

//1. 获取驱动的Metal设备,也就是GPU
_device = MTLCreateSystemDefaultDevice();
//2. 通过Metal设备创建命令队列
_commandQueue = [_device newCommandQueue];
//3. 通过命令队列创建命令缓存区
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
//4.从视图绘制中,获得渲染描述符
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
//6.通过渲染描述符renderPassDescriptor创建MTLRenderCommandEncoder 对象
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
[renderEncoder endEncoding];
//8.添加一个最后的命令来显示清除的可绘制的屏幕
[commandBuffer presentDrawable:view.currentDrawable];
//9.在这里完成渲染并将命令缓冲区提交给GPU
[commandBuffer commit];

说明:

  1. 驱动Metal的设备,也就是GPU
  2. 所有应用程序需要与GPU交互的第一个对象就是命令队列MTLCommandQueue
  3. 对于每一次渲染,均使用MTLCommandQueue 创建命令缓存区,并且传给当前可绘制对象。
  4. 命令缓存区需要添加命令编码器,这样当命令缓存区传给可绘制对象时,就有命令可执行了。
  5. 命令编码器的创建需要通过命令缓存区和渲染描述符。

注:Metal的运行只能在真机上

4、加载图片案例

案例地址: Metal实现图片加载

效果:

4.1 简单介绍

实现功能:
显示三角形

学习:

  1. 渲染流程
  2. 图元绘制流程
  3. 着色器的使用流程
  4. .metal文件的认识

4.2 ViewController文件

过程:

  1. 初始化MTKView
  2. 初始化渲染对象

代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    // Set the view to use the default device
    _view = (MTKView *)self.view;
    _view.device = MTLCreateSystemDefaultDevice();
    
    if(!_view.device)
    {
        NSLog(@"Metal is not supported on this device");
        return;
    }
    
    _renderer = [[CCRenderer alloc] initWithMetalKitView:_view];
    
    if(!_renderer)
    {
        NSLog(@"Renderer failed initialization");
        return;
    }
    
    // Initialize our renderer with the view size
    [_renderer mtkView:_view drawableSizeWillChange:_view.drawableSize];
    
    _view.delegate = _renderer;

}

4.3 着色器CCShaders.metal

顶点函数和片元函数的实现

函数的语法这里暂不说明,详情可以看

// 顶点着色器输出和片段着色器输入
//结构体
typedef struct
{
    //float4表示一个四维向量
    //处理空间的顶点信息
    float4 clipSpacePosition [[position]];//相当于GL_Position

    //颜色
    float4 color;//相当于GL_FragColor

} RasterizerData;

//顶点着色函数
/*
 vertexID当前处理的顶点号,第几个顶点(不是按顺序处理的)
 vertices,传入数据的入口
 */
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant CCVertex *vertices [[buffer(CCVertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])
{
    /*
     处理顶点数据:
        1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
        2) 将顶点颜色值传递给返回值
     */
    
    //定义out
    RasterizerData out; 

//    //初始化输出剪辑空间位置
//    out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
//
//    // 索引到我们的数组位置以获得当前顶点
//    // 我们的位置是在像素维度中指定的.
//    float2 pixelSpacePosition = vertices[vertexID].position.xy;
//
//    //将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型
//    vector_float2 viewportSize = vector_float2(*viewportSizePointer);
//
//    //每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
//    //计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.
//    out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
    out.clipSpacePosition = vertices[vertexID].position;

    //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
    out.color = vertices[vertexID].color;

    //完成! 将结构体传递到管道中下一个阶段:
    return out;
}

//当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.


// 片元函数
//[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.
//一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.
//被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
    
    //返回输入的片元颜色
    return in.color;
}

4.4 CCRenderer

4.4.1 渲染类的认识

它是一个渲染类,可以对MTKView视图进行渲染。继承自NSObject,但是需要遵守MTKViewDelegate协议。
我们专门创建一个渲染类对MTKView视图进行渲染,而不是在viewController中直接渲染,这是遵守苹果的分离渲染循环的建议。
因此在我们开发Metal 程序时,将渲染循环分为自己创建的类,是非常有用的一种方式,使用单独的类,我们可以更好管理初始化Metal,以及Metal视图委托.

在MTKViewDelegate 协议中有2个方法.

4.4.2 初始化MTKView

过程:

  1. 加载着色器文件
  2. 创建管道
  3. 创建命令队列

加载着色器文件:

代码:

//2.在项目中加载所有的(.metal)着色器文件
// 从bundle中获取.metal文件
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

创建管道:

过程:

  1. 先创建渲染管道描述符
  2. 给管道描述符添加片元顶点函数
  3. 设置颜色格式
  4. 通过管道描述符创建管道

代码:

MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程中各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//一组存储颜色数据的组件********************
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;

创建命令队列

代码:

//5.创建命令队列
        _commandQueue = [_device newCommandQueue];

4.3 drawableSizeWillChange代理方法

每当视图调整大小或改变方向时,会自动调用该代理

代码:

//每当视图改变方向或调整大小时调用
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
{
    // 保存可绘制的大小,因为当我们绘制时,我们将把这些值传递给顶点着色器
    _viewportSize.x = size.width;
    _viewportSize.y = size.height;
}

4.4 drawInMTKView渲染代理方法

每当视图需要渲染时系统会自动调用drawInMTKView方法

渲染过程与第三节的渲染流程一样在,只是渲染内容增加了图元绘制。
过程:
1、创建命令缓存区
2、创建命令编码器(通过命令描述符)并设置
3、命令编码器设置相应管道
4、传递参数
5、图元绘制
6、提交命令

渲染流程:

从创建命令缓存区开始,到添加命令到缓存区中,之后提交命令,这些上文已经详细说明过,这里只展示代码。

//1. 顶点数据/颜色数据
    static const CCVertex triangleVertices[] =
    {
        //顶点,    RGBA 颜色值
        { {  0.5, -0.25, 0.0, 1.0 }, { 1, 0, 0, 1 } },
        { { -0.5, -0.25, 0.0, 1.0 }, { 0, 1, 0, 1 } },
        { { -0.0f, 0.25, 0.0, 1.0 }, { 0, 0, 1, 1 } },
    };

    /*
     1、命令缓存区
     */
    //2.为当前渲染的每个渲染传递创建一个新的命令缓冲区
    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    //指定缓存区名称
    commandBuffer.label = @"MyCommand";
    
    /*
     2、创建渲染编码器(通过渲染描述符)并设置
     */
    // MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    //判断渲染目标是否为空
    if(renderPassDescriptor != nil)
    {
        //4.创建渲染命令编码器,这样我们才可以渲染到something
        id<MTLRenderCommandEncoder> renderEncoder =[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        //渲染器名称
        renderEncoder.label = @"MyRenderEncoder";
        /*
         6、提交命令
         */
        //9.表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离
        [renderEncoder endEncoding];

        //10.一旦框架缓冲区完成,使用当前可绘制的进度表
        [commandBuffer presentDrawable:view.currentDrawable];
    }

    //11.最后,在这里完成渲染并将命令缓冲区推送到GPU
    [commandBuffer commit];

图元绘制:
将绘制内容都添加到渲染命令上

过程:

  1. 设置绘制区域
  2. 添加管道状态对象
  3. 传递数据
  4. 图元绘制

代码:

//5.设置我们绘制的可绘制区域
        /*
        typedef struct {
            double originX, originY, width, height, znear, zfar;
        } MTLViewport;
         */
        //视口指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域
        //为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。
        MTLViewport viewPort = {
            0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0
        };
        [renderEncoder setViewport:viewPort];
        //[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];
        
        /*
         3、渲染编码器设置相应管道
         */
        //6.设置当前渲染管道状态对象
        [renderEncoder setRenderPipelineState:_pipelineState];
    
        
        /*
         4、传递数据(注意参数索引)
         */
        //7.从应用程序OC 代码 中发送数据给Metal 顶点着色器 函数
        //顶点数据+颜色数据
        //   1) 指向要传递给着色器的内存的指针
        //   2) 我们想要传递的数据的内存大小
        //   3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
        
        [renderEncoder setVertexBytes:triangleVertices
                               length:sizeof(triangleVertices)
                              atIndex:CCVertexInputIndexVertices];

        //viewPortSize 数据
        //1) 发送到顶点着色函数中,视图大小
        //2) 视图大小内存空间大小
        //3) 对应的索引
        [renderEncoder setVertexBytes:&_viewportSize
                               length:sizeof(_viewportSize)
                              atIndex:CCVertexInputIndexViewportSize];

       
        /*
         5、绘制顶点
         */
        //8.画出三角形的3个顶点
        // @method drawPrimitives:vertexStart:vertexCount:
        //@brief 在不使用索引列表的情况下,绘制图元
        //@param 绘制图形组装的基元类型
        //@param 从哪个位置数据开始绘制,一般为0
        //@param 每个图元的顶点个数,绘制的图型顶点数量
        /*
         MTLPrimitiveTypePoint = 0, 点
         MTLPrimitiveTypeLine = 1, 线段
         MTLPrimitiveTypeLineStrip = 2, 线环
         MTLPrimitiveTypeTriangle = 3,  三角形
         MTLPrimitiveTypeTriangleStrip = 4, 三角型扇
         */
    
        [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                          vertexStart:0
                          vertexCount:3];

重要API:

1. 设置可绘制区域:
绘制区域使用MTLViewport来实现,

MTLViewport:

typedef struct {
    double originX, originY, width, height, znear, zfar;
} MTLViewport;

[renderEncoder setViewport:viewPort];
视口指定Metal渲染内容的drawable区域。通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。实现自定义视口大小。

2. 绑定管道
[renderEncoder setRenderPipelineState:_pipelineState];
通过绑定管道,就可以获取管道中的着色器了。以此来对片元函数、顶点函数进行交互,添加命令。

3、传递数据到片元函数/顶点函数

- (void)setVertexBytes:(const void * )bytes length:(NSUInteger)length atIndex:(NSUInteger)index;

参数1:指向要传递给着色器的内存的指针
参数2:我们想要传递的数据的内存大小
参数3:一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。

4、图元绘制

- (void)drawPrimitives:(MTLPrimitiveType)primitiveType vertexStart:(NSUInteger)vertexStart vertexCount:(NSUInteger)vertexCount;

参数1:绘制图形组装的基元类型
参数2:从哪个位置数据开始绘制,一般为0
参数3:每个图元的顶点个数,绘制的图型顶点数量

只是调用的方法不一样,所需参数和OpenGL ES一样。

上一篇下一篇

猜你喜欢

热点阅读