美文网首页
Metal学习笔记(四)-- 传递数据

Metal学习笔记(四)-- 传递数据

作者: iOSer_jia | 来源:发表于2020-08-27 15:39 被阅读0次

编写完顶点着色函数和片元着色函数后,我们还需要往函数中传递数据比如顶点数据和纹理数据,本文将介绍如何往着色器函数中传递数据。

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这是一个可以持有纹理资源二进制数据的对象。通常有以下几种创建方式:

  1. 通过一个纹理描述对象--MTLTextureDescriptor创建,使用MTLDevicenewTextureWithDescriptor:方法,我们可以使用这个方式渲染一个png/jpg或tga格式的文件,这也是比较常用的方式。
  2. 用已经存在的IOSSurface对象去持有纹理数据,由MTLDevicenewTextureWithDescriptor:iosurface:plane:方法创建,这个方法同样需要一个纹理描述对象来描述IOSSurface重点纹理数据。
  3. 若要一个另一个纹理重新转化不同格式的新纹理,可以调用原MTLTexture对象的newTextureViewWithPixelFormat:newTextureViewWithPixelFormat:textureType:levels:slices:创建,新纹理选择的像素格式必须与原纹理的兼容,新纹理与原纹理分享相同的储存分配。如果对新纹理进行更改,这些将反应在原纹理中,反之亦凡。
  4. 我们也可以利用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对象传递顶点数据,我们也可以直接调用MTLRenderCommandEncodersetVertexBytes:length:atIndex:(NSUInteger)index方法将内存的顶点数组传递到Metal,不过这个方法也有限制,当顶点数量大于4000个左右时,使用MTLBuffer才能正确渲染出图形,当顶点数量不多时,使用和不使用MTLBuffer都是可以的。

纹理数据的传入

一般的业务场景是我们将一张图片渲染到屏幕上,使用Metal传递纹理数据有以下步骤:

  1. 创建纹理描述对象,并设置参数
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;

相关文章

网友评论

      本文标题:Metal学习笔记(四)-- 传递数据

      本文链接:https://www.haomeiwen.com/subject/vygcsktx.html