這裏是一篇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