unity hub是什么东西_Unity可编程渲染管线(SRP)教程:一、自定义管线
本文翻譯自Catlike Coding,原作者:Jasper Flick。
本文經原作者授權,轉載請說明出處。
原文鏈接在下:
https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/custom-pipeline/catlikecoding.com
自定義管線?控制渲染
創建pipeline asset 和 instance.
剔除、過濾、排序、渲染.
保持內存清潔.
提供良好的編輯體驗.
這是Unity scriptable render pipeline系列教程的第一部分。本教程假設你首先通過了Basics系列教程和Procedural Grid教程。Rendering系列的頭幾部分也很有幫助。
本教程使用Unity 2018.3.0f2完成。
創建一個Pipeline
渲染任何東西Unity都需要明確哪些形狀、何處、何時以及使用什么設置。這些可以變得非常復雜取決于涉及到多少效果。光源、陰影、半透明、圖像效果、體積效果以及等等都需要用正確的順序處理,以獲得最終的圖像。這個過程被稱之為渲染管線。
Unity 2017 支持兩種預定義渲染管線,一種是前向渲染,另一種是延遲渲染。也仍然支持Unity 5中引入的舊延遲渲染方法。這種管線是固定的。你可以開啟、關閉或者覆蓋管線中某些部分,但不可以徹底偏離它的設計。
Unity 2018 添加了可編程渲染管線的支持,使得從頭設計管線成為可能,盡管你仍然需要依賴Unity來完成許多單獨的步驟,例如剔除。Unity 2018引入了兩種使用這種新方法的管線,輕量級渲染管線和高分辨率渲染管線。兩種管線仍處于預覽階段并且可編程渲染管線API依然被標記為實驗技術。但是在這一點上對我們來說足夠穩定可以繼續創建并創建我們自己的管線。
在這個教程中我們會設置一個繪制Unlit圖形的最小渲染管線。一旦起作用了,我們可以在之后的教程中擴展我們的管線,添加光照、陰影、以及更多的高級特性。
1.1?項目設置
打開Unity 2018并創建一個新項目。我使用的是Unity 2018.2.9f1,但是任何2018.2版本或者更新版應該也可以使用。創建一個Standard 3D項目并禁用analytics。我們會創建我們自己的管線,所以不選擇任何管線選項。
項目打開后,由Window/Pakage Manager打開包管理器,并且移除所有默認包含的包,因為我們不需要它們。只保留不可以被移除的Package Manager UI。
初始的項目我們要在線性色彩空間下工作,但是Unity 2018依然默認使用gamma空間。所以由Edit/Project Setting/Player到player setting中,然后將Other Setting部分中的色彩空間切換到Linear。
線性色彩空間我們需要幾個簡單的材質去測試我們的管線。我創建了四種材質。第一種,默認的standard opaque材質帶有紅色albedo。第二種,一樣的材質但是Rendering Mode設置為Transparent以及藍色的albedo帶有減少的alpha。第三種,使用Unlit/Color shader并且顏色設置為黃色的材質。最后一種使用Unlit/Transparent shader并不做任何改變,因此顯示為純白色。
測試材質使用一些object填充場景,讓它們用完所有的四種材質。
場景顯示四種材質1.2?Pipeline Asset
現在,Unity使用默認的前向渲染管線。要使用自定義管線,我們需要在grahics settings中選擇,可以由Edit/Project Setting/Graphics找到。
使用默認管線要設置我們自己的管線,我們需要分配一個pipeline asset到Scriptable Render Pipeline Settings字段。這類asset需要擴展RenderPipelineAsset,這是一種ScriptableObject類型。
為我們的自定義pipeline asset創建一個新腳本。我們將簡單地命名我們的管線為My Pipeline。這個asset類型將因此成為MyPipelineAsset并且需要擴展RenderPipelineAsset,它被定義在UnityEngine.Experimental.Rendering命名空間中。
using UnityEngine;using UnityEngine.Experimental.Rendering;public class MyPipelineAsset : RenderPipelineAsset {}它會一直在experimental命名空間中嗎?它會在某個時刻被移出experimental命名空間,到UnityEngine.Rendering或者其他命名空間。當這種情況發生時,只需要更新using語句,除非API也發生了改變。
pipeline asset的主要目的是給Unity一個途徑去獲取負責渲染的管線對象實例。asset它本身只是一個句柄和存放管線設置的地方。我們目前還沒有任何設置,所以我們要做的是給Unity一個途徑去獲取我們的管線對象實例。這個通過重載InternalCreatePipeline方法來實現。但是我們還沒有定義我們的管線對象,所以此時我們將僅返回null。
InternalCreatePipeline返回值類型是IRenderPipeline。類型名的I前綴表示它是接口類型。
public class MyPipelineAsset : RenderPipelineAsset { protected override IRenderPipeline InternalCreatePipeline () { return null; }}什么是接口?接口就像一個類,除了定義一個功能合同且不提供它的實現。它只定義屬性、事件、索引器和方法簽名,它們都是被定義公開的。任何擴展接口的類型都要求包含接口定義的實現。慣例是在接口名前加個I前綴。
因為接口不包含具體的實現,所以類甚至結構體可以擴展多個接口。如果多個接口碰巧定義了相同的東西,它們只是同意功能應該存在。在類中是不可以的即使是抽象類,因為這個可能導致實現沖突。
現在我們需要為我們的項目添加一個這種類型的asset。要實現這點,添加CreateAssetMenu特性到MyPipelineAsset。
[CreateAssetMenu]public class MyPipelineAsset : RenderPipelineAsset {}這在Asset/Create菜單中添加了個入口。讓我們整理一下,把他放到Rendering的子菜單中。我們通過設置特性的menuName屬性為Rendering/My Pipeline來完成。這個屬性在可以被直接設置在特性類型之后的圓括號中。
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]public class MyPipelineAsset : RenderPipelineAsset {}使用新菜單項添加這個asset到項目,命名為My Pipeline。
Pipeline asset及其腳本然后把它分配到Scriptable Render Pipeline Settings。
使用中的Asset我們現在已經替換了默認的管線,更改了一些東西。首先,graphics setting中的大量選項不見了,Unity也在信息面板中提到了這些設置。其次,我們繞過了默認管線并不提供有效的替換,因此不會進行任何渲染。盡管,場景窗口仍然顯示天空盒,但是游戲窗口、場景窗口和材質預覽不再起作用,如果你由Window/Analysis/Frame Debugger打開幀調試器,并且啟用它,你會看見確實沒有在游戲窗口中繪制任何東西。
1.3?管線實例
創建一個有效的管線,我們需要提供一個實現IRenderPipeline并負責渲染流程的對象實例。所以為此創建一個類將其命名為MyPipeline。
using UnityEngine;using UnityEngine.Experimental.Rendering;public class MyPipeline : IRenderPipeline {}盡管我們可以實現由自己IRenderPipeline,但是更方便的是擴展RenderPipeline類。這個類型已經提供一個IRenderPipeline我們可以自己構建的實現。
public class MyPipeline : RenderPipeline {}現在我們可以在InternalCreatePipeline中返回一個MyPipeline的新實例。這意味著我們在技術上有了一個有效的管線,盡管這依然不能渲染任何東西。
protected override IRenderPipeline InternalCreatePipeline () { return new MyPipeline(); }2.?渲染
管線對象負責渲染每一幀。Unity會根據上下文以及激活的相機調用管線的Render方法。這是針對游戲窗口的,也適用于場景窗口和編輯器中的材質預覽。這需要我們適當地配置,找到需要渲染的內容,并且按正確的順序處理每個操作。
2.1?上下文
RenderPipeline包含了定義在IRenderPipeline接口中的Render方法的實現。它第一個參數是渲染上下文,這是ScriptableRenderContext結構,作為native code的外在表現。這第二個參數是一個包含所有需要渲染的相機的數組。
RenderPipeline.Render不繪制任何東西,但是會檢查管線對象是否有效用于渲染。如果無效,則會引發一個異常。我們會重載這個方法并調用基類的實現,來保證這個檢測。
public class MyPipeline : RenderPipeline { public override void Render ( ScriptableRenderContext renderContext, Camera[] cameras) { base.Render(renderContext, cameras); }}通過渲染上下文我們可以向Unity引擎發出指令去渲染和控制渲染狀態。一個最簡單的例子是繪制天空盒,它可以通過調用DrawSkybox方法來完成。
base.Render(renderContext, cameras); renderContext.DrawSkybox();DrawSkybox要求相機作為參數,我們會簡單的使用cameras中的第一個元素。
renderContext.DrawSkybox(cameras[0]);我們在游戲窗口中依然看不見天空盒顯示。這是因為我們發到上下文的指令在緩沖。實際的工作發生在我們由Submit方法提交執行之后。
renderContext.DrawSkybox(cameras[0]);??renderContext.Submit();天空盒最終呈現在游戲窗口,你也可以看見它出現在frame debugger中。
幀調試器顯示天空盒被繪制2.2?相機
我們提供了一系列相機,因為場景中會可以存在多個相機,它們都會被渲染。例如利用多相機設置多人分屏,小地圖以及后視鏡。每個相機都需要單獨處理。
現在我們不用擔心多個相機支持我們的管線。我們將簡單地創建一個另一種渲染方法其作用于單個相機。用它來繪制天空盒并提交。所以我們要提交每個相機。
void Render (ScriptableRenderContext context, Camera camera) { context.DrawSkybox(camera); context.Submit(); }為camers數組的每個元素調用新方法。這個例子中我使用一個foreach循環,因為Unity的管線也是用這個方法來循環相機數組。
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); } }foreach循環是是如何工作的?foreach (var e in a) { ... }相當于
for (int i = 0; i < a.Length; a++) { var e = a[i]; ... }假設a是一個數組。唯一的功能性區別在于我們不可以獲取迭代器變量i。
當a不是一個數組而是其他可枚舉類型,迭代器會起作用并且最后會創建臨時對象,這最好是要避免的。但是使用foreach對數組是安全的。
使用var定義元素變量是常見的,所以我使用它。它的類型是a的元素類型。
注意當前相機朝向不影響天空盒的渲染。我們傳遞相機到DrawSkybox,但是它只決定天空盒是否會被渲染,這是由相機的clear flag控制的。
要正確渲染天空盒以及整個場景,我們需要設置view-projection矩陣。這個變換矩陣組合了相機的位置和朝向的view矩陣,以及相機是透視或正交投影的projection矩陣。你可以在frame debugger中看見這個矩陣。它就是unity_MatrixVP,當有東西被繪制時所使用的shader屬性之一。
目前,unity_MatrixVP矩陣總是相同的。我們需要使用SetupCameraProperties方法設置相機的屬性到上下文中。這會設置矩陣以及其他的屬性。
void Render (ScriptableRenderContext context, Camera camera) { context.SetupCameraProperties(camera); context.DrawSkybox(camera); context.Submit(); }現在天空盒被正確的渲染了,在游戲窗口和場景窗口中都考慮到了相機屬性。
2.3?命令緩存
上下文會推遲實際的渲染直到我們提交它。在這之前,我們對其配置并添加命令以供之后執行。有些任務像是繪制天空盒可以由專用的方法發出,但是其他的指令必須要由獨立的命令緩存間接地發送。
命令緩沖可以被實例化一個新的CommandBuffer對象所創建,這個被定義在UnityEngine.Rendering名稱空間中。在可編程渲染管線加入之前命令緩存就已經存在了,所以它不是實驗性的。在我們繪制天空盒之前創建這樣的一個緩存。
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方法命令上下文執行緩存。再說明一下,上下文不會立即執行命令,而是復制他們到上下文的內部緩存中。
var buffer = new CommandBuffer(); context.ExecuteCommandBuffer(buffer);在Unity引擎的native level中,命令緩存聲明了資源用于儲存它們的命令。如果我們不再需要這些資源,那最好立即釋放他們。這個可以通過在調用ExecuteCommandBuffer之后直接調用緩存的Release方法完成。
var buffer = new CommandBuffer(); context.ExecuteCommandBuffer(buffer); buffer.Release();執行一個空命令緩存沒有任何作用。我們添加它以便我們清除render target,以確保渲染沒有被之前的繪制所影響。這可以由命令緩存執行,但不可以直接由上下文執行。
清除命令可以調用ClearRenderTarget來添加到緩存中。它要求三個參數:兩個booleans和一個color,并且如果使用了第三個參數的話,它是清除的顏色。舉例,讓我們清除清除深度數據,忽略顏色數據,并使用Color.clear作為清除顏色。
var buffer = new CommandBuffer(); buffer.ClearRenderTarget(true, false, Color.clear); context.ExecuteCommandBuffer(buffer); buffer.Release();frame debugger現在會向我們顯示清除render target的命令緩存被執行了。在這個案例中,它指定Z和stencil被清除了。Z指的是depth buffer,而stencil buffer始終被清除的。
清除depth和stencil緩沖每個相機通過它們的clear flags和background color配置決定被清除的內容。我們可以使用這些來代替硬編碼指定清除render target。
CameraClearFlags clearFlags = camera.clearFlags; buffer.ClearRenderTarget( (clearFlags & CameraClearFlags.Depth) != 0, (clearFlags & CameraClearFlags.Color) != 0, camera.backgroundColor );clear flag是如果工作的?CameraClearFlags是一個枚舉類型,它可以被用來設置bit flags。它的值的每一個比特位被用來指定某些特性被開啟或是關閉。
要從整個值中提取bit flag,值與所需標志位通過按位與(操作符 &)來組合。如果結果不為零,則設置這個標志。
因為我們沒有給命令緩存命名,調試器顯示的是默認名字,即Unamed command buffer。讓我們相機名來代替,就是將它分配給緩存的name屬性。我們將使用對象初始化器語法來完成。
var buffer = new CommandBuffer { name = camera.name };給命令緩存使用相機名對象初始化器語法是如何工作的?我們不用再寫buffer.name = camera.name;作為獨立的語句在調用構造函數之后。但是當創建新的對象時,你可以在構造函數指令中加一個代碼塊。然后你可以在塊中設置對象的字段和屬性,不用再顯式地引用對象實例。并且,它明確地指出只有這些字段和域被設置后實例才可用。除此之外,它可以只有一個單獨的語句的初始化,不再需要有許多參數變體的構造函數。
注意,我們省略了構造函數的空參數列表,這在使用對象初始化器語法時是被允許的。
2.4?剔除
我們可以渲染天空盒了,但是還不能渲染任何我們放在場景中的對象。我們只需要渲染相機能看見的而不是所有對象。從場景中的所有渲染器開始,然后剔除那些落在相機視錐體的之外的渲染器。
什么是渲染器?它是一個組件附加在游戲對象上,將他們轉變為可以貝渲染的東西。比較典型的是MeshRenderer組件。
找出可以被剔除的渲染器需要我們追蹤多個相機的設置和矩陣,對于這個我們使用ScriptableCullingParameters結構體。可以用委托靜態CullResults.GetCullingParameters方法來替代自己填充結構體。它需要一個相機作為輸入并生成剔除參數作為輸出。然而,他并不能返回參數結構體。相反,我們需要提供結構體作為第二個輸出參數,在結構體前加上out。
void Render (ScriptableRenderContext context, Camera camera) { ScriptableCullingParameters cullingParameters; CullResults.GetCullingParameters(camera, out cullingParameters); … }我們為什么要寫out?結構體是值類型,所以他們被作為普通的值來對待。他們不是擁有身份的對象,帶有變量和僅保存對他們在內存位置的引用的字段。所以參數傳遞結構體會提供一個帶有值的拷貝的方法。這個方法可以改變拷貝,但是對被拷貝的值沒有影響。
當結構體參數被定義為輸出參數,他就扮演一個類似于對象的引用,但是指向的是參數所在的內存棧。當方法改變參數時,它影響的是值,而不是拷貝。
out關鍵字告訴我們這個方法負責給參數設置正確的值,代替以前的值。
除了輸出參數,GetCullingParameters也會返回它是否可以創建有效的參數。不是所有的相機設置都是有效的,結果就是退化,它不可以被用于剔除。所以如果它失敗了,我們沒有東西可以渲染并從Render中退出。
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) { return; }一旦我們有剔除參數了,就可以用于剔除。通過調用同時帶有剔除參數和上下文作為參數的靜態CullResults.Cull方法來完成。結果就是一個包含了可見內容的信息的CullResults結構體。
在這個案例中,我們需要通過在它前面添加ref提供剔除參數作為引用參數。
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) { return; } CullResults cull = CullResults.Cull(ref cullingParameters, context);為什么我們要寫ref?它就像out一樣,除了在這個案例中,這個方法不要求給值賦值。并且無論誰調用這個方法都負責首先正確初始化這個值。所以它可以被用來樹葉也可以用于輸出。
為什么ScriptableCullingParameters是結構體?
這可能是一種優化嘗試,這個想法是你可以創建多個參數結構體而不必擔心內存分配。但是,ScriptableCullingParameters是一個非常大的結構體,這也是為什么引用參數被用在這里,再次出于性能的考慮。也許它開始時很小但是隨著時間膨脹成大的結構體。現在,可復用的結構體實例可能是更好的方法,但是我們只能用Unity技術所決定的。
2.5?繪制
一旦我們知道什么是可見的,我們就可以繼續渲染這些形狀。這是通過在上下文調用DrawRenderers,以cull.visibleRenderers作為參數,告訴它使用哪些渲染器來完成的。除此之外,我們還必須提供繪制設置和過濾設置。這者都是結構體-?DrawRendererSettings和FilterRenderersSettings確切來說,我們初始時使用它們的默認值。必須將繪圖設置作為引用傳遞。
buffer.Release(); var drawSettings = new DrawRendererSettings(); var filterSettings = new FilterRenderersSettings(); context.DrawRenderers( cull.visibleRenderers, ref drawSettings, filterSettings ); context.DrawSkybox(camera);為什么是FilterRenderersSettings而不是FilterRendererSettings呢?不知道。也許是打錯了。
我們還沒有看到任何對象,因為默認的過濾設置不包含任何內容。我們可以通過給FilterRenderersSettings的構造函數提供true參數來包含所有內容。這告訴它初始化自己,所以它包括所有內容。
var filterSettings = new FilterRenderersSettings(true);此外,我們必須通過為其構造函數提供相機和shader pass作為參數來配置繪制設置。相機用于設置排序和剔除圖層,而pass參數控制哪些shader pass用于渲染。
shader pass通過字符串來標識,該字符串必須封裝在ShaderPassName結構體中。由于在我們的管線中只支持unlit材料,因此我們將使用Unity默認的unlit材質,通過SRPDefaultUnlit字符串來識別。
var drawSettings = new DrawRendererSettings( camera, new ShaderPassName("SRPDefaultUnlit") );不透明球體現在可見了。我們只看到不透明的unlit形狀出現,但是沒有透明的unlit。但是,幀調試器顯示unlit的形狀都被繪制了。
所有unlit渲染器都被繪制出來。它們確實被繪制,但由于透明shader pass不會寫入深度緩沖區,因此它們最終會被天空盒覆蓋。解決方案是延遲繪制透明渲染器,直到天空盒被繪制之后。
首先,將天空盒之前的繪制限制為僅使用不透明的渲染器。這通過將過濾設置中的renderQueueRange設置為RenderQueueRange.opaque來完成的,該設置涵蓋了從0到2500(含2500)的渲染隊列。
var filterSettings = new FilterRenderersSettings(true) { renderQueueRange = RenderQueueRange.opaque };只有不透明渲染器被繪制了。接下來,改變隊列范圍至RenderQueueRange.transparent- 從2501到5000(含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 );不透明,天空盒然后透明。我們在天空盒之前繪制不透明的渲染器以防止重復繪制。由于形狀繪制總是在天空盒之前,所以我們先渲染形狀就避免多余的工作了。那是因為不透明的shader pass會寫入深度緩沖區,該緩沖區用于跳過稍后繪制的任何東西。
除了覆蓋部分天空,不透明的渲染器最終也會相互遮擋。理想情況下,對幀緩沖區中的每個fragment,只有最靠近攝像機的那個會被繪制。因此,為了盡可能減少重復繪制,我們應該先繪制最近的形狀。這可以通過在繪制之前對渲染器進行排序來完成,這是通過sorting flags來控制的。
繪制設置包含一個DrawRendererSortSettings類型的sorting結構體,其中包含sorting flags。在繪制不透明形狀之前將它設置為SortFlags.CommonOpaque。這指示Unity按距離,從前到后以及其他一些標準對渲染器進行排序。
var drawSettings = new DrawRendererSettings( camera, new ShaderPassName("SRPDefaultUnlit") ); drawSettings.sorting.flags = SortFlags.CommonOpaque;然而,透明渲染的工作方式不同。它將繪制的顏色與已經繪制的顏色相結合,因此結果顯得透明。這需要從后到前的反轉繪制順序。對于這種情況我們可以使用SortFlags.CommonTransparent。
context.DrawSkybox(camera); drawSettings.sorting.flags = SortFlags.CommonTransparent; filterSettings.renderQueueRange = RenderQueueRange.transparent; context.DrawRenderers( cull.visibleRenderers, ref drawSettings, filterSettings );我們的管線現在能夠正確地同時渲染不透明和透明的unlit物體。
3.?完善
能夠正確地渲染只是擁有功能性管線的一部分。還有其他的東西需要去考慮,例如它是否足夠快,不分配不必要的臨時對象,以及與Unity編輯器更好的集成。
3.1?內存分配
讓我們檢查管線在內存管理方面是否表現良好,或者它在每一幀分配內存,是否頻繁地觸發內存GC。通過Window/Analysis/Profiler打開分析器并在Hierarchy模式下檢查CPU占用數據。雖然你可以在編輯器中在play模式下打開分析器,但是最好是分析構建后的項目,通過創建一個development build并使它自動附加到分析器中,但是在這種情況下沒法深度分析。
選擇GC Alloc排序,你會發現每幀確實都分配了內存。其中有些不屬于我們的控制范圍,但是在管線的Render方法中分配了不少的字節。
最后的結果是剔除分配的內存最多。這個原因是CullResults雖然是個結構體,但是它包含了三個列表,它們都是對象。每次我們申請新的cull result時,最后都會為新的列表分配內存。所以CullResults是個結構體也沒多大的好處。
幸好,CullResults有另外的Cull方法,它接受一個結構體作為引用參數,而不是返回個新的。這樣列表就可以重復使用。我們需要做的就是把cull變成字段并把它提供給CullResults.Cull作為增加的參數,而不是將返回值分配給他。
CullResults cull; … void Render (ScriptableRenderContext context, Camera camera) { … //CullResults cull = CullResults.Cull(ref cullingParameters, context); CullResults.Cull(ref cullingParameters, context, ref cull); … }另一個持續的內存分配原因是我們使用的相機的名字屬性。每次我們給它值時,它從native code中獲取名字數據,它必須要創建一個字符串,而這是一個對象。所以讓我們將命令緩沖命名為Render Camera。
var buffer = new CommandBuffer() { name = "Render Camera" };使用常量緩沖名。最后,命令緩沖它自己也是一個對象。幸好,我們可以創建一次命令緩存并復用它。使用cameraBuffer字段來替換局部變量。感謝對象初始化器語法,我們可以創建一個命名了的命令緩沖作為默認值。其他的改變就是我們需要清空命令緩沖而不是釋放它,這個可以使用它的Clear方法。
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顯示的數據。Unity的管線顯示事件的嵌套層次結構,但是我們的管線都在根一級。可以使用命令緩沖去開始和結束采樣來構建一個層次結構。
讓我們在ClearRenderTarget之前調用BeginSample,立即在它之后調用EndSample。每次采樣必須都要包含開始和結束,而且都要提供完全相同的名字。除此之外,我發現最好使用和命令緩沖相同的名字來定義采樣。總之,命令緩沖的名字經常被使用。
cameraBuffer.BeginSample("Render Camera"); cameraBuffer.ClearRenderTarget(true, false, Color.clear); cameraBuffer.EndSample("Render Camera"); context.ExecuteCommandBuffer(cameraBuffer); cameraBuffer.Clear();采樣創建了一個層次結構。我們現在看見一個Render Camera層次內嵌在命令緩沖的原生Render Camera中,而它又包含清空操作。但是可以更進一步,內嵌所有其他與相機有關的動作。這要求我們延后調用結束采樣直到我們提交上下文之前。所以我們必須在此插入一個附加的ExecuteCommandBuffer,僅包含結束采樣的指令。為此使用相同的命令緩沖,并在完成后再次清空它。
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();嵌套的采樣。這看起來不錯,除了清空動作內嵌在多余的Render Camera層級中,而所有其他的動作都直接處于根層級之下。我不確定為什么會發生這種事,但是可以通過在清除之后開始采樣來避免它。
//cameraBuffer.BeginSample("Render Camera"); cameraBuffer.ClearRenderTarget(true, false, Color.clear); cameraBuffer.BeginSample("Render Camera"); context.ExecuteCommandBuffer(cameraBuffer); cameraBuffer.Clear();沒有多余的的嵌套。3.3?渲染默認管線。
因為我們的管線只支持unlit shaders,使用其他著色器的對象就不會被渲染,進而它們就不可見。雖然這是正確的,但是它隱藏了一些使用錯誤著色器的對象。如果我們使用Unity的error 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的默認surface shader有一個ForwardBase pass,它用來作為第一個前向渲染pass。我們可以使用它來識別那些擁有工作在默認管線下的材質的對象。通過新建繪制設置來選擇這個pass以及和新建的過濾設置一起作用于渲染。我們不關心排序或者分離不透明與透明渲染器,因為反正它們都是無效的。
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 ); }渲染前向pass。現在使用默認著色器的對象顯示出來了。它們在frame debugger中也是可見的。
所有的對象都被渲染了。因為我們的管線不支持forward base pass它們不會被正確地渲染。必要的數據沒有被設置,因此所有那些依賴光線的對象最后都是黑色的。然而,我們應該用error shader來渲染它們。為此,需要一個error材質。為它添加一個字段。然后,在DrawDefaultPipeline開始時,創建error材質(如果它不存在的話)。這可以通過Shader.Find遍歷Hidden/InternalErrorShader來完成,然后創建一個新材質使用此著色器。此外,將材質的hide flags設置為HideFlags.HideAndDonSave讓它不會顯示在project窗口中并且也不會和其他的asset一起被保存。
Material errorMaterial; … void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) { if (errorMaterial == null) { Shader errorShader = Shader.Find("Hidden/InternalErrorShader"); errorMaterial = new Material(errorShader) { hideFlags = HideFlags.HideAndDontSave }; } … }繪制設置的一個選項是覆蓋渲染時使用的材質,通過調用SetOverrideMaterial。它的第一個參數是要使用的材質。第二個參數是要用于渲染的材質著色器的pass的索引。由于error shader只有一個pass,所以索引為0;
var drawSettings = new DrawRendererSettings( camera, new ShaderPassName("ForwardBase") ); drawSettings.SetOverrideMaterial(errorMaterial, 0);使用error shader。使用不被支持的材質的對象現在清楚地顯示為錯誤的。但是這個只適用于Unity默認管線的材質,其著色器具有ForwardBase pass。我們可以使用不同的pass識別其他的內置著色器,特別是PrepassBase,Always,Vertex,VertexLMRGBM和VertexLM。
還好,可以通過調用SetShaderPassName來添加多個pass到繪制設置中。這個方法的第二個參數是名字。它的第一個參數是一個索引用于控制pass被繪制的順序索引。我們不用關心這個,所以什么順序都是可以的。pass由構造函數提供,它的索引總是為零,所以為添加的pass遞增索引值。
var drawSettings = new DrawRendererSettings( camera, new ShaderPassName("ForwardBase") ); drawSettings.SetShaderPassName(1, new ShaderPassName("PrepassBase")); drawSettings.SetShaderPassName(2, new ShaderPassName("Always")); drawSettings.SetShaderPassName(3, new ShaderPassName("Vertex")); drawSettings.SetShaderPassName(4, new ShaderPassName("VertexLMRGBM")); drawSettings.SetShaderPassName(5, new ShaderPassName("VertexLM")); drawSettings.SetOverrideMaterial(errorMaterial, 0);這覆蓋了Unity到現在提供的所有著色器,它們應該滿足在創建場景時指出使用不正確材質了。但是我們只需要在開發期間這樣做,不是在構建中。所以讓我們只在編輯器中調用DrawDefaultPipeline。一個辦法是給方法添加Conditional特性。
3.4?條件代碼執行
Conditional特性定義在System.Diagnostics名稱空間中。我們可以使用這個名稱空間,但不幸的是它還包含一個Debug類型,與UnityEngine.Debug沖突。由于我們只需要這個特性,可以使用別名來避免沖突。不使用完整的名稱空間,我們使用指定的類型并分配一個有效的類型名。在這個情況下,我們將定義Conditional作為System.Diagnostics.ConditionalAttribute的別名。
using UnityEngine;using UnityEngine.Rendering;using UnityEngine.Experimental.Rendering;using Conditional = System.Diagnostics.ConditionalAttribute;添加特性給我們的方法。它要求一個字符串參數來指定符號。如果在編譯期間定義了這個符號,那么這個方法調用將被正常地包含。但是如果符號沒有定義,則這個方法的調用(包括所有的參數)都被忽略。就好像DrawDefaultPipelint(context, camera);代碼好像不存在于編譯期一樣。
要僅在Unity編輯器編譯時包含調用,需要依賴UNITY_EDITOR符號。
[Conditional("UNITY_EDITOR")] void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) { … }我們可以再進一步,還在development builds中包含調用,僅排除release builds。為此添加DEVELOPMENT_BUILD作為額外的條件。
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) { … }3.5?場景窗口里的UI
到目前為止,還有一件我們沒有考慮到的事是Unity的游戲內UI。要測試它,添加一個UI元素到場景中,例如一個按鍵,通過GameObject/UI/Button。它創建一個帶有button的canvas和一個event system。
結果是我們什么都不用做,UI就被渲染在游戲窗口。Unity替我們考慮到了。Frame debugger顯示UI被獨立地渲染了,以overlay的方式。
UI被繪制在屏幕空間中。至少,當canvas被設置渲染在屏幕空間中就是這樣的情況。當設置渲染到世界空間中時,UI和其他的透明對象一起被渲染。
UI在世界空間中。盡管UI在游戲窗口中起作用,但是它在場景窗口中不顯示。在場景窗口中UI總是存在于世界空間,但是我們需要手動將它注入場景中。通過調用靜態ScriptableRenderContext.EmitWorldGeometruForSceneView方法來添加UI,當前的相機作為一個參數。這必須在剔除前調用。
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) { return; } ScriptableRenderContext.EmitWorldGeometryForSceneView(camera); CullResults.Cull(ref cullingParameters, context, ref cull);但是它也會在游戲窗口中再次添加UI。為了避免這種情況,我們只在渲染場景窗口時發出UI幾何體。這種情況就是當相機的cameraType等于CameraType.SceneView時。
if (camera.cameraType == CameraType.SceneView) { ScriptableRenderContext.EmitWorldGeometryForSceneView(camera); }這僅在編輯器下起作用。條件編譯確保EmitWorldGeometryForSceneView不會在構建編譯期存在,這意味著現在我們嘗試構建的話會得到一個編譯錯誤。為了使它再次起作用,我們必須讓代碼有條件地調用EmitWorldGeometryForSceneView。把代碼放在#if和#endif語句之間來實現。#if語句需要一個符號,就像Conditional特性。使用UNITY_EDITOER,代碼僅在編輯器的編譯期被包含。
void Render (ScriptableRenderContext context, Camera camera) { ScriptableCullingParameters cullingParameters; if (!CullResults.GetCullingParameters(camera, out cullingParameters)) { return; }#if UNITY_EDITOR if (camera.cameraType == CameraType.SceneView) { ScriptableRenderContext.EmitWorldGeometryForSceneView(camera); }#endif CullResults.Cull(ref cullingParameters, context, ref cull); … }下個教程是自定義著色器。
本章教程項目倉庫?(bitbucket.org/catlikecodingunitytutorials/scriptable-render-pipeline-01-custom-pipeline/src/master/)
聲明:發布此文是出于傳遞更多知識以供交流學習之目的。若有來源標注錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯系,我們將及時更正、刪除,謝謝。
作者:飛鳥
來源:https://zhuanlan.zhihu.com/p/77641752
More:【微信公眾號】?u3dnotes
總結
以上是生活随笔為你收集整理的unity hub是什么东西_Unity可编程渲染管线(SRP)教程:一、自定义管线的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020款帕萨特精英版刷隐藏功能能自己收
- 下一篇: python绘制概率密度曲线_[pyth