美文网首页
Metal(2)- 颜色渲染和绘制三角形案例解析

Metal(2)- 颜色渲染和绘制三角形案例解析

作者: 恍然如梦_b700 | 来源:发表于2020-09-16 16:28 被阅读0次

    颜色渲染案例

    本案例的目的在于了解metal相关API的使用,及简单渲染的流程

    整体的效果图如下

    image

    案例的整体流程如下

    image

    主要分为两部分

    • viewDidLoad函数:加载自定义的渲染视图MTKView,并将渲染交由自定的渲染循环类处理
    • 渲染循环类:处理metal渲染的相关操作

    viewDidLoad函数

    该函数中主要是加载view以及view传递给render渲染循环类,主要流程如下

    image

    分为以下几步

    • 获取view
    • 设置device
    • 创建render
    • 设置view的delaegate
    • 设置帧速率

    获取view
    获取MTKView的对象view的方式有两种,类似于GLKitGLKView

    • 可以在storyboard将view的类改为MTKView,
    • 可以创建MTKView对象,再将其添加到控制器的view中
    _view = (MTKView*)self.view;
    
    

    设置device
    主要是获取GPU的使用权限,一个MTLDevice对象代表着一个GPU,一般使用默认方法MTLCreateSystemDefaultDevice来获取默认的单个GPU对象,并且在创建完成后,需要判断是否获取GPU的使用权限,如果不成功,则中断渲染流程

    _view.device = MTLCreateSystemDefaultDevice();
    if (!_view.device) {
        NSLog(@"Metal is not supported on this device");
        return;
    }
    
    

    创建render
    在metal框架中,苹果建议在开发mental程序时,最好是将渲染循环独立成一个类,目的是为了更高的管理metal以及metal视图委托。创建完成后,同样需要判断是否创建成功,如果不成功,则中断渲染流程

    _render = [[CJLRenderer alloc] initWithMetalKitView:_view];
    
    //5.判断_render 是否创建成功
    if (!_render) {
        NSLog(@"Renderer failed initialization");
        return;
    }
    
    

    设置view的delegate
    将view的渲染处理加油render对象处理

    _view.delegate = _render;
    
    

    设置帧速率
    在view中可以通过设置帧速率,不同的触发试图渲染,然后回调MTKViewDelegate中的drawInMTKView方法

    _view.preferredFramesPerSecond = 60;
    
    

    渲染循环类

    管理metal的初始化以及metal中的视图委托,主要有以下四个函数

    • initWithMetalKitView函数:初始化,需要传入MTKView对象view获取GPU的使用权限等
    • makeFancyColor函数:主要是设置颜色,即随着帧率变化的颜色,这里就不展开讲解了,详情见完整代码
    • MTKViewDelegate协议
      • drawableSizeWillChange代理方法:view大小发生变化时回调
      • drawInMTKView代理方法:view需要渲染时回调

    由于本案例不涉及view大小的改变,所以着重讲initWithMetalKitViewdrawInMTKView方法

    initWithMetalKitView函数

    渲染循环类对外的初始化方法,主要是通过传入的view,获取metal设备以及创建命令队列,流程如下

    image
    • 设置device
      此处的device并不是新建的,是由传入view在外面创建好的,可以通过view获取
    _device = mtkView.device;
    
    
    • 设置命令队列
      命令队列是所有app与GPU交互的第一个对象,是使用MTLCommandQueue 去创建对象,并且加入MTLCommandBuffer 对象中.确保它们能够按照正确顺序发送到GPU.对于每一帧,一个新的MTLCommandBuffer 对象创建并且填满了由GPU执行的命令.
    _commandQueue = [_device newCommandQueue];
    
    

    drawInMTKView代理方法

    通过view设置的帧速率,每当到指定时间时,就会触发view的渲染,继而回调drawInMTKView代理方法进行绘制渲染。主要流程如下

    image
    • 根据makeFancyColor函数获取当前帧显示的颜色
    Color color = [self makeFancyColor];
    
    
    • 设置view的清屏颜色,通过由MTLClearColorMake创建,相当于OpenGL ES中的glClearColor
    view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha);
    
    
    • 创建渲染缓存区,目的是为了将渲染对象加入到渲染缓存区,使用MTLCommandQueue 创建对象并且加入到MTCommandBuffer对象中,且为当前渲染的每个渲染传递创建一个新的命令缓冲区
    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
        //设置渲染缓存区的命名
        commandBuffer.label = @"MyCommand";
    
    
    • 获取渲染描述修饰符,通过commandQueue获取,类型是MTLRenderPassDescriptor,用于在commandBuffer中创建commandEncoder
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    
    
    • 创建commandEncoder,通过渲染描述符renderPassDescriptor创建MTLRenderCommandEncoder 对象,即命令渲染编辑器,相当于OpenGL ES中的program,主要用途是用于绘制对象,但在这个案例中并没有需要绘制的东西
    id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
            //命令编辑器命名
            renderEncoder.label = @"MyRenderEncoder";
    
    
    • 结束渲染编辑:当没有需要绘制的任务时,即可结束MTLRenderCommandEncoder 工作
     [renderEncoder endEncoding];
    
    
    • 渲染到屏幕上,添加一个最后的命令来显示清除的可绘制的屏幕
      当编码器结束工作后,命令缓存区会收到两个命令:
      • present命令:渲染到屏幕上
      • commit命令:将commandBuffer提交至GPU
        主要是因为GPU不会直接渲染到屏幕上,如果不给命令,那么绘制的内容是不会显示到屏幕上的
    [commandBuffer presentDrawable:view.currentDrawable];
    
    
    • 完成渲染并将命令缓冲区提交给GPU,相当于OpenGL ES中的draw
    [commandBuffer commit];
    
    

    以上几个步骤,在metal渲染中几乎都要使用到,需要牢记!!

    绘制三角形

    本案例的目的在于理解Metal中使用着色器绘制三角形的流程

    整体效果图如下

    image

    整体的流程图如下

    image

    颜色的渲染加载相比,viewDidLoad函数基本没变化,主要变化的是initWithMetalKitView函数和drawInMTKView代理方法

    • initWithMetalKitView函数:增加了metal着色器文件初始化的准备工作
    • drawInMTKView代理方法:增加了metal绘制三角形的流程

    准备工作

    创建metal文件

    • command + N --> Metal File 创建metal着色器文件

      image
    • 定义顶点着色器输入和片元着色器输入,相当于OpenGL ES中的varying修饰的变量,即桥接变量

    //顶点着色器输出和片元着色器输入(相当于OpenGL ES中的varying修饰的变量,即桥接)
    typedef struct
    {
    //    处理空间的顶点信息,相当于OpenGL ES中的gl_Position
    //    float4 修饰符,是一个4维向量
        float4 clipSpacePosition [[position]];
    
    //    颜色,相当于OpenGL ES中的gl_FragColor
        float4 color;
    
    }RasterizerData;
    
    
    • 定义顶点着色器函数 和 片元着色器函数
    //顶点着色器函数
    /*
     vertex:修饰符,表示是顶点着色器
     RasterizerData:返回值
     vertexShader:函数名称,可自定义
    
     vertexID:metal自己反馈的id
     vertices:1)告诉存储的位置buffer 2)告诉传递数据的入口是CJLVertexInputIndexVertices
     vertices 和 viewportSizePointer 都是通过CJLRenderer 传递进来的
     */
    vertex RasterizerData
    vertexShader(uint vertexID [[vertex_id]],
                 constant CJLVertex *vertices [[buffer(CJLVertexInputIndexVertices)]],
                 constant vector_uint2 *viewportSizePointer [[buffer(CJLVertexInputIndexViewportSize)]])
    {
    
    }
    
    /*
     fragment:修饰符,表示是片元着色器
     float4:返回值,即颜色值RGBA
     fragmentShader:函数名称,可自定义
    
     RasterizerData:参数类型(可修改)
     in:形参变量(可修改)
     [[stage_in]]:属性修饰符,表示单个片元输入(由定点函数输出)(不可修改),相当于OpenGL ES中的varying
     */
    fragment float4 fragmentShader(RasterizerData in [[stage_in]])
    {
    
    }
    
    

    创建C 与 OC的桥接文件

    该头文件的目的是为了c代码与OC代码可以共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用

    • 定义缓存区索引值,表示向metal着色器传递数据的入口枚举值,相当于OpenGL ES中GLSL语言定义的顶点坐标入口position
    typedef enum CJLVertexInputIndex
    {
    //    顶点
        CJLVertexInputIndexVertices = 0,
    
    //    视图大小
        CJLVertexInputIndexViewportSize = 1,
    
    }CJLVertexInputIndex;
    
    
    • 定义图形数据的结构体,包含顶点和颜色值,类似于OpenGL ES中的顶点数据的结构体
    //结构体:顶点/颜色值
    typedef struct
    {
    //    像素空间的位置
    //    像素中心点(100,100)
        vector_float4 position;
    
    //    RGBA颜色
        vector_float4 color;
    }CJLVertex;
    
    

    viewDidLoad函数

    这个函数的流程如下

    image

    Metal 入门级01:颜色的渲染加载相比,只是多了一个设置视口大小,会触发MTKViewDelegate协议的drawableSizeWillChange方法,这里不做过多说明

    渲染循环类

    渲染循环类CJLRenderer是服务于MTKView的,用于管理view的渲染以及view的代理方法的回调

    initWithMetalKitView函数

    这部分的代码都是绘制前的准备工作,流程图如下

    image

    主要分为是三步

    • 设置device
    • metal着色器文件加载
    • 设置命令队列

    其中第一步和第三步在上个案例均有提及,这里不再说明,主要说说metal着色器文件的加载

    metal着色器文件加载

    根据上图所示,可以划分为以下几步

    • 加载metal文件
      从bundle中获取metal后缀的着色器,并获取顶点着色器和片元着色器所对应的函数,其中函数的获取需要通过MTLLibrary对象根据对应的函数名称获取
    //2.在项目中加载所有的(.metal)着色器文件
    // 从bundle中获取.metal文件
    id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
    //从库中加载顶点函数
    id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
    //从库中加载片元函数
    id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
    
    
    • 配置渲染管道
      通过MTLRenderPipelineDescriptor初始化渲染管道,并设置管管名称、顶点 & 片元着色器对应的函数以及存储颜色数据的组件
    //3.配置用于创建管道状态的管道,即渲染管道
    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    //管道名称
    pipelineStateDescriptor.label = @"Simple Pipeline";
    //可编程函数,用于处理渲染过程中的各个顶点
    pipelineStateDescriptor.vertexFunction = vertexFunction;
    //可编程函数,用于处理渲染过程中各个片段/片元
    pipelineStateDescriptor.fragmentFunction = fragmentFunction;
    //一组存储颜色数据的组件
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
    
    
    • 创建渲染管线状态对象
      通过device和渲染管道创建渲染管线状态的对象,并判断是否创建成功
    //4.同步创建并返回渲染管线状态对象
    _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
    //判断是否返回了管线状态对象
    if (!_pipelineState) {
        //如果我们没有正确设置管道描述符,则管道状态创建可能失败
        NSLog(@"Failed to created pipeline state, error %@", error);
        return nil;
    }
    
    

    drawInMTKView代理方法

    当view和render以及视口初始化后,MTKView对象view中默认的帧速率是60,与屏幕刷新的帧率是一致的,所以会随着屏幕刷新的帧率,不停的调用该绘制的代理方法

    image

    该方法整体的流程如下

    image

    相比颜色渲染的案例,多了metal绘制三角形的流程,下面主要说下该流程,三角形的绘制时在创建commandEncoder对象之后,结束commandEncoder工作之前完成的,且在绘制之前,需要在创建commandBuffer之前创建三角形的顶点和颜色数据

    //    1\. 顶点数据、颜色数据
    static const CJLVertex 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 } },
    };
    
    

    三角形的绘制主要分为以下几步:

    • 设置可绘制的区域,即设置视口,相当于OpenGL ES中的glViewPort
      • 通过MTLViewport创建视口对象
        视口主要用于指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域

      • 为管道分配自定义视口需要通过调用setViewport
        方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。

    MTLViewport viewPort = {
        0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0
    };
    //设置视口 相当于OpenGL ES中的glViewPort
    [renderEncoder setViewport:viewPort];
    
    //6.设置当前渲染管道状态对象
    [renderEncoder setRenderPipelineState:_pipelineState];
    
    
    • 传递数据
      从app的OC代码中传递数据到Metal 顶点着色器/片元着色器 函数,,相当于OpenGL ES中的glVertexAttribPointer,目前需要传递的数据有两种
      • 顶点+颜色数据
      • 视图大小平数据
    //   1) 指向要传递给着色器的内存的指针
    //   2) 我们想要传递的数据的内存大小
    //   3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
    //        CJLVertexInputIndexVertices 是顶点数据的入口,需要由自己定义,相当于OpenGL ES中glGetAttribLocation
    [renderEncoder setVertexBytes:triangleVertices
                           length:sizeof(triangleVertices)
                          atIndex:CJLVertexInputIndexVertices];
    
    //viewPortSize 数据
    //1) 发送到顶点着色函数中,视图大小
    //2) 视图大小内存空间大小
    //3) 对应的索引
    [renderEncoder setVertexBytes:&_viewportSize
                           length:sizeof(_viewportSize)
                          atIndex:CJLVertexInputIndexViewportSize];
    
    
    • 绘制三角形,相当于OpenGL ES中的glDrawArrays
      metal中的图元有5种,OpenGL ES中则是有9种,metal的图元类型如下表所示
    图元类型 说明
    MTLPrimitiveTypePoint = 0
    MTLPrimitiveTypeLine = 1 线
    MTLPrimitiveTypeLineStrip = 2 线环
    MTLPrimitiveTypeTriangle = 3 三角形
    MTLPrimitiveTypeTriangleStrip = 4 三角形扇
    // @method drawPrimitives:vertexStart:vertexCount:
    //@brief 在不使用索引列表的情况下,绘制图元
    //@param 绘制图形组装的基元类型
    //@param 从哪个位置数据开始绘制,一般为0
    //@param 每个图元的顶点个数,绘制的图型顶点数量
    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                      vertexStart:0
                      vertexCount:3];
    
    

    以上就是三角形的绘制步骤

    完善metal着色器函数的代码

    这部分使用的metal独有的语法,类似于OpenGL ES中的GLSL语言

    其中metal中的图形渲染管道的关系如下所示:顶点+颜色数据传入顶点着色器,顶点着色器处理顶点,中间经过metal自行完成的图元装配和光栅化,将处理后的数据传入片元着色器进行处理

    image

    顶点着色器函数
    主要有两部分处理

    • 顶点:执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
    • 颜色:将顶点颜色值传递给返回值
    vertex RasterizerData
    vertexShader(uint vertexID [[vertex_id]],
                 constant CJLVertex *vertices [[buffer(CJLVertexInputIndexVertices)]],
                 constant vector_uint2 *viewportSizePointer [[buffer(CJLVertexInputIndexViewportSize)]])
    {
           //1、定义out
        RasterizerData out;
    
    //    2、没有旋转等变换,原样输出
        //每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
        out.clipSpacePosition = vertices[vertexID].position;
    
        //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
        out.color = vertices[vertexID].color;
    
        //完成! 将结构体传递到管道中下一个阶段:
        return out;
    
    }
    
    

    片元着色器函数
    原样输出颜色值

    //当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.
    /*
     metal自行完成的过程
     1)图元装配
     2)光栅化
     */
    
    //片元着色器函数:描述片元函数
    fragment float4 fragmentShader(RasterizerData in [[stage_in]])
    {
        //返回输入的片元颜色
        return in.color;
    }
    
    

    相关文章

      网友评论

          本文标题:Metal(2)- 颜色渲染和绘制三角形案例解析

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