Metal学习笔记01 渲染一个三角形

Metal是苹果于2014年WWDC中发布的图像处理、通用计算的框架,而在今年的ios12中,OpenGL ES的API也被标记为废弃了,所以作为一个iOS开发者,有必要了解下这个框架。

这次还是和学习OpenGL的时候一样,先使用Metal画一个三角形。

在iOS下使用Metal

创建用于Metal显示内容的组件

在Metal中,可以选择使用CAMetalLayer或者MTKView,本文选择使用CAMetalLayer,需要注意的是,在项目的运行设备是模拟器的时候,会提示找不到CAMetalLayer这个类,这是因为Metal取消了模拟器的支持,只能使用64位处理器的真机进行开发和调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface ViewController ()
@property(nonatomic,weak)CAMetalLayer * mLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
   
    CAMetalLayer*layer = [[CAMetalLayer alloc]init];
	//设置layer的像素格式,这里设置为BGRA32
    layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
	//framebufferOnly根据描述是设置纹理是否只用作显示还是需要做一些采样和计算,一般情况下设置为YES,可以提高性能表现
    layer.framebufferOnly = YES;
    layer.frame = self.view.bounds;
    layer.drawableSize = self.view.bounds.size;
}

创建MTLDevice

1
2
3
4
5
6
	...
	@property(nonatomic,strong)id<MTLDevice>device;

	...
	self.device = MTLCreateSystemDefaultDevice();
    layer.device = self.device;

设备是一个遵循了MTLDevice协议的类,是对GPU的抽象,许多Metal对象都需要通过设备对象来获取。这里创建了默认的设备,并且设置为CAMetalLayer的设备。

创建shader

和OpenGL一样,Metal渲染也需要顶点着色器程序和片段着色器程序,使用Metal着色语言编写。在Metal开发中,可以通过xcode创建一个.metal文件,用于编写相应的着色器程序。

1
2
3
4
5
6
7
8
9
10
#include <metal_stdlib>
using namespace metal;

vertex float4 basic_vertex (
    constant packed_float3*vertex_array[[buffer(0)]],
    unsigned int vid[[vertex_id]]){
    
    
    return float4(vertex_array[vid], 1.0);
}

这里定义了一个顶点着色器程序,vertex关键字用于标记这是一个顶点着色器程序。float4表示这个函数的返回值是一个四维向量,在这里四个值分别表示为x,y,z,w,其中w用于做一些旋转平移缩放时方便计算,一般为1。之后是两个函数的参数,constant修饰第一个参数为常量,是一个三维向量的数组,也就是传入的顶点坐标。中间的[[buffer(0)]]表明是缓存数据,0是索引,索引值用于区分一些时候传入的数据不全是顶点数据时,比如传入的数据包含顶点坐标,颜色和纹理坐标,用索引来获取到正确的数据。第二个参数用与获取当前处理的顶点。函数体中就是返回了一个表示坐标的四维向量。

1
2
3
fragment float4 basic_fragment() {
    return float4(1.0,0,0,1);
}

这里定义了一个片段着色器程序,返回一个四维向量表示颜色的rgba,这里固定写为红色。

创建MTLLibrary

1
id<MTLLibrary>library = [self.device newDefaultLibrary];

创建MTLFunction

1
2
	id<MTLFunction>vertexFunc = [library newFunctionWithName:@"basic_vertex"];
    id<MTLFunction>fragmentFunc = [library newFunctionWithName:@"basic_fragment"];

创建一个的管道描述器

1
2
3
4
	MTLRenderPipelineDescriptor*descriptor = [[MTLRenderPipelineDescriptor alloc]init];
    descriptor.vertexFunction = vertexFunc;
    descriptor.fragmentFunction = fragmentFunc;
    descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;

创建一个管道

1
	id<MTLRenderPipelineState> pipelineState = [self.device newRenderPipelineStateWithDescriptor:descriptor error:nil];

这样就构建了一个完整的数据处理的管道,接下来需要将数据传入。

创建顶点坐标缓冲

1
2
3
4
5
6
float vertexArray[] = {
        1.0f,  1.0f, 0.0f,
        -1.0f, -1.0f, 0.0f,
        1.0f, -1.0f, 0.0f
    };
	id<MTLBuffer>vertexBuffer = [self.device newBufferWithBytes:vertexArray length:sizeof(vertexArray) options:MTLResourceCPUCacheModeDefaultCache];

这样就创建了一个顶点缓冲,其中MTLResourceCPUCacheModeDefaultCache表示它可以被GPU、CPU读写,同时也是操作也是有序的。

创建命令队列和命令缓冲

1
2
	id<MTLCommandQueue>commandQueue = [self.device newCommandQueue];
	id<MTLCommandBuffer>commandBuffer = [commandQueue commandBuffer];

创建一个渲染路径描述器

1
2
3
4
5
6
7
	id<CAMetalDrawable>drawable = [self.mLayer nextDrawable];
    
    MTLRenderPassDescriptor*renderPassDes = [[MTLRenderPassDescriptor alloc]init];
    
    renderPassDes.colorAttachments[0].texture = [drawable texture];
    renderPassDes.colorAttachments[0].loadAction = MTLLoadActionClear;
    renderPassDes.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0);

先取得layer的drawable,它是一个用于显示的资源,可以被Metal渲染或改写。然后创建了渲染路径描述,描述了一个清屏为白色然后再渲染的操作。

创建一个命令编码器

1
2
3
4
5
	id<MTLRenderCommandEncoder>renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDes];
    [renderEncoder setRenderPipelineState:pipelineState];
    [renderEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0];
    
    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:3];

创建一个编码器,并指定之前创建的pipeline和顶点,drawPrimitives:vertexStart:vertexCount,类似glDrawArray函数,不过它应该不是直接绘制,而是编码出一个绘制多边形的命令。

提交命令

1
2
	[commandBuffer presentDrawable:drawable];
    [commandBuffer commit];

这样就完成了一个三角形的绘制

<img src=”https://i.loli.net/2019/06/17/5d075fbe6222099023.jpg” alt=”Metal01.png” title=”Metal01.png” width= 36%/>

绘制矩形

1
2
3
4
5
6
7
8
9
10
	float vertexArray[] = {
        -1.0f,1.0f,0.0f,
        1.0f,  1.0f, 0.0f,
        -1.0f, -1.0f, 0.0f,
        1.0f, -1.0f, 0.0f
    };
	
	...
	
	[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];

和OpenGL一样,也可以使用4个顶点来绘制一个矩形,修改drawPrimitives:的参数为MTLPrimitiveTypeTriangleStrip,然后顶点顺序为z字形即可。

<img src=”https://i.loli.net/2019/06/17/5d0760983cf0b76962.jpg” alt=”Metal01.png” title=”Metal01.png” width= 36%/>

总结

Metal的主要逻辑和OpenGL类似,都是将顶点数据传给顶点着色器,计算出坐标后在传给片段着色器,由片段着色器计算出像素的颜色。但具体代码的实现上,Metal比OpenGL更符合iOS开发者的习惯,不过Metal感觉渲染的步骤较多,需要好好理解下。

demo地址 参考:
Metal