Metal 二、案例-背景渲染+三角形绘制
通过 Metal,实现 渲染颜色到屏幕 和 三角形的绘制 两个简单案例。
一、Metal 使用
类似于 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 }