美文网首页
Metal 处理图形渲染和加载图片

Metal 处理图形渲染和加载图片

作者: 大橘猪猪侠 | 来源:发表于2020-08-26 10:41 被阅读0次

    这篇文章将介绍如何使用Metal处理顶点数量多的图形和使用Metal去加载图片。

    首先我们来实现使用Metal来处理顶点数量多的图形。

    在之前我们介绍过如何使用Metal API的使用流程,在这个文章中,有一个案例就是如何使用Metal去实现一个三角形的效果。

    在实现三角形效果时,我们设置到顶点缓冲区时使用的是下面的方法:
    [setVertexBytes:<#(nonnull const void *)#> length:<#(NSUInteger)#> atIndex:<#(NSUInteger)#>]

    而上面的那个方法只能加载少量顶点的数据,因此,在加载大量顶点数据组成的图形时,我们就要使用renderEncoder setVertexBuffer:<#(nullable id<MTLBuffer>)#> offset:<#(NSUInteger)#> atIndex:<#(NSUInteger)#>这个方法来实现.

    下面我们先来看一下加载大量顶点的效果图:


    Simulator Screen Shot - iPhone SE -2nd generation- - 2020-08-25 at 11.01.29.png

    接下来我们来实现一下上面的效果:
    实现上面的效果,我们按照流程一步一步来实现:
    首先实现初始化工作流程
    1、创建Metal文件和加载Metal的类文件
    在类中调用Metal文件时,我们会需要进行传值操作,因此,我们需要定义一个.h文件来共享与 shader 和 C 代码

    // 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
    typedef enum VertexInputIndex{
        //顶点
        VertexInputIndexVertices = 0,
        //视图大小
        VertexInputIndexViewportSize = 1,
    }VertexInputIndex;
    
    
    typedef struct {
        // 像素空间的位置
        // 像素中心点(100,100)
        //float float
        vector_float2 position;
        // RGBA颜色
        //float float float float
        vector_float4 color;
    }Vertex;
    

    然后在Metal文件中引入.h的头文件

    typedef struct{
        float4 clipSpacePosition [[position]];
        float4 color;
    } RasterizerData;
    
    vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],constant Vertex *vertices [[buffer(VertexInputIndexVertices)]],constant vector_uint2 *viewportSizePointer [[buffer(VertexInputIndexViewportSize)]]){
        /*
         处理顶点数据:
         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.color = vertices[vertexID].color;
        
        //完成! 将结构体传递到管道中下一个阶段:
        return out;
    }
    
    fragment float4 fragmentShader(RasterizerData in [[stage_in]])
    {
        //返回输入的片元颜色
        return in.color;
    }
    
    

    在Metal文件中的VertexInputIndexVertices、VertexInputIndexViewportSize、position、color为外界传值给着色器调用,这边利用了一个枚举和结构体来定义值:

    
    typedef enum VertexInputIndex{
        //顶点
        VertexInputIndexVertices = 0,
        //视图大小
        VertexInputIndexViewportSize = 1,
    }VertexInputIndex;
    
    
    typedef struct {
        // 像素空间的位置
        // 像素中心点(100,100)
        //float float
        vector_float2 position;
        // RGBA颜色
        //float float float float
        vector_float4 color;
    }Vertex;
    
    

    2、加载着色器文件,包含顶点函数和片元函数

    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);
        }
        
    

    4、获取顶点数据

    + (nonnull NSData *)generateVertexData
    {
        //1.正方形 = 三角形+三角形
        const Vertex 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(Vertex);
        //四边形间距
        const float QUAD_SPACING = 50.0;
        //数据大小 = 单个四边形大小 * 行 * 列
        NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS;
        
        //2. 开辟空间
        NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];
        //当前四边形
        Vertex * currentQuad = vertexData.mutableBytes;
        
        
        //3.获取顶点坐标(循环计算)
        //行
        for(NSUInteger row = 0; row < NUM_ROWS; row++)
        {
            //列
            for(NSUInteger column = 0; column < NUM_COLUMNS; column++)
            {
                //1.左上角的位置
                vector_float2 upperLeftPosition;
                
                //2.计算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;
                
                //3.将quadVertices数据复制到currentQuad
                memcpy(currentQuad, &quadVertices, sizeof(quadVertices));
                
                //4.遍历currentQuad中的数据
                for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++)
                {
                    //修改vertexInQuad中的position
                    currentQuad[vertexInQuad].position += upperLeftPosition;
                }
                
                //5.更新索引
                currentQuad += 6;
            }
        }
        
        return vertexData;
        
    }
    

    5、创建命令队列

    _commandQueue = [_device newCommandQueue];
    

    在初始化工作完成后,我们就需要实现MTKViewDelegate两个代理方法
    重点部分在于将_vertexBuffer设置到顶点缓冲区和将_viewportSize设置到顶点缓冲区数据

    
    [renderEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:VertexInputIndexVertices];
     
    
    [renderEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:VertexInputIndexViewportSize];
    

    在设置完之后,开始绘制

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

    最后提交:

    [commandBuffer commit]; 
    

    实现将加载大量顶点绘制成的图形之后,我们来实现如何使用Metal来加载tga和jpg图片。

    同样的先来看一下效果图:

    Simulator Screen Shot - iPhone SE -2nd generation- - 2020-08-25 at 11.28.23.png

    同样的,加载图片和绘制图形代码有一些区别,首先在着色器代码中我们还需要传入一个纹理索引,在设置纹理对象时,将纹理索引进行赋值。

    在需要传值的.h文件中,加入一个索引枚举值

    // 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
    typedef enum VertexInputIndex
    {
        //顶点
        VertexInputIndexVertices     = 0,
        //视图大小
        VertexInputIndexViewportSize = 1,
    } VertexInputIndex;
    
    //纹理索引
    typedef enum CCTextureIndex
    {
        TextureIndexBaseColor = 0
    }TextureIndex;
    
    //结构体: 顶点/颜色值
    typedef struct
    {
        // 像素空间的位置
        // 像素中心点(100,100)
        vector_float2 position;
        // 2D 纹理
        vector_float2 textureCoordinate;
    } Vertex;
    

    下面贴出着色器代码

    // 顶点着色器输出和片段着色器输入
    //结构体
    typedef struct
    {
        float4 clipSpacePosition [[position]];
        float2 textureCoordinate;
        
    } RasterizerData;
    
    //顶点着色函数
    vertex RasterizerData
    vertexShader(uint vertexID [[vertex_id]],
                 constant Vertex *vertexArray [[buffer(VertexInputIndexVertices)]],
                 constant vector_uint2 *viewportSizePointer [[buffer(VertexInputIndexViewportSize)]])
    {
        //定义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;
    }
    
    fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                                   texture2d<half> colorTexture [[texture(TextureIndexBaseColor)]])
    {
        constexpr sampler textureSampler(mag_filter::linear,
                                         min_filter::linear);
        
        const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
        
        return float4(colorSampler);
        
        //返回输入的片元颜色
        //return in.color;
    }
    
    
    

    接下来,我们先实现加载tga和jpg公共部分代码:

    1.首先加载顶点操作

    
        //1.根据顶点/纹理坐标建立一个MTLBuffer
        static const Vertex 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(Vertex);
    

    2.设置渲染管道操作

    //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 = ccMTKView.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];
    

    3.实现MTKViewDelegate的两个代理方法:
    跟上一个加载图形其实是一摸一样的,这边不在写了。
    4.接下来实现加载图片操作了,首先加载tga图片,我们先将图片转化为一个对象
    这个转化方法:

    -(nullable instancetype) initWithTGAFileAtLocation:(nonnull NSURL *)location
    {
        self = [super init];
        if(self)
        {
            NSString *fileExtension = location.pathExtension;
            
            //判断文件后缀是否为tga
            if(!([fileExtension caseInsensitiveCompare:@"TGA"] == NSOrderedSame))
            {
                NSLog(@"此Image只加载TGA文件");
                return nil;
                
            }
            
            //定义一个TGA文件的头.
            typedef struct __attribute__ ((packed)) TGAHeader
            {
                uint8_t  IDSize;         // ID信息
                uint8_t  colorMapType;   // 颜色类型
                uint8_t  imageType;      // 图片类型 0=none, 1=indexed, 2=rgb, 3=grey, +8=rle packed
                
                int16_t  colorMapStart;  // 调色板中颜色映射的偏移量
                int16_t  colorMapLength; // 在调色板的颜色数
                uint8_t  colorMapBpp;    // 每个调色板条目的位数
                
                uint16_t xOffset;        // 图像开始右方的像素数
                uint16_t yOffset;        // 图像开始向下的像素数
                uint16_t width;          // 像素宽度
                uint16_t height;         // 像素高度
                uint8_t  bitsPerPixel;   // 每像素的位数 8,16,24,32
                uint8_t  descriptor;     // bits描述 (flipping, etc)
                
            }TGAHeader;
            
            NSError *error;
            
            //将TGA文件中整个复制到此变量中
            NSData *fileData = [[NSData alloc]initWithContentsOfURL:location options:0x0 error:&error];
            
            if(fileData == nil)
            {
                NSLog(@"打开TGA文件失败:%@",error.localizedDescription);
                return nil;
            }
            
            //定义TGAHeader对象
            TGAHeader *tgaInfo = (TGAHeader *)fileData.bytes;
            _width = tgaInfo->width;
            _height = tgaInfo->height;
            
            //计算图像数据的字节大小,因为我们把图像数据存储为/每像素32位BGRA数据.
            NSUInteger dataSize = _width * _height * 4;
            
            if(tgaInfo->bitsPerPixel == 24)
            {
                //Metal是不能理解一个24-BPP格式的图像.所以我们必须转化成TGA数据.从24比特BGA格式到32比特BGRA格式.(类似MTLPixelFormatBGRA8Unorm)
                NSMutableData *mutableData = [[NSMutableData alloc] initWithLength:dataSize];
                
                //TGA规范,图像数据是在标题和ID之后立即设置指针到文件的开头+头的大小+ID的大小.初始化源指针,源代码数据为BGR格式
                uint8_t *srcImageData = ((uint8_t*)fileData.bytes +
                                         sizeof(TGAHeader) +
                                         tgaInfo->IDSize);
                
                //初始化将存储转换后的BGRA图像数据的目标指针
                uint8_t *dstImageData = mutableData.mutableBytes;
                
                //图像的每一行
                for(NSUInteger y = 0; y < _height; y++)
                {
                    //对于当前行的每一列
                    for(NSUInteger x = 0; x < _width; x++)
                    {
                        //计算源和目标图像中正在转换的像素的第一个字节的索引.
                        NSUInteger srcPixelIndex = 3 * (y * _width + x);
                        NSUInteger dstPixelIndex = 4 * (y * _width + x);
                        
                        //将BGR信道从源复制到目的地,将目标像素的alpha通道设置为255
                        dstImageData[dstPixelIndex + 0] = srcImageData[srcPixelIndex + 0];
                        dstImageData[dstPixelIndex + 1] = srcImageData[srcPixelIndex + 1];
                        dstImageData[dstPixelIndex + 2] = srcImageData[srcPixelIndex + 2];
                        dstImageData[dstPixelIndex + 3] = 255;
                    }
                }
                _data = mutableData;
            }else
            {
                
                uint8_t *srcImageData = ((uint8_t*)fileData.bytes + sizeof(TGAHeader) + tgaInfo->IDSize);
                
                _data = [[NSData alloc] initWithBytes:srcImageData
                                               length:dataSize];
            }
            
        }
        return self;
        
    }
    

    在转化完之后,我们就可以设置纹理了:

     MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc]init];
     textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
    //设置纹理的像素尺寸
    textureDescriptor.width = image.width;
    textureDescriptor.height = image.height;
    //使用描述符从设备中创建纹理
    _texture = [_device newTextureWithDescriptor:textureDescriptor];
    //计算图像每行的字节数
    NSUInteger bytesPerRow = 4 * image.width;
    /3. 创建MTLRegion 结构体
        MTLRegion region = {
            {0,0,0},
            {image.width,image.height,1}
        };
        
        //4.复制图片数据到texture
        [_texture replaceRegion:region mipmapLevel:0 withBytes:image.data.bytes bytesPerRow:bytesPerRow];
    

    5.实现jpg图片的加载:

    -(void)setupTexturePNG
    {
        //1.获取图片
        UIImage *image = [UIImage imageNamed:@"kun.jpg"];
        //2.纹理描述符
        MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
        //表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
        textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
        //设置纹理的像素尺寸
        textureDescriptor.width = image.size.width;
        textureDescriptor.height = image.size.height;
        
        //3.使用描述符从设备中创建纹理
        _texture = [_device newTextureWithDescriptor:textureDescriptor];
    
        //MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
        //4. 创建MTLRegion 结构体  [纹理上传的范围]
        MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}};
        
        //5.获取图片数据
        Byte *imageBytes = [self loadImage:image];
        
        //6.UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
        if (imageBytes) {
            [_texture replaceRegion:region
                            mipmapLevel:0
                              withBytes:imageBytes
                            bytesPerRow:4 * image.size.width];
            free(imageBytes);
            imageBytes = NULL;
        }
        
    }
    
    //从UIImage 中读取Byte 数据返回
    - (Byte *)loadImage:(UIImage *)image {
        // 1.获取图片的CGImageRef
        CGImageRef spriteImage = image.CGImage;
        
        // 2.读取图片的大小
        size_t width = CGImageGetWidth(spriteImage);
        size_t height = CGImageGetHeight(spriteImage);
       
        //3.计算图片大小.rgba共4个byte
        Byte * spriteData = (Byte *) calloc(width * height * 4, sizeof(Byte));
        
        //4.创建画布
        CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
        
        //5.在CGContextRef上绘图
        CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
        
        //6.图片翻转过来
        CGRect rect = CGRectMake(0, 0, width, height);
        CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
        CGContextTranslateCTM(spriteContext, 0, rect.size.height);
        CGContextScaleCTM(spriteContext, 1.0, -1.0);
        CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
        CGContextDrawImage(spriteContext, rect, spriteImage);
    
        //7.释放spriteContext
        CGContextRelease(spriteContext);
        
        return spriteData;
    }
    
    

    案例demo

    相关文章

      网友评论

          本文标题:Metal 处理图形渲染和加载图片

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