Metal 处理图形渲染和加载图片
这篇文章将介绍如何使用Metal处理顶点数量多的图形和使用Metal去加载图片。
首先我们来实现使用Metal来处理顶点数量多的图形。
在之前我们介绍过如何使用Metal API的使用流程,在这个文章中,有一个案例就是如何使用Metal去实现一个三角形的效果。
在实现三角形效果时,我们设置到顶点缓冲区时使用的是下面的方法:
[setVertexBytes:<#(nonnull const void *)#> length:<#(NSUInteger)#> atIndex:<#(NSUInteger)#>]
而上面的那个方法只能加载少量顶点的数据,因此,在加载大量顶点数据组成的图形时,我们就要使用renderEncoder setVertexBuffer:<#(nullable id<MTLBuffer>)#> offset:<#(NSUInteger)#> atIndex:<#(NSUInteger)#>
这个方法来实现.
下面我们先来看一下加载大量顶点的效果图:
Simulator Screen Shot - iPhone SE -2nd generation- - 2020-08-25 at 11.01.29.png
接下来我们来实现一下上面的效果:
实现上面的效果,我们按照流程一步一步来实现:
首先实现初始化工作流程
1、创建Metal文件和加载Metal的类文件
在类中调用Metal文件时,我们会需要进行传值操作,因此,我们需要定义一个.h文件来共享与 shader 和 C 代码
// 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
typedef enum VertexInputIndex{
//顶点
VertexInputIndexVertices = 0,
//视图大小
VertexInputIndexViewportSize = 1,
}VertexInputIndex;
typedef struct {
// 像素空间的位置
// 像素中心点(100,100)
//float float
vector_float2 position;
// RGBA颜色
//float float float float
vector_float4 color;
}Vertex;
然后在Metal文件中引入.h的头文件
typedef struct{
float4 clipSpacePosition [[position]];
float4 color;
} RasterizerData;
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],constant Vertex *vertices [[buffer(VertexInputIndexVertices)]],constant vector_uint2 *viewportSizePointer [[buffer(VertexInputIndexViewportSize)]]){
/*
处理顶点数据:
1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
2) 将顶点颜色值传递给返回值
*/
//定义out
RasterizerData out;
//初始化输出剪辑空间位置
out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
// 索引到我们的数组位置以获得当前顶点
// 我们的位置是在像素维度中指定的.
float2 pixelSpacePosition = vertices[vertexID].position.xy;
//将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
//每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
//计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.
out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
//把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
out.color = vertices[vertexID].color;
//完成! 将结构体传递到管道中下一个阶段:
return out;
}
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
//返回输入的片元颜色
return in.color;
}
在Metal文件中的VertexInputIndexVertices、VertexInputIndexViewportSize、position、color为外界传值给着色器调用,这边利用了一个枚举和结构体来定义值:
typedef enum VertexInputIndex{
//顶点
VertexInputIndexVertices = 0,
//视图大小
VertexInputIndexViewportSize = 1,
}VertexInputIndex;
typedef struct {
// 像素空间的位置
// 像素中心点(100,100)
//float float
vector_float2 position;
// RGBA颜色
//float float float float
vector_float4 color;
}Vertex;
2、加载着色器文件,包含顶点函数和片元函数
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
3、创建管道用于创建管道状态
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程总的各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//设置管道中存储颜色数据的组件格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
//4.同步创建并返回渲染管线对象
NSError *error = NULL;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:&error];
//判断是否创建成功
if (!_pipelineState)
{
NSLog(@"Failed to created pipeline state, error %@", error);
}
4、获取顶点数据
+ (nonnull NSData *)generateVertexData
{
//1.正方形 = 三角形+三角形
const Vertex quadVertices[] =
{
// Pixel 位置, RGBA 颜色
{ { -20, 20 }, { 1, 0, 0, 1 } },
{ { 20, 20 }, { 1, 0, 0, 1 } },
{ { -20, -20 }, { 1, 0, 0, 1 } },
{ { 20, -20 }, { 0, 0, 1, 1 } },
{ { -20, -20 }, { 0, 0, 1, 1 } },
{ { 20, 20 }, { 0, 0, 1, 1 } },
};
//行/列 数量
const NSUInteger NUM_COLUMNS = 25;
const NSUInteger NUM_ROWS = 15;
//顶点个数
const NSUInteger NUM_VERTICES_PER_QUAD = sizeof(quadVertices) / sizeof(Vertex);
//四边形间距
const float QUAD_SPACING = 50.0;
//数据大小 = 单个四边形大小 * 行 * 列
NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS;
//2. 开辟空间
NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];
//当前四边形
Vertex * currentQuad = vertexData.mutableBytes;
//3.获取顶点坐标(循环计算)
//行
for(NSUInteger row = 0; row < NUM_ROWS; row++)
{
//列
for(NSUInteger column = 0; column < NUM_COLUMNS; column++)
{
//1.左上角的位置
vector_float2 upperLeftPosition;
//2.计算X,Y 位置.注意坐标系基于2D笛卡尔坐标系,中心点(0,0),所以会出现负数位置
upperLeftPosition.x = ((-((float)NUM_COLUMNS) / 2.0) + column) * QUAD_SPACING + QUAD_SPACING/2.0;
upperLeftPosition.y = ((-((float)NUM_ROWS) / 2.0) + row) * QUAD_SPACING + QUAD_SPACING/2.0;
//3.将quadVertices数据复制到currentQuad
memcpy(currentQuad, &quadVertices, sizeof(quadVertices));
//4.遍历currentQuad中的数据
for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++)
{
//修改vertexInQuad中的position
currentQuad[vertexInQuad].position += upperLeftPosition;
}
//5.更新索引
currentQuad += 6;
}
}
return vertexData;
}
5、创建命令队列
_commandQueue = [_device newCommandQueue];
在初始化工作完成后,我们就需要实现MTKViewDelegate两个代理方法
重点部分在于将_vertexBuffer设置到顶点缓冲区和将_viewportSize设置到顶点缓冲区数据
[renderEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:VertexInputIndexVertices];
[renderEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:VertexInputIndexViewportSize];
在设置完之后,开始绘制
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_numVertices];
[renderEncoder endEncoding];
[commandBuffer presentDrawable:view.currentDrawable];
最后提交:
[commandBuffer commit];
实现将加载大量顶点绘制成的图形之后,我们来实现如何使用Metal来加载tga和jpg图片。
同样的先来看一下效果图:
Simulator Screen Shot - iPhone SE -2nd generation- - 2020-08-25 at 11.28.23.png同样的,加载图片和绘制图形代码有一些区别,首先在着色器代码中我们还需要传入一个纹理索引,在设置纹理对象时,将纹理索引进行赋值。
在需要传值的.h文件中,加入一个索引枚举值
// 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
typedef enum VertexInputIndex
{
//顶点
VertexInputIndexVertices = 0,
//视图大小
VertexInputIndexViewportSize = 1,
} VertexInputIndex;
//纹理索引
typedef enum CCTextureIndex
{
TextureIndexBaseColor = 0
}TextureIndex;
//结构体: 顶点/颜色值
typedef struct
{
// 像素空间的位置
// 像素中心点(100,100)
vector_float2 position;
// 2D 纹理
vector_float2 textureCoordinate;
} Vertex;
下面贴出着色器代码
// 顶点着色器输出和片段着色器输入
//结构体
typedef struct
{
float4 clipSpacePosition [[position]];
float2 textureCoordinate;
} RasterizerData;
//顶点着色函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant Vertex *vertexArray [[buffer(VertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(VertexInputIndexViewportSize)]])
{
//定义out
RasterizerData out;
//初始化输出剪辑空间位置
out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
// 索引到我们的数组位置以获得当前顶点
// 我们的位置是在像素维度中指定的.
float2 pixelSpacePosition = vertexArray[vertexID].position.xy;
//将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
//每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
//计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.
out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
out.clipSpacePosition.z = 0.0f;
out.clipSpacePosition.w = 1.0f;
//把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
//完成! 将结构体传递到管道中下一个阶段:
return out;
}
fragment float4 fragmentShader(RasterizerData in [[stage_in]],
texture2d<half> colorTexture [[texture(TextureIndexBaseColor)]])
{
constexpr sampler textureSampler(mag_filter::linear,
min_filter::linear);
const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
return float4(colorSampler);
//返回输入的片元颜色
//return in.color;
}
接下来,我们先实现加载tga和jpg公共部分代码:
1.首先加载顶点操作
//1.根据顶点/纹理坐标建立一个MTLBuffer
static const Vertex 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数组初始化它
_vertices = [_device newBufferWithBytes:quadVertices
length:sizeof(quadVertices)
options:MTLResourceStorageModeShared];
//3.通过将字节长度除以每个顶点的大小来计算顶点的数目
_numVertices = sizeof(quadVertices) / sizeof(Vertex);
2.设置渲染管道操作
//1.创建我们的渲染通道
//从项目中加载.metal文件,创建一个library
id<MTLLibrary>defalutLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction>vertexFunction = [defalutLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defalutLibrary newFunctionWithName:@"fragmentShader"];
//2.配置用于创建管道状态的管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Texturing Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程总的各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//设置管道中存储颜色数据的组件格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = ccMTKView.colorPixelFormat;
//3.同步创建并返回渲染管线对象
NSError *error = NULL;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
//判断是否创建成功
if (!_pipelineState)
{
NSLog(@"Failed to created pipeline state, error %@", error);
}
//4.使用_device创建commandQueue
_commandQueue = [_device newCommandQueue];
3.实现MTKViewDelegate的两个代理方法:
跟上一个加载图形其实是一摸一样的,这边不在写了。
4.接下来实现加载图片操作了,首先加载tga图片,我们先将图片转化为一个对象
这个转化方法:
-(nullable instancetype) initWithTGAFileAtLocation:(nonnull NSURL *)location
{
self = [super init];
if(self)
{
NSString *fileExtension = location.pathExtension;
//判断文件后缀是否为tga
if(!([fileExtension caseInsensitiveCompare:@"TGA"] == NSOrderedSame))
{
NSLog(@"此Image只加载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;
}
在转化完之后,我们就可以设置纹理了:
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc]init];
textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
//设置纹理的像素尺寸
textureDescriptor.width = image.width;
textureDescriptor.height = image.height;
//使用描述符从设备中创建纹理
_texture = [_device newTextureWithDescriptor:textureDescriptor];
//计算图像每行的字节数
NSUInteger bytesPerRow = 4 * image.width;
/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];
5.实现jpg图片的加载:
-(void)setupTexturePNG
{
//1.获取图片
UIImage *image = [UIImage imageNamed:@"kun.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;
}
}
//从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.释放spriteContext
CGContextRelease(spriteContext);
return spriteData;
}
案例demo