演示如何用顶点缓冲管理大量的顶点。
概述
在你好三角形这个例子中,你学会了如何用Metal渲染 基本几何体(Geometry)。
在这个例子中,你将学会如何使用顶点缓冲去提高你的渲染效率。尤其,你将学会使用顶点缓冲 区存储和加载多个方形数据。
管理大量顶点数据
在你和三角形这个例子中,渲染3个顶点,每个顶点32字节,顶点数据共计96字节。通过调用
setVertexBytes:length:atIndex:
函数方法发送少量的顶点数据。该方法访问GPU申请少量
的内存,并且可以在每一帧中无性能成本申请。
不像你好三角形这个例子,本例需要渲染2250个顶点,每个32字节,顶点数据共计72000字节。如此
数量的顶点数据应该被更有效地管理。实际上,Metal在顶点数据超过4千字节(4096字节)的情况下,
是不可以使用方法setVertexBytes:length:atIndex:
的。而且,顶点数据不应该在每一帧中
都重新分配和拷贝一次。
通常,Metal应用和游戏会绘制上千顶点的模型,每个顶点有多个属性,这要消耗几Mb内存。
为了更好和更高效地管理这类应用和游戏的数据,Metal提供了一个专门的数据容器MTLBuffer
。
这些缓冲对象分配的内存能够被GPU访问,可以用于存放各种类型的自定义数据,尽管,
我们一般用于存放顶点数据。这个例子一次性分配了大量顶点数据,并拷贝到一个MTLBuffer
对象,
这样就可以在每一帧重复使用了。
分配,生成并复制顶点数据
在Objective-C,字节缓冲用NSData
或者NSMutableData
对象封装,他们是安全的,并便于使用。
例子中,每个顶点的数据类型是AAPLVertex
,并且每个方形由6个这样的顶点组成(每个方形由2个三角形组成)。
方形组成的30 x 20网格,共3,600个顶点,占115,200字节内存,这就是本例中分配顶点的数量。
const AAPLVertex quadVertices[] = { // Pixel positions, RGBA colors { { -20, 20 }, { 1, 0, 0, 1 } }, { { 20, 20 }, { 0, 0, 1, 1 } }, { { -20, -20 }, { 0, 1, 0, 1 } }, { { 20, -20 }, { 1, 0, 0, 1 } }, { { -20, -20 }, { 0, 1, 0, 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(AAPLVertex); const float QUAD_SPACING = 50.0; NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS; NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];
通常,Metal应用或者游戏都是从一个模型文件中加载顶点的。模型加载代码的复杂性因不同模型而异,
但是最终顶点数据都是存储到Metal能处理的字节缓冲区。为了避免引入加载模型代码,本例使用方法
generateVertexData
模拟了顶点数据的转换,她会在运行时生成简单的顶点数据。
NSData
和MTLBuffer
都可以存储自定义数据,这样意味着你的应用在读写操作时需要负责定义(内存布局)和解释数据。
在这个例子中顶点数据是只读的,内存布局用数据类型AAPLVertex
定义,这是顶点函数vertexShader
所需的。
vertex RasterizerData vertexShader(uint vertexID [[ vertex_id ]], device AAPLVertex *vertices [[ buffer(AAPLVertexInputIndexVertices) ]], constant vector_uint2 *viewportSizePointer [[ buffer(AAPLVertexInputIndexViewportSize) ]])
从根本上来说,NSData
和MTLBuffer
是非常相似的。但是 MTLBuffer
对象是一个能够被GPU访问的容器,
使渲染管线能从中读取顶点数据。
NSData *vertexData = [AAPLRenderer generateVertexData]; // Create a vertex buffer by allocating storage that can be read by the GPU _vertexBuffer = [_device newBufferWithLength:vertexData.length options:MTLResourceStorageModeShared]; // Copy the vertex data into the vertex buffer by accessing a pointer via // the buffer's `contents` property memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);
首先用方法newBufferWithLength:options:
创建一个具有字节大小和访问选项的MTLBuffer
新对象。
顶点数据占115,200字节内存(vertexData.length
)并由CPU写入再由GPU读取(MTLResourceStorageModeShared
)。
然后,使用函数memcpy()
从源NSData
对象拷贝到目标MTLBuffer
对象。_vertexBuffer.contents
查询
会返回一个CPU可访问的指向buffer内存的指针。通过源数据指针(vertexData.bytes
)拷贝顶点数据到目标,
拷贝的数量为vertexData.length
。
设置并绘制顶点数据
因为例子的顶点数据现在已经存储到MTLBuffer
对象了, 方法setVertexBytes:length:atIndex:
就不应该调用了;调用方法setVertexBuffer:offset:atIndex:
代替。这个方法携带一个顶点缓冲对象、
一个顶点数据在顶点缓冲中的字节偏移量和一个映射到顶点函数的索引。
Because the sample's vertex data is now stored in a MTLBuffer
object,
the setVertexBytes:length:atIndex:
method can no longer be called;
the setVertexBuffer:offset:atIndex:
method is called instead.
This method takes as parameters a vertex buffer, a byte offset to the
vertex data in that buffer, and an index that maps the buffer to the
vertex function.
- 注意: 使用了
MTLBuffer
作为顶点函数参数,并不妨碍应用或者游戏使用方法setVertexBytes:length:atIndex:
设置其他的参数。事实上,这个例子沿用了你好三角形中的参数viewportSizePointer
。
最后,通过发起绘制调用对所有顶点进行绘制,从数组的第一个顶点(0
)开始,
以数组的最后一个顶点结束(_numVertices
)。
[renderEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:AAPLVertexInputIndexVertices]; [renderEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:AAPLVertexInputIndexViewportSize]; // Draw the vertices of the quads [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_numVertices];
下一步
在这个例子中,你学会如何用顶点缓冲提供你的渲染效率。 在[纹理基础]这个例子中,你将学会如何加载图片数据并渲染纹理到四边形中。
英文原文"Basic Buffers"
已有 2980 位网友参与,快来吐槽:
发表评论