Metal -- 如何处理大批量顶点数据以及加载图片

一、首先我们要知道如何划分批量数据范围以及如何处理小批量的顶点数据
  • 苹果的官方文档中对数据有如下说明,当数据量小于4KB的时候,我们可以称为小批量数据,当顶点的数据量大于4KB时,数据量就很大了,我们可以叫大批量数据。主要区别在于苹果对这两种的数据量的处理方式不同。
  • 当我们的顶点数据小于4KB时候,我们可以直接将顶点数据放在数组中,并使用- (void)setVertexBytes:(const void *)bytes length:(NSUInteger)length atIndex:(NSUInteger)index方法将数据传递到顶点着色器函数。
  • 当顶点数据量大于4KB时候,我们可以使用一个叫MTLBuffer的对象,它能够将数据存储到顶点缓存区中,便于GPU对这些数据进行快速的访问处理。
二、如何使用Metal渲染一张图片githubDemo地址
  • 思路:
    1.Render类设置顶点相关操作、设置渲染管道相关操作、加载纹理、- (void)drawInMTKView:(nonnull MTKView *)view代理方法进行渲染;
    2.BaseShaderTypes.h文件来桥接OC和Metal方法;
    3.BaseShaders.metal设置Metal的顶点着色器函数和片元着色器函数;
    4.BaseImage类通过加载一个简单的TGA文件初始化这个图像.只支持32bit的TGA文件;
  • BaseShaderTypes.h桥接文件的具体实现,设置的结构体以及枚举即支持OC又支持Metal的调用,方法如下:

typedef enum BaseVertexInputIndex {
    
    //顶点
    BaseVertexInputIndexVertices   =0,
    
    //视图大小
    BaseVertexInputIndexViewportSize=1,
    
}BaseVertexInputIndex;
//纹理索引
typedef enum BaseTextureIndex
{
    BaseTextureIndexBaseColor = 0
}BaseTextureIndex;

//结构体
typedef struct {
    //像素空间位置
    vector_float2 position;
    
    //2D纹理
    vector_float2 textureCoordinate;
}BaseVertex;
  • Render类的实现逻辑

一、设置顶点相关操作:
1.根据顶点/纹理坐标建立一个MTLBuffer

static const BaseVertex quadVertices[] = {
        //像素坐标,纹理坐标
        { {  250,  -250 },  { 1.f, 0.f } },
        { { -250,  -250 },  { 0.f, 0.f } },
        { { -250,   250 },  { 0.f, 1.f } },
        
        { {  250,  -250 },  { 1.f, 0.f } },
        { { -250,   250 },  { 0.f, 1.f } },
        { {  250,   250 },  { 1.f, 1.f } },
        
    };

2.创建我们的顶点缓冲区,并用我们的Qualsits数组初始化它;

    _vertices = [_device newBufferWithBytes:quadVertices
                                     length:sizeof(quadVertices)
                                    options:MTLResourceStorageModeShared];

3.通过将字节长度除以每个顶点的大小来计算顶点的数目;

    _numVertices = sizeof(quadVertices) / sizeof(BaseVertex);

二、设置渲染管道相关操作:
1.创建我们的渲染通道;

//从项目中加载.metal文件,创建一个library
       id<MTLLibrary>defalutLibrary = [_device newDefaultLibrary];
       //从库中加载顶点函数
       id<MTLFunction>vertexFunction = [defalutLibrary newFunctionWithName:@"vertexShader"];
       //从库中加载片元函数
       id<MTLFunction> fragmentFunction = [defalutLibrary newFunctionWithName:@"fragmentShader"];

2.配置用于创建管道状态的管道;

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

3.同步创建并返回渲染管线对象;

 NSError *error = NULL;
    _pipeLineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
       //判断是否创建成功
    if (!_pipeLineState)
       {
           NSLog(@"Failed to created pipeline state, error %@", error);
       }

4.使用_device创建commandQueue;

_commandQueue = [_device newCommandQueue];

三、加载纹理TGA文件
1.获取tag的路径

 NSURL *imageFileLocation = [[NSBundle mainBundle] URLForResource:@"Image"withExtension:@"tga"];
    //将tag文件->CCImage对象
    BaseImage *image = [[BaseImage alloc]initWithTGAFileAtLocation:imageFileLocation];
    //判断图片是否转换成功
    if(!image)
    {
        NSLog(@"Failed to create the image from:%@",imageFileLocation.absoluteString);
        
    }

2.创建纹理描述对象

    MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc]init];
    //表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
    textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
    //设置纹理的像素尺寸
    textureDescriptor.width = image.width;
    textureDescriptor.height = image.height;
    //使用描述符从设备中创建纹理
    _texture = [_device newTextureWithDescriptor:textureDescriptor];
    //计算图像每行的字节数
    NSUInteger bytesPerRow = 4 * image.width;

3.创建MTLRegion 结构体

/*
     typedef struct
     {
     MTLOrigin origin; //开始位置x,y,z
     MTLSize   size; //尺寸width,height,depth
     } MTLRegion;
     */
    //MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
    MTLRegion region = {
        {0,0,0},
        {image.width,image.height,1}
    };

4.复制图片数据到texture

    [_texture replaceRegion:region mipmapLevel:0 withBytes:image.data.bytes bytesPerRow:bytesPerRow];

四、MTKViewDelegate代理方法实现
1.为当前渲染的每个渲染传递创建一个新的命令缓冲区;

 id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    //指定缓存区名称
    commandBuffer.label = @"MyCommand";

2.currentRenderPassDescriptor描述符包含currentDrawables的纹理、视图的深度、模板和sample缓冲区和清晰的值。

 MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

3.创建渲染命令编码器,这样我们才可以渲染到something;

 id<MTLRenderCommandEncoder> renderEncoder =
        [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        //渲染器名称
        renderEncoder.label = @"MyRenderEncoder";

4.设置我们绘制的可绘制区域;

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

5.设置渲染管道;

        [renderEncoder setRenderPipelineState:_pipelineState];

6.加载数据(大批量数据存储样式);

//将数据加载到MTLBuffer --> 顶点函数
        [renderEncoder setVertexBuffer:_vertices
                                offset:0
                               atIndex:CCVertexInputIndexVertices];
        //将数据加载到MTLBuffer --> 顶点函数
        [renderEncoder setVertexBytes:&_viewportSize
                               length:sizeof(_viewportSize)
                              atIndex:CCVertexInputIndexViewportSize];

7.设置纹理对象;

[renderEncoder setFragmentTexture:_texture atIndex:CCTextureIndexBaseColor];

8.绘制;

        // @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:_numVertices];

9.表示已该编码器生成的命令都已完成,并且从MTLCommandBuffer中分离;

        [renderEncoder endEncoding];

10.一旦框架缓冲区完成,使用当前可绘制的进度表;

        [commandBuffer presentDrawable:view.currentDrawable];

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

    [commandBuffer commit];
  • BaseShaders.metal类的实现
    1.设置结构体
//结构体
typedef struct
{
    
    float4 clipSpacePosition [[position]];
    float2 textureCoordinate;
    
} RasterizerData;

2.设置顶点着色函数

vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant BaseVertex *vertexArray [[buffer(BaseVertexInputIndexVertices)]],
             constant vector_float2 *viewportSizePointer [[buffer(BaseVertexInputIndexViewportSize)]])
{
    /*
     处理顶点数据:
     1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
     2) 将顶点颜色值传递给返回值
     */
    
    //定义out
    RasterizerData out;
    
    //初始化输出剪辑空间位置
    out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
    
    // 索引到我们的数组位置以获得当前顶点
    // 我们的位置是在像素维度中指定的.
    float2 pixelSpacePosition = vertexArray[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.z = 0.0f;
    out.clipSpacePosition.w = 1.0f;
    
    //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    
    //完成! 将结构体传递到管道中下一个阶段:
    return out;
}

3.片元着色器函数:当顶点函数执行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]],
                               texture2d<half> colorTexture [[texture(BaseTextureIndexBaseColor)]])
{
    constexpr sampler textureSampler(mag_filter::linear,
                                     min_filter::linear);
    
    const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
    
    return float4(colorSampler);
    
    //返回输入的片元颜色
    //return in.color;
}

  • 三、小批量顶点数据存储样式BaseMetalDemo地址
    - (void)setVertexBytes:(const void *)bytes length:(NSUInteger)length atIndex:(NSUInteger)index使用该方法来处理低于4KB的数据量,
- (void)drawInMTKView:(nonnull MTKView *)view {
   
    //1.顶点数据/颜色数据
    static const BaseVertex 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 } },
    };
    
    //2.当前渲染的每一个渲染创建一个新的命令缓存区
    id<MTLCommandBuffer>commandBuffer = [_commandQueue commandBuffer];
    
    //指定缓存区名字
    commandBuffer.label = @"MyCommand";
    
    //3.MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    //判断渲染目标是否为空
    if(renderPassDescriptor != nil){
        //4.创建渲染命令编码器,这样我们才能渲染事物
        id<MTLRenderCommandEncoder>renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        //渲染器名称
        renderEncoder.label = @"MyRenderEncoder";
        
        //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];
        
        //6.设置当前渲染管道对象
        [renderEncoder setRenderPipelineState:_pipelineState];
        
        
        //7.从应用程序OC 代码 中发送数据给Metal 顶点着色器 函数
        //顶点数据+颜色数据
        //   1) 指向要传递给着色器的内存的指针
        //   2) 我们想要传递的数据的内存大小
        //   3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
        [renderEncoder setVertexBytes:&triangleVertices length:sizeof(triangleVertices) atIndex:BaseVertexInputIndexVertices];
        
        //viewPortSize 数据
        //1) 发送到顶点着色函数中,视图大小
        //2) 视图大小内存空间大小
        //3) 对应的索引
        [renderEncoder setVertexBytes:&_viewportSize
                               length:sizeof(_viewportSize)
                              atIndex:BaseVertexInputIndexViewportSize];
        //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];
        
        //9.表示已该编码器生成的命令都已完成,并且从MTLCommandBuffer中分离
        [renderEncoder endEncoding];
        
        //10.一旦框架缓冲区完成,使用当前可绘制的进度表
        [commandBuffer presentDrawable:view.currentDrawable];

        
    }
    //11.最后,在这里完成渲染并将命令缓冲区推送到GPU
    [commandBuffer commit];
    
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343