基於SRP建立自定義渲染管線
這篇文章為翻譯文章,為避免翻譯的文章不在原創列表列裡,設定為原創,特此宣告
原文地址: https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/custom-pipeline/
- 建立一個管線資源和例項
- 剔除、過濾、排序、渲染
- 保證記憶體乾淨
- 提供一個好的編輯實踐
這是關於unity的可程式設計渲染管線的第一個教程。這裡假定你看過了Basics系列和Procedural Grid系列的教程。Rendering系列的前面一部分也很有用。
這個教程是基於Unity2018.3.0f2的
1. 建立一個管線
為了渲染一些東西出來,Unity必須決定在什麼地方,什麼時間使用什麼設定來繪製什麼形狀的物體。這個過程根據要包含哪些效果可能非常複雜。光照、陰影、透明、後處理、體積光或霧等都需要以一個合適的順序來出現在最終的影象內。這個過程就叫做渲染管線。
Unity2017支援兩個預定義的渲染管線,一個用於前向渲染一個用於延遲渲染。同時也支援一個更早前 Unity5中引入的延遲渲染方法。這些都是固定管線,你可以通過開關或者重寫部分管線功能,但是不能完全脫離這些管線的設定。
Unity2018加入了對可程式設計渲染管線的支援,使得從無到有設計一個管線成為可能,儘管依然需要在一些獨立的步驟上依賴Unity,例如視錐體裁剪(因為這些功能使用C++實現會更加高效,而且幾乎是固定的模式)。Unity2018引入了兩個使用這個新的可程式設計渲染管線方式實現的管線,輕量管線和高清管線(LWRP 和HDRP)。這兩個管線依然在預覽階段,而且可程式設計管線(SRP)的API依然標記為實驗技術。但是在目前的時間節點上,SRP已經足夠穩定到我們可以使用基於SRP建立自己的管線。
在這個教程中,我們將配置一個最小的管線,這個管線會繪製不受光的物體。一旦我們的管線可以工作,我們就能夠在後面的教程中進行擴充套件,新增光照、陰影等高階效果。
1.1 專案設定
開啟Unity 2018 ,建立一個標準3D專案,關閉統計服務。我們將建立自己的管線,因此不要選擇LWRP或HDRP這兩個管線。
工程開啟之後,開啟包管理器(Window/Package Manger)並移除所有預設包含進來的package。最終只留下Package Manager UI,這個包我們沒法移除。
我們將使用線性顏色空間,到那時Unity2018預設使用的時伽馬空間。在player setting面板(Editor/Project Setting/Player) 把顏色空間切換為Linear。
我們將需要幾個簡單的材質來測試我們的管線。建立4個材質,第一個是反照率(Albedo)設定為紅色的預設標準非透明材質。第二個是反照率設定為藍色的並且有一定透明度的標準透明材質。第三個是使用Unlit/Color這個shader的設定為黃色的不受光材質。最後一個是使用Unlit/Transparnet 這個shader並且不做任何修改的材質,因此這個材質顯示一個沒有透明度的白色。
使用這個四個材質,建立一些物體放到到場景內。
1.2 Pipeline Asset
當前unity使用的是預設的前向渲染管線。為了使用自定義管線,我們需要在grpahics settings(Edit->Project Settings -> Graphics)視窗選擇一個PipelineAsset
為了設定我們自己的管線,我們必須給Scriptable Render Pipeline Settings 這裡新增一個管線資產。這種資產必須繼承自RenerPipelineAsset,是一個ScriptableObject型別的物件。
為我們的自定義管線資產建立一個新的指令碼,我們把自己的管線稱為MyPiepline,因此它的資產叫做MyPipelineAsset,而且必須派生自RenderPipelineAsset,這個型別定義在UnityEngine.Experimental.Rendering 這個名稱空間下。
using UnityEngine;
using UnityEngine.Experimental.Rendering;
public class MyPipelineAsset:RenderPipelineAsset{}
從名稱空間可以看到這裡的RenderPipelineAsset還是位於實驗階段,但是可以預見的是,在以後的某個時間結點,從會實驗階段轉到一個正式的名稱空間下,如果到時候沒有API的修改,我們只需要修改下引用的名稱空間即可。
這裡設定PipelineAsset的主要目的就是以一種方式把一個進行渲染工作的管線物件例項給到Unity。這個Asset本身只是一個控制代碼和儲存管線設定的地方。當然我們現在還沒有任何的設定,因此我們需要做的就是給Unity一個能夠拿到我們的管線物件例項的方法。這是通過重寫InternalCreatePipeline方法來完成的。但是現在我們還沒有定義自己的管線物件型別,因此我們先返回一個空值。
InternalCreatePipeline的返回型別是IRenderPipeline。這裡的字首I代表這是一個介面型別。
public class MyPipelineAsset : RenderPipelineAsset {
protected override IRenderPipeline InternalCreatePipeline () {
return null;
}
}
介面就像一個類(Class),區別就是介面只提供了類的一個框架而沒有對應的實現。介面內只能定義屬性、事件、索引器和方法簽名,而且都預設使用public修飾。任何從介面擴展出的型別都需要實現介面定義的方法。習慣上使用I作為介面型別名稱的字首。
因為介面不包含具體的實現,因此類或結構體可以繼承自多個介面。如果多個介面碰巧定義了同樣的方法,只需要存在這個方法的實現即可。而這對於類(即使是抽線類)是不可能的,因為這樣會導致衝突。
現在我們需要新增一個這種型別的資產到我們的專案下,為了實現這一目的,我們為我們的MyPipelineAsset類新增一個CreateAssetMenu屬性。
[CreateAssetMenu]
public class MyPipelineAsset : RenderPipelineAsset {}
這會為Asset/Create選單新增一個新的選項。我們稍作修改,把這個操作放在Rendering子選單下。我們通過設定這個屬性的menuName引數為"“Rendering/MyPipeline”。
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]
public class MyPipelineAsset : RenderPipelineAsset {}
使用這個新的選單來新增一個MyPieplineAsset到我們的專案下,命名為MyPipeline。
然後把它賦值給ScriptableRenderPipelineSettings。
我們現在已經替換了預設的渲染管線,這個操作會改變一些內容。首先,graphics settings面板上的很多選項都消失了,unity會在一個訊息面板上做出提示。其次,因為我們用一個無效的空值替換了預設的渲染管線,什麼都不會渲染,包括game、scene、材質球預覽等視窗,儘管scene視窗依然顯示有一個skybox。如果此時開啟frame debugger(window/Analysis/Frame Debugger)並且開啟它,你會看到沒有任何的繪製命令。
1.3 管線例項
為了建立一個有效的渲染管線,我們需要提供一個實現了IRenderPipeline介面的物件例項,就是物件例項去執行事實上的渲染過程。因此建立一個命名為MyPipeline的類:
using UnityEngine;
using UnityEngine.Experimental.Rendering;
public class MyPipeline : IRenderPipeline {}
儘管我們可以自己實現IRenderPiepline介面,大師直接從抽象類RenderPipeline進行派生會更加方便。這個抽象類已經提供了一個IRenderPipeline介面的基本實現:
public class MyPipeline : RenderPipeline {}
現在我們可以子InternalCreatePipeline方法裡返回一個MyPipeline型別的例項物件。此時我們就有了一個有效的渲染管線,儘管它依然什麼都沒做。
protected override IRenderPipeline InternalCreatePipeline () {
return new MyPipeline();
}
2 渲染
管線物件進行每一幀的渲染操作。Unity做的事情就是呼叫這個管線的Render方法,同時會傳入當前的context和相機。game 視窗是如此邏輯,編輯器下的Scene視窗和材質預覽視窗同樣如此。如何配置這些內容,並決定渲染哪些物件以及以怎樣的順序進行渲染就全部是我們的工作了。
2.1 Context
RenderPipeline已經有了一個IRenderPipeline中介面定義的Render方法的實現。它的第一個引數是render context ,是一個ScriptableRenderContext型別的結構體,是一個和底層C++程式碼溝通的橋樑。它的第二個引數是一個包含了所有待渲染相機的陣列。
RenderPipeline.Render並不會繪製任何內容,但是會檢測這個用於渲染的管線物件是否有效。如果無效,會丟擲一個一場。我們將重寫該方法,並呼叫基類實現來保證這個檢測的邏輯依然存在:
public class MyPipeline : RenderPipeline {
public override void Render (
ScriptableRenderContext renderContext, Camera[] cameras
) {
base.Render(renderContext, cameras);
}
}
我們就是通過這個renderContext來發送渲染命令到Unity來渲染物體以及控制渲染狀態。一個最簡單的實踐就是繪製一個天空盒,可以通過呼叫DrawSkyBox方法實現:
base.Render(renderContext, cameras);
renderContext.DrawSkybox();
DrawSkyBox方法需要一個相機作為引數,因此我們簡單的使用相機列表的第一個元素。
renderContext.DrawSkybox(cameras[0]);
但是我們依然沒有在game視窗下看到天空盒。這是因為我們發起給當前context的命令被快取下來,只有我們提交了當前的命令緩衝區,命令才會真正執行,這可以通過Submit方法實現:
renderContext.DrawSkybox(cameras[0]);
renderContext.Submit();
最終在Game視窗下我們看到了天空盒,而且可以在幀偵錯程式(Frame debugger)裡看到繪製命令的呼叫:
2.2 Cameras
注意到Render函式傳入的引數裡有一個相機陣列,這是因為場景內可能存在多個需要渲染的相機。多相機的例子有,分屏多使用者應用,小地圖,鏡子等。每一個相機都需要分開操作。
我們暫時不考慮多相機的問題。我們簡單的建立另一個Render方法,這個方法只對單個相機進行操作。目前這個方法會繪製天空盒並且提交繪製命令。因此我們是每個相機提交一次繪製命令。
void Render (ScriptableRenderContext context, Camera camera) {
context.DrawSkybox(camera);
context.Submit();
}
遍歷相機陣列,對每一個相機都呼叫一次這個新的Rener方法。
public override void Render ( ScriptableRenderContext renderContext, Camera[] cameras ) {
base.Render(renderContext, cameras);
//renderContext.DrawSkybox(cameras[0]);
//renderContext.Submit();
foreach (var camera in cameras) {
Render(renderContext, camera);
}
}
執行我們的程式碼會發現,相機當前的朝向不會影響到天空盒的渲染。我們在呼叫DrawSkybox方法時傳入了相機物件,但是那只是用來決定天空盒時候應該被繪製,這是通過相機的clear flags進行控制的。
為了正確的渲染天空盒以及整個場景,我們需要設定觀察投影矩陣(view-projection matrix)。這個轉換矩陣是用相機的位置和朝向生成的觀察矩陣(view matrix)和相機的透視投影或者正交投影生成的投影矩陣(projection matrix)構成的。可以在幀偵錯程式裡看到這個矩陣,就是unity_MatrixVP,著色器(shader)用來繪製物體時的其中一個引數。
此時,這個unity_MatrixVP矩陣總是一樣的。我們必須通過SetupCameraProperties方法來把相機屬性應用到當前的渲染環境(context)下。這個方法會設定這個矩陣以及其他一些引數。
void Render (ScriptableRenderContext context, Camera camera) {
context.SetupCameraProperties(camera);
context.DrawSkybox(camera);
context.Submit();
}
現在當我們把相機引數考慮在內之後,天空盒的繪製就是正確的了,不止在Game檢視,Scene檢視同樣如此。
2.3 Command Buffer 命令緩衝
這個context會延遲執行真正的渲染操作直到我們提交了它。在那之前,我們都是配置和往context裡新增命令以備之後的執行。一些任務,例如繪製天空盒可以通過一個特定的方法進行呼叫,但是其他一些命令必須間接通過一個單獨的命令緩衝才能發起。
一個命令緩衝可以通過例項化一個新的CommandBuffer物件進行建立,這個物件定義在UnityEngine.Rendering名稱空間。在可程式設計渲染管線出現之前命令緩衝就已經出現了,因此他們不是實驗階段的API。在我們繪製天空盒之前建立一個命令緩衝:
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
public class MyPipeline : RenderPipeline {
…
void Render (ScriptableRenderContext context, Camera camera) {
context.SetupCameraProperties(camera);
var buffer = new CommandBuffer();
context.DrawSkybox(camera);
context.Submit();
}
}
我們可以通過呼叫ExecuteCommandBuffer方法通知context來執行這個命令緩衝。再次宣告,這個操作並不是立即執行命令,只是把命令拷貝到context的內部緩衝區。
var buffer = new CommandBuffer();
context.ExecuteCommandBuffer(buffer);
CommandBuffer會從Unity引擎的底層請求資源來儲存他們的命令,因此如果我們不再需要謝謝資源之後,最好立即釋放他們。這個可以通過呼叫Release方法來實現,通常是直接在呼叫ExecuteCommandBuffer之後就執行釋放操作。
var buffer = new CommandBuffer();
context.ExecuteCommandBuffer(buffer);
buffer.Release();
執行一個空的命令緩衝不會做任何事情。我們的目的是為了清空當前的渲染目標(render target),來確保當前的渲染不會受之前的渲染結果影響。這可以通過命令緩衝來完成,而不是直接通過context進行。
可以通過呼叫ClearRenderTarget方法來新增一個清空命令。它需要三個引數,分別是是否清空深度資訊的布林值,是否清空顏色資訊的布林值和清空所使用的顏色。例如我們清空深度資料,而忽略顏色資料,使用Color.clear來作為清空顏色。
var buffer = new CommandBuffer();
buffer.ClearRenderTarget(true, false, Color.clear);
context.ExecuteCommandBuffer(buffer);
buffer.Release();
幀偵錯程式現在會顯示一個命令緩衝被執行,它完成的操作是清空渲染目標。在我們的例子中,顯示的是Z和stencil被空了。Z就是深度緩衝,stencil就是模板緩衝(stencil buffer)。
更明確的做法是,通過每個相機對clear falgs的配置執行這個清空操作。我們可以使用這些設定而不是硬編碼來清空渲染目標。
CameraClearFlags clearFlags = camera.clearFlags;
buffer.ClearRenderTarget(
(clearFlags & CameraClearFlags.Depth) != 0,
(clearFlags & CameraClearFlags.Color) != 0,
camera.backgroundColor
);
我們可以看到在幀偵錯程式裡看到的是一個Unnamed command buffer,這是因為我們沒有給command buffer以指定的名字,因此就會預設顯示我們的看到的名字。我們使用相機的迷城作為command buffer的名字,把相機名字賦值給緩衝的name欄位即可。我們使用物件初始化器語法來完成這個操作:
var buffer = new CommandBuffer {
name = camera.name
};
物件初始化語法(objectinitializer syntax):注意當使用了物件初始化語法時,我們不需要呼叫空引數列表的建構函式(new 物件時後面跟的圓括號"()")。
2.4 Culling 裁剪
我們現在能夠渲染天空盒但是還不能渲染場景內的物體。我們只會渲染哪些相機能夠看到的物體而不是所有的物體。我們從場景內的所有渲染器開始,然後裁剪掉那些落在相機視錐體外部的渲染器。
什麼是渲染器(Renderer)?渲染器是一個附加在game object上的一個元件,用於把他們轉換為可以渲染的物體。通常是一個MensRenderer元件
為了知道哪些可以被裁剪掉,我們需要知道許多相機設定和矩陣,我們會使用ScriptableCullingParameters結構體來管理這些餐宿。我們可以把填充這個結構體的任務委託給靜態方法CullResults.GetCullingParameters。這個方法使用一個相機作為輸入引數,以裁剪引數作為輸出。然而,不是通過返回值,而是通過標記為 out 的函式引數來進行輸出:
void Render (ScriptableRenderContext context, Camera camera) {
ScriptableCullingParameters cullingParameters;
CullResults.GetCullingParameters(camera, out cullingParameters);
…
}
這裡使用out的意義?cullingParameters是一個結構體,結構體作為函式引數並被out 標記為輸出引數時,結構體的行為就類似於一個物件引用,函式內操作的就是位於堆疊上的這個引數,而不是其拷貝。
除了這個輸出引數,GetCullingParameters同樣會返回是否成功建立了有效的裁剪引數。並不是所有的相機設定都是有效的。因此如果返回結果表明建立失敗,我們就沒有需要渲染的物件,可以直接退出Render方法。
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
return;
}
有了裁剪引數之後,我們就可以執行裁剪操作了。通過呼叫靜態方法CullResults.Cull並使用裁剪引數和context作為函式引數。函式返回值時一個CullResult結構,它包含了場景內的可見性資訊。
在這裡,我們必須通過使用ref關鍵字設定裁剪引數作為一個引用引數。
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
return;
}
CullResults cull = CullResults.Cull(ref cullingParameters, context);
因為ScriptableCullingParameters是一個很大的結構體,而結構體作為函式引數會進行資料拷貝,因此這裡使用ref關鍵字,避免掉記憶體拷貝,就是因為效能的原因。可能這個結構在開始是小的,只是隨著時間推移越來越大。可重用的物件例項可能更適合,但是我們只能使用Unity Technologiest提供的方式。
2.5 Drawing
知道哪些內容是可見的之後,我們接下來可以渲染這些幾何體了。通過呼叫context的DrawRenderers方法並使用cull.visibleRenderers作為引數來告訴它使用哪些渲染器(renderers)來進行渲染。除此之外,我們必須設定繪製設定(draw settings)引數和過濾設定(filter settings)。這兩個設定都是結構體:DrawRendererSettings和FilterRenderersSettings,我們將使用他們的預設值進行初始化。繪製設定必須以引用的形式進行傳遞:
buffer.Release();
var drawSettings = new DrawRendererSettings();
var filterSettings = new FilterRenderersSettings();
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
context.DrawSkybox(camera);
為什麼是FilterRenderersSettings而不是FilterRendererSettings?可能只是一個程式碼編輯錯誤。
我們依然什麼都沒看到,因為預設的過濾設定什麼都沒有包含。我們可以通過提供true作為FilterRenderersSettings的建構函式的引數來設定包含所有物體。
var filterSettings = new FilterRenderersSettings(true);
同樣,我們必須通過提供相機和一個shader pass 作為draw setting的建構函式的引數。這個相機用來設定排序和裁剪層級(culling layers),而shader pass 控制使用那個shader pass進行渲染。
因為我們的管線只支援不受光的材質,我們使用unity的預設unlit材質,標記為"SRPDefaultUnlit"
var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("SRPDefaultUnlit")
);
我們能夠看到不透明的不受光幾何體出現了,但是沒有透明的物體。然而幀偵錯程式顯示透明物體也進行了渲染。
他們確實繪製了,但是因為透明著色器沒有寫入深度緩衝,他們最終被天空盒遮擋住了。解決方案就是在天空盒繪製之後繪製透明物體。
首先,限制天空盒前面的繪製只對不透明渲染器有效。通過設定filter setting 的 renderQueueRange引數為RenderQueueRange.opaque來達到這一目的。這會繪製把渲染佇列位於0-2500之間的物體。
var filterSettings = new FilterRenderersSettings(true) {
renderQueueRange = RenderQueueRange.opaque
};
然後,在渲染天空盒之後,再次渲染一次,只是此次我們設定佇列範圍為RenderQueueRnage.transparnet來渲染2501-5000之間的物體
var filterSettings = new FilterRenderersSettings(true) {
renderQueueRange = RenderQueueRange.opaque
};
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
context.DrawSkybox(camera);
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
我們在天空盒之前渲染非透明物體來避免overdraw。因為非透明物體會寫入深度緩衝,可以用來跳過哪些在相比非透明物體離相機更遠的畫素(這裡是天空盒和其他非透明物體)的繪製。
因此為了儘可能的降低overdraw,我們需要從近到遠繪製非透明物體。而這可以通過設定sorting flags來進行控制。
draw settings包含一個型別為DrawRendererSortSettings的名為sorting結構體包含有sort flags。在繪製非透明物體之前,設定這個引數為SortFlags.CommonOpaque。這回告訴Unity對渲染器按從近到遠進行排序。
var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("SRPDefaultUnlit")
);
drawSettings.sorting.flags = SortFlags.CommonOpaque;
然而,透明物體使用不同的策略。因為透明物體會混合已經填入顏色緩衝區的顏色和當前顏色來生成半透明效果。而這需要和非透明物體相反的順序,從後往前或從遠到近。我們可以使用SortFlags.CommonTransparnet來進行設定。
context.DrawSkybox(camera);
drawSettings.sorting.flags = SortFlags.CommonTransparent;
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
我們的渲染管線現在就可以正確渲染透明和非透明的不受光物體了。
3 優化
能夠正確的進行渲染只是管線功能的一部分。其他包括,這個管線是否足夠快,是否分配了不必要的臨時物件,以及和Unity編輯器的整合情況。
3.1 記憶體分配
通過Profile視窗檢視我們的管線在記憶體管理或者每一幀的記憶體分配問題。每一幀內的記憶體分配會觸發頻繁的GC。我們有幾個可以修改的地方進行記憶體優化:
第一點, CullResult雖然是一個結構型別,但是其內部有三個List型別,這個是一個物件,每一次我們new 一個CullResults物件都會為新的list分配記憶體空間。因此CullResult作為一個結構體並沒有什麼優勢。幸運的是CullResults有另一個Cull方法可以接受一個引用型別引數作為輸出,這樣我們就可以重用這個lists。
CullResults cull;
…
void Render (ScriptableRenderContext context, Camera camera) {
…
//CullResults cull = CullResults.Cull(ref cullingParameters, context);
CullResults.Cull(ref cullingParameters, context, ref cull);
…
}
第二個連續的記憶體分配是我們使用了camera 的 name屬性。每次我們使用camera.name,都會從底層程式碼裡獲取name資料,而這會迫使建立一個新的string型別,而string型別是一個引用型別。因此我們直接值用字面常量“Render Camera”初始化我們的commandbuffer的name屬性:
var buffer = new CommandBuffer() {
name = "Render Camera"
};
最後CommandBuffer自己本身就是一個物件型別。我們可以複用我們建立的CommandBuffer,每次渲染結束之後呼叫Clear方法而不是Release方法。
CommandBuffer cameraBuffer = new CommandBuffer {
name = "Render Camera"
};
…
void Render (ScriptableRenderContext context, Camera camera) {
…
//var buffer = new CommandBuffer() {
// name = "Render Camera"
//};
cameraBuffer.ClearRenderTarget(true, false, Color.clear);
context.ExecuteCommandBuffer(cameraBuffer);
//buffer.Release();
cameraBuffer.Clear();
…
}
經過這些修改之後,我們在每一幀之內就不再建立臨時物件了。
3.2 Frame Debugger Sampling
我們要做的另一件事是優化在frame debugger裡顯示的內容。通過呼叫BeginSample和EndSample來設定sample的名稱,開始和結束的名稱必須一樣,而且最好和定義這個sampleing的CommandBuffer保持一致。
cameraBuffer.BeginSample("Render Camera");
cameraBuffer.ClearRenderTarget(true, false, Color.clear);
//cameraBuffer.EndSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
…
cameraBuffer.EndSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
context.Submit();
這裡會發現這個clear指令嵌入在一個冗餘的RenderCamera層級下,而其他所有的指令都直徑二位於根層級下。我不確定為什麼會這樣,但是可以通過把BeginSample放在clear之後可以避免這個問題:
//cameraBuffer.BeginSample("Render Camera");
cameraBuffer.ClearRenderTarget(true, false, Color.clear);
cameraBuffer.BeginSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
3.3 Rendering the Default Pipeline
因為我們的管線目前只支援不受光的著色器,哪些使用其他著色器的物體不會被渲染,他們會看不到。儘管這符合我們的預期,但是隱藏了場景內有物體使用了錯誤的著色器的事實。如果我們能夠用unity的error shader來顯示這些看不見的物體,就可以明顯的提示我們場景內有材質使用了錯誤的shader。我們使用一個專用的DrawDefaultPipeline方法來實現這個功能:
void Render (ScriptableRenderContext context, Camera camera) {
…
drawSettings.sorting.flags = SortFlags.CommonTransparent;
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
DrawDefaultPipeline(context, camera);
cameraBuffer.EndSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
context.Submit();
}
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {}
Unity的預設表面著色器有一個ForwardBase pass。我們可以使用這個來識別那些使用了unity預設管線下的shader的物體。在DrawSetting裡設定pass的名稱,並且在filter setting裡設定渲染所有物體。我們不關心出錯的物體的排序和渲染順序。
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("ForwardBase")
);
var filterSettings = new FilterRenderersSettings(true);
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
}
那些使用了預設的shader的物體現在可以看到了,而且可以從幀偵錯程式中看到對他們的呼叫:
因為我們的管線不支援forward base pass,因此他們都渲染的不正確。一些shader內需要的資料都沒有設定,導致依賴於光照資料的物件都顯示為黑色。我們真正要做的是使用一個標誌出錯的材質渲染這些物件。
Material errorMaterial;
…
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
if (