编写完顶点着色函数和片元着色函数后,我们还需要往函数中传递数据比如顶点数据和纹理数据,本文将介绍如何往着色器函数中传递数据。
MTLBuffer -- “顶点缓冲区”
我们知道OpenGL中有顶点缓冲区的概念,Metal也有顶点缓冲区,不仅如此,Metal将缓冲区的概念具化为一个对象MTLBuffer
,更加方便开发者使用。
MTLBuffer
同样是一个不需要开发者实现的协议,开发者可以通过MTLDevice
对象创建一个MTLBuffer
对象。
- (id<MTLBuffer>)newBufferWithLength:(NSUInteger)length
options:(MTLResourceOptions)options;
- (id<MTLBuffer>)newBufferWithBytes:(const void *)pointer
length:(NSUInteger)length
options:(MTLResourceOptions)options;
- (id<MTLBuffer>)newBufferWithBytesNoCopy:(void *)pointer
length:(NSUInteger)length
options:(MTLResourceOptions)options
deallocator:(void (^)(void *pointer, NSUInteger length))deallocator;
除了大小,Metal框架并不关心MTLBuffer
里面的内容,开发者可以自己定义数据结构,只要确保app和Metal能正确的读写,比如我们可以定义一个结构体不仅OC/swift代码可以写入,shader也可以知道如何读取。
另外如果我们创建options为MTLStorageModeManaged
的buffer,我们必须调用MTLBuffer
对象的- (void)didModifyRange:(NSRange)range;
方法通知Metal复制变化到GPU中。
MTLTexture -- 纹理对象
MTLTexture也是一个不需要开发者实现的协议,MTLTexture
这是一个可以持有纹理资源二进制数据的对象。通常有以下几种创建方式:
- 通过一个纹理描述对象--
MTLTextureDescriptor
创建,使用MTLDevice
的newTextureWithDescriptor:
方法,我们可以使用这个方式渲染一个png/jpg或tga格式的文件,这也是比较常用的方式。 - 用已经存在的
IOSSurface
对象去持有纹理数据,由MTLDevice
的newTextureWithDescriptor:iosurface:plane:
方法创建,这个方法同样需要一个纹理描述对象来描述IOSSurface重点纹理数据。 - 若要一个另一个纹理重新转化不同格式的新纹理,可以调用原
MTLTexture
对象的newTextureViewWithPixelFormat:
或newTextureViewWithPixelFormat:textureType:levels:slices:
创建,新纹理选择的像素格式必须与原纹理的兼容,新纹理与原纹理分享相同的储存分配。如果对新纹理进行更改,这些将反应在原纹理中,反之亦凡。 - 我们也可以利用
MTLBuffer
对象保存纹理像素数据,并通过这个buffer对象的newTextureWithDescriptor:offset:bytesPerRow:
方法获取一个纹理对象,这个方法同样需要一个MTLTextureDescriptor
对象来描述纹理的属性。新的纹理对象共享缓冲区对象的存储分配,如果对纹理进行更改,这些改变将反应在缓冲区中,反之亦然。
当纹理对象创建完成后,它的大多数属性,比如大小,新类型,像素格式都是不能改变的,但是纹理的像素数据是可以改变。
另外,如果要复制内存的像素数据到纹理中,可以调用MTLTexture
对象的以下方法:
- (void)replaceRegion:(MTLRegion)region
mipmapLevel:(NSUInteger)level
slice:(NSUInteger)slice
withBytes:(const void *)pixelBytes
bytesPerRow:(NSUInteger)bytesPerRow
bytesPerImage:(NSUInteger)bytesPerImage;
- (void)replaceRegion:(MTLRegion)region
mipmapLevel:(NSUInteger)level
withBytes:(const void *)pixelBytes
bytesPerRow:(NSUInteger)bytesPerRow;
如果要将MTLTexture
的像素数据拷贝到内存,可以调用:
- (void)getBytes:(void *)pixelBytes
bytesPerRow:(NSUInteger)bytesPerRow
bytesPerImage:(NSUInteger)bytesPerImage
fromRegion:(MTLRegion)region
mipmapLevel:(NSUInteger)level
slice:(NSUInteger)slice;
- (void)getBytes:(void *)pixelBytes
bytesPerRow:(NSUInteger)bytesPerRow
fromRegion:(MTLRegion)region
mipmapLevel:(NSUInteger)level;
创建顶点数据类型
根据苹果的建议,我们可以定义一个结构体,能够让Metal和app进行读写,另外为了提高可读性,也可以一个枚举定义传入Metal的数据索引。
// 缓存区索引值
// 共享与 shader 和 C 代码,为了确保Metal Shader缓存区索引能够匹配,Metal API Buffer 设置的集合调用
typedef enum {
//顶点
LJVertexInputIndexVertices = 0,
// 试图大小
LJVertexInputIndexViewportSize = 1,
}LJVertexInputIndex;
// 顶点/颜色值
typedef struct {
// 顶点坐标
vector_float2 position;
// 像素坐标
vector_float2 textureCoord;
}LJVertex;
另外附上Metal的着色器方法代码
using namespace metal;
// 声明一个结构体作为顶点着色函数的返回值
typedef struct {
float4 clipSpacePosition [[position]]; // 用于接收顶点信息
float2 textureCoord; // 纹理坐标
}RasterizeData;
// 顶点着色函数
vertex RasterizeData
vertexShader(uint vertexID [[vertex_id]],
constant LJVertex *vertexArray [[buffer(LJVertexInputIndexVertices)]],
constant vector_uint2 *viewposrSizePointer [[buffer(LJVertexInputIndexViewportSize)]])
{
// 定义输出
RasterizeData out;
out.clipSpacePosition = float4(0.f, 0.f, 0.f, 1.f);
// 接收顶点信息
float2 pixelSpacePosition = vertexArray[vertexID].position;
// 视图大小
float2 viewportSize = float2(*viewposrSizePointer);
//归一化
out.clipSpacePosition.xy = (pixelSpacePosition / (viewportSize / 2.0));
out.textureCoord = vertexArray[vertexID].textureCoord;
return out;
}
// 片元着色函数
//[[stage_in]]修饰的参数是由顶点函数输出的数据,然后经由光栅化生成的。
fragment float4 fragmentShader(RasterizeData in [[stage_in]],
texture2d<float, access::sample> colorTexture [[texture(LJTextureIndexBaseColor)]])
{
// 定义采样器
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
// 获取像素
const float4 colorSampler = colorTexture.sample(textureSampler, in.textureCoord);
return colorSampler;
}
关于Metal的导入和渲染管道的创建这里就不再赘述。
顶点数据的传入
往Metal传入顶点数据可以有以下步骤
1.创建一个顶点数据:
static const LJVertex quadVertices[] = {
//像素坐标,纹理坐标
{ { 290, -175 }, { 1.f, 0.f } },
{ { -290, -175 }, { 0.f, 0.f } },
{ { -290, 175 }, { 0.f, 1.f } },
{ { 290, -175 }, { 1.f, 0.f } },
{ { -290, 175 }, { 0.f, 1.f } },
{ { 290, 175 }, { 1.f, 1.f } },
};
2.创建顶点缓冲区,并将顶点数据保存的缓冲区中
_vertices = [_device newBufferWithBytes:quadVertices
length:sizeof(quadVertices)
options:MTLResourceStorageModeShared];
或者,也可以这么创建和传入
_vertices = [_device newBufferWithLength:vertexData.length
options:MTLResourceStorageModeShared];
memcpy(_vertices.contents, quadVertices, quadVertices.length);
3.传入顶点着色函数
[renderEncoder setVertexBuffer:_vertices
offset:0
atIndex:LJVertexInputIndexVertices];
除了使用
MTLBuffer
对象传递顶点数据,我们也可以直接调用MTLRenderCommandEncoder
的setVertexBytes:length:atIndex:(NSUInteger)index
方法将内存的顶点数组传递到Metal,不过这个方法也有限制,当顶点数量大于4000个左右时,使用MTLBuffer
才能正确渲染出图形,当顶点数量不多时,使用和不使用MTLBuffer
都是可以的。
纹理数据的传入
一般的业务场景是我们将一张图片渲染到屏幕上,使用Metal传递纹理数据有以下步骤:
- 创建纹理描述对象,并设置参数
MTLTextureDescriptor *textureDesc = [[MTLTextureDescriptor alloc] init];
textureDesc.pixelFormat = MTLPixelFormatRGBA8Unorm;
// 图片的宽高, image是UIImage对象
textureDesc.width = image.size.width;
textureDesc.height = image.size.height;
2.创建MTLTexture对象
_texture = [_device newTextureWithDescriptor:textureDesc]
3.拷贝纹理数据到纹理对象
MTLRegion region = {
{0,0,0},
{image.size.width, image.size.height, 1.0}
};
NSUInteger bytesPerRow = image.size.width * 4;
// 获取图片位图数据
Byte *imageBytes = [self loadImage:image];
if (imageBytes) {
// 传递数据
[_texture replaceRegion:region
mipmapLevel:0
withBytes:imageBytes
bytesPerRow:bytesPerRow];
free(imageBytes);
imageBytes = nil;
}
4.传递到Metal
[renderEncoder setFragmentTexture:_texture atIndex:LJTextureIndexBaseColor];
最后附上UIImage转化为位图数据的代码:
- (Byte *)loadImage:(UIImage *)image {
CGImageRef cgImage = image.CGImage;
if (!cgImage) {
NSLog(@"image is nil");
return nil;
}
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
Byte *spriteData = (Byte *)calloc(width * height * 4, sizeof(Byte));
CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage);
CGContextRef imageContext = CGBitmapContextCreate(spriteData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGRect rect = CGRectMake(0, 0, (float)width, (float)height);
CGContextTranslateCTM(imageContext, 0, rect.size.height);
CGContextScaleCTM(imageContext, 1, -1);
CGContextDrawImage(imageContext, rect, cgImage);
CGContextRelease(imageContext);
return spriteData;
}
除了png/jpg/jpeg格式的图片,还有一种tga格式的图片,也附上解码代码:
// 最终结果
NSData *_data = nil;
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"Image" withExtension:@"tga"];
NSString *fileExtension = fileUrl.pathExtension;
if ([fileExtension caseInsensitiveCompare:@"TGA"] != NSOrderedSame) {
NSLog(@"it's not tga file");
return _data;
}
//定义一个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:fileUrl options:0x0 error:&error];
if (fileData == nil) {
NSLog(@"open tga file failed: %@", error.localizedDescription);
return nil;
}
TGAHeader *tgaInfo = (TGAHeader *)fileData.bytes;
// 获取图片宽高
_width = tgaInfo->width;
_height = tgaInfo->height;
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);
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 _data;
网友评论