在我對(duì)《UnityShader入門(mén)精要》——馮樂(lè)樂(lè) 第六章 進(jìn)行學(xué)習(xí)的過(guò)程中,對(duì)于第一個(gè)示例中有個(gè)疑問(wèn):基于頂點(diǎn)的著色(Gouraud shading)處理時(shí),并沒(méi)有看見(jiàn)重心插值得到片元顏色這部分邏輯的代碼呀。所以我又回頭讀了第二章,其實(shí)帶著問(wèn)題去讀,要比拿到書(shū)就從第一頁(yè)開(kāi)始讀效果更好。讀完之后覺(jué)得第二章涉及的知識(shí)點(diǎn),比閆令琪Game101課程中講的要詳細(xì)很多,本文對(duì)部分內(nèi)容做筆記,詳情參見(jiàn)原書(shū)。
一、概述
渲染流水線(xiàn)的工作任務(wù)在于由一個(gè)三維場(chǎng)景出發(fā)、生成(或者說(shuō)渲染)一張二維圖像。換句話(huà)說(shuō),計(jì)算機(jī)需要從一系列的頂點(diǎn)數(shù)據(jù)、紋理等信息出發(fā),把這些信息最終轉(zhuǎn)換成一張人眼可以看到的圖像。而這個(gè)工作通常是由CPU和GPU共同完成的。
《Real-Time Rendering, Third Edition》一書(shū)中將一個(gè)渲染流程分為了3個(gè)階段:應(yīng)用階段、幾何階段、光柵化階段。
注意,這里僅僅是概念性階段,每個(gè)階段本身通常也是一個(gè)流水線(xiàn)系統(tǒng),既包含了自流水線(xiàn)階段。下圖顯示了三個(gè)概念階段之間的關(guān)系。

二、應(yīng)用階段
渲染流水線(xiàn)的起點(diǎn)是CPU,即應(yīng)用階段,應(yīng)用階段大致可分為下面三個(gè)階段:
- (1)把數(shù)據(jù)加載到顯存中
- (2)設(shè)置渲染狀態(tài)
- (3)調(diào)用Draw Call
1.把數(shù)據(jù)加載到顯存中
所有渲染所需的數(shù)據(jù)都需要從硬盤(pán)(HDD)中加載到系統(tǒng)內(nèi)存(RAM)中。然后網(wǎng)格和紋理等數(shù)據(jù)又會(huì)被加載到顯卡上的存儲(chǔ)空間——顯存(VRAM)中。這是因?yàn)轱@卡對(duì)顯存的訪(fǎng)問(wèn)速度更快,而且大多數(shù)顯卡對(duì)于RAM沒(méi)有直接的訪(fǎng)問(wèn)權(quán)利。
2.設(shè)置渲染狀態(tài)
什么是渲染狀態(tài)?一個(gè)通俗的解釋是,這些狀態(tài)定義了場(chǎng)景中的網(wǎng)格是怎樣被渲染的。例如,使用哪個(gè)頂點(diǎn)著色器(Vertex Shader)/片元著色器(Fragment Shader)、光源屬性、材質(zhì)等。如果我們沒(méi)有更改渲染狀態(tài),那么所有網(wǎng)格都將使用同一種渲染狀態(tài)。
在準(zhǔn)備好上述工作后,CPU就需要調(diào)用一個(gè)渲染命令來(lái)告訴GPU:嘿,老兄我?guī)湍惆褦?shù)據(jù)準(zhǔn)備好啦,你可以按照我的設(shè)置開(kāi)始渲染啦“!”而這個(gè)渲染命令就是Draw Call。
3.調(diào)用Draw Call
Draw Call就是一個(gè)命令,它的發(fā)起方是CPU,接收方是GPU。這個(gè)命令僅僅會(huì)指向一個(gè)需要被渲染的圖元(primitives)列表,而不會(huì)再包含任何材質(zhì)信息——上一階段我們已經(jīng)完成。
當(dāng)給定了一個(gè)Draw Call時(shí),GPU就會(huì)根據(jù)渲染狀態(tài)(例如材質(zhì)、紋理。著色器等)和所有輸入的頂點(diǎn)數(shù)據(jù)來(lái)進(jìn)行計(jì)算,最終輸出成屏幕上顯示的那些漂亮的像素。而這個(gè)計(jì)算過(guò)程就是GPU流水線(xiàn)。
三、GPU流水線(xiàn)
對(duì)于概念的后兩個(gè)階段,即幾何階段和光柵化階段,開(kāi)發(fā)者無(wú)法擁有絕對(duì)的控制權(quán),其實(shí)現(xiàn)的載體是GPU。GPU通過(guò)實(shí)現(xiàn)流水線(xiàn)化,大大加快了渲染速度。雖然我們無(wú)法完全控制這兩個(gè)階段的實(shí)現(xiàn)細(xì)節(jié),但GPU向開(kāi)發(fā)者開(kāi)發(fā)了很多控制權(quán)。

從圖中可以看出,GPU渲染流水線(xiàn)接收頂點(diǎn)數(shù)據(jù)作為輸入。這些頂點(diǎn)數(shù)據(jù)是由應(yīng)用階段加載到顯存中,再由Draw Call指定的。這些數(shù)據(jù)隨后被傳遞給頂點(diǎn)著色器。
- 頂點(diǎn)著色器(Vertex Shader)是完全可編程的,它通常用于實(shí)現(xiàn)頂點(diǎn)的空間變換、頂點(diǎn)著色等功能。
- 曲面細(xì)分著色器(Tessellation Shader)是一個(gè)可選著色器,它用于細(xì)分圖元。
- 幾何著色器(Geometry Shader)同樣是一個(gè)可選的著色器,它可以被用于逐圖元(Per-primitive)的著色操作,或者被用于產(chǎn)生更多的圖元。
- 下一個(gè)流水線(xiàn)階段是裁剪(Clipping),這一階段的目的是將那些不在攝像機(jī)視野內(nèi)的頂點(diǎn)裁減掉,并剔除某些三角圖元的面片。這個(gè)階段是可配置的。例如,我們可以使用自定義的裁剪平面來(lái)配置裁剪區(qū)域,也可以通過(guò)指令控制裁剪三角圖元的正面還是背面。
- 幾何概念階段的最后一個(gè)流水線(xiàn)階段是屏幕映射(Screen Mapping)。這一階段是不可配置和編程的,它負(fù)責(zé)把每個(gè)圖元的坐標(biāo)轉(zhuǎn)換到屏幕坐標(biāo)系中。
- 光柵化概念階段中的三角形設(shè)置(Triangle Setup)和三角形遍歷(Triangle Traversal)階段也都是固定函數(shù)(Fixed-Function)的階段。
- 接下來(lái)的片元著色器(Fragment Shader),則是完全可編程的,它用于實(shí)現(xiàn)逐片元(Per-Fragment)的著色操作。
- 最后逐片元操作(Per-Fragment Operations)階段負(fù)責(zé)執(zhí)行很多重要的操作,例如修改顏色、深度緩沖、進(jìn)行混合等,它不是可編程的,但具有很高的可配置性。
1.頂點(diǎn)著色器
頂點(diǎn)著色器(Vertex Shader)是流水線(xiàn)的第一階段,它的輸入來(lái)自于CPU。頂點(diǎn)著色器的處理單位是頂點(diǎn),也就是說(shuō)輸入進(jìn)來(lái)的每個(gè)頂點(diǎn)都會(huì)調(diào)用一次頂點(diǎn)著色器。頂點(diǎn)著色器本身不可以創(chuàng)建或銷(xiāo)毀任何頂點(diǎn),而且無(wú)法得到頂點(diǎn)與頂點(diǎn)之間的關(guān)系。例如我們無(wú)法得知兩個(gè)頂點(diǎn)是否屬于同一個(gè)三角網(wǎng)格。但正是因?yàn)檫@樣的相互獨(dú)立性,GPU可以利用本身的特性并行化處理每個(gè)頂點(diǎn),這意味著這一階段處理速度會(huì)很快。
頂點(diǎn)著色器需要完成的主要工作有:坐標(biāo)變換和逐頂點(diǎn)光照。當(dāng)然,除了這兩個(gè)主要任務(wù)外,頂點(diǎn)著色器還可以輸出后續(xù)階段所需的數(shù)據(jù)。下圖展示了在頂點(diǎn)著色器中對(duì)頂點(diǎn)位置進(jìn)行坐標(biāo)變換并計(jì)算頂點(diǎn)顏色的過(guò)程。

插入一下:這里就提到了,需要時(shí)還可以計(jì)算和輸出頂點(diǎn)的顏色。也就是基于頂點(diǎn)的著色(Gouraud shading)。
2.頂點(diǎn)著色器中的坐標(biāo)變換
上圖中還說(shuō)了,這一步必須進(jìn)行頂點(diǎn)坐標(biāo)變換。也就是第五章示例代碼中將mul (UNITY_MVP, v.position)升級(jí)過(guò)后的o.pos = UnityObjectToClipPos(v.vertex);當(dāng)然第二章也專(zhuān)門(mén)花了篇幅說(shuō)明這一點(diǎn)。
頂點(diǎn)著色器可以在這一步中改變頂點(diǎn)的位置,這在頂點(diǎn)動(dòng)畫(huà)中是非常有用的。例如,我們可以改變頂點(diǎn)的位置來(lái)模擬水面、布料等。但需要注意的是,無(wú)論我們?cè)陧旤c(diǎn)著色器中怎樣改變頂點(diǎn)的位置,一個(gè)最基本的頂點(diǎn)著色器必須完成的一個(gè)工作是,把頂點(diǎn)坐標(biāo)從模型空間轉(zhuǎn)換到齊次裁剪空間,就是類(lèi)似下面的代碼,o.pos = mul (UNITY_MVP, v.position);
類(lèi)似上面這句代碼的功能,就是把頂點(diǎn)坐標(biāo)轉(zhuǎn)換到齊次裁剪坐標(biāo)系下,接著通常再由硬件做透視除法后,最終得到歸一化的設(shè)備坐標(biāo)(Normalized Device Coordinates, NDC)

需要注意的是,圖給出的坐標(biāo)范圍是OpenGL同時(shí)也是Unity使用的NDC,它的Z分量范圍在[-1, 1]之間,而在DirectX中,NDC的z分量范圍是[0,1]。頂點(diǎn)著色器可以有不同的輸出方式。最常見(jiàn)的輸出路徑是經(jīng)光柵化后交給片元著色器進(jìn)行處理。而在現(xiàn)代的Shader Model中,它還可以把數(shù)據(jù)發(fā)送給曲面細(xì)分著色器或幾何著色器,感興趣的話(huà)可以自行了解。
3.裁剪
由于我們的場(chǎng)景會(huì)很大,而攝像機(jī)的視野范圍很有可能不會(huì)覆蓋所有的場(chǎng)景物體,一個(gè)很自然的想法是,那些不在攝像機(jī)視野范圍內(nèi)的物體不需要處理。而裁剪(Clipping)就是為了完成這個(gè)目的被提出來(lái)的。
一個(gè)圖元和攝像機(jī)的視野關(guān)系有3種:完全在視野內(nèi)、部分在視野內(nèi)、完全在視野外。完全在視野內(nèi)的圖元就繼續(xù)傳遞給下一個(gè)流水線(xiàn)階段,完全在視野外的圖元不會(huì)向下傳遞,因?yàn)樗鼈儾恍枰讳秩?。而那些部分在視野?nèi)的圖元需要進(jìn)行一個(gè)處理,這就是裁剪。例如,一條線(xiàn)段的一個(gè)頂點(diǎn)在視野內(nèi),而另一個(gè)頂點(diǎn)不在視野內(nèi),那么視野外部的頂點(diǎn)應(yīng)該使用一個(gè)新的頂點(diǎn)來(lái)代替,這個(gè)新的頂點(diǎn)位于這條線(xiàn)段和視野邊界的交點(diǎn)處。
由于我們已知在NDC下的頂點(diǎn)位置,即頂點(diǎn)位置在一個(gè)立方體內(nèi),因此裁剪就變得很簡(jiǎn)單:只需要將圖元裁減到單位立方體內(nèi)。下圖展示了這樣的一個(gè)過(guò)程。

和頂點(diǎn)著色器不同,這一步是不可編程的,即我們無(wú)法通過(guò)編程來(lái)控制裁剪的過(guò)程,而是硬件上的固定操作,但我們可以自定義一個(gè)裁剪操作來(lái)對(duì)這一步進(jìn)行配置。
4.屏幕映射
這一步輸入的坐標(biāo)仍然是三維坐標(biāo)系下的坐標(biāo)(范圍在單位立方體內(nèi))。屏幕映射(Screen Mapping)的任務(wù)是把每個(gè)圖元的x和y坐標(biāo)轉(zhuǎn)換到屏幕坐標(biāo)系(Screen Coordinates)下。屏幕坐標(biāo)系是一個(gè)二維坐標(biāo)系,它和我們用于顯示畫(huà)面的分辨率有很大關(guān)系。
也許大家會(huì)有疑惑,輸入的Z坐標(biāo)會(huì)怎么樣。屏幕映射不會(huì)對(duì)輸入的Z坐標(biāo)進(jìn)行任何處理。實(shí)際上屏幕坐標(biāo)和Z坐標(biāo)一起構(gòu)成了一個(gè)坐標(biāo)系,叫做窗口坐標(biāo)系(Window Coordinates)。這些值會(huì)一起被傳遞到光柵化階段。屏幕映射得到的屏幕坐標(biāo)決定了這個(gè)頂點(diǎn)對(duì)應(yīng)屏幕上哪個(gè)像素以及距離這個(gè)像素有多遠(yuǎn)。
5.三角形設(shè)置
由這一步開(kāi)始進(jìn)入了光柵化階段。從上一個(gè)階段輸出的信息是屏幕坐標(biāo)系下的頂點(diǎn)位置以及和它們相關(guān)的額外信息,如深度值(z坐標(biāo))、法線(xiàn)方向、視角方向等。光柵化階段有兩個(gè)重要的目標(biāo):計(jì)算每個(gè)圖元覆蓋了哪些像素,以及為這些像素計(jì)算它們的顏色。
光柵化的第一個(gè)流水線(xiàn)階段是三角形設(shè)置(Triangle Setup)。這個(gè)階段會(huì)計(jì)算光柵化一個(gè)三角網(wǎng)格所需的信息。具體來(lái)說(shuō),上一個(gè)階段輸出的都是三角網(wǎng)格的頂點(diǎn),即我們得到的是三角網(wǎng)格每條邊的兩個(gè)端點(diǎn)。但如果要得到整個(gè)三角網(wǎng)格對(duì)像素的覆蓋情況,我們必須計(jì)算每條邊上的像素坐標(biāo)。為了能計(jì)算邊界像素的坐標(biāo)信息,我們需要得到三角形邊界的表示方式。這樣一個(gè)計(jì)算三角網(wǎng)絡(luò)表示數(shù)據(jù)的過(guò)程就叫做三角形設(shè)置。它的輸出是為了給下一個(gè)階段做準(zhǔn)備。
6.三角形遍歷
三角形遍歷(Triangle Traversal)階段將會(huì)檢查每個(gè)像素是否被一個(gè)三角網(wǎng)格所覆蓋。如果被覆蓋的話(huà),就會(huì)生成一個(gè)片元(fragment)。而這樣一個(gè)找到哪些像素被三角網(wǎng)格覆蓋的過(guò)程就是三角形遍歷,這個(gè)階段也被稱(chēng)為掃描變換(Scan Conversion)。
三角形遍歷會(huì)根據(jù)上一個(gè)階段的計(jì)算結(jié)果來(lái)判斷一個(gè)三角網(wǎng)格覆蓋了哪些像素,并使用三角網(wǎng)格3個(gè)頂點(diǎn)的頂點(diǎn)信息對(duì)整個(gè)覆蓋區(qū)域的像素進(jìn)行插值,下圖展示了三角形遍歷階段的簡(jiǎn)化計(jì)算過(guò)程。

這一步的傳出就是得到一個(gè)片元序列。需要注意的是,一個(gè)片元并不是真正意義上的像素,而是包含了很多狀態(tài)的集合,這些狀態(tài)用于計(jì)算每個(gè)像素的最終顏色。這些狀態(tài)包括了(但不限于)它的屏幕坐標(biāo)、深度信息,以及其他從幾何階段輸出的頂點(diǎn)信息,例如法線(xiàn)、紋理坐標(biāo)等。
7.片元著色器
片元著色器(Fragment Shader)是另一個(gè)非常重要的可編程著色器階段。在DirectX中,片元著色器被稱(chēng)為像素著色器(Pixel Shader),但片元著色器是一個(gè)更合適的名字,因?yàn)?strong>此時(shí)的片元并不是一個(gè)真正意義上的像素。
前面的光柵化階段實(shí)際上并不會(huì)影響屏幕上每個(gè)像素的顏色值,而是會(huì)產(chǎn)生一系列的數(shù)據(jù)信息,用來(lái)表述一個(gè)三角網(wǎng)格是怎樣覆蓋每個(gè)像素的。而每個(gè)片元就負(fù)責(zé)存儲(chǔ)這樣一系列數(shù)據(jù)。真正會(huì)對(duì)像素產(chǎn)生影響的階段是下一個(gè)流水線(xiàn)階段——逐片元操作(Per-Fragment Operations)。
片元著色器的輸入是上一個(gè)階段對(duì)頂點(diǎn)信息插值得到的結(jié)果,更具體來(lái)說(shuō),是根據(jù)那些從頂點(diǎn)著色器中輸出的數(shù)據(jù)插值得到的。而它的輸出是一個(gè)或者多個(gè)顏色值。下圖展示了這樣的一個(gè)過(guò)程。

這一階段可以完成很多重要的渲染技術(shù),其中重要的技術(shù)之一就是紋理采樣。為了在片元著色器中進(jìn)行紋理采樣,我們通常會(huì)在頂點(diǎn)著色器階段輸出每個(gè)頂點(diǎn)對(duì)應(yīng)的紋理坐標(biāo),然后經(jīng)過(guò)光柵化階段對(duì)三角網(wǎng)格的3個(gè)頂點(diǎn)對(duì)應(yīng)的紋理坐標(biāo)進(jìn)行插值后,就可以得到其覆蓋的片元的紋理坐標(biāo)了。
雖然片元著色器可以完成很多重要效果,但它的局限在于,它僅可以影響單個(gè)片元。也就是說(shuō),當(dāng)執(zhí)行片元著色器時(shí),它不可以將自己執(zhí)行的任何結(jié)果直接發(fā)送給它的鄰居們。有一個(gè)情況例外,就是片元著色器可以訪(fǎng)問(wèn)到導(dǎo)數(shù)信息(gradient,或derivative)。
8.逐片元操作
終于到了渲染管線(xiàn)的最后一步。逐片元操作(Per-Fragment Operations)是OpenGL中的說(shuō)法,在DirectX中,這一說(shuō)法被稱(chēng)為輸出合并階段(Output-Merger)。
這一階段有幾個(gè)主要任務(wù)
- 決定每個(gè)片元的可見(jiàn)性。這涉及了很多測(cè)試工作,例如深度測(cè)試、模板測(cè)試等。
- 如果一個(gè)片元通過(guò)了所有的測(cè)試,就需要把這個(gè)片元的顏色值和已經(jīng)存儲(chǔ)在顏色緩沖區(qū)中的顏色進(jìn)行合并,或者說(shuō)是混合。
需要指明的是,逐片元操作階段是高度可配置性的,即我們可以設(shè)置每一步的操作細(xì)節(jié)。我們后面會(huì)慢慢講到。
這個(gè)階段首先需要解決每個(gè)片元的可見(jiàn)性問(wèn)題,這需要進(jìn)行一系列測(cè)試。一個(gè)片元只有經(jīng)過(guò)了所有的測(cè)試,才能獲得最終個(gè)GPU談判的資格,這個(gè)資格是說(shuō)它可以和顏色緩沖區(qū)進(jìn)行合并。如果他沒(méi)有通過(guò)其中的任何一個(gè)測(cè)試,那么對(duì)不起,之前為了產(chǎn)生這個(gè)片元所做的所有工作都是白費(fèi)的,因?yàn)檫@個(gè)片元會(huì)被舍棄掉。下圖給出了簡(jiǎn)化后的逐片元操作所做的工作。

9.模板測(cè)試(Stencil Test)
我們先來(lái)看模板測(cè)試(Stencil Test)。與之相關(guān)的是模板緩沖(Stencil Buffer)。實(shí)際上。模板緩沖和我們經(jīng)常聽(tīng)到的顏色緩沖、深度緩沖幾乎是一類(lèi)的東西。如果開(kāi)啟了模板測(cè)試,GPU會(huì)首先讀取(使用讀取掩碼)模板緩沖區(qū)中該片元位置的模板值,然后將該值和讀?。ㄊ褂米x取掩碼)到的參考值(reference value)進(jìn)行比較,這個(gè)比較函數(shù)可以是由開(kāi)發(fā)者指定的,例如小于時(shí)舍棄該片元,或者大于等于時(shí)舍棄該片元。如果該片元沒(méi)有通過(guò)這個(gè)測(cè)試,該片元就會(huì)被舍棄。不管一個(gè)片元有沒(méi)有通過(guò)模板測(cè)試·,我們都可以通過(guò)模板測(cè)試和下面的深度測(cè)試結(jié)果來(lái)修改模板緩沖區(qū),這個(gè)修改操作也是由開(kāi)發(fā)者指定的。開(kāi)發(fā)者可以設(shè)置不同結(jié)果下的修改操作,例如在失敗時(shí),模板緩沖區(qū)保持不變,通過(guò)時(shí)將模板緩沖區(qū)中對(duì)應(yīng)的位置加1等。模板緩沖區(qū)通常用于限制渲染的區(qū)域。另外,模板測(cè)試還有一些更高級(jí)的用法,如渲染陰影、輪廓渲染等。
10.深度測(cè)試(Depth Test)
如果一個(gè)片元幸運(yùn)的通過(guò)了模板測(cè)試,那么它會(huì)進(jìn)行下一個(gè)測(cè)試——深度測(cè)試(Depth Test)。這個(gè)測(cè)試同樣也是可以高度配置的。如果開(kāi)啟了深度測(cè)試,GPU會(huì)把該片元的深度值和已經(jīng)存在于深度緩沖區(qū)中的深度值進(jìn)行比較。這個(gè)比較函數(shù)也是可以由開(kāi)發(fā)者設(shè)置的,例如小于時(shí)舍棄該片元,或者大于等于時(shí)舍棄該片元。通常這個(gè)比較函數(shù)是小于等于的關(guān)系,即如果這個(gè)片元的深度值大于等于當(dāng)前深度緩沖區(qū)中的值,那么就會(huì)舍棄他,因?yàn)?,我們總想只顯示出離攝像機(jī)最近的物體,那些被其他物體遮擋的物體就不需要出現(xiàn)在屏幕上。如果這個(gè)片元沒(méi)有通過(guò)這個(gè)測(cè)試,該片元就會(huì)被舍棄。和模板測(cè)試有些不同的是,如果一個(gè)片元沒(méi)有通過(guò)深度測(cè)試,它就沒(méi)有權(quán)利更改深度緩沖區(qū)中的值。而如果它通過(guò)了測(cè)試,開(kāi)發(fā)者還可以指定是否要用這個(gè)片元的深度值覆蓋掉原有的深度值,這是通過(guò)開(kāi)啟/關(guān)閉深度寫(xiě)入來(lái)做到的。后面我們會(huì)發(fā)現(xiàn),透明效果和深度測(cè)試以及深度寫(xiě)入的關(guān)系非常密切。
擴(kuò)展閱讀:
WEBGL Learning(四) 遮罩之模板測(cè)試
3.1 模板測(cè)試和深度測(cè)試
什么是模板測(cè)試,可以把他看作月餅?zāi)>?。模具壓下去,模具以?xún)?nèi)的留了下來(lái),其他部分被舍棄。
image.png
再看下專(zhuān)業(yè)一點(diǎn)的簡(jiǎn)單解釋?zhuān)篠tencil test是per-fragment operations的一種,這就意味著它處于fragment shader (片段著色器)之后,stencil test的主要作用就是根據(jù)stencil buffer的內(nèi)容,來(lái)丟棄不需要的fragments。
11.合并
如果一個(gè)幸運(yùn)的片元通過(guò)了上面的所有測(cè)試,那么它可以自豪地來(lái)到合并功能面前。
為什么需要合并?我們要知道,這里所討論的渲染的過(guò)程是一個(gè)物體接著一個(gè)物體畫(huà)到屏幕上的。而每個(gè)像素的顏色信息都被存儲(chǔ)在一個(gè)名為顏色緩沖的地方。因此當(dāng)我們執(zhí)行這次渲染時(shí),顏色緩沖往往已經(jīng)有了上次渲染之后的顏色結(jié)果,那么我們是使用這次渲染得到的顏色完全覆蓋掉之前的結(jié)果,還是進(jìn)行其它處理?這就是合并需要解決的問(wèn)題。
對(duì)于不透明物體,開(kāi)發(fā)者可以關(guān)閉混合(Blend)操作。這樣片元著色器計(jì)算得到的顏色值就會(huì)直接覆蓋掉顏色緩沖區(qū)中的像素值。但對(duì)于半透明物體,我們就需要使用混合操作來(lái)讓這個(gè)物體看起來(lái)是透明的。下圖是一個(gè)簡(jiǎn)化版的混合操作流程圖

從流程圖中我們可以發(fā)現(xiàn),混合操作也是可以高度配置的:開(kāi)發(fā)者可以選擇開(kāi)啟/關(guān)閉混合功能。
如果沒(méi)有開(kāi)啟混合功能,就會(huì)直接使用片元的顏色覆蓋掉顏色緩沖區(qū)中的顏色,這也是很多初學(xué)者無(wú)法得到透明效果的原因(沒(méi)有開(kāi)啟混合功能)。
如果開(kāi)啟了混合,GPU就會(huì)取出源顏色和目標(biāo)顏色,將兩種顏色進(jìn)行混合。源顏色指的是片元著色器得到的顏色值,而目標(biāo)顏色則是已經(jīng)存在于顏色緩沖區(qū)中的顏色值。之后就會(huì)用一個(gè)混合函數(shù)來(lái)進(jìn)行混合操作。這個(gè)混合函數(shù)通常和透明通道息息相關(guān),例如根據(jù)透明通道的值進(jìn)行相加、相減、相乘等?;旌虾芟馪hotoshop中對(duì)圖層的操作:每一圖層可以選擇混合模式,混合模式?jīng)Q定了該圖層和下層圖層的混合結(jié)果,而我們看到的圖片就是混合后的圖片。
上面給出的測(cè)試順序并不是唯一的,雖然從邏輯上來(lái)說(shuō)這些測(cè)試是在片元著色器之后進(jìn)行的,但對(duì)于大多數(shù)GPU來(lái)說(shuō),它們會(huì)盡可能在執(zhí)行片元著色器之前就進(jìn)行這些測(cè)試。想象一下,當(dāng)GPU在片元著色器階段花了很大的力氣終于計(jì)算出片元的顏色,卻發(fā)現(xiàn)這個(gè)片元根本沒(méi)有通過(guò)這些檢驗(yàn),也就是說(shuō)這個(gè)片元還是被舍棄了,那么之前花費(fèi)的計(jì)算成本全都浪費(fèi)了,如下圖所示:

作為一個(gè)想充分提高性能的GPU,它會(huì)希望盡可能早地知道哪些片元是會(huì)被舍棄的,對(duì)于這些片元就不需要在使用片元著色器來(lái)計(jì)算它們的顏色,在Unity給出的渲染流水線(xiàn)中,我們也可以發(fā)現(xiàn)它給出的深度測(cè)試是在片元著色器之前。這種將深度測(cè)試提前執(zhí)行的技術(shù)通常也被稱(chēng)為Early-Z技術(shù)。
當(dāng)模型的圖元經(jīng)過(guò)了上面層層計(jì)算和測(cè)試后,就會(huì)顯示到我們屏幕上。我們的屏幕顯示的就是顏色緩沖區(qū)的顏色值。但是為了避免我們看到那些正在進(jìn)行光柵化的圖元,GPU會(huì)使用雙重緩沖策略(Double Buffering)。這意味著,對(duì)場(chǎng)景的渲染是在幕后發(fā)生的,即在后置緩沖(Back Buffer)中。一旦場(chǎng)景已經(jīng)被渲染到了后置緩沖中,GPU就會(huì)交換后置緩沖區(qū)和前置緩沖(Front Buffer)中的內(nèi)容,而前置緩沖區(qū)是之前顯示在屏幕中的圖像。由此,保證了我們看到的圖像是連續(xù)的。
四、什么是OpenGL/DirectX
只要接觸過(guò)圖像編程就一定聽(tīng)說(shuō)過(guò)OpenGL和DirectX,也知道這兩者之間存在著競(jìng)爭(zhēng)關(guān)系。
這兩者實(shí)際上就是圖像應(yīng)用編程接口,這些接口用于渲染二維或三維圖形??梢哉f(shuō),這些接口架起了上層應(yīng)用程序和底層GPU的溝通橋梁。一個(gè)應(yīng)用向這些接口發(fā)送渲染命令,而這些接口會(huì)依次向顯卡驅(qū)動(dòng)發(fā)送渲染命令,這些顯卡驅(qū)動(dòng)是真正知道如何與GPU通信的,正是它們把OpenGL或者是DirectX的函數(shù)調(diào)用翻譯成了GPU能夠識(shí)別的指令。
另外,一塊顯卡除了有圖像處理單元GPU之外,還擁有自己的內(nèi)存,這個(gè)內(nèi)存通常被稱(chēng)為顯存。GPU可以在顯存中存儲(chǔ)任何數(shù)據(jù),但是對(duì)于渲染而言有一些數(shù)據(jù)類(lèi)型是必須的,所以一般顯存中都包含圖像緩存,深度緩存,紋理緩存,頂點(diǎn)緩存。
因?yàn)轱@卡驅(qū)動(dòng)的存在,幾乎所有的GPU都可以和OpenGL打交道,也可以和DirectX一起合作。從顯卡的角度出發(fā),實(shí)際上它只需要和顯卡驅(qū)動(dòng)打交道就可以了。而顯卡驅(qū)動(dòng)就好像一個(gè)中介,負(fù)責(zé)和兩方溝通。因此,一個(gè)顯卡制造商為了讓他們的顯卡可以同時(shí)和OpenGL和DirectX合作,就必須提供支持這兩個(gè)接口的顯卡驅(qū)動(dòng)。

五、什么是Draw Call
簡(jiǎn)單來(lái)說(shuō),Draw Call就是CPU調(diào)用GPU圖形繪制接口,如OpenGL中常用的glDrawArrays或者glDrawElements,DX中的DrawIndexedPrimitive等命令。用來(lái)命令GPU執(zhí)行相應(yīng)的繪制任務(wù)。
一個(gè)常見(jiàn)的誤區(qū)是,Draw Call中造成性能的元兇是GPU,認(rèn)為GPU上的狀態(tài)切換是耗時(shí)的,其實(shí)不是的,真正“拖后腿”其實(shí)的是CPU。
1.CPU和GPU是如何實(shí)現(xiàn)并行工作的?
如果沒(méi)有流水線(xiàn)化,那么CPU需要等到上一個(gè)GPU完成上一個(gè)渲染任務(wù)才能再一次發(fā)送渲染命令。但這種方法顯然會(huì)造成效率低下。因此我們需要讓CPU和GPU可以并行工作。而解決方法就是使用一個(gè)命令緩沖區(qū)(Command Buffer)。
命令緩沖區(qū)包含了一個(gè)命令隊(duì)列,由CPU向其中添加命令,而由GPU從中讀取命令,添加和讀取的過(guò)程是相互獨(dú)立的。命令緩沖區(qū)使得CPU和GPU可以相互獨(dú)立的工作。當(dāng)CPU需要渲染一些對(duì)象時(shí),它可以向命令緩沖區(qū)中添加命令,而當(dāng)GPU完成了上一次的渲染任務(wù)后,它就可以從命令隊(duì)列中再取出一個(gè)命令并執(zhí)行它。
命令緩沖區(qū)的命令有很多種類(lèi),而Draw Call是其中一種,其它命令還有改變渲染狀態(tài)等(例如改變使用的著色器,使用不同的紋理等。)下圖顯示了這樣的一個(gè)例子。

2.為什么Draw Call多了會(huì)影響幀率?
每次調(diào)用Draw Call之前,CPU需向GPU發(fā)送很多內(nèi)容,包括數(shù)據(jù)、狀態(tài)和命令等。在這一階段,CPU需要完成很多工作,例如檢查渲染狀態(tài)等。而一旦CPU完成了這些準(zhǔn)備工作,GPU就可以開(kāi)始本次渲染。GPU渲染能力是很強(qiáng)的,渲染200個(gè)或是2000個(gè)三角網(wǎng)格通常沒(méi)有什么區(qū)別,因此渲染速度往往快于CPU提交命令的速度。如果Draw Call的數(shù)量太多,CPU就會(huì)把大量時(shí)間花費(fèi)在提交Draw Call上,造成CPU過(guò)載。如下圖所示:

3.如何減少Draw Call
盡管減少Draw Call的方法很多,但我們這里僅討論使用批處理(Batching)的方法。
我們說(shuō)過(guò),提交大量的Draw Call會(huì)造成CPU的性能瓶頸,即CPU把時(shí)間都花費(fèi)在準(zhǔn)備Draw Call的工作上了。那么一個(gè)很顯然的優(yōu)化想法就是把很多小的DrawCall合并成一個(gè)大的DrawCall,這就是批處理的思想,下圖顯示了批處理所做的工作。
需要注意的是,由于我們需要在CPU內(nèi)存中合并網(wǎng)格,而合并的過(guò)程是需要消耗時(shí)間的。因此,批處理技術(shù)更加適合于那些靜態(tài)物體,例如不會(huì)移動(dòng)的大地、石頭等,對(duì)于這些靜態(tài)物體我們只需要合并一次即可。當(dāng)然,我們也可以對(duì)動(dòng)態(tài)物體進(jìn)行批處理。但是,由于這些物體是不斷運(yùn)動(dòng)的,因此每一幀都需要重新進(jìn)行合并然后發(fā)送給GPU,這對(duì)空間和時(shí)間都會(huì)造成一定影響。

在游戲開(kāi)發(fā)過(guò)程中,為了減小Draw Call的開(kāi)銷(xiāo),有兩點(diǎn)需要注意。
(1)避免使用大量很小的網(wǎng)格。當(dāng)不可避免的需要使用很小的網(wǎng)格結(jié)構(gòu)時(shí),考慮是否可以合并它們。
(2)避免使用過(guò)多的材質(zhì)。盡量在不同的網(wǎng)格之間公用同一個(gè)材質(zhì)。
六、什么是固定管線(xiàn)渲染
固定函數(shù)的流水線(xiàn)(Fixed-Function Pipeline),也簡(jiǎn)稱(chēng)為固定管線(xiàn),通常是指在較舊的GPU上實(shí)現(xiàn)的渲染流水線(xiàn)。這種流水線(xiàn)只給開(kāi)發(fā)者提供一些配置操作,但開(kāi)發(fā)者沒(méi)有對(duì)流水線(xiàn)階段的完全控制權(quán)。
固定管線(xiàn)通常提供了一系列接口,這些接口包含了一個(gè)函數(shù)入口點(diǎn)(Function Entry Points)集合,這些函數(shù)入口點(diǎn)會(huì)匹配GPU上的一個(gè)特定邏輯功能。開(kāi)發(fā)者們通過(guò)這些接口來(lái)控制渲染流水線(xiàn)。換句話(huà)說(shuō),固定渲染管線(xiàn)是只可配置的管線(xiàn)。一個(gè)形象的比喻是,我們?cè)谑褂霉潭ü芫€(xiàn)進(jìn)行渲染時(shí),就好像在控制電路上的多個(gè)開(kāi)關(guān),我們可以選擇打開(kāi)或關(guān)閉一個(gè)開(kāi)關(guān),但永遠(yuǎn)無(wú)法控制整個(gè)電路的排布。
隨著時(shí)代的發(fā)展,GPU的流水線(xiàn)越來(lái)越朝著更高的靈活性和可控性方向發(fā)展,可編程渲染管線(xiàn)應(yīng)運(yùn)而生。具體的可查閱相關(guān)文獻(xiàn),但是在這里說(shuō)明,如果不是為了對(duì)較舊的設(shè)備進(jìn)行兼容,不建議使用固定管線(xiàn)渲染方式。
