美文网首页
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