https://catlikecoding.com/unity/tutorials/custom-srp/custom-render-pipeline/
1. 新建 Render Pipeline
任何內容的渲染,最終都是要由 unity 決定在哪里,什么時候,以哪些參數進行渲染。根據目標效果的復雜程度,決定渲染的過程也很復雜。燈光,陰影,透明,圖像效果,體積效果等,必須以特定的順序渲染到最終的圖像。
實際項目中,建議從URP定制管線。本教程依然是從頭定制管線。
本篇教程展示基于前向渲染最簡單的 unlit 對象。之后會逐步加入光照,陰影等其它高級效果。
1.1 項目設置
創建3D項目。注意不要創建URP/HDRP項目。之后,可以到 Package Manager 中移除我們不需要的 package。我們只需要 Unity UI package 。
我們的項目要使用 linear color space,在 Edit/Project Settings/Player,平臺設置區域,Other Settings中,找到 Rendering,檢查并確保切換到 linear color space。
在場景中創建幾個對象,并為其指定材質:
-
紅色立方體:Standard shader
-
綠色,黃色立方體:Unlit/Color
-
藍色球:Standard shader,并切換到透明模式,指定貼圖
-
白色球:Unlit/Transparent
1.2 Pipeline Asset
我們按照URP的目錄組織方式,創建我們的目錄,并創建我們的 Pipeline Asset
創建 CustomeRenderPipelineAsset.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{protected override RenderPipeline CreatePipeline(){return null;}
}
-
[CreateAssetMenu] 語義,向資產右鍵創建菜單中添加菜單項
-
RP Asset 必須派生自 RenderPipelineAsset
-
必須實現 CreatePipeline 接口。Unity 通過調用該方法創建 RP 實例
在Asset窗口,右鍵 Create/Rendering/Custom Render Pipeline,創建 CustomeRenderPipeline.asset
在 Project Settings/Graphics 窗口,指定我們的管線:
由于我們目前沒有創建管線實例,因此,整個 Unity 的渲染窗口,都不會執行任何渲染。
1.3 Render Pipeline Instance
創建 CustomRenderPipeline.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;public class CustomRenderPipeline : RenderPipeline
{// RenderPipeline 定義的抽象接口,必須實現。但是由于 cameras 每幀分配內存,因此廢棄了。// 保持該方法為空即可protected override void Render(ScriptableRenderContext context, Camera[] cameras) { }// RenderPipeline 渲染入口protected override void Render(ScriptableRenderContext context, List<Camera> cameras){ }
}
2. 渲染
Unity 每幀調用 RP 實例的 Render 來執行渲染:
-
ScriptableRenderContext 提供引擎渲染接口,連接 Native Engine,我們將用該對象完成渲染。
-
cameras 場景中可能會使用多個對象,Unity 根據順序,用該參數傳入。
2.1 Camera Renderer
每個 Camera 都需要獨立渲染,我們可以直接在 CustomRenderPipeline.Render 中實現渲染邏輯,但是渲染邏輯代碼量會很大,為了代碼結構更易維護,更清晰,我們專門建立一個類,來渲染每個攝像機。為了方便,緩存下渲染參數。
using UnityEngine;
using UnityEngine.Rendering;public class CameraRenderer
{ScriptableRenderContext context;Camera camera;public void Render(ScriptableRenderContext context, Camera camera){this.context = context;this.camera = camera;}
}
基于 CameraRenderer,RP中的渲染代碼看起來是這樣的:
public class CustomRenderPipeline : RenderPipeline
{CameraRenderer cameraRenderer = new CameraRenderer();// RenderPipeline 渲染入口protected override void Render(ScriptableRenderContext context, List<Camera> cameras){ for(int i = 0; i < cameras.Count; i++){// 用 CameraRenderer 渲染每個攝像機cameraRenderer.Render(context, cameras[i]);}}
}
URP 中也是定義了 CameraRenderer 來執行渲染。用這種方法,如果未來希望每個攝像機用不同的方式渲染,擴展起來會很方便,例如一個攝像機是 first-person 視口,而另一個用來渲染3D地圖,或者使用 forward / deferred 渲染。
2.2 Draw the Skybox
CameraRenderer 渲染指定攝像機可以“看到”的對象。為了代碼的清晰,把這些任務實現到獨立的方法 DrawVisibleGeometry
中,同時先把 Skybox 繪制出來。
渲染時,通過方法 SetupCameraProperties
設置攝像機的VP矩陣,該矩陣可以在 shader 中,以 unity_MatrixVP 來訪問。
public class CustomRenderPipeline : RenderPipeline
{CameraRenderer cameraRenderer = new CameraRenderer();// RenderPipeline 渲染入口protected override void Render(ScriptableRenderContext context, List<Camera> cameras){ for(int i = 0; i < cameras.Count; i++){// 用 CameraRenderer 渲染每個攝像機cameraRenderer.Render(context, cameras[i]);}}
}
現在,渲染視口將正常渲染 Skybox,并且可以旋轉攝像機看到天空盒的不同角度。
2.3 Command Buffers
只有我們 Submit 之后,Context 才會渲染。在這之前,我們可以進行配置,以及添加我們的渲染指令。像繪制天空這種,有專門的接口來提交渲染,但是其它的渲染,則需要通過另外的 CommandBuffer 來進行渲染。場景中其它幾何體的渲染,就是用 CommandBuffer 來渲染的。
創建 CommandBuffer 我們可以直接創建一個 CommandBuffer,同時可以給它起名字,以在 Frame Debugger 中看到。
分析 CommandBuffer CommandBuffer 可以注入分析,通過調用 BeginSample
和 EndSample
實現。分析數據可以顯示在 Profiler 和 Frame Debugger 中。
執行 CommandBuffer CommandBuffer 執行通過調用 ExecuteCommandBuffer
。該方法將拷貝指令,不會清空它。我們后面要繼續復用該 CommandBuffer,因此我們要手動 Clear。我們把該流程定義成 ExecuteBuffer 方法。
現在,代碼看起來是這樣
public class CameraRenderer
{ScriptableRenderContext context;Camera camera;const string bufferName = "Render Camera";CommandBuffer buffer = new CommandBuffer{name = bufferName};public void Render(ScriptableRenderContext context, Camera camera){this.context = context;this.camera = camera;Setup();DrawVisibleGeometry();Submit();}void Setup(){buffer.BeginSample(bufferName);ExecuteBuffer();context.SetupCameraProperties(camera);}void DrawVisibleGeometry(){context.DrawSkybox(camera);}void Submit(){buffer.EndSample(bufferName);ExecuteBuffer();context.Submit();}void ExecuteBuffer(){context.ExecuteCommandBuffer(buffer);buffer.Clear();}
}
2.4 Clearing the Render Target
渲染結果最終體現在 Render Target 上,為了避免上一幀(也可能是上上幀)的圖像對當前幀產生影響,每次渲染,我們都要清理 Render Target,通過調用 CommandBuffer.ClearRenderTarget
完成。
ClearRenderTarget 會自動封裝一個以 CommandBuffer 的名字的采樣,因此在 FrameDebugger 中會出現嵌套
先執行 Clear,再啟用我們的 Sample ,可以避免。
如果執行 Clear 時,還沒有執行 SetupCameraProperties,Unity 會用 Hidden/InternalClear Shader 來渲染一個矩形的方式來“清理”(Draw GL),這種方式相對很慢。我們可以先執行 SetupCameraProperties,再 Clear,這樣 Unity 會通過API層的 Clear 調用來完成清理,效率高得多。
現在,代碼是這樣
void Setup(){context.SetupCameraProperties(camera);buffer.ClearRenderTarget(true, true, Color.clear);buffer.BeginSample(bufferName);ExecuteBuffer();}
2.5 Culling
根據當前攝像機,裁剪出所有在視錐體內的 Renderer Component。
-
camera.TryGetCullingParameters(out ScriptableCullingParameters p)
-
CullingResults cullingResults = context.Cull(ref p);
定義一個 Cull 方法實現裁剪,如果成功,則獲取裁剪結果:
CullingResults cullingResults;bool Cull(){if(camera.TryGetCullingParameters(out ScriptableCullingParameters p)){cullingResults = context.Cull(ref p);return true;}return false;}
在渲染中,執行裁剪,如果失敗,則中止渲染,直接返回。
CullingResults cullingResults;bool Cull(){if(camera.TryGetCullingParameters(out ScriptableCullingParameters p)){cullingResults = context.Cull(ref p);return true;}return false;}
2.6 Draw Geometry 分別繪制不透明和透明物體
得到裁剪結果后,就可以通過 context.DrawRenderers
來渲染他們了。在調用該接口前,需要進行設置:
-
DrawingSettings
-
通過 ShaderTagId 指定繪制 shader 的哪個 pass. 目前我們只繪制 Pass SRPDefaultUnlit
-
通過 SortingSetings 指定如何排序????指定排序策略為 SortingCriteria.CommonOpaque,從前到后的順序
-
-
FilteringSettings 指示渲染哪些隊列?通過為 filteringSettings 傳入參數 RenderQueueRange,指示渲染哪些內容。
代碼是這樣的:
void DrawVisibleGeometry(){// 渲染不透明物體var sortingSettings = new SortingSettings(camera){ criteria = SortingCriteria.CommonOpaque };var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);context.DrawSkybox(camera);// 渲染透明物體sortingSettings.criteria = SortingCriteria.CommonTransparent;drawingSettings.sortingSettings = sortingSettings;filteringSettings.renderQueueRange = RenderQueueRange.transparent;context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);}
現在渲染結果是這樣的:
3. Editor Rendering 編輯器渲染
現在我們的 RP 正確的渲染了 unlit 的材質,但是對于 standard 材質不能正確渲染。在編輯器中,我們要以特殊方式將無法渲染的材質渲染出來,并告訴用戶出錯了,這對用戶體驗很重要。
3.1 Drawing Legacy Shaders
如果項目過程中切到我們的 RP,場景中可能會使用一些我們不支持的 Shader。把不支持的 Shader 記錄下來,并在最后用特殊的材質將他們渲染出來,以向用戶提示這些材質需要更換。
void DrawVisibleGeometry(){// 渲染不透明物體var sortingSettings = new SortingSettings(camera){ criteria = SortingCriteria.CommonOpaque };var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);context.DrawSkybox(camera);// 渲染透明物體sortingSettings.criteria = SortingCriteria.CommonTransparent;drawingSettings.sortingSettings = sortingSettings;filteringSettings.renderQueueRange = RenderQueueRange.transparent;context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);}
錯誤材質
通過調用 new Material(Shader.Find("Hidden/InternalErrorShader")); 來創建一個材質,用來渲染材質錯誤的情況。
定義 DrawUnsupportedShaders 接口來渲染他們:
void DrawUnsupportedShaders(){if(errorMaterial == null){errorMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));}var drawingSettings = new DrawingSettings(legacyShaderTagIds[0], new SortingSettings(camera))// 指示錯誤的材質{ overrideMaterial = errorMaterial };for (int i = 1; i < legacyShaderTagIds.Length; i++){drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);}var filteringSettings = FilteringSettings.defaultValue;context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);}
不支持的 Standard Shader 將會以紫色顯示:
3.2 Partial Class
渲染錯誤材質,僅在編輯器下是有用的,在 Release 時是不需要被渲染的。得益于C# 的 partial 機制,可以讓我們將一個類的定義分散到多個文件中。因此我們把這部分代碼定義到 CameraRender.Editor.cs 中,同時用 UNITY_EDITOR 宏讓這部分代碼僅在編輯器時有效.
3.3 Draw Gizmos
可以通過 UnityEditor.Handles.ShouldRenderGizmos 判斷是否需要渲染 Gizmos,如果需要,則調用 context.DrawGizmos
-
第一個參數是攝像機
-
第二個參數指定要渲染的 Gizmos 的子集:
-
image effect 階段之前
-
image effect 階段之后
-
目前我們還沒有 image effect,因此直接渲染兩者:
partial void DrawGizmos();
#if UNITY_EDITORpartial void DrawGizmos(){if (Handles.ShouldRenderGizmos()){context.DrawGizmos(camera, GizmoSubset.PreImageEffects);context.DrawGizmos(camera, GizmoSubset.PostImageEffects);}}
#endif
3.4 Draw Unity UI
渲染 Scene 窗口 中的 UI
如果當前攝像機是 CameraType.SceneViewn 類型,通過調用 ScriptableRenderContext.EmitWorldGeometryForSceneView(camera) 提交UI的渲染。
partial void PrepareForSceneWindow();
#if UNITY_EDITOR partial void PrepareForSceneWindow(){if (camera.cameraType == CameraType.SceneView){ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);}}
#endif
在繪制中調用該接口:
public void Render(ScriptableRenderContext context, Camera camera)
{...PrepareForSceneWindow();if (!Cull())return;...
}
4. 多攝像機
場景中可能有多個攝像機,需要我們正確的處理
4.1 兩個攝像機
每個攝像機都有Depth
屬性,默認著攝像機的 depth =-1,多個攝像機以 depth 升序進行渲染。
我們之前為 CommandBuffer 設置的剖析時的名字,是用的固定的字符串。當有多個攝像機時,由于名字一致,導致無法將兩個攝像機的渲染區分開來。
因此我們需要根據攝像機的名字來設置剖析的名字
同時,在調用 BeginSample/EndSample 時,需要指定同樣的名字,否則編輯器會報 BeginSample and EndSample counts 不匹配的錯誤信息。
由于獲取攝像機名字,會導致內存分配,因此將其包裹在 "EditorOnly" 中,以做區分
#if UNITY_EDITOR string SampleName { get; set; }partial void PrepareBuffer(){// 由于獲取攝像機名字,會導致內存分配,因此將其包裹在 "EditorOnly" 中,以做區分Profiler.BeginSample("Editor Only");buffer.name = SampleName = camera.name;Profiler.EndSample();}
#elseconst string SampleName = bufferName;
#endif
void Setup(){context.SetupCameraProperties(camera);buffer.ClearRenderTarget(true, true, Color.clear);buffer.BeginSample(SampleName);ExecuteBuffer();}...void Submit(){buffer.EndSample(SampleName);ExecuteBuffer();context.Submit();}
4.2 Layers
可以在編輯器中設置對象的 Layer,并設置攝像機的 Culling Mask,使攝像機只能看到我們想讓它看到的東西。
4.3 Clear Flags
我們可以通過配置后續攝像機的 Clear Flags 來合并兩個攝像機的渲染結果。
camera.clearFlags屬性返回枚舉類型 CameraClearFlags
。然后在 ClearRenderTarget 時,適當的使用這個屬性。
如果使用攝像機顏色進行清空,也要正確使用攝像機顏色
void Setup(){context.SetupCameraProperties(camera);buffer.ClearRenderTarget(true, true, Color.clear);buffer.BeginSample(SampleName);ExecuteBuffer();}...void Submit(){buffer.EndSample(SampleName);ExecuteBuffer();context.Submit();}
如果修改了 Camera.ViewRect,則 Clear 將會利用 Hidden/InternalClear shader 進行清屏,效率低。