Metal基礎入門

一、背景和技術選型

關于技術方案的選型,最權威的肯定是Metal for OpenGL Developers。

API 優(yōu)勢 劣勢
OpenGLES 跨端 不支持多線程操作;不支持異步處理;將于iOS12棄用
Metal 低CPU開銷(Low CPU overhead)、多線程執(zhí)行(Multithread execution)、資源和同步控制(Resource and synchronization control) 無法做到跨端共享

下面是我在OpenGLES中文網站上找到關于坐標系統(tǒng)的一張圖:


轉換圖

局部空間和世界空間

局部空間:在建模軟件中創(chuàng)建一個模型(比如一個正方體),針對該模型自身而言:它所有的頂點都是在局部空間。就比如后面我們要指定的頂點坐標;

世界空間:各個不同的模型都堆砌到一起的空間。將某個模型放置在世界的不同位置,使用模型變換矩陣來實現(xiàn)。比如平移、旋轉、縮放等等;

每一個石子兒在世界空間中的位置

觀察空間

即用我們眼睛或者攝像機觀察世界空間內的各個模型。這里相機和模型之間就形成了一個向量(Look-at
direction),然后我們再定義基于相機的Up向量(Up direction),如下圖:



這就構成了一個二維坐標系,然后基于這個二維坐標系的單位向量計算叉乘得到第三個向量。這里主要做的就是將相機、物體,以及up向量(用于表征相機向上的向量)構成的坐標系,變換到標準笛卡爾坐標系中來:


裁剪空間

我們期望所有的坐標都能落在一個特定的范圍內,且任何在這個范圍之外的點都應該被裁剪掉(Clipped),比如下圖中綠色的球。被裁剪掉的坐標就會被忽略,所以剩下的坐標就將變?yōu)槠聊簧峡梢姷钠?;從觀察坐標到裁剪坐標的變換是由投影變換矩陣實現(xiàn)的,主要有正射投影和透視投影:


屏幕空間

屏幕對應的空間它對應的是歸一化屏幕坐標,使用左手坐標系。具體可以看下圖:


二、Device和library

下圖是WWDC中提到的各個類之間的相互關系:


Device

Device在Metal中的概念和OpenGL中的Context概念是類似的:



Device的創(chuàng)建代碼如下:

_device = MTLCreateSystemDefaultDevice();

Library

它的作用是什么呢?通俗來說和我們平時見到的Static Library(靜態(tài)庫)類似,它就是包含了基于MSL(Metal
Shader Language)編寫的函數(shù),比如基于vertex的頂點著色器、基于fragment的片元著色器以及基于kernel數(shù)據(jù)計算函數(shù)。

  • 1、newDefaultLibraryWithBundle在指定bundle下面加載名稱為default.metallib文件;
  • 2、newLibraryWithFile\newLibraryWithURL\newLibraryWithData和第一點的區(qū)別在于,我們可以自己指定Metal Library的名稱,即xxx.metallib。例如基于xxxxParaboloidShaders.metal編譯的xxxxParaboloidShaders.metallib;
  • 3、newLibraryWithSource常用于加載比較輕量的shader,它的參數(shù)是傳入一個字符串的源碼(和openGL類似);

生成自定義的Library,我們可以通過Xcode的File->New->Target->Metal Library來生成:


這里我們用前面介紹的方式生成了一個自定義的Library,然后在代碼中去創(chuàng)建對應的對象:

_library = [_device newLibraryWithFile:[[NSBundle mainBundle] pathForResource:@"xxxx" ofType:@"metallib"] error:&error];

三、渲染

流水線

這是Metal基本的渲染流水線:

  • Vertices:表示確定的所有頂點;
  • Vertex function:對于可編程管線中,使用類C++的Metal shader language(MSL)語言編寫。GPU會在處理每一個頂點的時候都會調用該函數(shù);
  • Rasterization:光柵化階段,可以簡單理解為由頂點確定了三角形之后,需要將該三角形切成多個正方形格子來填充三角形。它來調用下面Fragment function;
  • Fragment function:對于可編程管線中,用于處理每個片段。這里可以簡單理解為處理每一個像素點;
  • Pixels:生成最終的像素點;

RenderPipleline

由于現(xiàn)在都是可編程管線,所以Metal引入了MTLRenderPipelineDescriptor作為流水線的描述符。它描述了流水線所需的各種配置(比如上圖中的vertext
Function、Fragment Function,在這里先簡單配置這兩個著色器):

MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineDescriptor.vertextFunction = vertextFunciton;
pipelineDescriptor.fragmentFunction = fragmentFunction;
pipelineDescriptor.vertexDescritpor = vertexDescriptor;

id<MTLRenderPipelineState> plState = [self.device newRenderPipelineStateWithDescriptor:pipelineDescritptor error:&error];

Vertex Descriptor

頂點描述符的作用主要是為了描述頂點的數(shù)據(jù)的組合方式以及步長等等。在語音音波按鈕這邊的頂點數(shù)據(jù)類型為:


基于前面的描述,對應的vertex Descriptor實現(xiàn):


Depth Buffer

深度緩沖(或z緩沖(z-buffer))中的深度值(Depth Value),是用來確定一個片段是否處于其它片段的后方。下圖描述了深度測試在流水線中的位置:


深度測試(Depth Testing)被啟用的時候,Metal會將一個片段的深度值與深度緩沖的內容進行對比。如果對比通過(即未被覆蓋),深度緩沖將會更新為新的深度值。如果深度測試失敗了,片段將會被丟棄。

Depth Texture

現(xiàn)在我們回到pipelineDescriptor,并將其depthAttachmentPixelFormat的值設置為MTLPixelFormatDepth32Float。如果直接運行Metal會告訴我們,如果沒有指定對應的紋理信息需要將depthAttachmentPixelForma設置為MTLPixelFormatInvalid。因此接著是創(chuàng)建對應的紋理信息:

Blending

除了上面介紹的頂點著色器和片元著色器之外,還需要指定流水線的顏色混合模式,在Metal中是使用混合因子來實現(xiàn)顏色混合的。首先看看Blend在流水線中的位置,很顯然,它是在Fragment Function之后的。

Blending——混合因子和Operations

為了理解混合因子以及相關的操作,Apple官方的文檔是這樣說的:Blending使用高可配置的操作,使得FragmentFunction(片元著色器)返回的值和每個像素 attachment 的值進行混合。

四個常數(shù)混合因子:


五個BlendOperation:


混合操作將源值(source)乘以 MTLBlendFactor,將目標值乘以目標混合因子(DBF, Destination Blend Factor),并用 MTLBlendOperation 指定的方式進行組合(如果MTLBlendOperation將值同時設置為min或者max,那么將會忽略SBF和DBF)。

一種常見的混合操作是用source alpha定義目標顏色:

RGB = (Source.rbg * 1.0) + (Dest.rgb * (1 - Source.a));
ALPHA = (Source.rbg * 1.0) + (Dest.a * (1 - Source.a));

對應的Metal實現(xiàn)細節(jié):


RenderPass

在看了RenderPipeline配置之后,現(xiàn)在就來看看單次渲染。首先是MTLRenderPassDescriptor,它描述了在每次渲染過程中各個像素目標的顏色、以及深度相關的信息(就是我們前面講到的內容再整合到renderPass中)。


在上面提到的loadAction可以簡單的理解為,我們要渲染一幀圖像時我們需要做什么?常規(guī)的操作有加載原有的紋理信息(比如上一幀的殘存);clear清除所有的數(shù)據(jù),以指定的clearColor屬性來作為初始值;最后一種就是完全不關心渲染開始時候的操作。在這里我們選擇的是清除所有信息,以指定的值作為渲染開始時像素的初始值;

而storeAction則和loadAction是相反的,它是在渲染結束的時候我們要做的操作。這里我們使用存儲當前這一幀的數(shù)據(jù)(MTLStoreActionStore)。


CommandBuffer和CommandEncoder

通俗的講buffer和encoder,encoder是將當前某一幀數(shù)據(jù)渲染所需的所有信息進行編碼,而buffer管理者這些encoder。并隨后統(tǒng)一提交到commandQueue中。這里有個比較經典的圖,整合了前面所講的相關內容:



由于上面圖中有一些和我們這一節(jié)講的內容不相關的元素,下圖是我將那些內容去掉之后結合我自己的理解畫的:


下面代碼展示了commandQueue和commandEncoder整合前面提及的內容:


異步渲染

它稍微和上面的圖形渲染關系疏遠一點,離我們平時的OC開發(fā)接近一點,因此就拿它作為本節(jié)的收尾吧。在這里我們使用NSThread作為常駐線程的方式來實現(xiàn),首先是創(chuàng)建子線程并啟動它:



在創(chuàng)建完線程之后,我們需要創(chuàng)建一個CADisplayLink來獲取當前屏幕刷新的回調。并設置對應的刷新幀率,在當前的業(yè)務場景下設置的刷新幀率是30fps:


接著我們去方法runThread中實現(xiàn)線程的常駐工作。這里需要值得注意的地方是:我們自定義了一個runLoopMode:kMMSVoiceSearchParaboloidRunloopMode,其目的是通過不同的Mode將我們的渲染任務和其他任務不會相互影響。若我們想要停止該線程,只需要將_continueRunLoop設置為NO即可。


四、頂點

頂點生成先以二維平面的方式平鋪頂點的方式,比如我這邊只生成60*60個頂點(來構成一個二維平面)。其大致的圖示如下:



這里用到的數(shù)學公式。其中第一個是雙曲拋物面公式(用于生成馬鞍面,公式一):


平滑過渡圓角則是基于公式二:




x的取值范圍是[-1.0,1.0],基于此公式二我們可以計算出當前的y值。在計算了y值之后,我們可以根據(jù)公式一計算出對應z值。最后我們通過Metal提供的API,將這些頂點數(shù)據(jù)封裝成MTLBuffer以便Metal后續(xù)使用:



在生成了頂點之后,我們需要告訴Metal各個定點之間是如何關聯(lián)起來的?即這么多頂點,到底是誰先誰后呢?這時候就需要指定這些順序了,大致的思想如下:

同樣的我們也生成對應MTLBuffer以供后續(xù)使用:

在前面的『CommandBuffer和CommandEncode』遺留未盡的事情,現(xiàn)在就來將對應的頂點數(shù)據(jù)寫入到commandEncoder中。這里有個通俗的概念說一下:我們作為生產者的內存數(shù)據(jù)(頂點,全局變量等等)放入到commandEncoder中,而流水線里面的vertext
shader和fragment shader作為消費者則可以假想去commandEncoder中獲取數(shù)據(jù)。

這里的Index是在頂點著色器中獲取對應數(shù)據(jù)的下標,這個下標隨后解釋。這個offset是對于相同index情況下,使用不同的offset來存數(shù)據(jù)。現(xiàn)在我們就根據(jù)前面的頂點數(shù)據(jù)來繪制面片。不過drawIndexedPrimitives比較重要,所以著重說一下:

  • primitiveType:圖元的類型。常見的有Point(點)、Line和LineStrip(線)、Triangle和TriangleStrip(三角形)。其中不帶Strip后綴的Line和Triangle,對于Line而言如果頂點數(shù)目是不是偶數(shù)則丟棄最后一個頂點,對于Triangle而言如果頂點數(shù)目不是3的倍數(shù)則丟棄剩下的頂點;反之,帶Strip后綴的值其表示不會丟棄任何頂點;由于在我們這個場景下不能丟棄頂點,而且我們在設計頂點索引的時候也是基于三角形的,因此這里我們選擇MTLPrimitiveTypeTriangleStrip。
  • indexCount:頂點索引個數(shù);
  • indexBuffer:就是我們前面生成的頂點索引數(shù)據(jù);
  • indexBufferOffset:偏移量。我們這里只有一份頂點索引,因此偏移量為0即可;
  • instanceCount:表示我們要用同一份數(shù)據(jù)繪制的模型個數(shù)。我們這里需要繪制3個面片,因此這里輸入3;

著色器

就如我們最開始看到的那樣,這里我們需要頂點著色器和片元著色器。編寫著色器需要使用基于C++的Metal Shader Language,我們先基于下圖簡單得看看你們關鍵字和語法(語法基本上就是C++的語法):

vertex:頂點著色器;
fragment:片元著色器;
我們看到無論是頂點著色器函數(shù),還是片元著色器函數(shù)他們的參數(shù)都有一個限定符。那么我們就依次來看這里的限定符:

  • [[buffer(x)]]:這個限定符表明該參數(shù)是咱們前面commandEncoder中調用setVertexBuffer寫入的數(shù)據(jù)。而這里x在上圖中是0或者1,就是在調用setVertexBuffer方法時的index值(不是offset);
  • [[vertext_id]]:頂點著色器是在渲染每個頂點的時候都會執(zhí)行依次該函數(shù)。因此這個vertex_id當前的頂點下標(因為我們所有的頂點是一個數(shù)組);
  • [[stage_in]]:在片元著色器中的表示該參數(shù)是由頂點著色器傳入進來的。即頂點著色器會return一個數(shù)據(jù),而這個數(shù)據(jù)會根據(jù)stage_in限定符傳入到片元著色器中;
  • [[instance_id]]:這個限定符在上圖中沒有呈現(xiàn),但是由于我們在commandEncoder中繪制的時候是傳入的實例個數(shù)是3。所以我們需要根據(jù)instance_id來區(qū)分這3個實例中的不同實例;

著色器中的坐標變換

坐標變化就是將一個模型坐標一步一步轉換為屏幕坐標的過程(實際上只需要我們轉換到裁剪坐標即可,最后一步是設置了視口之后Metal幫我們完成了):


到這里渲染部分基本上結束了,而MSL更加詳細的文檔可以參考Apple的 《Metal Shading Language Specification》。

效果展示

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容