一文讀懂什麼是渲染管線(7k字)
渲染(Render)定義
渲染在電腦繪圖中是指軟體從模型生成影像的過程,通俗講就是在電腦裡面給虛擬世界”拍照”。渲染主要分為兩種,一種是預渲染(pre-rendering
),它的計算強度很大,通常用於電影製作;另一種是實時渲染(real-time rendering
),多用於三維遊戲,並且依靠顯示卡完成渲染過程.
渲染管線
渲染是一個非常複雜的過程,它需要從一系列的頂點、紋理等資訊出發,把這些資訊最終轉換成一張人眼可以看到的影像,在這個過程中,通常是需要CPU和GPU密切配合,渲染管線具體的實現細節會嚴重依賴於所使用的軟體和硬體,因此並不存在所謂通用的渲染管線。
tips💁♂:雖然管線的劃分粒度不一樣,但是每個階段的具體功能其實是差不多的,原理也是一樣的,並沒有太大的差異。
以Unity內置渲染管線(Unity Build-in Rendering Pipeline)
為例,可以將其分為CPU應用程式端渲染邏輯和GPU渲染管線,下面是它的完整流程圖,其中藍色區域是CPU端要做的工作,綠色部分是GPU端需要處理的內容。
這些流程看起來很複雜,其實我們可以類比成現實中的拍照。電視劇《隱秘的生活》中,張東升老師帶著他的岳父岳母去爬山,到山頂的時候說要給他們拍照留念,拍照分為三部分,首先是找到要拍照的人擺好POS,然後攝像機找好角度,對準焦距,最後按下快門,其中按快門這個操作對應的是Unity攝像機調用Render()
方法的過程 。在拍照的時候,如果物體超出了攝像機視野範圍或者被其他物體給擋住了,在影像中都不會顯示,這也就是剔除;背後的山在人的後面,這就對應不同的渲染順序;岳父岳母的位置、穿什麼衣服、太陽光照等資訊都會被攝像機捕獲,這些就是要打包的數據。最後調用SetPassCall、DrawCall,將渲染圖元傳遞給GPU渲染管線進行處理。比喻並非十分恰當,主要還是希望能夠加深印象。
剔除、設置渲染順序等操作我們已經記住了,但這些具體是什麼意思呢?
CPU渲染邏輯
該階段通常是由CPU負責實現,作為開發人員,可以對這個階段進行控制。在這個階段主要包含以下幾個步驟:
-
進行剔除(Culling)工作:剔除主要分為三類,分別是
視錐體剔除(Frustum Culling):如果場景中的物體和在視錐體外部,那麼說明物體不可見,不需要對其進行渲染.。在Unity中可以通過設置
Camera
的Field of view
,Clipping Planes
等屬性修改視錐體屬性。層級剔除(Layer Culling Mask):通過給物體設置不同的層級,讓攝像機不渲染某一層,在Unity中可以通過
Culling Mask
屬性設置層級可見性遮擋剔除(Occlusion Culling):當一個物體被其他物體遮擋而不在攝像機的可視範圍內時不對其進行渲染
-
設置渲染順序:渲染順序主要由渲染隊列(Render Queue)的值決定的,不透明隊列(
RenderQueue < 2500
),根據攝像機距離從前往後排序,這樣先渲染離攝像機近的物體,遠處的物體被遮擋剔除;半透明隊列(RenderQueue > 2500
),根據攝像機距離從後往前排序,這是為了保證渲染正確性,例如半透明黃色和藍色物體,不同的渲染順序會出現不一樣的顏色 。 -
打包數據: 將數據提交打包準備發送給GPU,這些數據主要包含三部分,分別是
模型資訊:頂點坐標、法線、UV、切線、頂點顏色、索引列表…
變換矩陣:世界變換矩陣、VP矩陣(根據攝像機位置和fov等參數構建)
燈光、材質參數:shader、材質參數、燈光資訊
-
調用SetPass Call, Draw Call:
SetPass Call: Shader腳本中一個Pass語義塊就是一個完整的渲染流程,一個著色器可以包含多個Pass語義塊,每當GPU運行一個Pass之前,就會產生一個SetPassCall,所以可以理解為調用一個完整渲染流程。
DrawCall:CPU每次調用影像編程介面命令GPU渲染的操作稱為一次Draw Call。Draw Call就是一次渲染命令的調用,它指向一個需要被渲染的圖元(primitive)列表,不包含任何材質資訊。GPU收到指令就會根據渲染狀態(例如材質、紋理、著色器等)和所有輸入的頂點數據來進行計算,最終輸出成螢幕上顯示的那些漂亮的像素。
💁♂
CPU渲染階段最重要的輸出是渲染所需的幾何資訊,即渲染圖元(rendering primitives),通俗來講,渲染圖元可以是點、線、三角面等,這些資訊會傳遞給GPU渲染管線處理。
GPU渲染管線
GPU渲染管線由許多步驟組成,比如頂點處理、圖元裝配及光柵化、片元處理、輸出合併等
頂點處理
-
頂點著色器(Vertex Shader)
頂點著色器的處理單位是頂點,也就是說,輸入進來的每個頂點都會調用一次頂點著色器。它主要執行坐標轉換和逐頂點光照的任務,坐標轉換是將頂點坐標從模型空間轉換到齊次裁剪空間中,它是通過MVP(Model、View、Projection)轉換得到的,在shader程式碼中,可以使用
UnityObjectToClipPos()
函數來實現。逐頂點光照得到的光照結果比較不自然,所以一般是在片元著色器中進行光照計算。 -
曲面細分著色器 (Tessellation Shader)
這是一個可選的著色器,主要是對三角面進行細分,以此來增加物體表面的三角面的數量。藉助它可以實現細節層次(
LOD,Level-of-
Detail
)的機制,使得離攝像機越近的物體具有更加豐富的細節,而遠離攝像機的物體具有較少的細節,如下圖所示。 -
幾何著色器(Geometry Shader)
它也是一個可選的著色器,它以完整的圖元(比如,點)作為輸入數據,輸出可以是一個或多個其他的圖元(比如,三角面),或者不輸出任何的圖元。幾何著色器的拿手好戲就是將輸入的點或線擴展成多邊形。下圖展示了幾何著色器如何將點擴展成多邊形。
圖元裝配(Primitive Assembly)
經過頂點處理階段,我們已經知道了頂點在裁剪空間的位置,接下來可以在裁剪空間中進行裁剪、背面剔除、螢幕映射等操作。
-
裁剪(Clipping)
一些圖元,它可能一部分位於攝像機視野內,另一部分在攝像機視野外部,外面這部分不需要進行渲染,可以將它裁剪掉. 例如,線段的兩個頂點一個位於視椎體內而另一個位於視椎體外,那麼位於外部的頂點將被裁剪掉,而且在視椎體與線段的交界處產生新的頂點來代替視野外部的頂點(在裁剪空間中進行)。
-
標準化設備坐標(Normalized Device Coordinates,NDC)
在裁剪空間的基礎上,進行透視除法
(perspective division)
後得到的坐標叫做NDC坐標,將坐標從裁剪空間的(-w,-w,w)變換為(-1,-1,1),即除 w,獲得NDC坐標是為了實現螢幕坐標的轉換與硬體無關。 -
背面剔除(Back-Face Culling)
背對攝像機的三角面剔除,上面我們講到過模型數據中含有索引列表,列表中的三個點組成一個三角片,如果這三個點是順時針排列的,認為是背面,否則認為是正面。
上圖中的圖元 t1,它的三個頂點v1,v2,v3很明顯是逆時針(count-clockwise,CCW)排列的,因此認為它是正面,t2則是順時針(clockwise,CW)排列,為背面。我們可以使用行列式
(determinant)
來確定投影后的2D三角形到底是CW還是CCW順序。行列式的第一行由頂點v1和v2坐標確定,而第二行由頂點v1和v3坐標確定。如果行列式的值為負數,那麼該三角面是背面朝向;如果為正數,則是正面朝向。 -
螢幕映射(ScreenMapping)
螢幕映射(ScreenMapping)的任務是把每個圖元的x和y坐標轉換到螢幕坐標系(Screen Coordinates)下。螢幕坐標系是一個二維坐標系,它和我們用於顯示畫面的解析度有很大關係。
假設,我們需要把場景渲染到一個窗口上,窗口的範圍是從最小的窗口坐標(x1,y1)到最大的窗口坐標(x2,y2),其中x1< x2且y1< y2。由於我們輸入的坐標範圍在-1到1,因此可以想像到,這個過程實際是一個縮放的過程,如圖2.10所示。你可能會問,那麼輸入的z坐標會怎麼樣呢?螢幕映射不會對輸入的z坐標做任何處理。實際上,螢幕坐標系和z坐標一起構成了一個坐標系,叫做窗口坐標系(WindowCoordinates)。這些值會一起被傳遞到光柵化階段。
光柵化(Rasterization)
該階段主要是將變換到螢幕空間的圖元離散化為片元的過程。
-
三角形設置(Triangle Setup)
我們從上一個階段獲得圖元的頂點資訊,也就是三角面每條邊的兩個端點,但如果要得到整個三角網格對像素的覆蓋情況,我們就必須計算每條邊上的像素坐標。為了能夠計算邊界像素的坐標資訊,我們就需要得到三角形邊界的表示方式。這樣一個計算三角網格表示數據的過程就叫做三角形設置。
-
三角形遍歷(Triangle Traversal)
三角形遍歷階段會根據上一個階段的計算結果來判斷一個三角網格覆蓋了哪些像素,並使用三角網格3個頂點的頂點資訊對整個覆蓋區域的像素進行插值。
這一步的輸出就是得到一個片元序列。需要注意的是,一個片元並不是真正意義上的像素,而是包含了很多狀態的集合,這些狀態用於計算每個像素的最終顏色。這些狀態包括了(但不限於)它的螢幕坐標、深度值Z、頂點顏色,以及其他從幾何階段輸出的頂點資訊,例如法線、紋理坐標等。
片元著色器(Fragment Shader)
它最主要的任務就是著色,光柵化階段實際上並不會影響螢幕上每個像素的顏色值,而是會產生一系列的數據資訊,用來表述一個三角網格是怎樣覆蓋每個像素的。而每個片元就負責存儲這樣一系列數據。著色有兩種最常見的技術,分別是紋理貼圖和光照技術。
紋理貼圖(Textures)
紋理貼圖也稱為紋理映射,是將影像資訊映射到三角形網格上的技術,以此來增加物體表面的細節,令物體更具有真實感。紋理技術有很多,常見的是凹凸貼圖(bump mapping)、法線貼圖(normal mapping)、高度紋理(height mapping)、陰影貼圖(shadowmap)等。例如圖中左邊地球儀是一個球形,但我們也可以將地圖繪製在右圖一張二維的平面上,那麼它們之間就存在著紋理映射的關係,我們想要獲取地球儀上任意一點的資訊,都可以從貼圖中尋找。
紋理貼圖是片段著色器的主要操作,通過貼圖技術可以實現很多高級的效果。我們將貼圖上的每個像素稱為紋素(texel,紋理像素texture pixel的意思,用於和像素進行區分),紋理映射其實就是進行紋素和像素對應的過程。
在上圖右邊是一副32*32的貼圖,它由一格一格的像素組成,每個像素都有一個地址,這個地址就叫做紋素地址。紋素地址可以使用一個二維數組來存儲,這個二維數組就稱為紋素數組。
我們一般使用一個二維的坐標(u,v)來表示紋理坐標,其中u是橫坐標,v是縱坐標,因此紋理坐標一般也被稱為UV坐標。UV坐標一般被歸一化到[0,1]之間,但是如果UV超出這個範圍,我們就需要指定紋理坐標的定址方式,也叫作平鋪方式。常見的定址方式有:重複定址(repeat)、邊緣鉗制定址(clamp,拉伸紋理邊緣)和鏡像定址(mirror)。在Unity中,可以通過設置貼圖的Wrap Mode來修改,其中per-axis可以單獨控制 Unity 如何在 U 軸和 V 軸上包裹紋理。下圖展示了Unity3d中紋理的重複定址和鉗制定址方式。
紋理取樣是指給定一個坐標,去尋找它在紋素數組中的值。由於紋素和像素通常不是一 一對應的(例如將10×10的圖片映射到50×50的螢幕中),所以我們需要決定像素所對應的紋素資訊時,需要用到紋理的濾波方式。
Unity中的濾波主要有三種,可以通過Filter Mode進行設置,
-
Pointer,點過濾,紋理在靠近時變為塊狀,會產生較為明顯的失真。
-
Bilinear,雙線性過濾,pixel對應的紋理坐標為中心,采該紋理坐標周圍4個texel的像素,再取平均,以平均值作為取樣值。
-
Trilinear,三線性過濾,以雙線性過濾為基礎。會對pixel大小與texel大小最接近的兩層Mipmap level分別進行雙線性過濾,然後再對兩層得到的結果進生線性插值。
如果我們要把一個很大的貼圖映射到很小的一塊區域裡面,可以想像效果肯定會很差,這時可以使用Mipmap紋理鏈,也就是根據原圖生成很多個大小不同的圖片,然後根據映射區域的大小,選擇使用哪一張圖片。
在Unity中可以通過勾選Generate Mip Maps屬性來啟用Mipmap,由於生成多張圖片,這會佔用一定的記憶體。勾選後可以通過滑動條來預覽不同等級的貼圖。
光照計算(Lighting)
光照由直接光和間接光組成,計算光照最常用的就是phong模型了,它是一個經驗模型,參數資訊都是經驗得到的,並沒有實際的物理意義,所以利用Phong模型會出現違背物理規則的時候。Phong模型將物體光照分為三個部分進行計算,分別是:漫反射、鏡面反射和環境光。
-
漫反射Difuse
漫反射是投射在粗糙表面上的光向各個方向反射的現象。當一束平行的入射光線射到粗糙的表面時,表面會把光線向著四面八方反射,所以入射線雖然互相平行,由於各點的法線方向不一致,造成反射光線向不同的方向無規則地反射。在漫反射中,視角的位置是不重要的,因為反射是完全隨機的,因此可以認為在任何反射方向上的分布都是一樣的。但是,入射光線的角度很重要。
-
鏡面發射Specular
鏡面反射是指若反射面比較光滑,當平行入射的光線射到這個反射面時,仍會平行地向一個方向反射出來。
-
環境光Ambient
環境光分量是用來模擬全局光照效果的,其實就是在物體光照資訊基礎上疊加上一個較小的光照常量,用來表示場景中其他物體反射的間接光照。
光照模型計劃在下一節進行詳細講解,並通過程式碼實現。
輸出合併(Output-Merger)
終於到了渲染流水線的最後一步,在DirectX
中,該階段被稱為輸出合併階段,而OpenGL
將其稱為逐片元操作(Per-Fragment Operations
),從稱呼中就可以看出,這個階段主要是對每一個片元進行一些輸出合併操作,包括Alpha測試、模板測試、深度測試和混合,它有一下幾個主要任務:
-
決定每個片元的可見性。這涉及了很多測試工作,例如深度測試、模板測試等。為什麼要進行測試呢?因為螢幕上的一個像素可能存在多個片元進行競爭,通過測試等規則,可以決定哪個片元最終能夠渲染到螢幕上
-
如果一個片元通過了所有的測試,就需要把這個片元的顏色值和已經存儲在顏色緩衝區中的顏色進行合併,或者說是混合。
Alpha測試
通過片元數據,可以獲取該片元的alpha值,如果alpha值小於某個數的話,則直接將該片元丟棄,不進行渲染,這是非常「粗暴」的(即只渲染透明度在某一範圍內的片元),可以用來做一些樹葉鏤空的效果。
模板測試(Stencil Test)
模板測試默認是不開啟的,我們可以通過glEnable(GL_STENCIL_TEST)
指令將其打開,這是一個開發者可以高度配置的階段。如果開啟了模板測試,GPU會首先讀取模板緩衝區中該片元位置的模板值,然後將該值和讀取到的參考值進行比較,這個比較函數可以是由開發者指定的,例如小於時捨棄該片元,或者大於等於時捨棄該片元。如果這個片元沒有通過這個測試,該片元就會被捨棄。
深度測試(Depth Test)
根據日常經驗,近處的物體會遮擋遠處的物體,這種效果我們可以通過深度測試來模擬實現。它通過將深度快取中的值和當前片元的深度進行比較,計算是否需要更新深度快取和顏色快取,如果不需要則將該片元丟棄,這於模板測試比較類似。我們在渲染半透明物體時, 需要開啟深度測試而關閉深度寫入功能。
混合(Blend)
一個片元經過層層測試,總算來到了混合功能面前,對於不透明物體,開發者可以關閉混合(Blend)操作。這樣片元著色器計算得到的顏色值就會直接覆蓋掉顏色緩衝區中的像素值。但對於半透明物體,我們就需要使用混合操作來讓這個物體看起來是透明的。下面是一個簡化版的混合操作流程圖。
這個階段也是高度可配置的,開發者可以選擇是否開啟混合功能。如果沒有開啟混合功能,就會直接使用片元的顏色覆蓋掉顏色緩衝區中的顏色,因此是無法得到透明效果的。
提前深度測試(Early-Z
)
由於深度/模板測試是在片段著色器之後進行的,所以導致著色器計算資源的浪費,因為這些被遮擋的片段對我們終的畫面是沒有任何貢獻的,而我們還花費了大量的資源對它們進行了複雜的光照等一系列計算。Early-Z Culling
正是在這種情況下出現的,它發生在頂點著色器和片元著色器之間。不過我們需要注意的是Early-Z Culling
本不是管線標準,只是硬體廠商用來加速渲染的一種優化手段,所以在不同的硬體上會有不同的實現,而且Early-Z Culling
並不保證一定有效,它需要硬體的支援。
幀快取(Frame Buffer)
可以簡單理解為一個臨時畫布,GPU渲染完成的資訊會存放在幀快取區,等待使用,上述各種測試也是在幀緩衝區進行的
幀緩衝區主要包含顏色緩衝區(Color Buffer)和深度緩衝區(Depth Buffer),假設下面藍色線框是一個幀緩衝區,需要對藍色和紅色三角形的片元進行渲染,紅色片元的深度值是0.8,藍色片元的深度值是0.5。渲染紅色片元時,由於它的深度值小於幀緩衝區的初始深度值,所以它的z-buffer值和color buffer值會覆蓋幀緩衝區對於位置的值。渲染藍色片元時,由於它的深度值比幀緩衝區的深度值都要小,所以可以覆蓋緩衝區中的內容。
當模型的圖元經過了上面層層計算和測試後,就會顯示到我們的螢幕上。我們的螢幕顯示的就是顏色緩衝區中的顏色值。但是,為了避免我們看到那些正在進行光柵化的圖元,GPU會使用雙重緩衝(Double Buffering)
的策略。這意味著,對場景的渲染是在幕後發生的,即在後置緩衝(Back Buffer)
中。一旦場景已經被渲染到了後置緩衝中,GPU就會交換後置緩衝區和前置緩衝(Front Buffer)
中的內容,而前置緩衝區是之前顯示在螢幕上的影像。由此,保證了我們看到的影像總是連續的。
總結
下一節開始實戰,計劃使用Unity Shader
實現經典的phong
光照模型。
引用
[1] UnityShader入門精要
[2]
[3]