天天看點

Unity渲染管線,初探SRP

渲染管線流程圖

Unity渲染管線,初探SRP

應用階段

主要任務:

  • 手動準備好場景資料,錄影機位置,視錐體,包含哪些模型,使用哪些光源等等。
  • 把不可見的物體,剔除出去
  • 設定模型的渲染狀态,包括材質(漫反射顔色、高光反射顔色),使用的紋理,Shader等,還有其他

作用:輸出幾何資訊,即渲染圖元,點,線,三角面等

階段:

  • 把資料加載到顯存中
  • 設定渲染狀态
  • 調用DrawCall(渲染指令)

幾何階段

主要任務:

  • 主要處理要繪制的幾何相關的事情,例如,需要繪制的圖元是什麼,怎麼,在哪繪制它們,幾何階段負責和每個渲染圖元打交道
  • 輸出螢幕的二維頂點坐标,每個頂點對應的深度值,着色等相關資訊,并傳遞給下一個階段

階段:

  • 頂點着色器(可程式設計):頂點的空間變換和頂點着色
  • 曲面細分着色器(可選):細分圖元(對于一些有大量曲面的模型,進行曲面細分可以讓曲面更加圓潤;如果為這些細分的頂點再準備一些位置資訊,那麼這些細分的頂點将有助于我們展現一個細節更加豐富的模型。這也是貼圖置換(Displacement Mapping)的基本思路。)
  • 幾何着色器(可選):執行逐圖元的着色操作,或者用于産生更多的圖元。在這個階段,開發者可以控制GPU對頂點進行增删改操作。幾何着色器與頂點着色器都可以對頂點的坐标進行修改,但幾何體着色器并行調用硬體困難,并行程度低,效率和頂點着色器有很大的差距;如果不是要做頂點增、删這些僅僅能用幾何着色器實作的效果,那麼還是用頂點着色器來完成吧。
  • 投影:GPU将頂點從錄影機觀察空間轉換到裁剪空間(又被稱為齊次裁剪空間),為之後的剔除過程以及投射到二維平面做準備
  • 裁剪(可配置):可自定義裁剪區域,比如裁剪正面還是背面,不在錄影機視野内的頂點裁剪掉,并剔除某些三角形圖元的面片
  • 螢幕映射:負責每個圖元的坐标轉換到螢幕坐标系中

光栅化階段

主要任務:

  • 使用上個階段傳遞的資料來産生螢幕上的像素,并渲染出最終的圖像
  • 決定每個渲染圖元中的哪些像素應該被繪制在螢幕上
  • 對上一個階段得到的逐頂點資料(紋理坐标、頂點顔色等)進行插值,然後在進行逐像素處理

階段:

  • 三角形設定:把頂點資料收集并組裝為簡單的基本體(線、點或三角形)
  • 三角形周遊:這個過程将檢驗螢幕上的某個像素是否被一個三角形網格所覆寫,被覆寫的區域将生成一個片元(Fragment)
  • 片元着色器(可程式設計):實作逐片元的着色操作
  • 逐片元操作(可配置):對每個片元進行操作,将它們的顔色以某種形式合并,得到最終在螢幕上像素顯示的顔色。主要的工作有兩個:對片元進行測試(Test)并進行合并。測試步驟決定了片元最終會不會被顯示出來。在OpenGL中,主要的測試有:裁剪測試(Scissor Test)、透明度測試(Alpha Test)、模闆測試(Stencil Test)以及深度測試(Depth Test)。這個階段是高度可配置的。如果一個片元通過了上面所有的測試,那它終于可以來到合并環節了。合并有兩種主要的方式,一種是直接進行顔色的替換,另一種是根據不透明度進行混合(Blend),而混合操作同樣是可配置的,程式員可以設定是把這兩種顔色進行相加、相減還是相乘等等。在經過上面的層層測試後,片元顔色就會被送到顔色緩沖區。GPU會使用雙重緩沖(Double Buffering)的政策,即螢幕上顯示前置緩沖(Front Buffer),而渲染好的顔色先被送入後置緩沖(Back Buffer),再替換前置緩沖,以此避免在螢幕上顯示正在光栅化的圖元。
  • 最終螢幕圖像

SRP/URP/HDRP之間的關系

下圖是各個管線的關系圖

Unity渲染管線,初探SRP

根據上圖所示,URP是Unity可程式設計渲染管線(SRP)的一種,是以了解URP之前需要先了解SRP是什麼。

SRP是什麼

SRP全稱為Scriptable Render Pipeline(可程式設計渲染管線/腳本化渲染管線),是Unity提供的新渲染系統,可以在Unity通過C#腳本調用一系列API配置和執行渲染指令的方式來實作渲染流程,SRP将這些指令傳遞給Unity底層圖形體系結構,然後再将指令發送給圖形API。

說白了就是我們可以用SRP的API來建立自定義的渲染管線,可用來調整渲染流程或修改或增加功能。

它主要把渲染管線拆分成二層:

  • 一層是比較底層的渲染API層,像OpenGL,D3D等相關的都封裝起來。
  • 另一層是渲染管線上層,上層代碼使用C#來編寫。在C#這層不需要關注底層在不同平台上渲染API的差别,也不需要關注具體如何做一個Draw Call

URP是什麼

它的全稱為Universal Render Pipeline(通用渲染管線), 它是Unity官方基于SRP提供的模闆,它的前身是LWRP(Lightweight RP即輕量級渲染管線), 在2019.3開始改名為URP,它涵蓋了範圍廣泛的不同平台,是針對跨平台開發而建構的,性能比内置管線要好,另外可以進行自定義,實作不同風格的渲染,通用渲染管線未來将成為在Unity中進行渲染的基礎 。

平台範圍:可以在Unity目前支援的任何平台上使用

HDRP是什麼

它的全稱為High Definition Render Pipeline(高清晰度渲染管線),它也是Unity官方基于SRP提供的模闆,它更多是針對高端裝置,如遊戲主機和高端桌上型電腦,它更關注于真實感圖形和渲染,該管線僅于以下平台相容:

  • Windows和Windows Store,帶有DirectX 11或DirectX 12和Shader Model 5.0
  • 現代遊戲機(Sony PS4和Microsoft Xbox One)
  • 使用金屬圖形的MacOS(最低版本10.13)
  • 具有Vulkan的Linux和Windows平台

在此文章對HDRP不過多描述。

為什麼誕生SRP

内置渲染管線的缺陷

  • 定制性差:過去,Unity有一套内置渲染管線,渲染管線全部寫在引擎的源碼裡。大家基本上不能改動,除非是買了Unity源碼客戶,當然大部分開發者是不會去改源碼,是以過去的管線對開發者來說,很難進行定制。
  • 代碼膿腫,效果效率無法做到最佳:内置渲染管線在一個渲染管線裡面支援所有的二十多個平台,包括非常高端的PC平台,也包括非常低端的平台,很老的手機也要支援,是以代碼越來越濃腫,很難做到使效率和效果做到最佳。

目的:

  • 為了解決僅有一個預設渲染管線,造成的可配置型、可發現性、靈活性等問題。決定在C++端保留一個非常小的渲染核心,讓C#端可以通過API暴露出更多的選擇性,也就是說,Unity會提供一系列的C# API以及内置渲染管線的C#實作;這樣一來,一方面可以保證C++端的代碼都能嚴格通過各種白盒測試,另一方面C#端代碼就可以在實際項目中調整。
Unity渲染管線,初探SRP

DrawCall Batch SetPassCalls

​在看URP 和 内置渲染管線 性能對比之前最好先了解DrawCall,Batches,SetPassCalls分别是什麼值。

Set Pass Call代表渲染狀态切換,主要出現在材質不一緻的時候,進行渲染狀态切換。我們知道一個batch包括,送出vbo(頂點緩沖區對象),送出ibo(索引緩沖區對象),送出shader,設定好硬體渲染狀态,設定好光源屬性等(注意送出紋理嚴格意義上并不包括在一個batch内,紋理可以被緩存并多幀複用)。如果一個batch和另一個batch使用的不是同種材質或者同一個材質的不同pass,那麼就要觸發一次set pass call來重新設定渲染狀态。例如,Unity要渲染20個物體,這20個物體使用同種材質(但不一定mesh等價),假設兩次dynamic batch各自合批了10個物體,則對于這次渲染,set pass call為1(隻需要渲染一個材質),batch為2(向GPU送出了兩次VBO,IBO等資料)。

Draw call嚴格意義上,CPU每次調用圖形API的渲染函數(使用OpenGL舉例,是glDrawElements或者DrawIndexedPrimitive)都算作一次Draw Call,但是對于Unity而言,它可以多個Draw Call合并成一個Batch去渲染。

真正造成開銷較大的地方,第一個在于在于切換渲染狀态,第二在于整理和送出資料。在真正的實踐過程當中,可以不用過于介意Draw call這個數字(因為沒有送出資料或者切換渲染狀态的話,其實多來幾個draw call沒什麼所謂),但是Set Pass Call和Batch兩個數字都要想辦法降低。由于二者存在強相關性,那麼通常降低一個,就一并可以降低第二個。

Unity提供了三種批次合并的方法,分别是Static Batching,GPU Instancing和Dynamic Batching。它們的原理分别如下:

Static Batching,将靜态物體集合成一個大号vbo送出,但是隻對要渲染的物體送出其IBO。這麼做不是沒有代價。比如說,四個物體要靜态批次合并前三個物體每個頂點隻需要位置,第一套uv坐标資訊,法線資訊,而第四個物體除了以上資訊,還多出來切線資訊,則這個VBO會在每個頂點都包括所有的四套資訊,毫無疑問組合這個VBO是要對CPU和顯存有額外開銷的。要求每一次Static Batching使用同樣的material,但是對mesh不要求相同。

Dynamic Batching将物體動态組裝成一個個稍大的vbo+ibo送出。這個過程不要求使用同樣的mesh,但是也一樣要求同樣的材質。但是,由于每一幀CPU都要将每個物體的頂點從模型坐标空間變換到組裝後的模型的坐标空間,這樣做會帶來一定的計算壓力。是以對于Unity引擎,一個批次的動态物體頂點數是有限制的。

GPU Instancing是隻送出一個物體的mesh,但是将多個使用同種mesh和material的物體的差異化資訊(包括位置,縮放,旋轉,shader上面的參數等。shader參數不包括紋理)組合成一個PIA(per instanced attribute執行個體屬性)送出。在GPU側,通過讀取每個物體的PIA資料,對同一個mesh進行各種變換後繪制。這種方式相比static和dynamic節約顯存,又相比dynamic節約CPU開銷。但是相比這兩種批次合并方案,會略微給GPU帶來一定的計算壓力。但這種壓力通常可以忽略不計。限制是必須相同材質相同物體,但是不同物體的材質上的參數可以不同。

是以Unity預設政策是優先static,其次gpu instancing,最後dynamic。當然如果頂點數過于巨大(比如渲染它幾千顆使用同種mesh的樹),那麼gpu instancing或許比static batching是一個更加合适的方案。

URP 和 内置渲染管線 性能對比

主要提速的有兩個方面

1. 光照處理(包括陰影)

2. SRP Bacher (SRP 批處理)(重點)

Unity渲染管線,初探SRP

首先來說說光照處理部分

Unity渲染管線,初探SRP

​如上圖所示,老的渲染管線使用Multi-Pass的Forward Rendering,就是多Pass的前向渲染。最大的問題是如果要在場景裡要加很多動态光的話,每一個動态光都有可能會增加一個Pass,這個動态光所影響的物體要多畫一遍。

這就導緻如果遊戲裡想要有多個動态光的話,可能這個場景會被畫很多遍,性能會很差。它帶來的問題是所有的遊戲幾乎都不會用多個動态光,因為實在太費性能了。

在過去制作移動的遊戲的過程中,大家的标準做法都是烘焙Lightmap。

現在URP就解決了這個問題。實作了一個單PASS的正向渲染。可以支援多盞動态光,但是全部動态燈光都會放在一個Pass裡渲染,這樣帶來的問題是要限制燈光的數量,因為每次Draw Call去畫的時候,傳給GPU的參數是有限的。

如果燈光數量特别多,參數太多,那就會無法在一次Draw Call裡完成很多個燈光。是以我們有一些限制,在輕量級渲染管線LWRP裡,目前是支援1盞平行光,每個對象可能隻能接受4個動态光。每個錄影機也有一些限制,這是為了我們可以把所有的計算放在一個Pass裡面。

接下來看看内置渲染管線和URP各種情況下的光照處理對比

以下是分别在四種情況下對比所得出的結論

  1. 無光源。 (沒差別)
  2. 一個平行光,無陰影。(沒差別)
  3. 一個平行光,一個點光源,無陰影。

    結論:内置渲染管線跟隻有一個平行光時比起來Batches将近增加了一倍,而URP的Batches和SetPass calls跟一個平行光時一樣,一點都沒有增加。

  4. 一個動态光,有陰影。

    結論:在陰影的處理方面URP性能比内置渲染管線好很多。

URP光照處理最終結論:

1. 性能上陰影處理方面比内置渲染管線好很多。

2. URP平行光基礎上添加動态光沒有帶來額外的Batches和SetPass calls性能開銷。

接下來看看SRP Batcher(重點)

SRP Batcher 是什麼

SRP Batcher 是一個底層渲染循環,可通過許多使用同一着色器變體的材質來加快場景中的 CPU 渲染速度。

就算是不同的材質球,隻要是用一個着色器變體的物體都可以批處理到一塊。(在2019.3.4版本 渲染順序也會打斷批處理,這點上官網沒有說明,也許後續版本已經處理了),它的主要目的是減少渲染狀态設定的開銷,還有就是把物體屬性用專用代碼快速更新。

上面解釋都提到了變體,那麼變體怎麼了解呢?

multi_compile和shader_feature這兩個關鍵字就是實作着色器變體的指令。

下面是自定義的multi_compile 關鍵字。

Unity渲染管線,初探SRP

上圖中定義了兩個multi_complie,white和black兩個關鍵字,此時Unity會生成兩個變體,一個是走white邏輯的變體,另外一個是走black邏輯的變體。然後在腳本中通過Material.EnableKeyWord和Shader.EnableKeyword來開啟某功能,通過Material.DisableKeyword和Shader.DisableKeyword來關閉某功能。為什麼這麼做呢?主要是為了避免分支語句(if )導緻的性能下降。

除了關鍵字還有這些

​參數不一緻的話也無法批處理到一塊。

shader_feature關鍵字跟multi_compile不一樣的是未被選擇的變體會在打包的時候被舍棄(multi_complie不會),shader_feature主要是在材質球選項上控制開關,比如

Unity渲染管線,初探SRP

​這個選項。

URP的Lit Shader裡有很多實作變體指令,是以這些Shader生成的變體也有很多。如圖:

Unity渲染管線,初探SRP

​過去,Unity 中,可以在一幀内的任何時間修改任何材質的屬性。但是,這種做法有一些缺點。當Draw Calls使用新材質時,需要進行很多處理。場景内的材質越多,設定GPU資料所需的CPU資源就越多。解決此問題的傳統方法是減少 DrawCall 的數量以優化 CPU 渲染成本,因為 Unity 在發出 DrawCall 之前必須進行很多設定。實際的 CPU 成本來自該設定,而不是來自 GPU DrawCall 本身(DrawCall 隻是 Unity 需要推送到 GPU 指令緩沖區的少量位元組)。

這是官網說的提速效果:

Unity 2018引入了可程式設計渲染管線SRP,其中包含新的底層渲染循環SRP Batcher批處理器,它可以大幅提高CPU在渲染時的處理速度,根據場景内容的不同,提升效果為原來的1.2~4倍不等。

SRP Batcher是怎麼優化的?

SRP Batcher使材質資料持久保留在 GPU 記憶體中。如果材質内容不變,SRP Batcher 不需要設定緩沖區并将緩沖區上傳到 GPU。還有 SRP Batcher 會使用專用的代碼路徑來快速更新大型 GPU 緩沖區中的 Unity 引擎屬性,如下圖。

Unity渲染管線,初探SRP

上面的功能能解決什麼問題呢?也就是CPU不需要再設定渲染狀态和一大堆渲染資料設定,隻需要物體跟緩沖區的資料綁定就可以了。

SRP Batcher 正是通過批處理一系列 

Bind

 和 

Draw

 GPU 指令來減少 DrawCall 之間的 GPU 設定。

内置渲染管線和URP的CPU原理圖對比

Unity渲染管線,初探SRP

内置渲染管線:(紅框部分就是SRP Batcher優化的性能部分) 

Unity渲染管線,初探SRP

URP:

在把材質資料和物體資料上傳好後的流程圖:(GPU沒有詳細畫,主要看CPU)

Unity渲染管線,初探SRP

上面流程圖中綁定的意思是大家都知道Shader裡有很多變量,如紋理貼圖,Property定義的變量以及内置變量等,個人了解是把緩沖區裡存的渲染資料設定給了Shader變量。

根據上面内容我們可以知道SRP Batcher并沒有減少DrawCall,而是優化了DrawCall之前的設定開銷。

SRP Batch值我們可以在Frame Debug視窗可以看得到。

SRP Batcher 相容性

為了使 SRP Batcher 代碼路徑能夠渲染對象:

  • 渲染的對象必須是mesh或者skinned mesh。該對象不能是粒子。
  • 着色器必須與 SRP Batcher 相容。HDRP 和 URP 中的所有光照和無光照着色器均符合此要求(這些着色器的“粒子”版本除外)。

為了使着色器與 SRP Batcher 相容:

  • 必須在一個名為“UnityPerDraw”的 CBUFFER 中聲明所有内置引擎屬性。例如:

    unity_ObjectToWorld

  • 必須在一個名為 

    UnityPerMaterial

     的 CBUFFER 中聲明所有材質屬性。
Unity渲染管線,初探SRP

Property定義的屬性也是屬于PerMaterial.

Unity渲染管線,初探SRP

​可以在 Inspector 面闆中檢視着色器的相容性狀态。

Unity渲染管線,初探SRP

繼續閱讀