Metal與圖形渲染入門篇:繪制圖片

零. 前言

OpenGL秩冈,一個被廣大語言運用的庫本缠,在iOS12后,被蘋果打上了Deprecated的標簽入问,如果現(xiàn)在的工程還引用著這個庫丹锹,則會被不勝其煩地提示:該庫已過期。

把OpenGL踢出蘋果渲染舞臺的庫芬失,叫作Metal楣黍,官方也聲明已經(jīng)把底層渲染支持替換為Metal,并鼓勵開發(fā)者使用Metal渲染棱烂,以取代工程中的OpenGL租漂。雖然這個庫只有蘋果所用,但他的易讀性高、維護性強哩治、Debug能力好秃踩、性能棒,足以成為蘋果推薦該庫的理由业筏,蘋果特地在WWDC19介紹了Metal憔杨,并給出了如何將OpenGL遷移到Metal的指引,詳情看這個視頻:Bringing OpenGL Apps to Metal蒜胖。

作為一個對圖形渲染一竅不通的小白消别,突然接到一個Metal相關的需求,一開始過于急躁翠勉,像個無頭蒼蠅一樣亂撞妖啥,但遲遲找不到通往Metal開發(fā)大門的入口,索性靜下心來对碌,好好從圖形渲染開始理解,慢慢入門蒿偎,強迫自己書寫Demo更新博客朽们,慢工出細活地成長,也把自己理解的心路歷程與還沒入門的同學分享诉位,希望能一舉兩得骑脱。

這是我對Metal的第一篇文章,目前的狀態(tài)是對Metal也是剛剛入門苍糠,希望自己能通過書寫博客更好地成長叁丧,一步一個腳印,以完成自己的目標岳瞭。第一篇文章主要講解一下這些天來摸爬滾打搜集的一些資料和自己的一些入門級見解拥娄,如果有不對的地方歡迎指出探討。

一. 圖形渲染

工欲善其事必先利其器瞳筏,如果對圖形學沒有一點入門理解稚瘾,還是好好先看一看圖形渲染的步驟,最好了解一下OpenGL的工作原理姚炕,不要因為OpenGL在蘋果被廢棄掉了就對其嗤之以鼻摊欠,因為這個庫在蘋果以外的很多地方還是被廣泛應用到的,學會了圖形渲染柱宦,對Metal的理解會有很大幫助些椒。該篇章取自Learn OpenGL中文文檔

1. 基本原理概括

在OpenGL中掸刊,任何事物都在3D空間中免糕,而屏幕和窗口卻是2D像素數(shù)組,這導致OpenGL的大部分工作都是關于把3D坐標轉變?yōu)檫m應你屏幕的2D像素。3D坐標轉為2D坐標的處理過程是由OpenGL的圖形渲染管線(Graphics Pipeline说墨,大多譯為管線骏全,實際上指的是一堆原始圖形數(shù)據(jù)途經(jīng)一個輸送管道,期間經(jīng)過各種變化處理最終出現(xiàn)在屏幕的過程)管理的尼斧。圖形渲染管線可以被劃分為兩個主要部分:第一部分把你的3D坐標轉換為2D坐標姜贡,第二部分是把2D坐標轉變?yōu)閷嶋H的有顏色的像素。

圖形渲染管線接受一組3D坐標棺棵,然后把它們轉變?yōu)槟闫聊簧系挠猩?D像素輸出楼咳。圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入烛恤。所有這些階段都是高度專門化的(它們都有一個特定的函數(shù))母怜,并且很容易并行執(zhí)行。正是由于它們具有并行執(zhí)行的特性缚柏,當今大多數(shù)顯卡都有成千上萬的小處理核心苹熏,它們在GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數(shù)據(jù)币喧。這些小程序叫做著色器(Shader)轨域。

以下是圖形渲染管線的每個階段的抽象展示,也是渲染圖片的一個重要步驟杀餐,相當于給一幅畫勾勒出線條干发,再上色歹颓,三維混合(如有必要)辟犀,以達到我們想要的圖畫效果。

2. 圖形渲染的根基——三角形與像素點

在圖形渲染中裆蒸,有個非常非常非常重要的概念——三角形琼讽,可以這樣說必峰,如果呈現(xiàn)在屏幕上的圖像是一座美麗的布達拉宮,那么三角形就是里面的一座地基跨琳、一根根柱子自点。

而你所看到的前三個步驟,就是從幾個點脉让,以三角形的方式勾勒出了整個線條桂敛。而第四個步驟則把線條做成一格一格的像素點。

  • 頂點著色器:該階段的輸入是頂點數(shù)據(jù)(Vertex Data) 數(shù)據(jù)溅潜,比如以數(shù)組的形式傳遞 3 個 3D 坐標用來表示一個三角形术唬。頂點數(shù)據(jù)是一系列頂點的集合。頂點著色器主要的目的是把 3D 坐標轉為另一種 3D 坐標滚澜,同時頂點著色器可以對頂點屬性進行一些基本處理粗仓。

  • 形狀(圖元)裝配:該階段將頂點著色器輸出的所有頂點作為輸入,并將所有的點裝配成指定圖元的形狀。圖中則是一個三角形借浊。圖元(Primitive) 用于表示如何渲染頂點數(shù)據(jù)塘淑,如:點、線蚂斤、三角形存捺。

  • 幾何著色器:該階段把圖元形式的一系列頂點的集合作為輸入,它可以通過產(chǎn)生新頂點構造出新的(或是其它的)圖元來生成其他形狀曙蒸。例子中捌治,它生成了另一個三角形。(其實個人覺得這里應該加多個頂點才對纽窟,不然好像有點讓人誤解多出來的那條線是怎么來的)

  • 光柵化階段(Rasterization Stage):根據(jù)幾何著色器的輸出肖油,把圖元映射為最終屏幕上相應的像素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)臂港。

3. 紋理森枪、采樣與著色

到光柵化這一步,我們已經(jīng)可以獲取到未被上色的像素了审孽,一個圖像有了初步的一些輪廓疲恢,那么他是怎么被上色,甚至被組合形成一個三維圖案的呢瓷胧?片段著色器就是上色的重要一環(huán)了。

  • 片段著色器的主要目的是計算一個像素的最終顏色棚愤,這也是所有OpenGL高級效果產(chǎn)生的地方搓萧。通常,片段著色器包含3D場景的數(shù)據(jù)(比如光照宛畦、陰影瘸洛、光的顏色等等),這些數(shù)據(jù)可以被用來計算最終像素的顏色次和。

那么反肋,他的顏色從哪里來呢?程序員可以根據(jù)自己想要的顏色進行上色踏施,即直接在片段著色器寫死顏色的rgba值石蔗,比如生成一個橘色的三角形:

那如果我們想讀取一張圖片渲染到上面去呢?像下面一樣畅形,把羅伊斯的照片貼到屏幕上去养距。

這時候需要引入一個同樣重要的概念:紋理。

  • 紋理是一個2D圖片(甚至也有1D和3D的紋理)日熬,它可以用來添加物體的細節(jié)棍厌;你可以想象紋理是一張繪有磚塊的紙,無縫折疊貼合到你的3D的房子上,這樣你的房子看起來就像有磚墻外表了耘纱。因為我們可以在一張圖片上插入非常多的細節(jié)敬肚,這樣就可以讓物體非常精細而不用指定額外的頂點。

上面的概念可能有點籠統(tǒng)束析,在渲染的知識里面艳馒,你需要暫時先將一張圖片看成一個一個像素點,采樣器(sampler)將圖片上的像素點一一采樣畸陡,再映射到已經(jīng)光柵化的像素點中鹰溜,使其上色,最終得到一個個上色后的像素點丁恭。后文會著重介紹怎么采樣紋理和給光柵化像素上色曹动。

最后,如果涉及到3D渲染(本文暫不涉及)牲览,該階段會檢測片段的對應的深度值(z 坐標)墓陈,判斷這個像素位于其它物體的前面還是后面,決定是否應該丟棄第献。此外贡必,該階段還會檢查 alpha 值( alpha 值定義了一個物體的透明度),從而對物體進行混合庸毫。因此仔拟,即使在片段著色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同飒赃。

4. 重點——頂點著色器與片段著色器

前面主要給大家介紹了從0到1的渲染過程利花,那么本文則會著重介紹一下MSL(Metal Shader Language) 給我們提供的接口,也就是說载佳,我們只需要著手這兩個著色器的開發(fā)炒事,其他步驟無需我們動手。

在基于Metal介紹這兩個著色器之前蔫慧,請大家再著重復習一下幾個重要的概念:

  • 像素:一個圖像由許多許多像素組成挠乳。

  • 頂點著色器:將原圖像的3D坐標轉換成適應屏幕的3D坐標,同時建立需要繪制的頂點坐標 與 需要采樣的紋理坐標的映射關系姑躲。在開發(fā)中睡扬,我們需要預先設好頂點坐標與紋理坐標的映射,供系統(tǒng)內(nèi)部光柵化處理肋联,最后傳到片段著色器中威蕉。

  • 紋理:用于被采樣器采樣,給片段著色器上色的圖像橄仍。在開發(fā)中韧涨,我們需要讀取圖像的字節(jié)牍戚,調用接口生成紋理。

  • 片段著色器:基于頂點著色器的輸出虑粥、紋理的采樣結果如孝,輸出一個個著色后的像素,這些像素組成了一整個圖像娩贷。在開發(fā)中第晰,我們需要根據(jù)頂點著色器輸出(光柵化處理后)的數(shù)據(jù)、紋理數(shù)據(jù)彬祖,對紋理進行采樣茁瘦,并輸出該光柵化像素對應的rgba。(多個像素即為一張圖片)

接下來將會介紹Metal如何運用上面幾個概念储笑,在屏幕上渲染出一張圖片出來甜熔,如果讀到后面有疑惑,不妨回頭再看看這幾個概念和他們的職能突倍。

二. Why Metal腔稀?

在進行Metal開發(fā)之前,需要思考一個問題羽历,為什么要用Metal進行開發(fā)焊虏,這句疑問代表著兩個含義:第一,Metal在蘋果開發(fā)中承擔著什么樣的角色秕磷;第二诵闭,為什么是Metal而不是OpenGL,下面會對這兩個疑問進行解答澎嚣。

1. Metal在蘋果開發(fā)中承擔著什么樣的角色

理解Metal擔任的角色之前涂圆,需要先了解一下CPU、GPU和顯示器的概念:


手機包含兩個不同的處理單元币叹,CPU 和 GPU。CPU 是個多面手模狭,并且不得不處理所有的事情颈抚,而 GPU 則可以集中來處理好一件事情,就是并行地做浮點運算嚼鹉。事實上贩汉,圖像處理和渲染就是在將要渲染到窗口上的像素上做許許多多的浮點運算。通過有效的利用 GPU锚赤,可以成百倍甚至上千倍地提高手機上的圖像渲染能力匹舞。下面的流程圖顯示了一個圖像渲染到屏幕的流程。

通過流程圖我們可以看到线脚,在我們?nèi)粘5匿秩局写突琌penGL/Metal已經(jīng)默默地替我們承擔了很多渲染的操作叫榕,如果感興趣可以在iOS 圖像渲染原理看看這些圖像是怎么一步步渲染下去的。

CPU姊舵、GPU晰绎、顯示器的工作方式
渲染的流水線圖

總的來說,Metal擔任的就是CPU和GPU交互的一個橋梁括丁,他負責一個管理圖形渲染的隊列荞下,在屏幕刷新一幀的時候,將隊列的內(nèi)容提交給GPU史飞,以及時地渲染到屏幕上尖昏。

即:CPU => Metal => GPU => 顯示器

2. 為什么是Metal而不是OpenGL

對于有著超過25年歷史的 OpenGL 技術本身,隨著現(xiàn)代圖形技術的發(fā)展构资,遇到了一些問題:

  • 現(xiàn)代 GPU 的渲染管線已經(jīng)發(fā)生變化抽诉。
  • 不支持多線程操作。
  • 不支持異步處理蚯窥。
  • 較為復雜的開發(fā)語言掸鹅。

隨著圖形學的發(fā)展,OpenGL 本身設計上存在的問題已經(jīng)影響了 GPU 真正性能的發(fā)揮拦赠,因此 Apple 設計了 Metal巍沙。

為了解決這些問題,Metal 誕生了荷鼠。
它為現(xiàn)代 GPU 設計句携,并面向 OpenGL 開發(fā)者。它擁有:

  • 更高效的 GPU 交互允乐,更低的 CPU 負荷矮嫉。
  • 支持多線程操作,以及線程間資源共享能力牍疏。
  • 支持資源和同步的控制蠢笋。
  • 語言更符合開發(fā)者的開發(fā)習慣。
  • 可逐幀調試鳞陨。

Metal 簡化了 CPU 參與渲染的步驟昨寞,盡可能地讓 GPU 去控制資源。與此同時厦滤,擁有更現(xiàn)代的設計援岩,使操作處于可控,結果可預測的狀態(tài)掏导。在優(yōu)化設計的同時享怀,它仍然是一個直接訪問硬件的框架。與 OpenGL 相比趟咆,它更加接近于 GPU添瓷,以獲得更好的性能梅屉。

Metal早在2014年就已經(jīng)被蘋果推出,并在WWDC2018宣稱OpenGL ES 將于 iOS 12 棄用仰坦。當在真機上調試 OpenGL 程序時履植,控制臺會打印出啟用 Metal 的日志。根據(jù)這一點可以猜測悄晃,Apple 已經(jīng)實現(xiàn)了一套機制將 OpenGL 命令無縫橋接到 Metal 上玫霎,由 Metal 擔任真正于硬件交互的工作。而OpenGL未來會不會被永久拋棄妈橄,我們不得而知庶近。

三. How Metal?

好了,鋪墊了這么多理論知識眷蚓,下面應該開始手動實操了鼻种,我們今天的目的,是用Metal語言沙热,將一張圖片繪制到屏幕上去:

1. Metal API

Metal的簡要流程圖如下:

  • 命令緩存區(qū)(Command Buffer)是從命令隊列(Command Queue)創(chuàng)建的
  • 命令編碼器(Command Encoder)將命令編碼到命令緩存區(qū)中
  • 提交命令緩存區(qū)并將其發(fā)送到GPU
  • GPU執(zhí)行命令并將結果呈現(xiàn)為可繪制

那么叉钥,我們要實現(xiàn)一個Metal圖像的繪制,需要用到哪些API呢篙贸?

(1) MTKView與MTLDevice

在MetalKit中提供了一個視圖類MTKView投队,類似于GLKit中GLKView,它是NSView(macOS中的視圖類)或者UIView(iOS爵川、tvOS中的視圖類)的子類敷鸦。用于處理metal繪制并顯示到屏幕過程中的細節(jié)。

MTLDevice代表GPU設備寝贡,提供創(chuàng)建緩存扒披、紋理等的接口,在初始化時候需要賦給MTKView

// 初始化MTKView
self.mtkView = [[MTKView alloc] init];
self.mtkView.delegate = self;
self.device = self.mtkView.device = MTLCreateSystemDefaultDevice();
self.mtkView.frame = self.view.bounds;
[self.view addSubview:self.mtkView];

MTKView的Delegate是MTKViewDelegate圃泡,我們必須實現(xiàn)這個協(xié)議的方法:

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
    // MTKView的大小改變
    self.viewportSize = (vector_uint2){size.width, size.height};
}

- (void)drawInMTKView:(MTKView *)view {
    // 用于向著色器傳遞數(shù)據(jù)
...具體實現(xiàn)
}

(2) MTLCommandQueue

在獲取了GPU后碟案,還需要一個渲染隊列,即命令隊列Command Queue類型是MTLCommandQueue颇蜡,該隊列是與GPU交互的第一個對象蟆淀,隊列中存儲的是將要渲染的命令MTLCommandBuffer

隊列的獲取需要通過MTLDevice對象獲取澡匪,且每個命令隊列的生命周期很長,因此commandQueue可以重復使用褒链,而不是頻繁創(chuàng)建和銷毀唁情。

_commandQueue = [_device newCommandQueue];

(3) MTLRenderPipelineState

渲染管道狀態(tài) Render Pipeline State是一個協(xié)議,定義了圖形渲染管道的狀態(tài)甫匹,包括放在.metal文件的頂點和片段函數(shù)甸鸟。

(4) MTLTexture

紋理 MTLTexture表示一個圖片數(shù)據(jù)的紋理惦费,關于紋理前面的介紹已經(jīng)很多了,可以往前回顧一下抢韭。我們可以根據(jù)紋理描述器 MTLTextureDescriptor來生成MTLTexture

(5) MTLBuffer

代表一個我們自定義的數(shù)據(jù)存儲資源對象薪贫,在本章中,用于存儲頂點與紋理坐標數(shù)據(jù)刻恭,通過MTLDevice獲取瞧省。

(6) MTLCommandBuffer

命令緩存區(qū) Command Buffer主要是用于存儲編碼的命令,其生命周期是知道緩存區(qū)被提交到GPU執(zhí)行為止鳍贾,單個的命令緩存區(qū)可以包含不同的編碼命令鞍匾,主要取決于用于構建它的編碼器的類型和數(shù)量。

命令緩存區(qū)的創(chuàng)建可以通過調用MTLCommandQueuecommandBuffer方法骑科。且command buffer對象的提交只能提交至創(chuàng)建它的MTLCommandQueue對象中

commandBuffer在未提交命令緩存區(qū)之前橡淑,是不會開始執(zhí)行的,提交后咆爽,命令緩存區(qū)將按其入隊的順序執(zhí)行梁棠,使用[commandBuffer commit]提交命令。

- (void)drawInMTKView:(MTKView *)view {
    // 用于向著色器傳遞數(shù)據(jù)
    id <MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
    
... 設置MTLRenderCommandEncoder進行Encode

    // 提交
    [commandBuffer presentDrawable:view.currentDrawable];
    [commandBuffer commit];
}

(7) MTLRenderCommandEncoder

渲染命令編碼器 Render Command Encoder表示單個渲染過程中相關聯(lián)的渲染狀態(tài)和渲染命令斗埂,有以下功能:

  • 指定圖形資源符糊,例如緩存區(qū)和紋理對象,其中包含頂點蜜笤、片元濒蒋、紋理圖片數(shù)據(jù)
  • 指定一個MTLRenderPipelineState對象,表示編譯的渲染狀態(tài)把兔,包含頂點著色器和片元著色器的編譯&鏈接情況
  • 指定固定功能沪伙,包括視口、三角形填充模式县好、剪刀矩形围橡、深度、模板測試以及其他值
  • 繪制3D圖元

由當前隊列的緩沖MTLCommandBuffer根據(jù)描述器MTLRenderPassDescriptor的接口獲嚷乒薄(這個可以通過MTKView的currentRenderPassDescriptor拿到翁授,代表每一幀當前渲染視圖的一些紋理、緩沖晾咪、大小等數(shù)據(jù)的描述器)收擦。

id <MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
MTLRenderPassDescriptor *renderDesc = view.currentRenderPassDescriptor;
if (!renderDesc) {
    [commandBuffer commit];
    return;
}
// 獲取MTLRenderCommandEncoder
id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderDesc];

然后需要對之前提到的MTLRenderPipelineState(映射.metal文件用)、MTLTexture(讀取圖片獲得的紋理數(shù)據(jù))谍倦、MTLBuffer(頂點坐標和紋理坐標構成的緩沖)進行設置塞赂,最后調用drawPrimitives進行繪制,再endEncoding昼蛀。

id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderDesc];
[renderEncoder setViewport:(MTLViewport){0, 0, self.viewportSize.x, self.viewportSize.y, -1, 1}];
// 映射.metal文件的方法
[renderEncoder setRenderPipelineState:self.pipelineState];
// 設置頂點數(shù)據(jù)
[renderEncoder setVertexBuffer:self.vertices offset:0 atIndex:0];
// 設置紋理數(shù)據(jù)
[renderEncoder setFragmentTexture:self.texture atIndex:0];
// 開始繪制
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:self.numVertices];
// 結束渲染
[renderEncoder endEncoding];
// 提交
[commandBuffer presentDrawable:view.currentDrawable];
[commandBuffer commit];

和OpenGL一樣宴猾,我們可以使用4個頂點來繪制一個矩形圆存,修改drawPrimitives:的參數(shù)為MTLPrimitiveTypeTriangleStrip,然后頂點順序為z字形即可仇哆。

2. Metal在OC/Swift層的渲染步驟

了解到以上用到的API后沦辙,我們就可以開始介紹一下渲染步驟了:

首先我們需要初始化,把本次渲染只需創(chuàng)建一次的內(nèi)容初始化出來讹剔,包括:MTKView油讯、MTLCommandQueue、MTLRenderPipelineState辟拷。

- (void)setupMTKView {
    // 初始化MTKView
    self.mtkView = [[MTKView alloc] init];
    self.mtkView.delegate = self;
    self.device = self.mtkView.device = MTLCreateSystemDefaultDevice();
    self.mtkView.frame = self.view.bounds;
    [self.view addSubview:self.mtkView];
}

- (void)setupPineline {
    // 初始化pipelineState
    MTLRenderPipelineDescriptor *pinelineDesc = [MTLRenderPipelineDescriptor new];
    id <MTLLibrary> library = [_device newDefaultLibrary];
    pinelineDesc.vertexFunction = [library newFunctionWithName:@"vertexShader"];
    pinelineDesc.fragmentFunction = [library newFunctionWithName:@"fragmentShader"];
    pinelineDesc.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
    self.pipelineState = [_device newRenderPipelineStateWithDescriptor:pinelineDesc error:nil];
    
}

- (void)setupCommandQueue {
    // 初始化commandQueue
    self.commandQueue = [_device newCommandQueue];
}

然后需要預先加載好紋理數(shù)據(jù)撞羽,因為這里我們用到了圖片,所以需要讀取圖片對應的字節(jié)

- (Byte *)loadImage:(UIImage *)image {
    CGImageRef imageRef = image.CGImage;
    
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    
    Byte *data = (Byte *)calloc(width * height * 4, sizeof(Byte)); // rgba 4個字節(jié)
    
    CGContextRef context = CGBitmapContextCreate(data, width, height, 8, width * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
    
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    
    CGContextRelease(context);
    
    return data;
    
}

再根據(jù)圖像字節(jié)獲取到id <MTLTexture>類型的紋理數(shù)據(jù):

- (void)setupFragment {
    UIImage *image = self.image;
    MTLTextureDescriptor *textureDesc = [MTLTextureDescriptor new];
    textureDesc.pixelFormat = MTLPixelFormatRGBA8Unorm;
    textureDesc.width = image.size.width;
    textureDesc.height = image.size.height;
    self.texture = [_device newTextureWithDescriptor:textureDesc];
    
    MTLRegion region = {
        {0, 0, 0},
        {textureDesc.width, textureDesc.height, 1}
    };
    Byte *imageBytes = [self loadImage:image];
    if (imageBytes) {
        [self.texture replaceRegion:region mipmapLevel:0 withBytes:imageBytes bytesPerRow:image.size.width * 4];
        free(imageBytes);
        imageBytes = NULL;
    }
}

然后我們需要設置頂點數(shù)據(jù)衫冻,這里需要說明一下Metal的坐標系:

頂點坐標系是四維的(x, y, z, w)诀紊,原點在圖片的正中心。

頂點坐標系

紋理坐標系是二維的(x, y)隅俘,原點在圖片的左上角邻奠。

紋理坐標系

得結構體:

typedef struct {
    vector_float4 position;
    vector_float2 textureCoordinate;
} HobenVertex;

當我們需要繪制一個矩形圖片時,需要將頂點坐標和紋理坐標一一對應

float heightScaling = 1.0;
float widthScaling = 1.0;
HobenVertex vertices[] = {
    // 頂點坐標 x, y, z, w  --- 紋理坐標 x, y
    { {-widthScaling,  heightScaling, 0.0, 1.0}, {0.0, 0.0} },
    { { widthScaling,  heightScaling, 0.0, 1.0}, {1.0, 0.0} },
    { {-widthScaling, -heightScaling, 0.0, 1.0}, {0.0, 1.0} },
    { { widthScaling, -heightScaling, 0.0, 1.0}, {1.0, 1.0} },
};

配置好MTKView为居、MTLDevice以及MTLCommandQueue后碌宴,也設置好紋理數(shù)據(jù)后,接下來我們就開始處理渲染回調了蒙畴。

前文有提到贰镣,渲染回調主要是設置好MTLCommandBuffer的數(shù)據(jù),并且commit掉膳凝,而這個過程中碑隆,主要是把紋理、頂點等數(shù)據(jù)放進.metal文件處理蹬音,獲取到對應像素的顏色上煤。

#pragma mark - MTKViewDelegate

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
    // MTKView的大小改變
    self.viewportSize = (vector_uint2){size.width, size.height};
}

- (void)drawInMTKView:(MTKView *)view {
    // 用于向著色器傳遞數(shù)據(jù)
    id <MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
    MTLRenderPassDescriptor *renderDesc = view.currentRenderPassDescriptor;
    if (!renderDesc) {
        [commandBuffer commit];
        return;
    }
    renderDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1);
    [self setupVertex:renderDesc];
    id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderDesc];
    [renderEncoder setViewport:(MTLViewport){0, 0, self.viewportSize.x, self.viewportSize.y, -1, 1}];
    // 映射.metal文件的方法
    [renderEncoder setRenderPipelineState:self.pipelineState];
    // 設置頂點數(shù)據(jù)
    [renderEncoder setVertexBuffer:self.vertices offset:0 atIndex:0];
    // 設置紋理數(shù)據(jù)
    [renderEncoder setFragmentTexture:self.texture atIndex:0];
    // 開始繪制
    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:self.numVertices];
    // 結束渲染
    [renderEncoder endEncoding];
    // 提交
    [commandBuffer presentDrawable:view.currentDrawable];
    [commandBuffer commit];
}

簡要回顧介紹一下流程圖:

--- 初始化階段 ---

  1. 配置 Device 、 Queue著淆、MTKView(初始化階段劫狠,只初始化一次)
  2. 配置 PipelineState (設置和.metal文件映射方法,只初始化一次)
  3. 創(chuàng)建資源永部,讀取紋理MTLTexture(只初始化一次)
  4. 設置頂點MTLBuffer(最好只初始化一次)

--- 渲染階段独泞,drawInMTKView回調,每幀渲染一次 ---

  1. 根據(jù)Queue獲取 CommandBuffer
  2. 根據(jù)CommandBuffer和RenderPassDescriptor配置 CommandBufferEncoder
  3. Encoder Buffer 【如有需要的話可以用 Threadgroups 來分組 Encoder 數(shù)據(jù)】

--- 結束苔埋,提交渲染命令懦砂,在完成渲染后,將命令緩存區(qū)提交至GPU ---

  1. 提交到 Queue 中


3. Metal在Shader層的渲染步驟

Metal Shader語言,即MSL孕惜,是基于C++ 11.0設計的,關于語言規(guī)范有個超詳細的官方文檔晨炕,也有別人博客總結的太長不看版衫画,當你讀到這里的時候可能會比較懵,可以再回到第一章節(jié)復習一下渲染的步驟和概念瓮栗,著重看一下頂點著色器削罩、片段著色器和紋理的概念,再繼續(xù)看费奸。這一節(jié)簡單地講講本次需求需要的.metal文件弥激。

1) 結構體

MSL的結構體可以自定義,但是對于渲染來說愿阐,一般至少需要這兩種數(shù)據(jù):頂點坐標(xyzw四維)微服、紋理坐標(xy兩維),這里我們定義一個包含上述兩個變量的數(shù)據(jù)結構:

typedef struct {
    float4 vertexPosition [[ position ]];
    float2 textureCoor;
} RasterizerData;

[[ position ]]是一個句柄缨历,即聲明了vertexPosition這個變量是[[ position ]]類型的以蕴,這個類型的變量表明:

  • 在頂點著色函數(shù)中,表示當前的頂點信息辛孵,類型是float4

  • 還可以表示描述了片元的窗口的相對坐標(x丛肮,y,z魄缚,1/w)宝与,即該像素點在屏幕上的位置信息

我們聲明了一個結構體,這個結構體會在頂點著色器(Vertex Shader)生成冶匹,經(jīng)過系統(tǒng)處理(形狀裝配习劫、幾何著色器、光柵化)后徙硅,作為結構體來到片段著色器(Fragment Shader)榜聂。

2) 頂點著色器

著色器函數(shù)和C++函數(shù)大同小異,有一個聲明嗓蘑,有一個返回值须肆,一個函數(shù)名,n個輸入桩皿。

vertex RasterizerData vertexShader(uint vertexId [[ vertex_id ]],
                                   constant HobenVertex *vertexArray [[ buffer(0) ]]) {
    RasterizerData out;
    out.vertexPosition = vertexArray[vertexId].position;
    out.textureCoor = vertexArray[vertexId].textureCoordinate;
    return out;
}

頂點著色器以vertex為修飾符豌汇,返回RasterizerData數(shù)據(jù)結構并作為片段著色器的輸入,需要輸入索引和頂點緩存數(shù)組泄隔。

[[ vertex_id ]] 是頂點id標識符拒贱,即索引,他并不由開發(fā)者傳遞;

[[buffer(index)]] 是index的緩存類型逻澳,對應OC語言的

[renderEncoder setVertexBuffer:buffer offset:0 atIndex:index];

這里的buffer就是我們事先設置好的坐標映射:

HobenVertex vertices[] = {
    // 頂點坐標 x, y, z, w  --- 紋理坐標 x, y
    { {-widthScaling,  heightScaling, 0.0, 1.0}, {0.0, 0.0} },
    { { widthScaling,  heightScaling, 0.0, 1.0}, {1.0, 0.0} },
    { {-widthScaling, -heightScaling, 0.0, 1.0}, {0.0, 1.0} },
    { { widthScaling, -heightScaling, 0.0, 1.0}, {1.0, 1.0} },
};

我們根據(jù)OC傳入的一堆HobenVertex類型的頂點和對應的索引闸天,將其轉化為MSL對應的結構體RasterizerData,頂點著色器渲染完畢斜做。

3) 片段著色器

當系統(tǒng)處理好一切苞氮,返回給我們一個光柵化后的數(shù)據(jù)時,我們需要根據(jù)OC傳入的紋理數(shù)據(jù)進行采樣瓤逼、上色笼吟。

fragment float4 fragmentShader(RasterizerData input [[ stage_in ]],
                               texture2d <float> colorTexture [[ texture(0) ]]) {
    constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);
    float4 colorSample = colorTexture.sample(textureSampler, input.textureCoor);
    return float4(colorSample);
}

片段著色器以fragment為修飾符,返回float4數(shù)據(jù)結構(即該像素的rgba)霸旗,需要輸入光柵化處理好的數(shù)據(jù)和紋理數(shù)據(jù)贷帮。

[[ stage_in ]]是由頂點著色函數(shù)輸出然后經(jīng)過光柵化生成的數(shù)據(jù),這是系統(tǒng)生成的诱告,無需我們進行設置和輸入撵枢。

[[ texture(index) ]]代表紋理數(shù)據(jù),index對應OC語言設置里面的

// 設置紋理數(shù)據(jù)
[renderEncoder setFragmentTexture:texture atIndex:index];

texture2d<T, access a = access::sample>代表這是一個紋理數(shù)據(jù)蔬啡,其中T可以是half诲侮、float、short箱蟆、int等沟绪,access表示紋理訪問權限,當access沒寫時空猜,默認是sample绽慈,還可以設置為sample(可讀寫可采樣)、read(只讀)辈毯、write(可讀寫)坝疼。

當然我們還需要設置一個采樣器去對紋理進行采樣,在Metal程序中初始化的采樣器必須使用constexpr修飾符聲明谆沃,所以需要用constexpr sampler聲明钝凶。采樣器的其他設置看下圖:

最后根據(jù)光柵化數(shù)據(jù)的紋理坐標進行采樣即可勇垛。至此再登,片段著色器著色結束,我們所有的渲染流程也結束了辙喂。

四. 總結

可能看到這里据沈,你已經(jīng)懵掉了哟沫,怎么畫個圖片也這么難?這是很正常的锌介,如果你一點圖形渲染的知識都沒有掌握的話嗜诀,看完這篇文章并好好消化一下猾警,你就可以初步認識圖形渲染、Metal渲染的相關知識了隆敢。這也是我根據(jù)多篇文章摸爬滾打探索出來的一些知識发皿,如果對你有幫助的話不妨點個贊吧~

好久好久沒更新博客了,最近幾個月忙拂蝎,也遇到了一些小困難雳窟,需要掙扎掙扎著慢慢前行,希望自己能夠放下浮躁的心匣屡,務實地成長吧!接下來有幾個小目標:用Metal處理視頻流拇涤、學會Metal調試捣作、完成老大給的需求、做一些比較炫酷的特效鹅士,希望自己能繼續(xù)加油券躁!

附源碼,多敲幾遍就熟了:

//
//  ViewController.m
//  HobenLearnMetal
//
//  Created by Hoben on 2021/1/4.
//

#import "HobenMetalImageController.h"
#import <MetalKit/MetalKit.h>
#import "HobenShaderType.h"
#import <AVFoundation/AVFoundation.h>

typedef NS_ENUM(NSUInteger, HobenRenderingResizingMode) {
    HobenRenderingResizingModeScale = 0,
    HobenRenderingResizingModeAspect,
    HobenRenderingResizingModeAspectFill,
};

@interface HobenMetalImageController () <MTKViewDelegate>

@property (nonatomic, strong) MTKView *mtkView;

@property (nonatomic, strong) id <MTLRenderPipelineState> pipelineState;

@property (nonatomic, strong) id <MTLCommandQueue> commandQueue;

@property (nonatomic, strong) id <MTLBuffer> vertices;

@property (nonatomic, assign) NSUInteger numVertices;

@property (nonatomic, strong) id <MTLTexture> texture;

@property (nonatomic, strong) UIImage *image;

@property (nonatomic, assign) vector_uint2 viewportSize;

@property (nonatomic, weak) id <MTLDevice> device;

@end

@implementation HobenMetalImageController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.image = [UIImage imageNamed:@"reus"];
    
    [self setupMTKView];
    
    [self setupCommandQueue];
    
    [self setupFragment];
    
    [self setupPineline];
}

- (void)setupFragment {
    UIImage *image = self.image;
    MTLTextureDescriptor *textureDesc = [MTLTextureDescriptor new];
    textureDesc.pixelFormat = MTLPixelFormatRGBA8Unorm;
    textureDesc.width = image.size.width;
    textureDesc.height = image.size.height;
    self.texture = [_device newTextureWithDescriptor:textureDesc];
    
    MTLRegion region = {
        {0, 0, 0},
        {textureDesc.width, textureDesc.height, 1}
    };
    Byte *imageBytes = [self loadImage:image];
    if (imageBytes) {
        [self.texture replaceRegion:region mipmapLevel:0 withBytes:imageBytes bytesPerRow:image.size.width * 4];
        free(imageBytes);
        imageBytes = NULL;
    }
}

- (Byte *)loadImage:(UIImage *)image {
    CGImageRef imageRef = image.CGImage;
    
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    
    Byte *data = (Byte *)calloc(width * height * 4, sizeof(Byte)); // rgba 4個字節(jié)
    
    CGContextRef context = CGBitmapContextCreate(data, width, height, 8, width * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
    
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    
    CGContextRelease(context);
    
    return data;
    
}

- (void)setupMTKView {
    // 初始化MTKView
    self.mtkView = [[MTKView alloc] init];
    self.mtkView.delegate = self;
    self.device = self.mtkView.device = MTLCreateSystemDefaultDevice();
    self.mtkView.frame = self.view.bounds;
    [self.view addSubview:self.mtkView];
}

- (void)setupPineline {
    // 初始化pipelineState
    MTLRenderPipelineDescriptor *pinelineDesc = [MTLRenderPipelineDescriptor new];
    id <MTLLibrary> library = [_device newDefaultLibrary];
    pinelineDesc.vertexFunction = [library newFunctionWithName:@"vertexShader"];
    pinelineDesc.fragmentFunction = [library newFunctionWithName:@"fragmentShader"];
    pinelineDesc.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
    self.pipelineState = [_device newRenderPipelineStateWithDescriptor:pinelineDesc error:nil];
    
}

- (void)setupCommandQueue {
    // 初始化commandQueue
    self.commandQueue = [_device newCommandQueue];
}

#pragma mark - MTKViewDelegate

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
    // MTKView的大小改變
    self.viewportSize = (vector_uint2){size.width, size.height};
}

- (void)drawInMTKView:(MTKView *)view {
    // 用于向著色器傳遞數(shù)據(jù)
    id <MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
    MTLRenderPassDescriptor *renderDesc = view.currentRenderPassDescriptor;
    if (!renderDesc) {
        [commandBuffer commit];
        return;
    }
    renderDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1);
    [self setupVertex:renderDesc];
    id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderDesc];
    [renderEncoder setViewport:(MTLViewport){0, 0, self.viewportSize.x, self.viewportSize.y, -1, 1}];
    // 映射.metal文件的方法
    [renderEncoder setRenderPipelineState:self.pipelineState];
    // 設置頂點數(shù)據(jù)
    [renderEncoder setVertexBuffer:self.vertices offset:0 atIndex:0];
    // 設置紋理數(shù)據(jù)
    [renderEncoder setFragmentTexture:self.texture atIndex:0];
    // 開始繪制
    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:self.numVertices];
    // 結束渲染
    [renderEncoder endEncoding];
    // 提交
    [commandBuffer presentDrawable:view.currentDrawable];
    [commandBuffer commit];
}

- (void)setupVertex:(MTLRenderPassDescriptor *)renderPassDescriptor {
    
    if (self.vertices) {
        return;
    }
    UIImage *image = self.image;
    float heightScaling = 1.0;
    float widthScaling = 1.0;
    CGSize drawableSize = CGSizeMake(renderPassDescriptor.colorAttachments[0].texture.width, renderPassDescriptor.colorAttachments[0].texture.height);
    CGRect bounds = CGRectMake(0, 0, drawableSize.width, drawableSize.height);
    CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(image.size, bounds);
    
    HobenRenderingResizingMode fillMode = HobenRenderingResizingModeAspect;
    
    switch (fillMode) {
        case HobenRenderingResizingModeScale: {
            widthScaling = 1.0;
            heightScaling = 1.0;
        };
            break;
        case HobenRenderingResizingModeAspect:
        {
            widthScaling = insetRect.size.width / drawableSize.width;
            heightScaling = insetRect.size.height / drawableSize.height;
        };
            break;
        case HobenRenderingResizingModeAspectFill:
        {
            widthScaling = drawableSize.height / insetRect.size.height;
            heightScaling = drawableSize.width / insetRect.size.width;
        };
            break;
    }
    
    HobenVertex vertices[] = {
        // 頂點坐標 x, y, z, w  --- 紋理坐標 x, y
        { {-widthScaling,  heightScaling, 0.0, 1.0}, {0.0, 0.0} },
        { { widthScaling,  heightScaling, 0.0, 1.0}, {1.0, 0.0} },
        { {-widthScaling, -heightScaling, 0.0, 1.0}, {0.0, 1.0} },
        { { widthScaling, -heightScaling, 0.0, 1.0}, {1.0, 1.0} },
    };
    
    self.vertices = [_device newBufferWithBytes:vertices length:sizeof(vertices) options:MTLResourceStorageModeShared];
    self.numVertices = sizeof(vertices) / sizeof(HobenVertex);
}

@end

//
//  HobenShaderType.h
//  HobenLearnMetal
//
//  Created by Hoben on 2021/1/4.
//

#ifndef HobenShaderType_h
#define HobenShaderType_h

typedef struct {
    vector_float4 position;
    vector_float2 textureCoordinate;
} HobenVertex;

#endif /* HobenShaderType_h */
//
//  Shaders.metal
//  HobenLearnMetal
//
//  Created by Hoben on 2021/1/4.
//

#include <metal_stdlib>
#import "HobenShaderType.h"

using namespace metal;

typedef struct {
    float4 vertexPosition [[ position ]];
    float2 textureCoor;
} RasterizerData;

vertex RasterizerData vertexShader(uint vertexId [[ vertex_id ]],
                                   constant HobenVertex *vertexArray [[ buffer(0) ]]) {
    RasterizerData out;
    out.vertexPosition = vertexArray[vertexId].position;
    out.textureCoor = vertexArray[vertexId].textureCoordinate;
    return out;
}

fragment float4 fragmentShader(RasterizerData input [[ stage_in ]],
                               texture2d <float> colorTexture [[ texture(0) ]]) {
    constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);
    float4 colorSample = colorTexture.sample(textureSampler, input.textureCoor);
    return float4(colorSample);
}

參考文章:

Learn OpenGL中文文檔

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

Metal 案例04:加載圖片

iOS 圖像渲染原理

計算機那些事(8)——圖形圖像渲染原理

WWDC 2018:寫給 OpenGL 開發(fā)者們的 Metal 開發(fā)指南

Metal 系列教程(1)- Metal 介紹及基本使用

超詳細的Metal官方文檔

Metal Shader language (著色語言規(guī)范)總結

Metal學習筆記01 渲染一個三角形

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掉盅,一起剝皮案震驚了整個濱河市也拜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趾痘,老刑警劉巖慢哈,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異永票,居然都是意外死亡卵贱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門侣集,熙熙樓的掌柜王于貴愁眉苦臉地迎上來键俱,“玉大人,你說我怎么就攤上這事世分”嗾瘢” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵臭埋,是天一觀的道長踪央。 經(jīng)常有香客問我,道長斋泄,這世上最難降的妖魔是什么杯瞻? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮炫掐,結果婚禮上魁莉,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好旗唁,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布畦浓。 她就那樣靜靜地躺著,像睡著了一般检疫。 火紅的嫁衣襯著肌膚如雪讶请。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天屎媳,我揣著相機與錄音夺溢,去河邊找鬼。 笑死烛谊,一個胖子當著我的面吹牛风响,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丹禀,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼状勤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了双泪?” 一聲冷哼從身側響起持搜,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎焙矛,沒想到半個月后葫盼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡村斟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年剪返,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邓梅。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡脱盲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出日缨,到底是詐尸還是另有隱情钱反,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布匣距,位于F島的核電站面哥,受9級特大地震影響,放射性物質發(fā)生泄漏毅待。R本人自食惡果不足惜尚卫,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尸红。 院中可真熱鬧吱涉,春花似錦刹泄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鳖链,卻和暖如春姆蘸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芙委。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工逞敷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灌侣。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓兰粉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親顶瞳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容