可編程腳本渲染管線SRP
Unity 2018.1 beta中引入的Scriptable Render Pipeline可編程腳本渲染管線,簡稱SRP。是一種在Unity中通過C#腳本配置和執行渲染的方式。在編寫自定義渲染管線之前,必須要先理解渲染管線的含義。本文將幫助你開始學習編寫自定義SRP。
本文演示項目,請訪問Github下載:
https://github.com/stramit/SRPBlog/tree/master/SRP-Demo
什麽是渲染管線
渲染管線是將對象顯示到屏幕上所需的一系列技術的總稱。它包含: 剔除、渲染對象、後期處理等一系列高級概念。這些高級概念還可以分別根據你所希望的執行方式繼續分解。
例如:渲染對象可以按照以下方式進行
-
多通道渲染:每個光照每個對象一個通道
-
單通道渲染:每個對象一個通道
-
延遲渲染:渲染表面屬性到一個G-Buffer,執行屏幕空間光照。
這些就是當你編寫一個自定義SRP時需要作出的決定。每項技術都有一些需要考慮的性能成本。
渲染入口點
當使用SRP時,你需要定一個類,用於控制渲染;這就是你將要創建的渲染管線。入口點是一個對“Render”函數的調用,它需要兩個參數,渲染上下文以及一個需要渲染的攝像機列表。
public class BasicPipeInstance : RenderPipeline { public override void Render(ScriptableRenderContext context, Camera[] cameras){} }
渲染管線上下文
SRP渲染采用的是延遲執行的方式。用戶要設置好需要執行的命令列表,然後再執行。用來設置這些命令的對象叫做“ScriptableRenderContext”。當你向上下文填充完操作命令後,可以通過調用“Submit”提交隊列中的所有繪制調用。
舉例來說,使用一個由渲染上下文執行的命令緩沖區清除一個渲染目標:
//新建一個命令緩沖區 //用於向渲染上下文發送命令 var cmd = new CommandBuffer(); //發送一個清除渲染目標的命令 cmd.ClearRenderTarget(true, false, Color.green); //執行命令緩沖區 context.ExecuteCommandBuffer(cmd);
一個簡單渲染管線示例
下面有一個完整的渲染管線代碼,僅僅用於清除屏幕。
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Experimental.Rendering; [ExecuteInEditMode] public class BasicAssetPipe : RenderPipelineAsset { public Color clearColor = Color.green; #if UNITY_EDITOR [UnityEditor.MenuItem("SRP-Demo/01 - Create Basic Asset Pipeline")] static void CreateBasicAssetPipeline() { var instance = ScriptableObject.CreateInstance<BasicAssetPipe>(); UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/1-BasicAssetPipe/BasicAssetPipe.asset"); } #endif protected override IRenderPipeline InternalCreatePipeline() { return new BasicPipeInstance(clearColor); } } public class BasicPipeInstance : RenderPipeline { private Color m_ClearColor = Color.black; public BasicPipeInstance(Color clearColor) { m_ClearColor = clearColor; } public override void Render(ScriptableRenderContext context, Camera[] cameras) { base.Render(context, cameras); var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, true, m_ClearColor); context.ExecuteCommandBuffer(cmd); cmd.Release(); context.Submit(); } }
剔除
剔除是確定要在屏幕上顯示什麽對象的過程。
在Unity中,剔除包括:
-
視錐剔除:計算存在於攝像機遠近視平面之間的對象。
-
遮擋剔除:計算哪些對象被其它對象擋住,並將它們從渲染中排除。
當渲染開始時,首先要計算的是到底要渲染什麽。這包括獲取攝像機,並從攝像機的視角進行剔除操作。剔除操作會返回一個可為攝像機進行渲染的對象和光照的列表。這些對象隨後將被用在渲染管線中。
SRP中的剔除操作
在SRP中,你通常會選擇某個攝像機的視角執行對象渲染。這與Unity內置渲染所使用的攝像機對象是相同的。SRP提供了一系列API用於剔除操作。整個流程通常看起來像下面這樣:
//新建一個結構體,用於存儲剔除參數 ScriptableCullingParameters cullingParams; //填充來自攝像機的剔除參數 if (!CullResults.GetCullingParameters(camera, stereoEnabled, out cullingParams)) continue; //如果你想修改剔除參數,可在這裏進行 cullingParams.isOrthographic = true; //新建一個結構體,用於存儲剔除結果 CullResults cullResults = new CullResults(); //執行剔除操作 CullResults.Cull(ref cullingParams, context, ref cullResults);
現在可以使用填充的剔除結果執行渲染了。
繪制
現在我們已有了一組剔除結果,可以將它們渲染到屏幕了。但還有很多東西需要配置,所以有一些選擇需要事先確定。這些選擇的驅動因素有:
-
渲染管線的目標硬件
-
希望獲得的觀感
-
制作的項目的類型
例如一個移動2D滾軸遊戲和一個PC端第一人稱遊戲,這些遊戲所受的約束條件大不相同,因此它們的渲染管線自然也就大相徑庭。以下是一些實際可能需要面對的選擇:
-
高動態範圍 vs 低動態範圍
-
線性 vs 伽馬
-
多重采樣抗鋸齒(MSAA) vs 後期處理抗鋸齒
-
PBR材質 vs 簡單材質
-
光照 vs 無光照
-
光照技術
-
陰影技術
在編寫渲染管線時作好這些決定將有助於確定在創作時遇到的許多約束。
現在我們將演示一個無光照的簡易渲染器,這個渲染器可以將一些對象渲染為不透明。
過濾:渲染區域(Bucket)和圖層(Layer)
一般來說,渲染對象會有某個特定分類,比如不透明、透明、次面,或其它的什麽類別。Unity用一個稱為隊列(queue) 的概念表示需要進行渲染的對象,這些隊列進而組成存放對象的區域(bucket)(源自對象上的材質)。當從SRP調用渲染時,需要制定所使用區域的範圍。
除了區域之外,標準的Unity圖層也可以被用於過濾。這為通過SRP繪制對象時提供了額外的過濾能力。
//獲取不透明渲染過濾器設置 var opaqueRange = new FilterRenderersSettings(); //設置不透明隊列的範圍 opaqueRange.renderQueueRange = new RenderQueueRange() { min = 0, max = (int)UnityEngine.Rendering.RenderQueue.GeometryLast, }; //Include all layers包括所有圖層 opaqueRange.layerMask = ~0;
繪制設置:應當如何繪制
過濾和剔除確定了渲染什麽,但隨後我們還需要確定如何渲染。SRP提供了一系列不同的選項,配置被過濾對象的渲染方式。用於進行這個數據的結構體叫做“DrawRenderSettings”。這個結構體可對許多方面進行配置:
-
排序——對象渲染的順序,例如自後向前和自前向後
-
每渲染器標誌 —— 應當從Unity傳遞給著色器的“內置”設置,這包括每個對象的光照探頭,每個對象的光照貼圖之類的東西。
-
渲染標誌 —— 用於進行批處理的算法,實例化 vs 非實例化
-
著色器通道 —— 當前繪制調用應當使用哪個著色器通道
//新建繪制渲染設置 //註意它需要輸入一個著色器通道名 var drs = new DrawRendererSettings(Camera.current, new ShaderPassName("Opaque")); //啟用繪制調用上的實例化 drs.flags = DrawRendererFlags.EnableInstancing; //傳遞光照探針和光照貼圖數據給每個渲染器 drs.rendererConfiguration = RendererConfiguration.PerObjectLightProbe | RendererConfiguration.PerObjectLightmaps; //像普通不透明對象一樣排序對象 drs.sorting.flags = SortFlags.CommonOpaque;
繪制
現在我們已有了發送一個繪制調用所需的三樣東西:
-
剔除結果
-
過濾規則
-
繪制規則
我們可以發送一個繪制調用了。就像SRP中的所有東西一樣,繪制調用也是以一個針對上下文發出的調用。在SRP中,你通常不會渲染單獨的網格,而是發出一個調用,一次性渲染大批量的網格。這不僅減少了腳本執行上的開銷,也使CPU上的執行得以快速作業化。
要發送一個繪制調用,需要將我們已有的東西進行合並。
//繪制所有渲染器 context.DrawRenderers(cullResults.visibleRenderers, ref drs, opaqueRange); //提交上下文,這將執行所有隊列中的命令。 context.Submit();
這將會把對象繪制到當前綁定的渲染目標中。你可以使用一個命令緩沖區,按需切換渲染目標。
這是一個可以渲染不透明對象的渲染器:
using System; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Experimental.Rendering; [ExecuteInEditMode] public class OpaqueAssetPipe : RenderPipelineAsset { #if UNITY_EDITOR [UnityEditor.MenuItem("SRP-Demo/02 - Create Opaque Asset Pipeline")] static void CreateBasicAssetPipeline() { var instance = ScriptableObject.CreateInstance<OpaqueAssetPipe>(); UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/2-OpaqueAssetPipe/OpaqueAssetPipe.asset"); } #endif protected override IRenderPipeline InternalCreatePipeline() { return new OpaqueAssetPipeInstance(); } } public class OpaqueAssetPipeInstance : RenderPipeline { public override void Render(ScriptableRenderContext context, Camera[] cameras) { base.Render(context, cameras); foreach (var camera in cameras) { ScriptableCullingParameters cullingParams; if (!CullResults.GetCullingParameters(camera, out cullingParams)) continue; CullResults cull = CullResults.Cull(ref cullingParams, context); context.SetupCameraProperties(camera); var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, false, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); var settings = new DrawRendererSettings(camera, new ShaderPassName("BasicPass")); settings.sorting.flags = SortFlags.CommonOpaque; var filterSettings = new FilterRenderersSettings(true) { renderQueueRange = RenderQueueRange.opaque }; context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings); context.DrawSkybox(camera); context.Submit(); } } }
這個示例可以進一步擴展,添加透明渲染:
using System; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Experimental.Rendering; [ExecuteInEditMode] public class TransparentAssetPipe : RenderPipelineAsset { #if UNITY_EDITOR [UnityEditor.MenuItem("SRP-Demo/03 - Create Transparent Asset Pipeline")] static void CreateBasicAssetPipeline() { var instance = ScriptableObject.CreateInstance<TransparentAssetPipe>(); UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/3-TransparentAssetPipe/TransparentAssetPipe.asset"); } #endif protected override IRenderPipeline InternalCreatePipeline() { return new TransparentAssetPipeInstance(); } } public class TransparentAssetPipeInstance : RenderPipeline { public override void Render(ScriptableRenderContext context, Camera[] cameras) { base.Render(context, cameras); foreach (var camera in cameras) { ScriptableCullingParameters cullingParams; if (!CullResults.GetCullingParameters(camera, out cullingParams)) continue; CullResults cull = CullResults.Cull(ref cullingParams, context); context.SetupCameraProperties(camera); var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, false, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); var settings = new DrawRendererSettings(camera, new ShaderPassName("BasicPass")); settings.sorting.flags = SortFlags.CommonOpaque; var filterSettings = new FilterRenderersSettings(true) { renderQueueRange = RenderQueueRange.opaque }; context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings); context.DrawSkybox(camera); settings.sorting.flags = SortFlags.CommonTransparent; filterSettings.renderQueueRange = RenderQueueRange.transparent; context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings); context.Submit(); } } }
這裏要重點註意的是,渲染透明時,渲染順序會變為自後向前。
小結
我們希望本篇文章能幫你入門,開始編寫你自己的自定義SRP。下載Unity 2018.1 beta 即刻開始創作自定義SRP的旅程吧。更多關於Unity 2018.1的信息請訪問Unity Connect平臺!
可編程腳本渲染管線SRP