Metal入門教程(一)圖片繪製

落影發表於2018-07-07

前言

這裡是一篇Metal新手教程,先定個小目標:把繪製一張圖片到螢幕上。
Metal系列教程的程式碼地址
OpenGL ES系列教程在這裡

你的star和fork是我的源動力,你的意見能讓我走得更遠

正文

核心思路

通過MetalKit,儘量簡單地實現把一張圖片繪製到螢幕,核心的內容包括:設定渲染管道設定頂點和紋理快取簡單的shader理解

效果展示

Metal入門教程(一)圖片繪製

具體步驟

1、新建MTKView
    // 初始化 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裝置,提供建立快取、紋理等的介面;

2、設定渲染管道
// 設定渲染管道
-(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]建立的是指令佇列,用來存放渲染的指令;

3、設定頂點資料
- (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建立的快取。

4、設定紋理資料
- (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,用於表明紋理資料的存放區域;

5、具體渲染過程
- (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的關係如下:

CommandQueue、CommandBuffer和CommandEncoder的關係
CommandQueue、CommandBuffer和CommandEncoder的關係

6、Shader處理
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一樣,需要有一定的圖形學基礎,才能理解具體的含義。
本文為了降低上手的門檻,簡化掉一些邏輯,增加很多註釋,同時保留最核心的幾個步驟以便理解。

這裡可以下載demo程式碼。


相關文章