你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 使用Metal打造令人驚歎的游戲效果

使用Metal打造令人驚歎的游戲效果

編輯:IOS開發基礎

1000143F3-0.jpg

蘋果最新推出的Metal框架支持GPU硬件加速、高級3D圖形渲染以及大數據並行運算。且提供了先進而精簡的API來確保框架的細粒度(fine-grain),並且在組織架構、程序處理、圖形呈現、運算指令以及指令相關數據資源的管理上都支持底層控制。其核心目的是盡可能的減少CPU開銷,而將運行時產生的大部分負載交由GPU承擔。

——Metal編程指南

Metal是一套用於在iPhone和iPad上GPU編程的高效框架。而Metal這個名稱的來源是想說明這個圖形框架的的確確是非常底層的——底層到已經非常接近金屬板了(Metal)。

該框架的設計主要為了兩個目的:3D圖形渲染和並行運算。這也是平時圖形學上最常涉及的兩個東西,它們都是基於特殊的代碼在GPU上對大量的數據並行計算。

Metal適用於誰

在談論API和Metal本身的著色語言之前我們需要來談論一下Metal能為開發者們帶來什麼。正如前面所言,Metal提供了兩大功能:3D圖形渲染和並行運算。

對於正在需求游戲引擎的人來說,Metal並非不二之選。光是蘋果自己家的Scene Kit(3D)和Sprite Kit(2D)就已足矣,這些API提供的是高層游戲引擎,其中還包括了物理仿真。另外還有功能更為全面的3D引擎,比如Epic家的Unreal Engine,還有Unity,而且這些引擎還不僅局限於蘋果平台。然而對於上述這些引擎,也都可以使用Metal來代替使用它們,並會(或者可能會)從中受益。

再論及使用底層圖形API的渲染引擎,也有可替代Metal的OpenGL和OpenGL ES,OpenGL的優勢不僅是支持包括OS X,Windows,Linux和Android多種平台,還因為其擁有大量的教程、書籍以及實踐指南。相較而言Metal的資料少得可憐,而且僅僅局限於搭載了64位處理器的iPhone或iPad上。不過從另一方面說,OpenGL在運行性能上比起Metal來說就遜色不少,在編寫的時候還會碰見各種各樣的問題。

而當需要在iOS平台上尋求高效的並行運算解決方案的時候,使用哪種引擎顯而易見,Metal毋庸置疑是首選。由於OpenCL在iOS上是私有框架(private framework),使用了OpenCL的Core Image在這種需求下顯得既低效又笨拙。

使用Metal的好處

最大的好處就是相比OpenGL ES而言可以大大的減少資源開銷。當使用OpenGL來創建一段緩存或者紋理,往往會產生一份拷貝以防GPU在使用這段數據時有其他的訪問(譯者按:比如來自CPU的訪問)。如果想要在確保數據安全的情況下拷貝緩存區或是紋理這樣的大資源,那麼產生的消耗會是相當大的。而反觀Metal,就會發現它並無需這樣的處理方式,開發者可以在CPU和GPU之間同步使用這些數據。為此蘋果還提供了一個更為優秀的接口使得這種同步訪問的情況變得更為容易:Grand Cnetral Dispatch(GCD技術)。不過使用Metal同樣能做到這點,所謂先進的引擎就是能避免產生拷貝的情況下高效渲染那些需要加載或撤掉的資源。

另一大好處就是Metal能預判GPU的狀態從而避免那些多余的校驗和編輯。在使用OpenGL的時候,習慣上我們會挨個兒設置GPU的狀態,然後每次進行繪制調用之前必須要校驗一道GPU的狀態。在最糟糕的情況下你甚至需要為了一個新的GPU狀態重編譯一遍陰影效果,當然,在這裡采用預判GPU狀態就顯得相當必要了。不過Metal另辟蹊徑,在初始化渲染引擎的時候,GPU的狀態會被打包進一個預估的渲染通道,(render pass),此狀態下渲染通道會與多種資源一起被使用,而其他的狀態不會有任何影響。Metal使用的渲染通道不需要多余的校驗,因而最大限度的減少了API負載,且對於每一幀的渲染都有質的提升。

Metal API

雖然很多API都通過具體類來實現平台支持,不過Metal使用的方法是基於協議的。因為Metal中具體的類型是由運行的設備所決定的。這很好的鼓勵了程序員選擇面向接口編程而非面向實現,以降低程序的耦合。當然也意味著需要冒著風險大量的在Objective C運行時來對Metal的類型添加繼承和擴展類型。

為了提高效率Metal必然也犧牲了一部分安全性,在對待出錯情況的時候,蘋果的其他框架都會顯得更為健壯和安全。而到了Metal這裡就完全不一樣了,在某些情況下你甚至會得到一個指向內部緩沖區的裸指針,所以在執行同步的時候必須多加小心。在用OpenGL出錯的時候,撐死了就是黑屏,但是在Metal的情況下就是一切皆有可能(藍屏?),可能會發生閃屏或者間歇性死機之類的情況,所有這些都歸咎於Metal框架所針對的特殊處理器恰巧在CPU旁。

值得一提的是蘋果尚未針對虛擬機上運行Metal提供任何輔助軟件,所以在測試的時候僅支持真機。

一個基本的Metal程序

在本節中,我們會為介紹關於第一個Metal程序所必要知識。這一簡單程序繪制了一個自旋轉的正方形,你可以在GitHub上下載示例程序源代碼。

盡管我們不能詳述所有細節,但是會盡量提及大部分相關內容,如果你想了解得更多的話可以自行參閱源碼,或是查閱網上的相關資料。

使用UIKit創建設備(Device)和接口

在Metal中,設備(Device)被作為GPU抽象概念,被用於去創建其他的對象,包括緩存、材質以及函數庫。使用MTLCreateSystemDefaultDevice()方法可以獲取默認設備:

id device = MTLCreateSystemDefaultDevice();

注意這裡的device並不是一個特殊的具體類,不過它遵循的是MTLDebice協議。

下面的這一段代碼演示的是如何創建一個Metal層並將其作為UIView背景層的子層。

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

CAMetalLayer是CALayer的子類,能夠表現一個Metal framebuffer中的內容。我們需要告知圖層metalLayer我們使用了那個device(就是剛剛創建的那個),並設置其顯示格式,在這裡設置的是8bit通道BGRA的格式,也就是說每個像素包括藍、綠、紅以及Alpha(透明度)四個值,每個值的取值范圍為0-255。

函數和庫

Metal程序中大多數功能是依照頂點和片段功能來寫的,通俗點說就是著色器。Metal的著色器是Metal Shading language寫的,下面我們會詳細說說這個東西。Metal的一大優勢就在於著色功能是在應用生成中間代碼時才編譯,並在應用啟動時保存有效時間值。

Metal庫就是對於各種功能集合。你在自己的項目中所寫的所有著色器功能都會編譯進這個默認的庫裡面,它們可以從你的device裡面提取恢復:

id library = [device newDefaultLibrary]

我們在下面構建渲染管線狀態的時候需要用到這個庫。

指令序列

所有指令會被集結為指令序列之後提交到Metal device,指令序列允許指令在線程安全的情況下改變或者序列化他們的執行。你可以直截了當的創建一個指令序列:

id commandQueue = [device newCommandQueue];

創建管線

Metal中的管線編程,也就意味著頂點數據在被渲染的時候會發生不同尋常的變化。頂點著色器和片段著色器都是可編程渲染管線,另外也還有一些其他的必要的操作(比如剪切、光柵掃描、視口變換)則不需要我們直接去控制。後面這些不需要去控制的特性則組成了固定渲染管線。

在Mrtal中創建管線的時候,我們需要分別對每個頂點和像素指定使用哪種頂點和片段函數。同樣還需要告知管線在frambuffer中的像素格式。在這種情況下,Metal要求像素格式必須與圖層的格式匹配,因為這樣才能將其繪制到屏幕上。

我們可以從庫中使用其名字調用這些函數:

id vertexProgram = [library newFunctionWithName:@"vertex_function"];
id fragmentProgram = [library newFunctionWithName:@"fragment_function"];

接下來創建一個管線描述符並使用其方法和像素格式來配置。

MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; 
[pipelineStateDescriptor setVertexFunction:vertexProgram];
[pipelineStateDescriptor setFragmentFunction:fragmentProgram]; 
pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;

最後我們利用描述符來創建管線態(pipeline state),這樣在程序運行的時候它就會根據硬件將中間代碼優化之後編譯著色器功能。

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

加載數據到Buffer中

現在我們已經成功創建了管線,當然還需要將其填滿數據。在本例中,我們只需要繪制一個簡單的幾何圖形:旋轉的正方形。它由兩個共邊的直角三角形組成。

static float quadVertexData[] = 
{ 
     0.5, -0.5, 0.0,1.0,      1.0, 0.0, 0.0, 1.0,
     -0.5, -0.5, 0.0, 1.0,      0.0, 1.0, 0.0, 1.0, 
     -0.5, 0.5, 0.0, 1.0,      0.0, 0.0, 1.0, 1.0,
     0.5, 0.5, 0.0, 1.0,      1.0, 1.0, 0.0, 1.0,
     0.5, -0.5, 0.0, 1.0,      1.0, 0.0, 0.0, 1.0,
      -0.5, 0.5, 0.0, 1.0,      0.0, 0.0, 1.0, 1.0,
};

每一行的前四個值分別表示每個頂點的x,y,z,w值(譯者按:即四元數),後面四個數代表的是紅、綠、藍和Alpha值。

你也許會好奇為什麼在3D空間中表示頂點的位置信息要用四個數,第四個數w其實是為了我們在對頂點進行變換(旋轉、平移和縮放)的時候在數學上更加方便。這一細節在本例中不作為重點。

要使用Metal來繪制頂點數據,我們需要先把數據放進一個Buffer中,這個Buffer就是存在於內存中能被CPU和GPU共享的非結構化二進制串。

vertexBuffer = [device newBufferWithBytes:quadVertexData 
                                   length:sizeof(quadVertexData) 
                                  options:MTLResourceOptionCPUCacheModeDefault];

我們還需要另一個Buffer來存儲旋轉矩陣以便讓正方形旋轉,僅需創建一個指定長度的Buffer為其提供空間就行了,不需要預先初始化數據:

uniformBuffer = [device newBufferWithLength:sizeof(Uniforms) 
                                    options:MTLResourceOptionCPUCacheModeDefault];

動畫

為了使正方形旋轉,需要將頂點作為頂點著色器的一部分進行變換。也就是說要為每幀畫面更新統一的數據緩存區。為此,我們利用三角函數從當前的旋轉角生成旋轉矩陣並將其復制到統一的緩存區中。

下面的Uniform結構體僅有一個4×4旋轉矩陣作為成員,類型是matrix_float4x4,這是蘋果SIMD庫中的數據結構,這個庫中包含了多種適用於數據並行處理的類型:

typedef struct
{ 
     matrix_float4x4 rotation_matrix; 
} Uniforms;

為了成功復制旋轉矩陣的數據到緩存中,我們需要得到指向其數據的指針然後使用memcpy來拷貝數據:

Uniforms uniforms; 
uniforms.rotation_matrix = rotation_matrix_2d(rotationAngle); 
void *bufferPointer = [uniformBuffer contents]; 
memcpy(bufferPointer, &uniforms, sizeof(Uniforms));

准備繪制

在繪制之前先要從圖層中獲取drawable,該對象管理了一組用於渲染的紋理:

id drawable = [metalLayer nextDrawable];

然後創建一個MTLRenderPassDescriptor,用於描述Metal在渲染先後需要完成的操作。下面我們將使用它來清除framebuffer然後用白色填充,然後執行繪制調用,最後將結果存儲在frambuffer中顯示出來。

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

發布繪制調用

使用指令序列需要現將它們編碼進指令緩存中,指令緩存就是將已經按照GPU能理解的方式編碼的指令集合。:

id commandBuffer = [self.commandQueue commandBuffer];

為了真正的對指令序列進行編碼,需要一個對象來將所有的繪制調用轉換為GPU能夠讀取的語言。這一對象類型叫做MTLRenderCommandEncoder,可以通過向指令緩存區請求編碼以及之前創建的描述符來創建

這個MTLRenderCommandEncoder:
id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];

在調用之前,需要用前面預編譯好的管線態(pipeline state)來配置渲染指令的編碼器,然後設置這段緩存,這段緩存將作為我們頂點著色器的參數:

[renderEncoder setRenderPipelineState:pipelineState]; 
[renderEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0]; 
[renderEncoder setVertexBuffer:uniformBuffer offset:0 atIndex:1];

為了能渲染出幾何形狀,我們要告訴Metal我們希望繪制哪一種圖形(三角形),以及需要從緩存區中解析出多少個頂點(在本例中是六個):

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];

最後告知編碼器我們已經完成了繪制調用的分發,調用endEncoding:

[renderEncoder endEncoding];

Framebuffer的呈現

現在我們的繪制調用已經准備妥當,於是需要告知指令緩存區將結果顯示到屏幕上。在此調用presentDrawable方法並以drawable為參數:

[commandBuffer presentDrawable:drawable];

通知緩存區准備執行,使用commit方法:

[commandBuffer commit];

一切完成~

Metal 著色語言(Shading Language)

雖然Metal是跟著Swift一起在WWDC2014上作為重磅內容發布的,不過它的著色語言還是基於C++11標准,並添加了少部分特性以及一些關鍵字。

Metal著色語言練習

為了能在著色器中使用頂點數據,我們要定義一個結構體類型,相當於將頂點的布局用Objective-C來表示。

typedef struct
{ 
float4 position; 
float4 color; 
} VertexIn;

我們還另需要一個相似的的結構體來描述從頂點著色器到片段著色器傳遞的頂點類型。不過,在這種情況下,需要確定結構中的哪個成員應該被認為是頂點的位置信息(通過使用[[position]])。

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

頂點函數會對每個頂點都執行一次,它接受的一個指向全部頂點、統一數據的相關信息以及旋轉矩陣的指針。第三個索引參數則告訴函數當前的頂點的操作。

注意頂點函數的的參數是根據函數用途不同安排的。在本例中的所用的buffer參數中,參數的索引相當於我們在設置渲染指令編碼器的時候指定的索引。這也是Metal分辨這些參數誰是誰的方法。

在頂點函數中,我們將頂點位置與旋轉矩陣相乘。我們之前定的旋轉矩陣能夠使正方形圍繞它的中心旋轉,我們將變換後的數據存儲在輸出頂點中,頂點顏色則完全拷貝輸入頂點:

vertex VertexOut vertex_function(device VertexIn *vertices [[buffer(0)]], 
                                 constant Uniforms &uniforms [[buffer(1)]], 
                                 uint vid [[vertex_id]]) 
{ 
     VertexOut out; 
     out.position = uniforms.rotation_matrix * vertices[vid].position; 
     out.color = vertices[vid].color; 
     return out; 
}

片段函數則是對每個像素執行一次,所需參數則是Metal在光柵化過程中生成的,這一過程還會對每個頂點中指定的顏色和位置信息進行插值。在這個簡單的片段函數中,我們傳出的插值顏色是Metal已經算好的,就是最終顯示在屏幕上的像素的顏色值。

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

為啥就不拓展OpenGL

蘋果也是OpenGL體系架構審查委員會的一員,其在iOS上支持OpenGL的擴展也有歷史了。不過從內部更改OpenGL顯然是很難的,畢竟設計的目的就不是全心全意支持iOS設備,更何況OpenGL是要支持如此雜亂繁多的硬件設備的。雖然OpenGL也在不斷的提升之中,但是其變化會將緩慢而有待商榷。

Metal,從另一方面看,在蘋果的設計下是獨占型的。協議型的API從一開始就顯得與眾不同,而且和蘋果其他框架又如此兼容。Metal的開發語言是Objective-C,又基於Foundation框架,且在GPU於CPU同時工作時使用了GCD技術。在GPU管線抽象上比起OpenGL來說更加先進,完全無需重寫。

Mac上的Metal

Metal移植到OS X,這只是一個時間問題。它的API並不只局限於iPhone和iPad上的ARM處理器,Metal的大多數優點在當前的很多GPU上來說都是可移植的。另外在iPhone和iPad上的GPU和CPU是共享RAM的,也就是會說它們無需復制來交換數據。而現在的Mac本並不支持內存的共用,不過這也只是一個時間問題。也許Metal的API會慢慢更改以支持獨立RAM的架構,或者說Metal只應用於未來那些共享RAM的Mac本。

總結

我們希望通過本文對Metal框架進行一個有用而客觀的介紹。

當然,大多數游戲開發者可能對此不會有什麼接觸,不過不少主流引擎已經開始青睐Metal,而開發者也將得益於在開發中不需要自己去創建API。此外,對那些設備發燒友來說,Metal的出現,會讓開發者們營造更加令人驚歎的游戲畫面效果,同時運行也會更為流暢,對他們而言這無疑是一大利好。

資源

Metal編程指南

Metal著色語言指南

Metal實例


本站其他資源

iOS 8 Metal Swift教程 :開始學習

iOS 8 Metal Swift 教程(二):3D圖形

Metal Framework基礎使用教程

Metal基本圖像處理實例

(本文由CocoaChina翻譯自objc.io轉載請注明出處)

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved