美文网首页iOS视觉
二十八 Metal加载纹理(tag/png/jpg)

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

作者: 王俏 | 来源:发表于2020-09-10 09:14 被阅读0次

    效果图

    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<MTLLibrary> defaultLirary = [_device newDefaultLibrary];
            //加载顶点函数
        id<MTLFunction> vertexFuction = [defaultLirary newFunctionWithName:@"vertexShader"];
            //加载片元函数
        id<MTLFunction> 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<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
      commandBuffer.label = @"command buffer";
    
      //2. MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。
      MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
      if (renderPassDescriptor!=nil) {
          //创建渲染命令编码器
          id<MTLRenderCommandEncoder> 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)

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