Metal 三、案例-Metal 大量顶点数据时处理方案 MTLBuffer + 加载纹理

一、Metal 大量顶点数据处理

如上图,setVertexByte: 方法对数据是有限制的 不能大于4K。当大量数据,超过了4K时,我们可以使用 MTLBuffer。

1、MTLBuffer

当顶点数量太多时,对CPU的消耗会增大,尤其在游戏、AI等场景中,为更好的扩展管理 (并不是为了图形图片的加载),Metal 提出了一个新的对象:MTLBuffer;

Metal --> MTLBuffer --> 缓存区(可以存储了大量的自定义数据,GPU直接访问 <-- 缓存区存在显存中) --> 存储顶点数据

1.1、当顶点数据超过了 4K 上限时,"setVertexBytes:length:atIndex:" 方法会不再生效 --> 简单的绘制三角形 每个顶点 xyzw 占用4*8=32 字节 --> 我们可根据实际业务场景考量是否需要使用 MTLBuffer

2、MTLBuffer 使用

这里不再重复贴代码,可 通过绘制三角形 Demo 进行对比。更多案例可通过 Metal Sample Code 官方 Demo 示例 来下载了解 Metal 相关 API 的使用。

通过 buffer 传值:

// 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:MyVertexInputIndexVertices];

/* ##不需要设置缓冲区,普通传递顶点数据的方法 -- 具体流程使用见绘制三角形 demo
// 顶点 + 颜色
[renderEncoder setVertexBytes:triangleVertices  length:sizeof(triangleVertices) atIndex:MyVertexInputIndexVertices];
*/

缓冲区的创建:

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

二、加载纹理

Metal 加载 .jpg .png 文件

1、加载流程

2、主要代码

纹理处理代码

// JPG 图片
-(void)setupTextureJPG {

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

Metal 代码:

// 顶点函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant MyVertex *vertexArray [[buffer(MyVertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(MyVertexInputIndexViewportSize)]]) {

    /*
     处理顶点数据:
     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;
}


// 当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段 --> 栅格化/光栅化.


// 片元函数
fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                               texture2d<half> colorTexture [[texture(MyTextureIndexBaseColor)]]) {

    constexpr sampler textureSampler(mag_filter::linear,
                                     min_filter::linear);
    
    const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
    
    return float4(colorSampler);
}

Demo 地址

原文地址:https://www.cnblogs.com/zhangzhang-y/p/13572630.html