1. 程式人生 > 其它 >自定義SRP(一)

自定義SRP(一)

自定義SRP管線(一)

建立RenderPipelineAsset

建立自定義SRP管線,我們首先需要一個RenderPipelineAsset,這可以通過使用指令碼繼承RenderPipelineAsset這個抽象類來建立自己的RenderPipelineAsset。

具體程式碼如下:

//拓展編輯器,這樣可以使用右鍵建立一個Asset
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline Asset")]
//RenderPipelineAsset繼承自ScriptableObject,是一個Unity的資產檔案,類似XML,JSON等可以儲存資料,但是我們可以通過Inspector面板看到裡面的資料。執行時可以通過引用獲取到裡面的資料,相比傳統Monobehavior的值拷貝傳遞,節省空間。儲存不變資料最好使用ScriptableObject
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
    //繼承自RenderPipelineAsset這個抽象類的類必須實現這個抽象方法,返回一個自定義的RenderPipeline
    protected override RenderPipeline CreatePipeline()
    {
        return new CustomRenderPipeline();
    }
}

這個指令碼編譯之後,可以在Asset下右鍵依次點選Create/Rendering/Custom Render Pipeline Asset建立一個Asset,之後可以在Edit/Project Settings/Graphics下指定需要使用的RenderPipelineAsset,

指定之後,Scene視窗和Game視窗都會完全變成黑色,這是因為我們只是建立了一個Pipeline,但是還沒有實現任何渲染流程。

建立RenderPipeline

上面的這個Asset可以和引擎核心進行交流,確定使用的RenderPipeline,因此必須返回一個RenderPipeline。RenderPipeline裡我們可以手動實現每一步的渲染流程,這就很像OpenGL的程式設計了,區別是Unity給我們封裝好了很多函式,比OpenGL方便很多。

建立一個自定義的RenderPipeline,我們需要繼承自RenderPipeline這個抽象類。

//自定義RenderPipeline,需要繼承自RenderPipeline抽象類
public class CustomRenderPipeline : RenderPipeline
{
    //這個RenderPipeline可以持有很多Renderer,首先我們需要一個攝像機Renderer,渲染出所有相機觀察到的東西
    private readonly CameraRenderer m_CameraRenderer = new();
    //引擎每一幀都會呼叫這個自己實現的抽象方法Render,裡面有一個context和所有的攝像機
    protected override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        //對每個攝像機,使用攝像機渲染場景
        foreach (var camera in cameras)
        {
            m_CameraRenderer.Render(context,camera);
        }
    }
}

上面的ScriptableRenderContext是當前使用的圖形API的抽象,可使用 ScriptableRenderContext 向 GPU 排程和提交狀態更新和繪製命令。

實現CameraRenderer

public class CameraRenderer
{
    private ScriptableRenderContext m_Context;
    private Camera m_Camera;
    private const string BufferName = "Render Camera";
    //在 Scriptable Render Pipeline 中,當 Unity 執行剔除操作時,它會將結果儲存在 CullingResults 結構中。此資料包括有關可見物件、燈光和反射探測器的資訊。Unity 使用此資料來渲染物件和處理燈光。 CullingResults 結構還提供了幾個函式來幫助陰影渲染。
   	private CullingResults m_CullingResults;
    //支援的SRP ShaderID
    private static readonly ShaderTagId UnlitShaderTagId = new("SRPDefaultUnlit");
    private readonly CommandBuffer m_Buffer = new()
        {
            name = BufferName
        };
}

CameraRenderer肯定需要持有當前的ScriptableRenderContext,和所使用的相機。CommandBuffer則包含一組圖形命令,比如設定渲染目標,取樣渲染命令顯示到Frame Debugger中。目前主要是把渲染的所有Draw Call顯示出來。

public void Render(ScriptableRenderContext context, Camera camera)
{
    m_Context = context;
    m_Camera = camera;
    if (!Cull())
    {
        return;
    }

    SetUp();
    DrawVisibleGeometry();
    DrawUnsupportedShaders();
    Submit();
}

在Render函式裡,接收RenderPipeline中傳來的context和相機。

private void SetUp()
{
    m_Context.SetupCameraProperties(m_Camera);
    m_Buffer.ClearRenderTarget(true, true, Color.clear);
    //開始取樣
    m_Buffer.BeginSample(BufferName);
    ExecuteBuffer();
}

SetUp主要是設定攝像機的檢視和投影矩陣,清屏(類似glClear(),glClearColor())防止上一幀的畫面影響當前幀。然後使用CommandBuffer開始取樣,這主要是想讓FrameDebugger顯示渲染的所有DrawCall資訊。

private void DrawVisibleGeometry()
{
    //渲染不透明物體
    var sortingSettings = new SortingSettings(m_Camera)
    {
        criteria = SortingCriteria.CommonOpaque
    };
    var drawingSettings = new DrawingSettings(UnlitShaderTagId, sortingSettings);
    var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
    m_Context.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
    //渲染天空盒
    m_Context.DrawSkybox(m_Camera);
    //渲染透明物體
    sortingSettings.criteria = SortingCriteria.CommonTransparent;
    drawingSettings.sortingSettings = sortingSettings;
    filteringSettings.renderQueueRange = RenderQueueRange.transparent;
    m_Context.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
}

Submit則是提交自己在Context中配置的所有DrawCall

private void Submit()
{
    //結束取樣
    m_Buffer.EndSample(BufferName);
    ExecuteBuffer();
    m_Context.Submit();
}

在上面DrawVisibleGeometry()渲染各種物體的時候,需要一些引數,比如渲染不透明物體的時候,我們一般由近到遠渲染物體,因為如果後面的物體被擋到,我們就可以不花費時間去渲染它了。

這就需要我們設定

var sortingSettings = new SortingSettings(m_Camera)
    {
        criteria = SortingCriteria.CommonOpaque
    };

而在渲染透明物體的時候,我們又需要從遠到近渲染,這是因為我們最終可能要混合各種不透明物體的顏色

因此設定

sortingSettings.criteria = SortingCriteria.CommonTransparent;

filteringSettings則是設定的過濾條件,Unity對於不透明和透明物體,分別設定了兩條渲染佇列,把兩類物體分在兩個佇列裡,依次渲染。

//只渲染不透明物體佇列中的物體
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
//只渲染透明物體佇列中的物體
filteringSettings.renderQueueRange = RenderQueueRange.transparent;

在設定好這些引數之後,就可以使用下列程式碼讓context渲染了

ref關鍵字表示按引用傳遞,類似C++中的const-to-references,減少拷貝帶來的效能消耗。

 m_Context.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);

這裡的m_CullingResults則是相機的剔除結果,獲得 CullingResults 結構,呼叫 ScriptableRenderContext.Cull。

private bool Cull()
{
    //獲取相機剔除引數
    if (!m_Camera.TryGetCullingParameters(out var p)) return false;
    //使用引數進行剔除
    m_CullingResults = m_Context.Cull(ref p);
    return true;
}

渲染Unity UI物體

上面的這些程式碼並不會在Scene視窗下渲染各種Unity 內部的UI,如果我們想在scene下看到UI,我們需要顯式告訴Unity,通過下列程式碼:

partial void PrepareForSceneWindow()
{
    //如果相機是Scene視窗的相機,那麼渲染UI物體
    if (m_Camera.cameraType == CameraType.SceneView)
    {
        ScriptableRenderContext.EmitWorldGeometryForSceneView(m_Camera);
    }
}

渲染Gizmoz

partial void DrawGizmos()
{
    if (!Handles.ShouldRenderGizmos()) return;
        //Gizmoz有兩種,一起渲染
    m_Context.DrawGizmos(m_Camera, GizmoSubset.PreImageEffects);
    m_Context.DrawGizmos(m_Camera, GizmoSubset.PostImageEffects);
}

渲染不支援的Shader

在URP中,我們可以看到普通的shader呈現出一種粉色,表示不支援這個shader,給開發者一個提示,換成支援的SRP shader

我們可以把不支援的shader列出來

private static readonly ShaderTagId[] LegacyShaderTagIds =
{
    new("Always"),
    new("ForwardBase"),
    new("PrepassBase"),
    new("Vertex"),
    new("VertexLMRGBM"),
    new("VertexLM")
};

然後渲染這些不支援的shader

private static Material _errorMaterial;
partial void DrawUnsupportedShaders()
{
    if (_errorMaterial == null)
    {
        //把_errorMaterial設定為粉色的shader
        _errorMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));
    }

    var drawingSettings = new DrawingSettings(
        LegacyShaderTagIds[0], new SortingSettings(m_Camera)
    )
    {
        overrideMaterial = _errorMaterial
    };
    for (var i = 1; i < LegacyShaderTagIds.Length; i++)
    {
        //配置所有不支援的shader
        drawingSettings.SetShaderPassName(i, LegacyShaderTagIds[i]);
    }

    var filteringSettings = FilteringSettings.defaultValue;
    m_Context.DrawRenderers(
        //使用粉色shader渲染
        m_CullingResults, ref drawingSettings, ref filteringSettings
    );
}

粉色是使用不支援的shader的物體

完整Render程式碼

public void Render(ScriptableRenderContext context, Camera camera)
{
    //設定使用的context和camera
    m_Context = context;
    m_Camera = camera;
    //配置buffer名稱,讓Frame Debugger顯示
    PrepareBuffer();
    //渲染UI
    PrepareForSceneWindow();
    //剔除
    if (!Cull())
    {
        return;
    }
	//設定相機引數,清空螢幕快取
    SetUp();
    //渲染可見物體
    DrawVisibleGeometry();
    //渲染不支援的shader
    DrawUnsupportedShaders();
    //渲染Gizmoz
    DrawGizmos();
    //提交上面的所有渲染命令
    Submit();
}

ClearFlags

每個相機都有一個ClearFlags引數

從上到下依次為Skybox,Solid Color,Depth Only,Not Clear。

依次表示清空所有,只清空天空盒並保留顏色和深度快取,清空天空盒和背景顏色並只保留深度快取,和什麼都不清空。

我們建立兩個相機,放在相同位置,第一個選擇天空盒子。

在SetUp中,設定當CameraClearFlags<=CameraClearFlags.Depth清空顏色緩衝,這表示當我們設定flag為Skybox和Solid Color和Depth Only的時候清空深度緩衝。

然後設定 flags == CameraClearFlags.Color的時候清空顏色緩衝,這表示只有當我們設定相機flag為Solid Color的時候清空顏色緩衝。

private void SetUp()
{
    m_Context.SetupCameraProperties(m_Camera);
    var flags = m_Camera.clearFlags;
    m_Buffer.ClearRenderTarget(flags <= CameraClearFlags.Depth,
        flags == CameraClearFlags.Color,
        flags == CameraClearFlags.Color ? m_Camera.backgroundColor.linear : Color.clear);
    //開始取樣
    m_Buffer.BeginSample(sampleName);
    ExecuteBuffer();
}

如果第二個選擇天空盒,表示第二個相機全部重新渲染,因此第一個相機的所有畫面都看不到了。

如果第二個選擇Solid Color,表示清空顏色緩衝,並且使用自己相機的背景顏色重新渲染,因此第一個相機的天空盒和渲染結果,被第二個相機的背景顏色完全覆蓋了,表現為

當我們設定第二個相機為Depth Only,表示只清除第一個相機的深度緩衝,因此第二個相機的畫面都會在第一個相機畫面的前方。

當我們設定第二個相機為Not Clear,我們對於第一個相機的渲染結果不做任何處理,

主要區別在這個地方,紫色方塊是在綠色方塊後方的,如果我們清空深度緩衝,第二張圖的渲染結果會覆蓋第一張圖,因此粉色方塊反而出現在綠色方塊的前方了,不清空深度緩衝的話,渲染結果就正確了。

參考

Custom Render Pipeline