Metal 大批量顶点数据的图形渲染

317 阅读5分钟

效果图如下:

本文主要介绍使用`MTLBuffer`将大量的顶点数据存储到顶点缓冲区,GPU可以访问该缓冲物的顶点数据,`MTLBuffer`缓冲区的数据需要通过`setVertexBuffer(_:offset:index:)`传递到顶点着色器。

划分批量数据范围以及如何处理小批量的顶点数据

* [苹果的官方文档](https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fmetal%2Fmtlrendercommandencoder%2F1515846-setvertexbytes)中对数据有如下说明,当数据量小于4KB的时候,我们可以称为小批量数据,当顶点的数据量大于4KB时,数据量就很大了,我们可以叫大批量数据。主要区别在于苹果对这两种的数据量的处理方式不同。* 当我们的顶点数据小于4KB时候,我们可以直接将顶点数据放在数组中,并使用`- (void)setVertexBytes:(const void *)bytes length:(NSUInteger)length atIndex:(NSUInteger)index`方法将数据传递到顶点着色器函数。* 当顶点数据量大于4KB时候,我们可以使用一个叫`MTLBuffer`的对象,它能够将数据存储到顶点缓存区中,便于GPU对这些数据进行快速的访问处理。 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/409eed56249c4b1cb71239866abe97ff~tplv-k3u1fbpfcp-zoom-1.image)

如何使用Metal渲染一张图片

  1. Render类设置顶点相关操作、设置渲染管道相关操作、加载纹理、- (void)drawInMTKView:(nonnull MTKView *)view代理方法进行渲染;
  2. BaseShaderTypes.h文件来桥接OC和Metal方法;
  3. BaseShaders.metal设置Metal的顶点着色器函数和片元着色器函数;
  4. BaseImage类通过加载一个简单的TGA文件初始化这个图像.只支持32bit的TGA文件;
  5. BaseShaderTypes.h桥接文件的具体实现,设置的结构体以及枚举即支持OC又支持Metal的调用

绘制流程

Viewcontroller主要代码
    _view = (MTKView *)self.view;
    //一个MTLDevice 对象就代表这着一个GPU,通常我们可以调用方法MTLCreateSystemDefaultDevice()来获取代表默认的GPU单个对象.
    _view.device = MTLCreateSystemDefaultDevice();
    if(!_view.device)
    {
        NSLog(@"Metal is not supported on this device");
        return;
    }

    //2.创建CCRender
    _renderer = [[CCRenderer alloc] initWithMetalKitView:_view];
    if(!_renderer)
    {
        NSLog(@"Renderer failed initialization");
        return;
    }
    //用视图大小初始化渲染器
    [_renderer mtkView:_view drawableSizeWillChange:_view.drawableSize];
    //设置MTKView代理
    _view.delegate = _renderer;
render渲染类代码
    //渲染的设备(GPU)
    id<MTLDevice> _device;

    //渲染管道:顶点着色器/片元着色器,存储于.metal shader文件中
    id<MTLRenderPipelineState> _pipelineState;

    //命令队列,从命令缓存区获取
    id<MTLCommandQueue> _commandQueue;

    //顶点缓存区
    id<MTLBuffer> _vertexBuffer;

    //当前视图大小,这样我们才能在渲染通道中使用此视图
    vector_uint2 _viewportSize;

    //顶点个数
    NSInteger _numVertices;

初始化方法

//初始化
- (instancetype)initWithMetalKitView:(MTKView *)mtkView
{
    self = [super init];
    if(self)
    {
         //1.初始GPU设备
        _device = mtkView.device;
        //2.加载Metal文件
        [self loadMetal:mtkView];
    }

    return self;
}

- (void)loadMetal:(nonnull MTKView *)mtkView
{
    //1.设置绘制纹理的像素格式
    mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;

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

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

    //4.同步创建并返回渲染管线对象
    NSError *error = NULL;
    _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
                                                             error:&error];
    //判断是否创建成功
    if (!_pipelineState)
    {
        NSLog(@"Failed to created pipeline state, error %@", error);
    }

    //5.获取顶点数据
    NSData *vertexData = [CCRenderer generateVertexData];
    //创建一个vertex buffer,可以由GPU来读取
    _vertexBuffer = [_device newBufferWithLength:vertexData.length
                                         options:MTLResourceStorageModeShared];
    //复制vertex data 到vertex buffer 通过缓存区的"content"内容属性访问指针
    /*
     memcpy(void *dst, const void *src, size_t n);
     dst:目的地
     src:源内容
     n: 长度
     */
    memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);
    //计算顶点个数 = 顶点数据长度 / 单个顶点大小
    _numVertices = vertexData.length / sizeof(CCVertex);

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

不同点:_vertexBuffer = [_device newBufferWithLength:vertexData.length options:MTLResourceStorageModeShared];
memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);使用这两个方法开辟顶点缓冲区,并且将顶点数据拷贝到缓冲区

generateVertexData 获取顶点数据的方法

+ (nonnull NSData *)generateVertexData
{
    //1.正方形 = 三角形+三角形
    const CCVertex quadVertices[] =
    {
        // Pixel 位置, RGBA 颜色
        { { -20,   20 },    { 1, 0, 0, 1 } },
        { {  20,   20 },    { 1, 0, 0, 1 } },
        { { -20,  -20 },    { 1, 0, 0, 1 } },

        { {  20,  -20 },    { 0, 0, 1, 1 } },
        { { -20,  -20 },    { 0, 0, 1, 1 } },
        { {  20,   20 },    { 0, 0, 1, 1 } },
    };
    //行/列 数量
    const NSUInteger NUM_COLUMNS = 25;
    const NSUInteger NUM_ROWS = 15;
    //顶点个数
    const NSUInteger NUM_VERTICES_PER_QUAD = sizeof(quadVertices) / sizeof(CCVertex);
    //四边形间距
    const float QUAD_SPACING = 50.0;
    //数据大小 = 单个四边形大小 * 行 * 列
    NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS;

    //2\. 开辟空间
    NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];
    //当前四边形
    CCVertex * currentQuad = vertexData.mutableBytes;

    //3.获取顶点坐标(循环计算)
    //行
    for(NSUInteger row = 0; row < NUM_ROWS; row++)
    {
        //列
        for(NSUInteger column = 0; column < NUM_COLUMNS; column++)
        {
            //A.左上角的位置
            vector_float2 upperLeftPosition;

            //B.计算X,Y 位置.注意坐标系基于2D笛卡尔坐标系,中心点(0,0),所以会出现负数位置
            upperLeftPosition.x = ((-((float)NUM_COLUMNS) / 2.0) + column) * QUAD_SPACING + QUAD_SPACING/2.0;

            upperLeftPosition.y = ((-((float)NUM_ROWS) / 2.0) + row) * QUAD_SPACING + QUAD_SPACING/2.0;

            //C.将quadVertices数据复制到currentQuad
            memcpy(currentQuad, &quadVertices, sizeof(quadVertices));

            //D.遍历currentQuad中的数据
            for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++)
            {
                //修改vertexInQuad中的position
                currentQuad[vertexInQuad].position += upperLeftPosition;
            }

            //E.更新索引
            currentQuad += 6;
        }
    }
    return vertexData;  
}

代理方法

-(void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size

    _viewportSize.x = size.width;
    _viewportSize.y = size.height;

-(void)drawInMTKView:(nonnull MTKView *)view

    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    commandBuffer.label = @"MyCommand";

    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    //判断渲染目标是否为空
    if(renderPassDescriptor != nil)
    {
        id<MTLRenderCommandEncoder> renderEncoder =
        [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        renderEncoder.label = @"MyRenderEncoder";

        [renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];

        [renderEncoder setRenderPipelineState:_pipelineState];

        //将_vertexBuffer 设置到顶点缓存区中
        [renderEncoder setVertexBuffer:_vertexBuffer
                                offset:0
                               atIndex:CCVertexInputIndexVertices];

        //将 _viewportSize 设置到顶点缓存区绑定点设置数据
        [renderEncoder setVertexBytes:&_viewportSize
                               length:sizeof(_viewportSize)
                              atIndex:CCVertexInputIndexViewportSize];

        [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                          vertexStart:0
                          vertexCount:_numVertices];
        [renderEncoder endEncoding];
        [commandBuffer presentDrawable:view.currentDrawable];
    }
    [commandBuffer commit];

由于顶点坐标使用的是像素坐标空间的位置,在顶点着色器中要做归一化处理。

vertexShader(uint vertexID [[vertex_id]],
             constant CCVertex *vertices [[buffer(CCVertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])
{
    //定义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.color = vertices[vertexID].color;

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