Metal學習:用Metal畫一個三角形

Metal基本概念

Metal是Apple提出的新一代的Graphics API架構,用來代替OpenGL。從2014年Metal提出開始,到2019年Apple正式廢棄OpenGL/OpenGL ES的支持,Metal發展是非常快的,基本上以後在ios/Mac的開發中,metal是底層圖形開發的唯一的選擇。
基於以下的原因,OpenGL需要被Metal替代:

  1. OpenGL是25年前的標準,而現代的GPU設計和25年前已經非常的不同了
  2. OpenGL沒有考慮多線程支持,它的執行像一個巨大的狀態機,每次執行都必須查詢這個狀態機的所有狀態,使得它的效率不高
  3. OpenGL沒有異步支持,CPU和GPU之間交互是非常耗時的操作

Metal具有的特性:

  1. 低的CPU負載,Metal把運算放到GPU中來執行,降低CPU的負載
  2. 多線程支持
  3. 分支預測技術支持,避免像OpenGL那樣,分支對性能影響非常大
  4. 資源和同步控制,GPU和CPU可以互相交互

在學習Metal之前,有些基本的概念需要先了解一下:

  • 渲染管道
    渲染管道是指一個一堆原始數據,經過各種變化處理,最終在屏幕上顯示的過程,這個過程是在GPU中執行的。比如OpenGL的執行過程如下所示
    在這裏插入圖片描述
  • Vertex Shader
    頂點着色器是頂點數據的處理階段,接受從CPU來的頂點數據,有可能還附帶一些頂點的屬性數據,輸出則是經過各種變換以後的頂點數據。可以簡單理解爲輸入是真實世界的3D點座標,然後在vertex shader中經過在各種空間座標系下的轉換(model,view,perspective),最終變化爲屏幕上的2D座標,可以輸出渲染管道的後續部分處理。
  • Primitive Assembly
    這是圖元裝配階段,從頂點處理得到的是一個個單獨的點,這時候我們需要對這些點進行重新的組合,使得他們成爲一個個的圖元(點,線,三角形),這樣後面才能以這個形狀來進行處理。
  • Geometry Shader
    幾何着色器可以對圖元裝配產生的圖元,通過產生新的頂點來構造出新的圖元,比如上圖中是產生了另外一個三角形。(一般這個階段很少需要變動)
  • Rasterization
    光柵化階段是將圖元真正映射到屏幕上,對應屏幕上的像素,生成給片段着色器需要的片段。一般是一個像素組成一個片段,但是也有可能幾個像素組成一個片段。視圖以外的所有像素會進行裁剪,以提高渲染效率
  • Fragment Shader
    片段着色器是決定每個像素的顏色,也是所有的後處理,光照,陰影等效果產生的地方。片段着色器產生的結果一般還要經過深度測試(決定這個像素是否被其他物體遮擋)和混合測試(和預先設置的模板進行操作),才最終顯示到屏幕上來。
  1. Shaders
    可以看到圖形渲染管道非常複雜,但是一般我們只要關心頂點着色器和片段着色器就可以了,這也是可編程管道要求我們必須實現的。也就是我們可以自己定義圖形渲染管道中的頂點和片段兩個處理階段,通過GPU的程序來實現,我們稱之爲着色器(shaders)。可以簡單理解着色器就是可以在GPU上執行的小程序,遵循專門的着色器語言規範,在OpenGL中是GLSL(OpenGL Shading language),在Metal中是“Metal shading language”。
  2. Uniform
    Uniform表示CPU向GPU發送數據的一種方式,它是全局性的,也就是在一次渲染過程中,當你將Uniform數據傳送到GPU,它會被所有的着色器程序在任意階段訪問。Uniform設置後,就會一直保存他的數據,直到它被重置或者更新。

Metal對象

Device和CommandQueue

不同於OpenGL,在Metal中基本上所有組件都是基於對象的。比如GPU抽象爲一個MTLDevice的對象,從MTLDevice可以創建command queue,texture,buffer和pipeline等渲染對象。

id<MTLDevice> device = MTLCreateSystemDefaultDevice(); 

從Device可以創建Queue,它是用來執行命令的(command buffer),一般在初始化的時候創建一個Queue即可

id<MTLCommandQueue> commnadQueue = [device newCommandQueue];

Textures,Buffers, Pipelines

Textures, Buffers和Pipelines我們統一把他們稱之爲渲染對象,他們都是從device對象中創建。
創建TextureObject首先需要有Texture Descriptor來描述這個對象,具體包括這個Texture的type,size,format和存儲模式等信息。其中存儲模式可以決定這個texture是否和CPU進行共享。

MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor new];
textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
textureDescriptor.width = 512;
textureDescriptor.height = 512;
textureDescriptor.storageMode = MTLStorageModeShared;

id<MTLTexture> texture = [device newTextureWithDescriptor:textureDescriptor];

可以上傳數據到Texture,如下所示:

NSUInteger bytesPerRow = 4 * image.width;
MTLRegion region = {
    {0, 0, 0},
    {512, 512, 1}
};
[texture replaceRegion:region mipmapLevel:0 withBytes:imageData bytesPerRow:bytesPerRow];

Metal中所有的數據都是buffer,例如頂點,索引和uniform。創建buffer和更新數據如下所示

id<MTLBuffer> buffer = [device newBufferWithLength:bufferDataByteSize options:MTLResourceStorageModeShared];
struct MyUniforms *uniforms = (struct MyUniforms*)buffer.content;
uniforms->modelViewProjection = modelViewProjection;
uniforms->sunPosition = sunPosition;

需要注意的是buffer是採用自動對齊的機制,例如雖然一個float是佔用4個字節,但是float3和float4一樣,都是佔用16字節。
Pipeline對象由device創建,代表的是渲染的具體過程。和Texture一樣,他也需要一個MTLRenderPipelineDescriptor的對象來描述。主要是包括vertex和fragment的shader程序,以及渲染的pixelFormat。

id <MTLLibrary> defaultLibrary = [device newDefaultLibrary];
id <MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id <MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

MTLRenderPipelineDescriptor *pipelineStateDescriptor = [MTLRenderPipelineDescriptor new];
pipelineStateDescriptor.vertexFunction = vertexFunction;
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
pipelineStateDescriptor.colorAttachement[0].pixelFormt = MTLPixelFormatRGBA8Unorm;

id<MTLRenderPipelineState> pipelineState;
pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:nil];

渲染過程

上面提到的對象都是全局的對象,是運行過程中長期存在的,需要在初始化階段就分配好。當進入渲染階段,
我們通過Command Buffer和Command Encoder兩個對象來進行提交渲染任務。
Command Buffer通過Command Queue產生,它主要控制任務的提交, 一次幀渲染可以有多個command buffer,同時這些command buffer可以在不同的線程中產生,每個command buffer可以註冊一個任務完成的回調。

id <MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
//Encode Commands

[commandBuffer addCompletedHander:^(id<MTLCommandBuffer> commandBuffer){
//GPU is done with my buffer
}]
[commandBuffer commit]

Command Encoder負責具體的渲染過程,它是從Command Buffer分配出來的,需要通過一個MTLRenderPassDescritor來描述。

MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new];
desc.colorAttachment[0].texture = myColorTexture;
desc.colorAttachment[0].loadAction = MTLLoadActionClear;
desc.colorAttachment[0].clearColor = MTLClearColorMake(1.0f, 1.0f, 1.0f, 1.0f);
desc.colorAttachment[0].storeAction = MTLStoreActionStore;
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:desc];

[encoder setPipelineState:myPipeline];
[encoder setVertexBuffer:myVertexData offset:0 atIndex:0];
[encoder setVertexBuffer:myUniforms offset:0 atIndex:1];
[encoder setFragmentBuffer:myUniforms offset:0 atIndex:1];
[encoder setFragmentTexture:myTexture atIndex:0];

[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:numVertices];

[encoder endEncoding];

用Metal畫一個三角形

創建iOS項目

首選打開XCode創建一個Single View Application,並且使用Object-C作爲編程語言。
在ViewController.h中包含以下頭文件

#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>

CAMetalLayer提供一個可以給Metal來進行渲染的紋理,我們後面需要使用這個類型的layer來進行渲染

初始化

對於需要長期存在的對象,我們在interface中聲明爲全局的類對象,在ViewController.m中,聲明如下的對象

id<MTLDevice> mtlDevice;
id<MTLCommandQueue> mtlCommandQueue;
id<MTLRenderPipelineState> renderPipelineState;
id<MTLBuffer> vertexBuffer;

CAMetalLayer *metalLayer;
id<CAMetalDrawable> frameDrawable;

CADisplayLink *displayLink;

然後在viewDidLoad方法中,初始化這些對象

mtlDevice = MTLCreateSystemDefaultDevice();
mtlCommandQueue = [mtlDevice newCommandQueue];

id<MTLLibrary> mtlLibrary = [mtlDevice newDefaultLibrary];
id<MTLFunction> vertexProgram = [mtlLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentProgram = [mtlLibrary newFunctionWithName:@"fragmentShader"];
MTLRenderPipelineDescriptor *mtlRenderPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
[mtlRenderPipelineDescriptor setVertexFunction:vertexProgram];
[mtlRenderPipelineDescriptor setFragmentFunction:fragmentProgram];
mtlRenderPipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;

renderPipelineState = [mtlDevice newRenderPipelineStateWithDescriptor:mtlRenderPipelineDescriptor error:nil];
static float vertices[] = {
    0.0, 0.5, 0.0, 1.0,
    0.5, -0.5, 0.0, 1.0,
    -0.5, -0.5, 0.0, 1.0
};

vertexBuffer = [mtlDevice newBufferWithBytes:vertices length:sizeof(vertices) options:MTLResourceOptionCPUCacheModeDefault];

metalLayer = [CAMetalLayer layer];
metalLayer.device = mtlDevice;
metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
metalLayer.frame = self.view.bounds;
[self.view.layer addSublayer:metalLayer];

displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderScene)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

其中displayLink是一個計時器,它會週期性的觸發函數renderScene,我們在這個函數中進行每一幀的渲染,也就是畫一個三角形。

Render Pass

在函數renderScene中,做如下的操作

frameDrawable = [metalLayer nextDrawable];
id<MTLCommandBuffer> mtlCommandBuffer = [mtlCommandQueue commandBuffer];

MTLRenderPassDescriptor *mtlRenderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
mtlRenderPassDescriptor.colorAttachments[0].texture = frameDrawable.texture;
mtlRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
mtlRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0);
mtlRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;

id<MTLRenderCommandEncoder> renderEncoder = [mtlCommandBuffer renderCommandEncoderWithDescriptor:mtlRenderPassDescriptor];
[renderEncoder setRenderPipelineState:renderPipelineState];
[renderEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
[renderEncoder endEncoding];

[mtlCommandBuffer presentDrawable:frameDrawable];
[mtlCommandBuffer commit];

Metal Library

新建一個metal文件,File->New->File,選擇類型爲Metal File。添加如下代碼

vertex float4 vertexShader(device float4* vertices [[buffer(0)]],
                            uint vid [[vertex_id]]) {
    return vertices[vid];
}

fragment float4 fragmentShader(float4 in [[stage_in]]) {
    return float4(1.0, 0.0, 0.0, 1.0);
}

運行程序,屏幕上顯示出三角形。

頂點添加顏色

上面畫的三角形是一個全部是紅色的三角形,我們可以利用傳遞給頂點的數據帶上一個顏色信息,然後給三角形畫上指定的顏色。
在metal中,不論是attribute還是uniform都是MTLBuffer,首先我們申明一個MTLBuffer的 colorBuffer;

id<MTLBuffer> colorBuffer;

然後給這個MTLBuffer填充顏色數據,每一個頂點分別填充紅綠藍三個顏色,

static float colors[] = {
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0
};
colorBuffer = [mtlDevice newBufferWithBytes:colors length:sizeof(colors) options:MTLResourceOptionCPUCacheModeDefault];

在renderpass中,將這個colorBuffer傳遞到頂點着色器,注意它的index是1,這個就是在着色器中對應的索引值

[renderEncoder setVertexBuffer:colorBuffer offset:0 atIndex:1];

相應的metal文件修改如下:

struct VertexOut {
    float4 position [[position]];
    float4 color;
};

vertex VertexOut vertexShader(device float4* position [[buffer(0)]],
                            constant float4* color[[buffer(1)]],
                            uint vid [[vertex_id]]) {
    VertexOut vert;
    vert.position = position[vid];
    vert.color = color[vid];
    return vert;
}

fragment float4 fragmentShader(VertexOut in [[stage_in]]) {
    return in.color;
}

這樣我們就得到一個彩色的三角形,因爲頂點着色器的數據在經過光柵化後,它的頂點座標和相應的顏色值都會進行線性插值處理,所以看到的是彩色的三角形。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章