之前的文章,我们绘制了彩色的三角形,略显的有些枯燥乏味。今天我们尝试利用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 中,texture 用 MTLTexture 来描述。
@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 可以通过 MTLDevice 的 makeTexture 方法创建,需要传入一个纹理描述对象 MTLTextureDescriptor。
MTLTextureDescriptor 和我们之前用到的几个 descriptor 一样,是对纹理相关属性的配置,比如 pixelFormat,width,height,mipmapped 等。
//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 到对应内存中。
通过调用 MTLTexture 的 replaceRegion: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
}