- 前言
2018年,Unity 導入了 Scriptable Render Pipeline(之後簡稱 SRP),
讓開發者可以自訂專屬的 render pipeline ,大大的提升效能優化和特殊渲染需求的靈活性。
但使用自訂 render pipeline 也意味著之前 Unity 幫使用者處理的功能,
像是 culling、shadow map ... 等,都必須重新建置。
因此, Unity 也提供了兩個範本供開發者使用,包含:
- Lightweight Render Pipeline(改名為Universal Render Pipeline),提供給低端設備使用
- High Definition Render Pipeline,提供給高端設備使用
開發者可以根據需求直接修改範本。
雖然可以直接修改範本,但仍需要一些基礎知識才能有要利用。
因此本文將不使用 Unity 提供的範本,選擇從無開始建構一個完全客製的 render pipeline,
期望能在建構途中了解 SRP 的基礎原理,以方便之後能有效利用並修改 Unity 提供的範本。
客製化的 render pipeline 在 mobile 上的實行結果
- 安裝與設定
本文使用的開發環境為 Unity 2019.3.2f1,
程式碼所使用的函數名稱或所屬的 namespace 可能會根據 Unity 版本不同而有變化,
在閱讀與實作上請注意。
首先,為了要能夠使用 SRP,需要從 Window\Package Manager 下載專用的 package
接著,產生一個 C# 檔案並任意取個名字(本文命名 DemoPipeline.cs),然後編輯內容如下
DemoPipeline.cs 主要是描述 Render Pipeline 的運作內容,
需要繼承 UnityEngine.Rendering.RenderPipeline。
渲染時,會呼叫 Render 函式, 並根據 Render 函式實作內容,決定渲染流程。
接下來,為了能讓 Unity 使用客製的 render pipeline,
需要建立一個 RenderPipelineAsset 檔案,作為接口,
使 Unity 知道要如何取得 DemoPipeline.cs 內的實作內容。
為了達到這個目的,同樣地,
在專案下建立一個 C# 檔案並任意取個名字(本文命名為 DemoPipelineAsset.cs),
然後編輯內容如下
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; public class DemoPipeline : UnityEngine.Rendering.RenderPipeline { protected override void Render(ScriptableRenderContext context, Camera[] cameras) { } }
DemoPipeline.cs 主要是描述 Render Pipeline 的運作內容,
需要繼承 UnityEngine.Rendering.RenderPipeline。
渲染時,會呼叫 Render 函式, 並根據 Render 函式實作內容,決定渲染流程。
接下來,為了能讓 Unity 使用客製的 render pipeline,
需要建立一個 RenderPipelineAsset 檔案,作為接口,
使 Unity 知道要如何取得 DemoPipeline.cs 內的實作內容。
為了達到這個目的,同樣地,
在專案下建立一個 C# 檔案並任意取個名字(本文命名為 DemoPipelineAsset.cs),
然後編輯內容如下
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; [CreateAssetMenu(menuName = "Demo Pipeline")] public class DemoPipelineAsset : UnityEngine.Rendering.RenderPipelineAsset { protected override RenderPipeline CreatePipeline() { return new DemoPipeline(); } }
由於我在腳本上使用了 CreateAssetMenu 這個 Unity內建的 Attribute,
因此我可以在 Unity 編輯器內,點擊滑鼠右鍵產生 DemoPipelineAsset 的 Asset。

因此我可以在 Unity 編輯器內,點擊滑鼠右鍵產生 DemoPipelineAsset 的 Asset。

只要把這個 Asset,告知 Unity,Unity就知道要使用哪個 SRP 作為渲染流程。
告知 Unity 的方法有兩個
1. 在 Project Settings\Graphics 設定,將 Asset 用滑鼠拖曳到指定欄位
告知 Unity 的方法有兩個
1. 在 Project Settings\Graphics 設定,將 Asset 用滑鼠拖曳到指定欄位
2. 在 Script 呼叫 GraphicsSettings.renderPipelineAsset [1]
public DemoPipelineAsset demoAsset; GraphicsSettings.renderPipelineAsset = demoAsset;
如果按照上述說明並實作至此,會發現到完全沒有畫面。
這是正確的,因為我還沒有為 DemoPipeline::Render 這個函式製作任何內容。
因此接下來要做的事情就是實裝 Render 函數的內容。
首先,觀察到函數帶了兩個引數 context 和 cameras
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
context 是一個 native code,裡面封裝了所有渲染物件的資料
cameras 是一個 camera 陣列,Scene 裡面所有的 camera 都會被包裝到這個陣列裡面。
因為渲染是畫出 camera 所看到的內容,因此第一步我必須要針對所有的 camera 做處理。
protected override void Render(ScriptableRenderContext context, Camera[] cameras) { for(int i = 0; i < cameras.Length; ++i) { // To Do } }
接著,畫出一個天空球,讓畫面不會太孤單
protected override void Render(ScriptableRenderContext context, Camera[] cameras) { for(int i = 0; i < cameras.Length; ++i) { if (cameras[i].clearFlags == CameraClearFlags.Skybox) context.DrawSkybox(cameras[i]); } }
由於天空球是個渲染物件,因此他被封裝在 context 裡面,
我必須要透過呼叫專用的函式(DrawSkyBox),去渲染天空球。
但呼叫後並不會馬上被執行,這是因為我是在 CPU 呼叫指令,但執行是在 GPU,
因此,我必須要在設定好所有指令後,將指令送給GPU執行。
protected override void Render(ScriptableRenderContext context, Camera[] cameras) { for(int i = 0; i < cameras.Length; ++i) { if (cameras[i].clearFlags == CameraClearFlags.Skybox) context.DrawSkybox(cameras[i]); } context.Submit(); }
透過呼叫 context.Submit(),將所有渲染指令送到 GPU。
執行到這裡,不論是 Game View 或 Scene View,畫面都會出現熟悉的背景天空。
但仔細一看會發現不論我怎麼轉動攝影機,畫面永遠維持不變。
這是因為我並沒有把攝影機的資料傳入(攝影機的面向、轉角 ... etc)。
既然如此,我知道我接下來要做的事是設定攝影機的資料,並把資料傳給 GPU 做運算。
protected override void Render(ScriptableRenderContext context, Camera[] cameras) { for(int i = 0; i < cameras.Length; ++i) { context.SetupCameraProperties(cameras[i]); if (cameras[i].clearFlags == CameraClearFlags.Skybox) context.DrawSkybox(cameras[i]); } context.Submit(); }
context.SetupCameraProperties() 函數會自動幫我設定渲染所需攝影機的所有資料,
這樣,背景的天空就會隨著攝影機的角度而有不同的畫面。
到了這裡,看似已經完成一個背景天空渲染的基礎 SRP,
但熟悉 render pipeline 的人,應該會發現我沒有做一個做基礎的動作。
對,就是清除 render buffer。
在本文範例中,因為是全畫面渲染,因此清不清除 render buffer 都看不出問題來。
但如果畫面中不是一個全螢幕渲染,這時就會出現破綻。
因此,我必須在每個攝影機執行渲染動作前,
先下指令清除上一個 frame 留下了的 render buffer 資料。
但 render buffer 並不是一個渲染物件,因此沒有被封裝在 context 裡面。
對此,Unity 提供了 CommandBuffer 這個 class,讓開發者可以呼叫 context 不支援的指令。
protected override void Render(ScriptableRenderContext context, Camera[] cameras) { CommandBuffer cmdBuff = new CommandBuffer(); for(int i = 0; i < cameras.Length; ++i) { ClearRenderBuffer(cmdBuff, context, cameras[i]); context.SetupCameraProperties(cameras[i]); if (cameras[i].clearFlags == CameraClearFlags.Skybox) context.DrawSkybox(cameras[i]); } context.Submit(); cmdBuff.Release(); } void ClearRenderBuffer(CommandBuffer cmd, ScriptableRenderContext context, Camera cam) { CameraClearFlags clearFlags = cam.clearFlags; bool isClearDepth = true; bool isClearColor = true; switch (clearFlags) { case CameraClearFlags.Depth: isClearColor = false; break; case CameraClearFlags.Nothing: isClearDepth = false; isClearColor = false; break; } cmd.ClearRenderTarget(isClearDepth, isClearColor, cam.backgroundColor); context.ExecuteCommandBuffer(cmd); cmd.Clear(); }
透過設定 CommandBuffer,然後呼叫 context.ExecuteCommandBuffer,
如此 context 內部會複製 CommandBuffer 設定的指令,
當 context 資料傳送給 GPU執行時,這些被複製的指令也會一併被 GPU 執行。
執行的順序會依據呼叫 context 的任何函數的先後來決定。
- 結論
本章節簡單的說明如何安裝並建立一個簡單的 SRP,
之後的章節會延續本章節的結果,一步一步的新增(修改)程式碼,
並簡單說明新增(修改)目的,直到能達成章節一開始所展示的客製 SRP 結果。
- References