Metal 二、案例-背景渲染+三角形绘制

通过 Metal,实现 渲染颜色到屏幕 和 三角形的绘制 两个简单案例。

一、Metal 使用

Metal Programing Guide 官方文档

类似于 OpenGL ES 的 GLKit, 苹果为开发者提供了 MetalKit 。并对开发者做了以下建议(当然这只是建议,不是必须遵循的规则):

1、Separate Your Rendering Loop  --  单独处理你的渲染循环

即:当我们使用 Metal 开发程序时,将 渲染循环单独做一个类来处理。使用单独的类,我们可以更好的管理 Metal 及 Metal 的视图委托。

Metal 的相关处理 和 viewController 分开。

// 示例:
_render = [[YourRenderClass alloc] initWithMetalKitView:_view];
_view.delegate = _render;

2、Respond to View Event  --  视图响应事件

// 在 MTKViewDelegate 协议中有 2 个方法
//
// 每当窗口大小变化或者重新布局(设备方向更改)时,视图就会调用此方法.
 - (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size;
    
// 每当视图需要渲染时调用 
// 视图可以根据视图属性上设置 View.preferredFramesPerSecond 帧速率(指定时间来调用 drawInMTKView 方法) --> 默认 60
- (void)drawInMTKView:(nonnull MTKView *)view;

3、Metal Command Object  --  Metal 命令对象

Metal 命令对象之间的关系

1)命令缓存区(command buffer) 是从 命令队列(command queue) 创建的

2)命令编码器(command encoders) 将 命令 编码到 命令缓存区

3)提交 命令缓冲区 并将其发送到 GPU

4)GPU 执行命令 并将结果 呈现为可绘

// 一个 MTLDevice 对象表示 GPU,
// 方法 MTLCreateSystemDefaultDevice(): 获取 默认的 GPU 单个对象.
_view.device = MTLCreateSystemDefaultDevice();

// 应用程序 需要与 GPU 交互的第一个对象是 MTLCommandQueue 对象
_commandQueue = [_device newCommandQueue];

// Create a new command buffer for each render pass to the current drawable
// 为当前渲染的每个渲染传递 创建一个新的命令缓冲区
// 使用 MTLCommandQueue 创建对象并且加入到 MTCommandBuffer 对象中去。确保它们能够按照正确顺序发送到 GPU,对于每一帧,一个新的 MTLCommandBuffer 对象 创建且填满了由 GPU 执行的命令
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

二、案例

此案例实现比较简单,主要是 Metal 的API 是使用,过程均在代码注释中。

渲染管线的 3 大阶段:

1、背景色渲染

Render class:

  1 // .h
  2 #import <Foundation/Foundation.h>
  3 
  4 @import MetalKit;
  5 
  6 NS_ASSUME_NONNULL_BEGIN
  7 
  8 @interface MyMetalRender : NSObject <MTKViewDelegate>
  9 
 10 - (id)initWithMTKView:(MTKView *)mtkView;
 11 
 12 @end
 13 
 14 NS_ASSUME_NONNULL_END
 15 
 16 
 17 // .m
 18 #import "MyMetalRender.h"
 19 
 20 // 定义颜色结构体
 21 typedef struct {
 22     float red, green, blue, alpha;
 23 } Color;
 24 
 25 @implementation MyMetalRender
 26 {
 27     id<MTLDevice> _device;// GPU
 28     id<MTLCommandQueue> _commandQueue;
 29 }
 30 
 31 - (id)initWithMTKView:(MTKView *)mtkView {
 32     
 33     if (self = [super init]) {
 34         _device = mtkView.device;
 35         // 所有应用程序需要与GPU交互的第一个对象是一个对象。MTLCommandQueue.
 36         // 你使用MTLCommandQueue 去创建对象,并且加入MTLCommandBuffer 对象中.确保它们能够按照正确顺序发送到GPU.对于每一帧,一个新的MTLCommandBuffer 对象创建并且填满了由GPU执行的命令.
 37         _commandQueue = [_device newCommandQueue];
 38     }
 39     return self;
 40 }
 41 
 42 // 设置颜色
 43 - (Color)configColor {
 44     
 45     // 1. 增加颜色/减小颜色的 标记
 46     static BOOL       growing = YES;
 47     // 2.颜色通道值(0~3)
 48     static NSUInteger primaryChannel = 0;
 49     // 3.颜色通道数组 colorChannels(颜色值) --> 初始值:红色 透明度1
 50     static float      colorChannels[] = {1.0, 0.0, 0.0, 1.0};
 51     // 4.颜色调整步长 -- 每次变化
 52     const float DynamicColorRate = 0.015;
 53     
 54     // 5.判断
 55     if(growing) {
 56         // 动态信道索引 (1,2,3,0)通道间切换
 57         NSUInteger dynamicChannelIndex = (primaryChannel+1)%3;
 58         
 59         // 修改对应通道的颜色值 调整0.015
 60         colorChannels[dynamicChannelIndex] += DynamicColorRate;
 61         
 62         // 当颜色通道对应的颜色值 = 1.0
 63         if(colorChannels[dynamicChannelIndex] >= 1.0) {
 64             // 设置为NO
 65             growing = NO;
 66             
 67             // 将颜色通道修改为动态颜色通道
 68             primaryChannel = dynamicChannelIndex;
 69         }
 70     }
 71     else {
 72         // 获取动态颜色通道
 73         NSUInteger dynamicChannelIndex = (primaryChannel+2)%3;
 74         
 75         // 将当前颜色的值 减去0.015
 76         colorChannels[dynamicChannelIndex] -= DynamicColorRate;
 77         
 78         // 当颜色值小于等于0.0
 79         if(colorChannels[dynamicChannelIndex] <= 0.0) {
 80             // 又调整为颜色增加
 81             growing = YES;
 82         }
 83     }
 84     
 85     // 创建颜色
 86     Color color;
 87     // 修改颜色的RGBA的值
 88     color.red   = colorChannels[0];
 89     color.green = colorChannels[1];
 90     color.blue  = colorChannels[2];
 91     color.alpha = colorChannels[3];
 92     
 93     // 返回颜色
 94     return color;
 95 }
 96 
 97 #pragma mark - MTKView delegate -
 98 // 每当视图渲染时 调用
 99 - (void)drawInMTKView:(nonnull MTKView *)view {
100     
101     // 拿到颜色
102     Color color = [self configColor];
103     
104     // 1. 设置颜色 --> 类似OpenGL ES 的 clearColor
105     view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha);
106     // 2. Create a new command buffer for each render pass to the current drawable
107     // 使用MTLCommandQueue 创建对象并且加入到MTCommandBuffer对象中去.
108     // 为当前渲染的每个渲染传递创建一个新的命令缓冲区
109     id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
110     commandBuffer.label = @"MyCommandBuffer";
111     
112     // 3.从视图中,获得 渲染描述符
113     MTLRenderPassDescriptor *renderDescriptor = view.currentRenderPassDescriptor;
114     if (renderDescriptor) {
115         
116         // 4.通过渲染描述符 renderPassDescriptor  创建 MTLRenderCommandEncoder 对象
117         id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderDescriptor];
118         renderEncoder.label = @"MyRenderEncoder";
119         
120         // 5.我们可以使用 MTLRenderCommandEncoder 来绘制对象,此 demo 仅仅创建编码器就可以了,我们并没有让 Metal 去执行我们绘制的东西,这个时候表示我们的任务已经完成.
121         // 即: 可结束 MTLRenderCommandEncoder 工作
122         [renderEncoder endEncoding];
123         
124         /*
125          当编码器结束之后,命令缓存区就会接受到 2 个命令.
126          1) present
127          2) commit
128          因为 GPU 是不会直接绘制到屏幕上,因此若不给出指令,则不会有任何内容渲染到屏幕上.
129         */
130         // 6.添加最后一个命令 来显示清楚的可绘制的屏幕
131         [commandBuffer presentDrawable:view.currentDrawable];
132         
133     }
134     // 7. 完成渲染并将命令缓冲区提交给 GPU
135     [commandBuffer commit];
136 }
137 
138 // 当 MTKView 视图发生大小改变时调用
139 - (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
140     
141 }
142 
143 @end

viewController.m

 1 #import "ViewController.h"
 2 #import "MyMetalRender.h"
 3 
 4 @interface ViewController () {
 5     
 6     MTKView *_view;
 7     MyMetalRender *_render;
 8 }
 9 
10 @end
11 
12 @implementation ViewController
13 
14 - (void)viewDidLoad {
15     [super viewDidLoad];
16     // Do any additional setup after loading the view.
17     
18     _view = (MTKView *)self.view;
19     // 一个 MTLDevice 对象 表示 GPU
20     _view.device = MTLCreateSystemDefaultDevice();
21     // 判断是否设置成功
22     if (!_view.device) {
23         NSLog(@"Metal is not supported on this device");
24         return;
25     }
26     
27     // render
28     _render = [[MyMetalRender alloc] initWithMTKView:_view];
29     
30     _view.delegate = _render;
31     // 设置帧速率 --> 指定时间来调用 drawInMTKView 方法--视图需要渲染时调用 默认60
32     _view.preferredFramesPerSecond = 60;
33 }
34 
35 @end

2、绘制三角形

我们只给了三角形的三个顶点的颜色, Metal 会自动计算过度颜色差值,和 OpenGL 一样。

主要代码:

 1 // 1. 顶点/颜色 数据
 2     static const MyVertex triangleVertices[] =
 3     {
 4         // 顶点 xyzw,                 颜色值RGBA
 5         { {  0.5, -0.25, 0.0, 1.0 }, { 1, 0, 0, 1 } },
 6         { { -0.5, -0.25, 0.0, 1.0 }, { 0, 1, 0, 1 } },
 7         { { -0.0f, 0.25, 0.0, 1.0 }, { 0, 0, 1, 1 } },
 8     };
 9     // 2.为当前渲染的每个渲染 传递 创建 一个新的命令缓冲区
10     id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
11     commandBuffer.label = @"MyCommandBuffer";
12     
13     // 3.MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。
14     MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
15     if (renderPassDescriptor) {
16         // 4.创建 渲染命令编码
17         id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
18         renderEncoder.label = @"MyRenderEncoder";
19         // 5.设置 可绘制区域 Viewport
20         /*
21         typedef struct {
22             double originX, originY, width, height, znear, zfar;
23         } MTLViewport;
24          */
25         // 视口指定 Metal 渲染内容的 drawable 区域。 视口是具有x和y偏移,宽度和高度以及*和远*面的 3D 区域
26         // 为管道分配自定义视口,需要通过调用 setViewport:方法将 MTLViewport 结构 编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的 drawable 相同。
27         MTLViewport viewport = {
28             0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0
29         };
30         [renderEncoder setViewport:viewport];
31 //        [renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];
32         
33         // 6.设置当前渲染管道状态对象
34         [renderEncoder setRenderPipelineState:_pipelineState];
35 
36         // 7.数据传递给着色函数 -- 从应用程序(OC 代码)中发送数据给 Metal 顶点着色器 函数
37         // 顶点 + 颜色
38         //   1) 指向要传递给着色器的内存的指针
39         //   2) 我们想要传递的数据的内存大小
40         //   3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
41         [renderEncoder setVertexBytes:triangleVertices
42                                length:sizeof(triangleVertices)
43                               atIndex:MyVertexInputIndexVertices];
44 
45         // viewPortSize 数据
46         //  1) 发送到顶点着色函数中,视图大小
47         //  2) 视图大小内存空间大小
48         //  3) 对应的索引
49         [renderEncoder setVertexBytes:triangleVertices length:sizeof(_viewportSize) atIndex:MyVertexInputIndexViewportSize];
50         
51         
52         // 8.画出 三角形的 3 个顶点
53         // @method drawPrimitives:vertexStart:vertexCount:
54         // @brief 在不使用索引列表的情况下,绘制图元
55         // @param 绘制图形组装的基元类型
56         // @param 从哪个位置数据开始绘制,一般为0
57         // @param 每个图元的顶点个数,绘制的图型顶点数量
58         /*
59          MTLPrimitiveTypePoint = 0, 点
60          MTLPrimitiveTypeLine = 1, 线段
61          MTLPrimitiveTypeLineStrip = 2, 线环
62          MTLPrimitiveTypeTriangle = 3,  三角形
63          MTLPrimitiveTypeTriangleStrip = 4, 三角型扇
64          */
65         [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
66         
67         // 9.编码完成 - 表示该编码器生成的命令都已完成,并且从 MTLCommandBuffer 中分离
68         [renderEncoder endEncoding];
69         
70         // 10.一旦框架缓冲区完成,使用当前可绘制的进度表
71         [commandBuffer presentDrawable:view.currentDrawable];
72     }
73     // 11. 最后,完成渲染并将命令缓冲区推送到 GPU
74     [commandBuffer commit];
75     

Metal 代码:

 1 // 顶点着色器输出和片段着色器输入 -- 经过了光栅化的数据
 2 // 结构体
 3 typedef struct {
 4     // 处理空间的顶点信息
 5     float4 clipSpacePosition[[position]];
 6     // 颜色
 7     float4 color;
 8 } RasterizerData;
 9 
10 
11 // 顶点着色函数
12 /*
13 处理顶点数据:
14    1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
15    2) 将顶点颜色值传递给返回值
16 */
17 vertex RasterizerData vertexShader (uint vertexID [[vertex_id]],
18                                     constant MyVertex *vertices [[buffer(MyVertexInputIndexVertices)]],
19                                     constant vector_uint2 *viewportSizePointer [[buffer(MyVertexInputIndexViewportSize)]]) {
20     
21     // 定义输出
22     RasterizerData out;
23     out.clipSpacePosition = vertices[vertexID].position;
24 
25     out.color = vertices[vertexID].color;
26     
27     // 完成! 将结构体传递到管道中下一个阶段:
28     return out;
29 }
30 
31 
32 // 当顶点函数执行3次,三角形的每个顶点都执行一次后,则执行管道中的下一个阶段 --> 栅格化/光栅化 之后 --> 片元函数
33 
34 
35 // 片元函数
36 fragment float4 fragmentShader (RasterizerData in [[stage_in]]) {
37     
38     // 返回输入的片元颜色
39     return in.color;
40 }

 

Demo 地址

 

 

 

 

 

 

 

 

 

 

 

posted @ 2020-08-27 23:55  张张_z  阅读(781)  评论(0编辑  收藏  举报