自定義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,我們對於第一個相機的渲染結果不做任何處理,
主要區別在這個地方,紫色方塊是在綠色方塊後方的,如果我們清空深度緩衝,第二張圖的渲染結果會覆蓋第一張圖,因此粉色方塊反而出現在綠色方塊的前方了,不清空深度緩衝的話,渲染結果就正確了。