Metal之MTLBuffer批量加载顶点数量较多的图形渲染

网友投稿 716 2022-05-30

本文是基于“Metal渲染绘制三角形”这样顶点较少图形基础之上的延伸, 在渲染三角形的时候, 顶点数据的存储使用的是数组,当顶点传递时通过setVertexBytes(_:length:index:)方法,主要是由于绘制三角形时,所需的顶点只有三个,顶点数据很少,所以可以通过数组存储,此时的数据是存储在CPU中的;

Metal三角形的渲染绘制请参考:Metal之渲染绘制三角形

对于小于4KB(即4096字节)的一次性数据,使用setVertexBytes(:length:index:),如果数据长度超过4KB 或者需要多次使用顶点数据时,需要创建一个MTLBuffer对象,创建的buffer的目的就是为了将顶点数据存储到顶点缓存区,GPU可以直接访问该缓存区获取顶点数据,并且buffer缓存的数据需要通过setVertexBuffer(:offset:index:)方法传递到顶点着色器。

当图形的顶点数据较多时, 顶点的传递与存储过程如下:

① Metal -> MTLBuffer -> 缓存区(存储非常多自定义数据,GPU直接访问 -> 显存) -> 存储顶点数据;

② 创建的buffer的目的就是为了将顶点数据存储到顶点缓存区,GPU可以直接访问该缓存区获取顶点数据,并且buffer缓存的数据需要通过 setVertexBuffer(_:offset:index:)方法传递到顶点着色器。

metal文件中,在顶点着色函数需要对顶点坐标进行归一化处理,因为顶点数据初始化时使用的是物体坐标。顶点坐标的归一化主要有以下步骤:

定义顶点着色器输出

初始化输出剪辑空间位置

获取当前顶点坐标的xy:主要是因为绘制的图形是2D的,其z都为0

将传入的视图大小转换为vector_float2二维向量类型

顶点坐标归一化:可以通过一行代码同时分隔两个通道x和y,并执行除法,然后将结果放入输出的x和y通道中,即从像素空间位置转换为裁剪空间位置

#include // 使用命名空间 Metal using namespace metal; // 导入Metal shader代码和执行Metal API命令的C代码之间共享的头 #import "YDWShaderTypes.h" // 顶点着色器输出和片段着色器输入 // 结构体 typedef struct { // 处理空间的顶点信息 float4 clipSpacePosition [[position]]; // 颜色 float4 color; } RasterizerData; // 顶点着色函数 vertex RasterizerData vertexShader(uint vertexID [[vertex_id]], constant CCVertex *vertices [[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 = 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; } //当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化. // 片元函数 // [[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]]) { // 返回输入的片元颜色 return in.color; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

主要需要加载metal文件来获取顶点数据

获取GPU设备device: 通过视图控制器中初始化render对象时传入的MTKView对象view,利用view来获取GPU的使用权限

_device = mtkView.device;

1

设置绘制纹理的像素格式

mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;

1

从项目中加载所以的.metal着色器文件

// 从项目中加载所以的.metal着色器文件 id defaultLibrary = [_device newDefaultLibrary]; // 从库中加载顶点函数 id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // 从库中加载片元函数 id fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

1

2

3

4

5

6

配置用于创建管道状态的管道描述符

// 配置用于创建管道状态的管道 MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; // 管道名称 pipelineStateDescriptor.label = @"Simple Pipeline"; // 可编程函数,用于处理渲染过程中的各个顶点 pipelineStateDescriptor.vertexFunction = vertexFunction; // 可编程函数,用于处理渲染过程总的各个片段/片元 pipelineStateDescriptor.fragmentFunction = fragmentFunction; // 设置管道中存储颜色数据的组件格式 pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;

1

2

3

4

5

6

7

8

9

10

同步创建并返回渲染管线对象

// 同步创建并返回渲染管线对象 NSError *error = NULL; _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];

1

2

3

4

获取顶点数据

// 获取顶点数据 NSData *vertexData = [YDWRenderer 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);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// 顶点数据 + (nonnull NSData *)generateVertexData { // 正方形 = 三角形+三角形 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; // 开辟空间 NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize]; // 当前四边形 CCVertex * currentQuad = vertexData.mutableBytes; // 获取顶点坐标(循环计算) // 行 for(NSUInteger row = 0; row < NUM_ROWS; row++) { // 列 for(NSUInteger column = 0; column < NUM_COLUMNS; column++) { // 左上角的位置 vector_float2 upperLeftPosition; // 计算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; // 将quadVertices数据复制到currentQuad memcpy(currentQuad, &quadVertices, sizeof(quadVertices)); // 遍历currentQuad中的数据 for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++) { //修改vertexInQuad中的position currentQuad[vertexInQuad].position += upperLeftPosition; } // 更新索引 currentQuad += 6; } } return vertexData; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

创建命令队列

// 创建命令队列 _commandQueue = [_device newCommandQueue];

1

Metal之MTLBuffer批量加载顶点数量较多的图形渲染

2

主要加载顶点缓冲区数据

为当前渲染的每个渲染传递创建一个新的命令缓冲区

// 为当前渲染的每个渲染传递创建一个新的命令缓冲区 id commandBuffer = [_commandQueue commandBuffer]; // 指定缓存区名称 commandBuffer.label = @"MyCommand";

1

2

3

4

创建渲染描述符

MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor; // 判断渲染目标是否为空 if(renderPassDescriptor != nil) { // 创建渲染命令编码器,这样才可以渲染到something id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; // 渲染器名称 renderEncoder.label = @"MyRenderEncoder"; }

1

2

3

4

5

6

7

8

9

设置我们绘制的可绘制区域

/*设置绘制的可绘制区域 * *typedef struct { double originX, originY, width, height, znear, zfar; } MTLViewport; */ [renderEncoder setViewport:(MTLViewport){ 0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];

1

2

3

4

5

6

7

8

9

设置渲染管道

// 设置渲染管道 [renderEncoder setRenderPipelineState:_pipelineState];

1

2

为了从OC代码找发送数据预加载的MTLBuffer 到Metal 顶点着色函数中

// 将_vertexBuffer 设置到顶点缓存区中 [renderEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:CCVertexInputIndexVertices]; // 将 _viewportSize 设置到顶点缓存区绑定点设置数据 [renderEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:CCVertexInputIndexViewportSize];

1

2

3

4

5

6

7

8

9

开始绘图

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_numVertices];

1

2

3

结束编码,表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离

[renderEncoder endEncoding];

1

一旦框架缓冲区完成,使用当前可绘制的进度表

[commandBuffer presentDrawable:view.currentDrawable];

1

完成渲染并将命令缓冲区推送到GPU

[commandBuffer commit];

1

Metal之MTLBuffer批量加载顶点数量较多的图形渲染

渲染

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Excel数据分析功能使用教程
下一篇:php面试知识点总结
相关文章