二十八 Metal加载纹理(tag/png/jpg)

效果图

image

加载tga纹理

1. 可分为5个功能模块

  • ViewController


    image
  • 定义渲染循环类HTRender


    image
  • 定义tag纹理转NSData对象类 HTImage


    image
  • 公用头文件HTShadersTypes.h,用于C/OBJC 源之间共享的类型和枚举常数


    image
  • .metal文件


    image

2.重要函数解析

  • 设置顶点相关操作 -setupVertex:
    • 定义顶点和纹理坐标
    • 创建顶点缓冲区
    • 计算顶点个数
  -(void)setupVertex
{
        //1.根据顶点/纹理坐标建立一个MTLBuffer
    static const HTVertex 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数组初始化它
    _vertexBuffer = [_device newBufferWithBytes:quadVertices length:sizeof(quadVertices) options:MTLResourceStorageModeShared];

    //3.通过将字节长度除以每个顶点的大小来计算顶点的数目
    _numVertices = sizeof(quadVertices) / sizeof(HTVertex);
}
  • 设置渲染管道相关操作 -setupPipeLine:
    • 设置绘制纹理的像素格式
    • 在项目中加载所有的(.metal)着色器文件(加载顶点函数,加载片元函数)
    • 同步创建并返回渲染管线状态对象,并判断是否回了管线状态对象
    • 配置用于创建管道状态的管道
    • 获取顶点数据,创建顶点缓冲区
    • 拷贝顶点数据到顶点缓冲区
    • 计算顶点个数
    • 创建命令队列MTLCommandQueue

-(void)setupPipeLine
{
        //1.设置绘制纹理的像素格式
    htMTKView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;

        //2.加载.metal着色器文件
    id defaultLirary = [_device newDefaultLibrary];
        //加载顶点函数
    id vertexFuction = [defaultLirary newFunctionWithName:@"vertexShader"];
        //加载片元函数
    id fragmentFunction = [defaultLirary newFunctionWithName:@"fragmentShader"];

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

        //4.同步创建并返回渲染管线对象
    NSError *error = NULL;
    _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
    if(!_pipelineState){
        NSLog(@"Failed to created pipeline state, error: %@", error);
    }
        //5.使用_device创建commandQueue
    _commandQueue = [_device newCommandQueue];
}
  • 加载纹理TGA 文件 -setupTexture:
    • 获取tag的路径
    • 将tga转换为HTImage对象
    • 创建纹理描述对象
    • 使用描述符从设备中创建纹理
    • 复制图片数据到texture
 
-(void)setupTexture
{
    //1.获取tag的路径
    NSURL *imageFileLocation = [[NSBundle mainBundle] URLForResource:@"Image" withExtension:@"tga"];

    //将tga转换为HTImage对象
    HTImage *image = [[HTImage 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];
    //计算图像每行的字节数
    NSInteger bytesPerRow = 4*image.width;

    /*
     typedef struct
     {
     MTLOrigin origin; //开始位置x,y,z
     MTLSize   size; //尺寸width,height,depth
     } MTLRegion;
     */
        //MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
    //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];


}
  • MTKViewDelegate

    每当视图需要渲染时调用 -drawInMTKView:

    • 当前渲染的每个渲染传递创建一个新的命令缓冲区
    • 获取渲染目标MTLRenderPassDescriptor,并判空
    • 创建渲染命令编码器
    • 设置可绘制的区域,即设置视口,
    • 通过MTLViewport创建视口对象
    • 为管道分配自定义视口需要通过调用setViewport
    • 传递数据
    • 顶点、颜色数据
    • 视图大小
    • 设置纹理对象
    • 绘制三角形
    • 编码器生成的命令完成,
    • present显示清除的可绘制屏幕
    • commit 将命令缓冲区提交给GPU
    • 当MTKView视图发生大小改变时调用
 //每当视图需要渲染帧时调用
- (void)drawInMTKView:(nonnull MTKView *)view
{

  //1.为当前渲染的每个渲染传递创建一个新的命令缓冲区
  id commandBuffer = [_commandQueue commandBuffer];
  commandBuffer.label = @"command buffer";

  //2. MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。
  MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
  if (renderPassDescriptor!=nil) {
      //创建渲染命令编码器
      id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
      renderEncoder.label = @"myRenderEncoder";

      //3.设置我们绘制的可绘制区域
      /*
       typedef struct {
       double originX, originY, width, height, znear, zfar;
       } MTLViewport;
       */
      [renderEncoder setViewport:(MTLViewport){0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0}];

       //4. 设置渲染管道
      [renderEncoder setRenderPipelineState:_pipelineState];

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

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

      //设置纹理对象
      [renderEncoder setFragmentTexture:_texture atIndex:HTTextureIndexBaseColor];

      //6.开始绘图
      // @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];

      //7.表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离
      [renderEncoder endEncoding];

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

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

}

mtkView: drawableSizeWillChange:
保存可绘制的大小,当绘制时,我们将把这些值传递给顶点着色器

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

  // 保存可绘制的大小,因为当我们绘制时,我们将把这些值传递给顶点着色器
  _viewportSize.x = size.width;
  _viewportSize.y = size.height;
}

tag纹理转NSData对象类 HTImage

  • HTImage.h:定义图片的宽高属性和图片数据属性和加载tag文件初始化接口

  • HTImage.m:-initWithTGAFileAtLocation:解析tga文件

  • 通过HTImage类解析tga纹理图片的实现代码如下

-(nullable instancetype) initWithTGAFileAtLocation:(nonnull NSURL *)location
{
    self = [self init];
    if (self) {
        NSString *fileExtension = location.pathExtension;

        //判断是否为tga
        if (!([fileExtension caseInsensitiveCompare:@"TGA"] == NSOrderedSame)) {
            NSLog(@"只加载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;

        NSData *fileData = [[NSData alloc] initWithContentsOfURL:location options:0x00 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 *scrImageData = ((uint8_t *)fileData.bytes +sizeof(TGAHeader)+tgaInfo->IDSize);

            //初始化将存储转换后的BGRA图像数据的目标指针
            uint8_t *dstImageData = mutableData.mutableBytes;

            //图像的每一行
            for(NSUInteger y = 0; y < _height; y++){
                //对于当前行的每一列
                for (NSInteger x = 0 ; x < _width; x++) {
                    //计算源和目标图像中正在转换的像素的第一个字节的索引.
                    NSInteger srcPixelIndex = 3*(y*_width+x);
                    NSInteger dstPixelIndex = 4*(y*_width+x);

                    //将BGR信道从源复制到目的地,将目标像素的alpha通道设置为255
                    dstImageData[dstPixelIndex + 0] = scrImageData[srcPixelIndex + 0];
                    dstImageData[dstPixelIndex + 1] = scrImageData[srcPixelIndex + 1];
                    dstImageData[dstPixelIndex + 2] = scrImageData[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;
}

4.公用头文件HTShadersTypes.h

C/OBJC 源之间共享的类型和枚举常数

5.metal文件

定义 顶点着色函数 和 片元着色器函数

加载png/jpg纹理

与加载tga纹理不同的是纹理数据的获取,不是通过HTImage类解析,而是通过图形上下文来解析


image

   //从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._
   CGContextRelease(spriteContext);

   return spriteData;
}

-(void)setupTexturePNG
{
       //1.获取图片
   UIImage *image = [UIImage imageNamed:@"meimei.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;
   }

}

完整的实例代码参考:github

你可能感兴趣的:(二十八 Metal加载纹理(tag/png/jpg))