这里是一篇Metal新手教程,先定个小目标:把绘制一张图片到屏幕上。
Metal系列教程的代码地址;
OpenGL ES系列教程在这里;git
你的star和fork是个人源动力,你的意见能让我走得更远。
经过MetalKit,尽可能简单地实现把一张图片绘制到屏幕,核心的内容包括:设置渲染管道、设置顶点和纹理缓存、简单的shader理解。github
// 初始化 MTKView self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds]; self.mtkView.device = MTLCreateSystemDefaultDevice(); // 获取默认的device self.view = self.mtkView; self.mtkView.delegate = self; self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};
MTKView
是MetalKit提供的一个View,用来显示Metal的绘制;MTLDevice
表明GPU设备,提供建立缓存、纹理等的接口;缓存
// 设置渲染管道 -(void)setupPipeline { id<MTLLibrary> defaultLibrary = [self.mtkView.device newDefaultLibrary]; // .metal id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // 顶点shader,vertexShader是函数名 id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"]; // 片元shader,samplingShader是函数名 MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; pipelineStateDescriptor.vertexFunction = vertexFunction; pipelineStateDescriptor.fragmentFunction = fragmentFunction; pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat; self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:NULL]; // 建立图形渲染管道,耗性能操做不宜频繁调用 self.commandQueue = [self.mtkView.device newCommandQueue]; // CommandQueue是渲染指令队列,保证渲染指令有序地提交到GPU }
MTLRenderPipelineDescriptor
是渲染管道的描述符,能够设置顶点处理函数、片元处理函数、输出颜色格式等;[device newCommandQueue]
建立的是指令队列,用来存放渲染的指令;app
- (void)setupVertex { static const LYVertex quadVertices[] = { // 顶点坐标,分别是x、y、z、w; 纹理坐标,x、y; { { 0.5, -0.5, 0.0, 1.0 }, { 1.f, 1.f } }, { { -0.5, -0.5, 0.0, 1.0 }, { 0.f, 1.f } }, { { -0.5, 0.5, 0.0, 1.0 }, { 0.f, 0.f } }, { { 0.5, -0.5, 0.0, 1.0 }, { 1.f, 1.f } }, { { -0.5, 0.5, 0.0, 1.0 }, { 0.f, 0.f } }, { { 0.5, 0.5, 0.0, 1.0 }, { 1.f, 0.f } }, }; self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices length:sizeof(quadVertices) options:MTLResourceStorageModeShared]; // 建立顶点缓存 self.numVertices = sizeof(quadVertices) / sizeof(LYVertex); // 顶点个数 }
顶点数据里包括顶点坐标,metal的世界坐标系与OpenGL ES一致,范围是[-1, 1],故而点(0, 0)是在屏幕的正中间;
顶点数据里还包括纹理坐标,纹理坐标系的取值范围是[0, 1],原点是在左下角;[device newBufferWithBytes:quadVertices..]
建立的是顶点缓存,相似OpenGL ES的glGenBuffer建立的缓存。函数
- (void)setupTexture { UIImage *image = [UIImage imageNamed:@"abc"]; // 纹理描述符 MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init]; textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm; textureDescriptor.width = image.size.width; textureDescriptor.height = image.size.height; self.texture = [self.mtkView.device newTextureWithDescriptor:textureDescriptor]; // 建立纹理 MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}}; // 纹理上传的范围 Byte *imageBytes = [self loadImage:image]; if (imageBytes) { // UIImage的数据须要转成二进制才能上传,且不用jpg、png的NSData [self.texture replaceRegion:region mipmapLevel:0 withBytes:imageBytes bytesPerRow:4 * image.size.width]; free(imageBytes); // 须要释放资源 imageBytes = NULL; } }
MTLTextureDescriptor
是纹理数据的描述符,能够设置像素颜色格式、图像宽高等,用于建立纹理;
纹理建立完毕后,须要用 -replaceRegion: mipmapLevel:withBytes:bytesPerRow:
接口上传纹理数据;MTLRegion
相似UIKit的frame
,用于代表纹理数据的存放区域;性能
- (void)drawInMTKView:(MTKView *)view { // 每次渲染都要单首创建一个CommandBuffer id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer]; MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor; // MTLRenderPassDescriptor描述一系列attachments的值,相似GL的FrameBuffer;同时也用来建立MTLRenderCommandEncoder if(renderPassDescriptor != nil) { renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0f); // 设置默认颜色 id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; //编码绘制指令的Encoder [renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }]; // 设置显示区域 [renderEncoder setRenderPipelineState:self.pipelineState]; // 设置渲染管道,以保证顶点和片元两个shader会被调用 [renderEncoder setVertexBuffer:self.vertices offset:0 atIndex:0]; // 设置顶点缓存 [renderEncoder setFragmentTexture:self.texture atIndex:0]; // 设置纹理 [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:self.numVertices]; // 绘制 [renderEncoder endEncoding]; // 结束 [commandBuffer presentDrawable:view.currentDrawable]; // 显示 } [commandBuffer commit]; // 提交; }
drawInMTKView:
方法是MetalKit每帧的渲染回调,能够在内部作渲染的处理;
绘制的第一步是从commandQueue里面建立commandBuffer,commandQueue是整个app绘制的队列,而commandBuffer存放每次渲染的指令,commandQueue内部存在着多个commandBuffer。
整个绘制的过程与OpenGL ES一致,先设置窗口大小,而后设置顶点数据和纹理,最后绘制两个三角形。
CommandQueue、CommandBuffer和CommandEncoder的关系以下:ui
typedef struct { float4 clipSpacePosition [[position]]; // position的修饰符表示这个是顶点 float2 textureCoordinate; // 纹理坐标,会作插值处理 } RasterizerData; vertex RasterizerData // 返回给片元着色器的结构体 vertexShader(uint vertexID [[ vertex_id ]], // vertex_id是顶点shader每次处理的index,用于定位当前的顶点 constant LYVertex *vertexArray [[ buffer(0) ]]) { // buffer代表是缓存数据,0是索引 RasterizerData out; out.clipSpacePosition = vertexArray[vertexID].position; out.textureCoordinate = vertexArray[vertexID].textureCoordinate; return out; } fragment float4 samplingShader(RasterizerData input [[stage_in]], // stage_in表示这个数据来自光栅化。(光栅化是顶点处理以后的步骤,业务层没法修改) texture2d<half> colorTexture [[ texture(0) ]]) // texture代表是纹理数据,0是索引 { constexpr sampler textureSampler (mag_filter::linear, min_filter::linear); // sampler是采样器 half4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate); // 获得纹理对应位置的颜色 return float4(colorSample); }
Shader如上。与OpenGL ES的shader相比,最明显是输入的参数能够用结构体,返回的参数也能够用结构体;LYVertex
是shader和Objective-C公用的结构体,RasterizerData
是顶点Shader返回再传给片元Shader的结构体;
Shader的语法与C++相似,参数名前面的是类型,后面的[[ ]]
是描述符。编码
Metal和OpenGL同样,须要有必定的图形学基础,才能理解具体的含义。
本文为了下降上手的门槛,简化掉一些逻辑,增长不少注释,同时保留最核心的几个步骤以便理解。spa
这里能够下载demo代码。3d