Metal(5)——加载纹理

935 阅读5分钟

之前的文章,我们绘制了彩色的三角形,略显的有些枯燥乏味。今天我们尝试利用Metal来完成图片的绘制,让Metal的学习之路,变得有趣一点。

1.主要流程

渲染管线的工作流程,已经通过前面的案例练习过了。那么利用Metal来绘制一张图片,与绘制一个彩色三角形,有哪些不用之处呢?

1.1 类型定义

之前我们定了一个结构体ZVertex,包含了顶点信息与颜色值。

//结构体: 顶点/颜色值
typedef struct
{
    // 像素空间的位置
    // 像素中心点(100,100)
    vector_float2 position;

    // RGBA颜色
    vector_float4 color;
} ZVertex;

现在我们不需要颜色值了,改用纹理坐标替代。

typedef struct {
    // 像素空间的位置
    // 像素中心点(100,100)
    vector_float2 position;
    // 2D 纹理
    vector_float2 textureCoordinate;
} ZVertex;

对应的顶点数据的定义也要发生变换了:

let quadVertices = [ZVertex(position: [250, -250], textureCoordinate: [1.0, 0.0]),
                    ZVertex(position: [-250, -250], textureCoordinate: [0.0, 0.0]),
                    ZVertex(position: [-250, 250], textureCoordinate: [0.0, 1.0]),
                            
                    ZVertex(position: [250, -250], textureCoordinate: [1.0, 0.0]),
                    ZVertex(position: [-250, 250], textureCoordinate: [0.0, 1.0]),
                    ZVertex(position: [250, 250], textureCoordinate: [1.0, 1.0]),
        ]

同时,在头文件中追加一个纹理索引的枚举。我们暂时只有一个纹理,所以里面只有一个枚举值

//纹理索引
typedef enum ZTextureIndex{
    ZTextureIndexBaseColor = 0,
}ZTextureIndex;

1.2 着色器

片元着色器,增加了一个纹理参数,由程序传递给片元着色器。
创建一个纹理采样器sampler,根据传入的纹理坐标,采集片元颜色,来进行显示。

fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                               texture2d<half> colorTexture [[texture(ZTextureIndexBaseColor)]]){
                               
    constexpr sampler textureSampler(mag_filter::linear,
                                     min_filter::linear);
    
    const half4 colorSampler = colorTexture.sample(textureSampler, in.textureCoordinate);
    
    return float4(colorSampler);
}

1.3 创建纹理对象

Metal 中,textureMTLTexture 来描述。

@available(iOS 8.0, *)
public protocol MTLTexture : MTLResource {
    // ...
}

它也属于 MTLResource,和 MTLBuffer 是一类的,但是用途上不同。

  • MTLBuffer:An allocation of unformatted memory that can contain any type of data
  • MTLTexture:An allocation of formatted image data with a specified texture type and pixel format

MTLTexture 可以通过 MTLDevicemakeTexture 方法创建,需要传入一个纹理描述对象 MTLTextureDescriptor
MTLTextureDescriptor 和我们之前用到的几个 descriptor 一样,是对纹理相关属性的配置,比如 pixelFormatwidthheightmipmapped 等。

//2.创建纹理描述对象
let textureDescriptor = MTLTextureDescriptor()
 //表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
textureDescriptor.pixelFormat = MTLPixelFormat.bgra8Unorm
//设置纹理的像素尺寸
textureDescriptor.width = Int(image.width)
textureDescriptor.height = Int(image.height)

//使用描述符从设备中创建纹理
texture = device?.makeTexture(descriptor: textureDescriptor)

接下去,就是将解码后的图像数据,copy 到对应内存中。 通过调用 MTLTexturereplaceRegion:mipmapLevel:withBytes:bytesPerRow: 方法,即可完成数据的传递。需要传递第一个MTLRegion 结构体类型的参数,MTLRegion 定义了 texture 中对应的图像区域,我们一般和图像的实际大小保持一致即可。

//计算图像每行的字节数
let bytesPerRow = image.width * 4

//MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
//3. 创建MTLRegion 结构体
let region = MTLRegion(origin:MTLOriginMake(0, 0, 0), size: MTLSizeMake(Int(image.width), Int(image.height), 1))
        
//4.复制图片数据到texture
texture?.replace(region: region, mipmapLevel: 0, withBytes: [UInt8](image.data), bytesPerRow: Int(bytesPerRow))

注意:上面用到的image, 将在第二部分讲解。

1.4 使用纹理对象

drawInMTKView:方法中,通过编码器,将纹理对象传到片元着色器中。这与前面片元着色函数中新加纹理参数正好对应上。

//7.设置纹理对象
renderEncoder?.setFragmentTexture(texture, index: Int(ZTextureIndexBaseColor.rawValue))

2. 加载图片

2.1 TGA

定义工具类来将TGA格式的文件解压成位图,拿到它的图像数据和图片大小。

- (instancetype)initWithTGAFileAtLocation:(NSURL *)location{
    if (self == [super self]) {
        NSString *fileExtension = location.pathExtension;
        
         //判断文件后缀是否为tga
        if (!([fileExtension caseInsensitiveCompare:@"TGA"] == NSOrderedSame)) {
            NSLog(@"此ZImage只加载TGA文件");
            return nil;
        }
        
        //定义一个TGA文件的头.
        //使用__attribute__ ((packed))修饰 编译时不会内存对齐
        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:NSDataReadingMappedIfSafe 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之后, 所以要想得到图像数据的开始位置的指针, 需要在fileData指针的开始位置再向后偏移 (头的大小+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;
}

2.2 PNG/JPG

对于PNG/JPG格式的图片,也需要进行解压。

func loadImage(image: UIImage) -> UnsafeMutablePointer<GLubyte>? {
        // 1.获取图片的CGImageRef
        guard let spriteImage = image.cgImage else {
            return nil
        }
        
        // 2.读取图片的大小
        let width = spriteImage.width
        let height = spriteImage.height
        
        //3.计算图片大小.rgba共4个byte
        let spriteData = UnsafeMutablePointer<GLubyte>.allocate(capacity: MemoryLayout<GLubyte>.size*width*height*4)
        
        UIGraphicsBeginImageContext(CGSize(width: width, height: height))
        
        //4.创建画布
        let spriteContext = CGContext(data: spriteData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width*4, space: spriteImage.colorSpace!, bitmapInfo: spriteImage.bitmapInfo.rawValue)
        
        //5.翻转
        spriteContext?.translateBy(x: 0, y: CGFloat(height))
        spriteContext?.scaleBy(x: 1, y: -1)
        
        //5.在CGContextRef上绘图
        spriteContext?.draw(spriteImage, in: CGRect(x: 0, y: 0, width: width, height: height))
        
        UIGraphicsEndImageContext()
        
        
        return spriteData
    }

完整demo