十五、Metal - 图片加载的实现

音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总

使用Metal实现图片的加载,重点在于纹理的处理和metal文件的书写

1、简单介绍

案例地址: 图片加载

效果:

主要学习内容:

  1. metal的语法回顾
  2. 多顶点数据的传递
  3. 图片转纹理数据的过程
  4. 纹理数据的传递

2、metal语法回顾

为了回顾metal语法,我这里会把metal中涉及的语法都分析一下。
具体的metal语法,可以查看我之前写的博客十四、Metal - Metal Shader language (着色语言规范)总结

传递数据结构体,顶点着色器输出和片元着色器输入

结构体包含有顶点数据和纹理数据,分别作用于顶点着色器和片元着色器。

typedef struct
{
    //顶点数据
    //[[position]]是内建的变量修饰符,表示顶点数据
    float4 clipSpacePosition [[position]];
    //纹理数据
    float2 textureCoordinate;
    
} RasterizerData;

说明:

  1. float4表示四维向量,向量数据为float类型,float表示为二维向量。
  2. [[position]]是内建的变量修饰符,表示顶点数据

顶点着色器

代码:

//顶点着色函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant CCVertex *vertexArray [[buffer(CCVertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])
{
    /*
     处理顶点数据:
     1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
     2) 将顶点颜色值传递给返回值
     */
    
    //定义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;
}

说明:

  1. vertex表示函数类型,这个函数是一个顶点函数(在metal中称呼着色器为函数)
  2. RasterizerData返回值,就是上面定义的结构体,会将结果返回,之后会传到片元着色器中。
  3. vertexShader是函数名,可以任意修改,当然要写成有意义的。
  4. uint vertexID [[vertex_id]]是一个参数,unit表示无符号int型,vertextId是参数名,[[vertex_id]]是内建的变量修饰符,表示顶点ID,用来获取当前的顶点。每个顶点函数都要加上这个参数。
  5. constant CCVertex *vertexArray [[buffer(CCVertexInputIndexVertices)]]:顶点数组变量。
    1. constant是地址空间修饰符,表示vertexArray是存储在显存中,且是个常量,只读不可写
    2. CCVertex是参数类型,是一个结构体
    3. [[buffer(CCVertexInputIndexVertices)]]是属性标识符,说明这个变量是从外界传入的数据,CCVertexInputIndexVertices就是传入的通道标识,在外部进行传入时要写上对应的通道标识。因此这个变量是constant,所以需要写成buffer。
  6. constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]]。视口大小变量
    1. vector_uint2其实就是uint2,也就是无符号的int型,而且是二维的向量。
    2. [[buffer(CCVertexInputIndexViewportSize)]]说明这个参数也是外界传入的,CCVertexInputIndexViewportSize是传入的通道标识。因为这个变量是constant,所以需要写成buffer。
  7. float2 pixelSpacePosition = vertexArray[vertexID].position.xy;获取到当前顶点
    1. vertexArray[vertexID]:通过vertextID获取顶点数组中的当前顶点
    2. .position.xy得到当前顶点的坐标。
  8. out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    1. 得到当前顶点的纹理信息。

片元着色器

代码:

// 片元函数
//[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.
//一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.
//被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.
fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                               texture2d colorTexture [[texture(CCTextureIndexBaseColor)]])
{
    constexpr sampler textureSampler(mag_filter::linear,
                                     min_filter::linear);
    
    const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
    
    return float4(colorSampler);
    
    //返回输入的片元颜色
    //return in.color;
}

说明:

  1. fragment是函数修饰符,表示这个函数是片元函数
  2. RasterizerData in [[stage_in]]表示从顶点着色器传入的变量
    1. RasterizerData是参数类型
    2. in是参数变量
    3. [[stage_in]]是内建变量修饰符,表示这个变量是从顶点着色器传递进入的。
  3. texture2d colorTexture [[texture(CCTextureIndexBaseColor)]]是纹理参数
    1. texture2d表示纹理类型,而且写入颜色时的数据类型是half,没有写access,默认是sample
    2. [[texture(CCTextureIndexBaseColor)]]是属性修饰符,texture表示这个变量是纹理,CCTextureIndexBaseColor是传入的通道ID,直接从CPU传递过来的数据,而不是通过顶点着色器
  4. constexpr sampler textureSampler(mag_filter::linear,min_filter::linear);是设置采样器类型
    1. constexpr是设置采样器类型的修饰符,必须写
    2. sampler表示是个采样器
    3. textureSampler是采样器名称
    4. (mag_filter::linear,min_filter::linear)是对采样器进行设置,这里设置了最大过滤是邻近过滤,最小过滤方式也是邻近过滤
  5. const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);获取采样的色值
    1. const表示常量,half4是四维向量
    2. colorTexture.sample是对纹理进行采样,传入的参数有采样器和纹理坐标。

3、大量顶点数据的传递

前文我们通过setVertexBytes的API传递顶点数据,但是如果顶点数据特别多,就无法使用了,上限是4000多个字节。此时就应该使用setVertexBuffer的API传递数据。

过程:

  1. 使用MTLBuffer来存储顶点数据
  2. 将内存中的顶点数据拷贝到MTLBuffer
  3. 设置的MTLBuffer数据到顶点着色器中

API:
拷贝数据到MTLBuffer

    //顶点缓存区
    id _vertexBuffer;
    
    
    //5.获取顶点数据
    NSData *vertexData = [CCRenderer generateVertexData];
    //创建一个vertex buffer,可以由GPU来读取
    _vertexBuffer = [_device newBufferWithLength:vertexData.length                                       options:MTLResourceStorageModeShared];
    //复制vertex data 到vertex buffer 通过缓存区的"content"内容属性访问指针
    /*
     memcpy(void *dst, const void *src, size_t n);
     dst:目的地
     src:源内容
     n: 长度
     */
    memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);
    //计算顶点个数 = 顶点数据长度 / 单个顶点大小
    _numVertices = vertexData.length / sizeof(CCVertex);

传递数据到顶点着色器

//5.我们调用-[MTLRenderCommandEncoder setVertexBuffer:offset:atIndex:] 为了从我们的OC代码找发送数据预加载的MTLBuffer 到我们的Metal 顶点着色函数中
        /* 这个调用有3个参数
            1) buffer - 包含需要传递数据的缓冲对象
            2) offset - 它们从缓冲器的开头字节偏移,指示“顶点指针”指向什么。在这种情况下,我们通过0,所以数据一开始就被传递下来.偏移量
            3) index - 一个整数索引,对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。注意,此参数与 -[MTLRenderCommandEncoder setVertexBytes:length:atIndex:] “索引”参数相同。
         */
        
        //将_vertexBuffer 设置到顶点缓存区中
        [renderEncoder setVertexBuffer:_vertexBuffer
                                offset:0
                               atIndex:CCVertexInputIndexVertices];

4、纹理数据的加载

过程:

  1. 使用MTLTexture纹理对象来存储纹理数据
  2. 将tag/png/jpg图片数据解析到纹理对象中(重点)
  3. 将纹理数据传递到顶点着色器(就是一个API)
  4. 在metal中纹理数据的处理

注:metal对纹理数据的处理在上文已经说明了,这里就不再解读了

4.1 解析纹理

通过图片得到纹理的过程

  1. 获取图片对象
  2. 设置纹理描述符,纹理的像素尺寸
  3. 通过纹理描述符创建纹理
  4. 通过图片对象获取图片数据
  5. 设置纹理的图片数据。

代码:

    // Metal 纹理对象
    id _texture;
    
    //1.获取图片
    UIImage *image = [UIImage imageNamed:@"cat.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];
    
    /*
     typedef struct
     {
     MTLOrigin origin; //开始位置x,y,z
     MTLSize   size; //尺寸width,height,depth
     } MTLRegion;
     */
    //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;
    }

通过图片获取图片数据

详细过程之前的博客已经有详细的解读
请查看八、OpenGL ES - GLSL的使用的4.3节加载纹理

- (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;
}

4.2 将纹理数据传递到顶点着色器

//7.设置纹理对象
[renderEncoder setFragmentTexture:_texture atIndex:CCTextureIndexBaseColor];

可以看到就是调用setFragmentTexture的API,参数传递纹理对象即可。

你可能感兴趣的:(十五、Metal - 图片加载的实现)