2020年6月23日 星期二

Scriptable Render Pipeline (1)


  • 前言

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),然後編輯內容如下

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。



只要把這個 Asset,告知 Unity,Unity就知道要使用哪個 SRP 作為渲染流程。

告知 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

沒有留言:

張貼留言