之前我们已经对顶点缓冲对象
MTLBuffer有了简单的了解和使用。加载纹理对象,我们需要借助
MTLTexture
在 Metal 中,MTLBuffer 可以用来传递一些未格式化的信息,例如顶点坐标、纹理坐标等;MTLTexture可以用来传递图像信息。
MTLTexture
MTLTexture对象是保存格式化后的图片数据的对象,用于向 Metal 程序传递图像信息。
创建
根据MTLTextureDescriptor纹理描述符对象,创建MTLTexture纹理对象
// 创建纹理描述符
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
// 设置像素颜色格式
textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
// 设置纹理的像素尺寸
textureDescriptor.width = image.size.width;
textureDescriptor.height = image.size.height;
// 使用纹理描述符创建纹理
_texture = [_device newTextureWithDescriptor:textureDescriptor];
加载纹理
加载图像数据
- 加载
TGA图片(参考官方文档)
- (void)setupTexture {
// 1、获取TGA文件路径 --- TGA文件解压
NSURL *imageFileLocation = [[NSBundle mainBundle] URLForResource:@"circle" withExtension:@"tga"];
//将tag文件->TGAImage
TGAImage *image = [[TGAImage alloc] initWithTGAFileAtLocation:imageFileLocation];
//判断图片是否转换成功
if (!image) {
NSLog(@"Failed to create the image from:%@",imageFileLocation.absoluteString);
return;
}
// 2、创建纹理描述对象 & 设置属性 --- CJLImage --> 纹理(即位图变成纹理对象)
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
//表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
//位图信息
textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
//设置纹理的像素尺寸,即纹理的分辨率
textureDescriptor.width = image.width;
textureDescriptor.height = image.height;
// 3、创建纹理对象:使用描述符从设备中创建纹理
_texture = [_device newTextureWithDescriptor:textureDescriptor];
//计算图像每行的字节数
NSUInteger bytesPerRow = 4 * image.width;
// 4、创建MTLRegion结构体
/*
typedef struct
{
MTLOrigin origin; //开始位置x,y,z
MTLSize size; //尺寸width,height,depth
} MTLRegion;
*/
//MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
MTLRegion region = {
{0,0,0},
{image.width, image.height, 1},
};
// 5、复制图片数据到texture
/*
将图片复制到纹理0中(即用纹理替换region表示的区域)
- (void)replaceRegion:(MTLRegion)region mipmapLevel:(NSUInteger)level withBytes:(const void *)pixelBytes bytesPerRow:(NSUInteger)bytesPerRow;
参数1-region:像素区域在纹理中的位置
参数2-level:从零开始的值,指定哪个mipmap级别是目标。如果纹理没有mipmap,请使用0。
参数3-pixelBytes:指向要复制图片的字节数
参数4-bytesPerRow:对于普通或压缩像素格式,源数据行之间的跨度(以字节为单位)。对于压缩像素格式,跨度是从一排块的开头到下一行的开始的字节数。
*/
[_texture replaceRegion:region mipmapLevel:0 withBytes:image.data.bytes bytesPerRow:bytesPerRow];
}
// TGAImage.m
- (nullable instancetype) initWithTGAFileAtLocation:(nonnull NSURL *)location
{
self = [super init];
if(self)
{
NSString *fileExtension = location.pathExtension;
//判断文件后缀是否为tga
if(!([fileExtension caseInsensitiveCompare:@"TGA"] == NSOrderedSame))
{
NSLog(@"此CCImage只加载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;
}
加载
PNG/JPG图片与 TGA 图片加载不同,解压 PNG/JPG 图片可以采用
CoreImage框架,重绘成位图即可
- (void)setupTexture {
UIImage *image = [UIImage imageNamed:@"wlop.png"];
MTLTextureDescriptor *textureDes = [[MTLTextureDescriptor alloc] init];
textureDes.pixelFormat = MTLPixelFormatRGBA8Unorm;
textureDes.width = image.size.width;
textureDes.height = image.size.height;
_texture = [_device newTextureWithDescriptor:textureDes];
MTLRegion region = {
{0, 0, 0},
{image.size.width, image.size.height, 1},
};
Byte *imageBytes = [self loadImage:image];
NSAssert(imageBytes, @"imageBytes load failed");
[_texture replaceRegion:region
mipmapLevel:0
withBytes:imageBytes
bytesPerRow:4 * image.size.width];
free(imageBytes);
imageBytes = NULL;
}
- (Byte *)loadImage:(UIImage *)image {
CGImageRef imageRef = image.CGImage;
NSAssert(imageRef, @"Image load failed");
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
Byte *spriteData = (Byte*)calloc(width * height * 4, sizeof(Byte));
CGContextRef contextRef = CGBitmapContextCreate(spriteData, width, height, 8, width * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(contextRef, rect, imageRef);
CGContextTranslateCTM(contextRef, 0, rect.size.height);
CGContextScaleCTM(contextRef, 1.0, -1.0);
CGContextDrawImage(contextRef, rect, imageRef);
CGContextRelease(contextRef);
CGImageRelease(imageRef);
return spriteData;
}
将纹理复制到 MTLTexture 对象
replaceRegion: mipmapLevel: withBytes: bytesPerRow:
// MTLRegion 结构体用于标识纹理的特定区域
MTLRegion region = {
{0, 0, 0},
{image.size.width, image.size.height, 1},
};
/*
将图片复制到纹理0中(即用纹理替换region表示的区域)
- (void)replaceRegion:(MTLRegion)region mipmapLevel:(NSUInteger)level withBytes:(const void *)pixelBytes bytesPerRow:(NSUInteger)bytesPerRow;
参数1-region:像素区域在纹理中的位置
参数2-level:从零开始的值,指定哪个mipmap级别是目标。如果纹理没有mipmap,请使用0。
参数3-pixelBytes:指向要复制图片的字节数
参数4-bytesPerRow:对于普通或压缩像素格式,源数据行之间的跨度(以字节为单位)。对于压缩像素格式,跨度是从一排块的开头到下一行的开始的字节数。
*/
[_texture replaceRegion:region
mipmapLevel:0
withBytes:imageBytes
bytesPerRow:4 * image.size.width];
传递图像数据
通过命令编码器,将当前纹理对象传递到 Metal 程序的片段函数中
[commandEncoder setFragmentTexture:_texture atIndex:TextureIndexBaseColor]
Shader 处理
顶点函数与之前 OpenGL ES 加载纹理一样,之比之前传入的参数是变量,而 Metal 接收的是结构体,返回的也可以是结构体
typedef struct
{
float4 clipSpacePosition [[position]]; // position的修饰符表示这个是顶点
float2 textureCoordinate; // 纹理坐标,会做插值处理
} RasterizerData;
vertex RasterizerData // 返回给片元着色器的结构体
vertexShader(uint vertexID [[ vertex_id ]], // vertex_id是顶点shader每次处理的index,用于定位当前的顶点
constant Vertex *vertexArray [[ buffer(0) ]]) { // buffer表明是缓存数据,0是索引
RasterizerData out;
out.clipSpacePosition = vertexArray[vertexID].position;
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
return out;
}
fragment float4
samplingShader(RasterizerData input [[stage_in]], // stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
texture2d<half> colorTexture [[ texture(0) ]]) // texture表明是纹理数据,0是索引
{
constexpr sampler textureSampler (mag_filter::linear,
min_filter::linear); // sampler是采样器
half4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate); // 得到纹理对应位置的颜色
return float4(colorSample);
}
总结
Metal 的图片加载和 OpenGL ES非常的相似,核心关键是:
- 纹理图像解压成位图
- 将位图信息存储到
MTLTexture对象 - 在绘制时,将
MTLTexure对象传递到 Metal 着色器程序的片段函数 - 片段函数设置采样器,获取纹理对应位置的颜色值