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