效果图如下:
本文主要介绍使用`MTLBuffer`将大量的顶点数据存储到顶点缓冲区,GPU可以访问该缓冲物的顶点数据,`MTLBuffer`缓冲区的数据需要通过`setVertexBuffer(_:offset:index:)`传递到顶点着色器。
划分批量数据范围以及如何处理小批量的顶点数据
* [苹果的官方文档](https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fmetal%2Fmtlrendercommandencoder%2F1515846-setvertexbytes)中对数据有如下说明,当数据量小于4KB的时候,我们可以称为小批量数据,当顶点的数据量大于4KB时,数据量就很大了,我们可以叫大批量数据。主要区别在于苹果对这两种的数据量的处理方式不同。* 当我们的顶点数据小于4KB时候,我们可以直接将顶点数据放在数组中,并使用`- (void)setVertexBytes:(const void *)bytes length:(NSUInteger)length atIndex:(NSUInteger)index`方法将数据传递到顶点着色器函数。* 当顶点数据量大于4KB时候,我们可以使用一个叫`MTLBuffer`的对象,它能够将数据存储到顶点缓存区中,便于GPU对这些数据进行快速的访问处理。 
如何使用Metal渲染一张图片
Render
类设置顶点相关操作、设置渲染管道相关操作、加载纹理、- (void)drawInMTKView:(nonnull MTKView *)view
代理方法进行渲染;BaseShaderTypes.h
文件来桥接OC和Metal方法;BaseShaders.metal
设置Metal的顶点着色器函数和片元着色器函数;BaseImage
类通过加载一个简单的TGA文件初始化这个图像.只支持32bit的TGA文件;BaseShaderTypes.h
桥接文件的具体实现,设置的结构体以及枚举即支持OC又支持Metal的调用
绘制流程
Viewcontroller主要代码
_view = (MTKView *)self.view;
//一个MTLDevice 对象就代表这着一个GPU,通常我们可以调用方法MTLCreateSystemDefaultDevice()来获取代表默认的GPU单个对象.
_view.device = MTLCreateSystemDefaultDevice();
if(!_view.device)
{
NSLog(@"Metal is not supported on this device");
return;
}
//2.创建CCRender
_renderer = [[CCRenderer alloc] initWithMetalKitView:_view];
if(!_renderer)
{
NSLog(@"Renderer failed initialization");
return;
}
//用视图大小初始化渲染器
[_renderer mtkView:_view drawableSizeWillChange:_view.drawableSize];
//设置MTKView代理
_view.delegate = _renderer;
render渲染类代码
//渲染的设备(GPU)
id<MTLDevice> _device;
//渲染管道:顶点着色器/片元着色器,存储于.metal shader文件中
id<MTLRenderPipelineState> _pipelineState;
//命令队列,从命令缓存区获取
id<MTLCommandQueue> _commandQueue;
//顶点缓存区
id<MTLBuffer> _vertexBuffer;
//当前视图大小,这样我们才能在渲染通道中使用此视图
vector_uint2 _viewportSize;
//顶点个数
NSInteger _numVertices;
初始化方法
//初始化
- (instancetype)initWithMetalKitView:(MTKView *)mtkView
{
self = [super init];
if(self)
{
//1.初始GPU设备
_device = mtkView.device;
//2.加载Metal文件
[self loadMetal:mtkView];
}
return self;
}
- (void)loadMetal:(nonnull MTKView *)mtkView
{
//1.设置绘制纹理的像素格式
mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
//2.从项目中加载所以的.metal着色器文件
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
//3.配置用于创建管道状态的管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程总的各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//设置管道中存储颜色数据的组件格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
//4.同步创建并返回渲染管线对象
NSError *error = NULL;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:&error];
//判断是否创建成功
if (!_pipelineState)
{
NSLog(@"Failed to created pipeline state, error %@", error);
}
//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);
//6.创建命令队列
_commandQueue = [_device newCommandQueue];
}
不同点:_vertexBuffer = [_device newBufferWithLength:vertexData.length options:MTLResourceStorageModeShared];
memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);使用这两个方法开辟顶点缓冲区,并且将顶点数据拷贝到缓冲区
generateVertexData 获取顶点数据的方法
+ (nonnull NSData *)generateVertexData
{
//1.正方形 = 三角形+三角形
const CCVertex quadVertices[] =
{
// Pixel 位置, RGBA 颜色
{ { -20, 20 }, { 1, 0, 0, 1 } },
{ { 20, 20 }, { 1, 0, 0, 1 } },
{ { -20, -20 }, { 1, 0, 0, 1 } },
{ { 20, -20 }, { 0, 0, 1, 1 } },
{ { -20, -20 }, { 0, 0, 1, 1 } },
{ { 20, 20 }, { 0, 0, 1, 1 } },
};
//行/列 数量
const NSUInteger NUM_COLUMNS = 25;
const NSUInteger NUM_ROWS = 15;
//顶点个数
const NSUInteger NUM_VERTICES_PER_QUAD = sizeof(quadVertices) / sizeof(CCVertex);
//四边形间距
const float QUAD_SPACING = 50.0;
//数据大小 = 单个四边形大小 * 行 * 列
NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS;
//2\. 开辟空间
NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];
//当前四边形
CCVertex * currentQuad = vertexData.mutableBytes;
//3.获取顶点坐标(循环计算)
//行
for(NSUInteger row = 0; row < NUM_ROWS; row++)
{
//列
for(NSUInteger column = 0; column < NUM_COLUMNS; column++)
{
//A.左上角的位置
vector_float2 upperLeftPosition;
//B.计算X,Y 位置.注意坐标系基于2D笛卡尔坐标系,中心点(0,0),所以会出现负数位置
upperLeftPosition.x = ((-((float)NUM_COLUMNS) / 2.0) + column) * QUAD_SPACING + QUAD_SPACING/2.0;
upperLeftPosition.y = ((-((float)NUM_ROWS) / 2.0) + row) * QUAD_SPACING + QUAD_SPACING/2.0;
//C.将quadVertices数据复制到currentQuad
memcpy(currentQuad, &quadVertices, sizeof(quadVertices));
//D.遍历currentQuad中的数据
for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++)
{
//修改vertexInQuad中的position
currentQuad[vertexInQuad].position += upperLeftPosition;
}
//E.更新索引
currentQuad += 6;
}
}
return vertexData;
}
代理方法
-(void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
_viewportSize.x = size.width;
_viewportSize.y = size.height;
-(void)drawInMTKView:(nonnull MTKView *)view
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
commandBuffer.label = @"MyCommand";
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
//判断渲染目标是否为空
if(renderPassDescriptor != nil)
{
id<MTLRenderCommandEncoder> renderEncoder =
[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
renderEncoder.label = @"MyRenderEncoder";
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];
[renderEncoder setRenderPipelineState:_pipelineState];
//将_vertexBuffer 设置到顶点缓存区中
[renderEncoder setVertexBuffer:_vertexBuffer
offset:0
atIndex:CCVertexInputIndexVertices];
//将 _viewportSize 设置到顶点缓存区绑定点设置数据
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:CCVertexInputIndexViewportSize];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:_numVertices];
[renderEncoder endEncoding];
[commandBuffer presentDrawable:view.currentDrawable];
}
[commandBuffer commit];
由于顶点坐标使用的是像素坐标空间的位置,在顶点着色器中要做归一化处理。
vertexShader(uint vertexID [[vertex_id]],
constant CCVertex *vertices [[buffer(CCVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])
{
//定义out
RasterizerData out;
//初始化输出剪辑空间位置
out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
// 索引到我们的数组位置以获得当前顶点
// 我们的位置是在像素维度中指定的.
float2 pixelSpacePosition = vertices[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.color = vertices[vertexID].color;
//完成! 将结构体传递到管道中下一个阶段:
return out;
}