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

前言

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

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

正文

核心思路

通過MetalKit,盡量簡單地實現把一張圖片繪制到屏幕,核心的內容包括:設置渲染管道設置頂點和紋理緩存簡單的shader理解

效果展示

具體步驟

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的關系

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代碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容