可編程渲染管線1 自定義通道

原文:https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/custom-pipeline/

該教程基於 Unity 2018.3.0f2.

1、創建通道

想要渲染任何東西,Unity都需要確定繪製什麼形狀,何時繪製,在哪繪製,以及使用何種設置。取決於涉及的效果的數量,這可能會非常複雜。將光照、陰影、透明度、屏幕效果、體積效果(volumetric effects)等等效果以正確的順序處理並傳輸到最終的畫面,這個過程就稱之爲渲染管道(Render Pipeline)

Unity2017支持兩個預定義的渲染渲染管道、正向渲染和延遲渲染,另外也保存了Unity5版本遺留的舊版延遲渲染。然而這些管線都是是固定的,你可以啓用、關閉或者重寫管線中的某些部分,但你不可能大幅偏離它原本的設計方向。

Unity2018添加了對可編輯渲染管線的支持,讓從頭搭建渲染管線成爲可能,雖然在許多個別的步驟(如剔除 culling)仍然需要依賴Unity。Unity2018還介紹了用這種方法創建的兩個渲染管線,輕量級渲染管線(lightweight pipeline)和高清渲染管線(highdefinition pipeline).目前這兩個渲染管線仍處於預覽狀態,可編輯渲染管線的API也被標記爲測試技術。但這已經足夠讓我們用於創建自己的渲染管線了。

1.1 項目設置

打開Unity2018,創建一個新項目,我使用的是Unity 2018.2.9f1,但是Unity2018.2以及更高的版本應該都可以使用。選擇一個標準的3D項目,關閉Unity Analytics. 因爲我們要寫自己的渲染管線,所以Template不要選擇帶管線的選項,選擇3D就好。

打開項目後,移除Window / Package Manager 下除了Package Manager UI的其餘組件,因爲我們用不上它們。(我用的Unity2018.3.2.f1好像不給我刪- -||)

我們需要在線性顏色空間下工作,但是Unity2018默認使用伽馬空間。所以我們通過Edit / Project Settings / Player找到player setings,在Other Settingssection裏將色彩空間轉換爲線性。

我們需要一些簡單的材質用於測試我們的通道。我創建了四個材質,第一個材質設爲默認標準着色器的不透明(Opaque)類型,並使用紅色的反照率(albedo)。第二個使用和上一個同樣的材質,但是渲染模式改爲透明(Transparent),並將反照率設爲一個alpha值較低的藍色。第三個材質使用Unlit/Color  shader,顏色設爲黃色。最後一個使用Unlit/Transparent shader ,不做任何改動,這樣會顯示爲純白色。

用一些物體填充場景,要讓四個材質都有被使用

1.2 Pipeline Asset

目前,Unity使用默認的正向渲染管線。要使用自定義的渲染管線,我們需要在 graphics settings裏進行設置,通過Edit / Project Settings / Graphics 找到下面的選擇框。

想要設置自己的渲染管線,我們必須讓Scriptable Render Pipeline Settings 這個字段指向一個pipeline asset。該asset繼承自屬於ScriptableObject 類型的RenderPipelineAsset

爲我們的自定義管線創建一個新腳本。我們把我們的通道簡單的命名爲My Pipeline  ,所以這個asset的類型名就設爲 MyPipelineAsset,該類繼承 RenderPipelineAsset(定義在UnityEngine.Experimental.Rendering 命名空間) 。

using UnityEngine;
using UnityEngine.Experimental.Rendering;

public class MyPipelineAsset : RenderPipelineAsset {}

他會一直在這個命名空間中嗎

等時機成熟他會被移出Experimental命名空間,不是移到UnityEngine.Rendering就是其他命名空間。到時候,只需要更新using語句即可,除非他的api也變了

pipeline asset的主要作用是給unity一個途徑去獲取負責渲染的管道對象實例,這個asset本身只是一個句柄和存儲Pipeline相關設置的地方,我們現在還沒有任何設置,所以我們現在要做的只是給Unity一個途徑去得到pipeline 對象實例。這通過重寫 InternalCreatePipeline完成。但是目前我們還沒有定義自己的pipeline對象類型,所以我們先暫時返回null。

 InternalCreatePipeline方法的返回值是IRenderPipeline 。I前綴表示它是一個接口

public class MyPipelineAsset : RenderPipelineAsset {

	protected override IRenderPipeline InternalCreatePipeline () {
		return null;
	}
}

什麼是接口: ...略

現在我們以這個類型作爲asset添加到我們的項目中去。想要作爲asset,需要爲MyPipelineAsset添加特性CreateAssetMenu

[CreateAssetMenu]
public class MyPipelineAsset : RenderPipelineAsset {}

 這會在Asset/Create 面板放置一個條目。處於整潔,我們把它放在Rendering 子面板中。通過設置特性的nemuName屬性爲Rendering/Create/My Pipeline即可。這個屬性可以在特性後面的圓括號內直接設置。

[CreateAssetMenu(menuName = "Rendering/My Pipeline")]
public class MyPipelineAsset : RenderPipelineAsset {}

用新增的面板項目添加對應的asset到我們的項目中,並把它命名爲MyPipeline。

然後把它指派給Scriptable Render Pipeline Settings.。  

我們現在替換了默認的pipeline,一些事情發生了變化,首先,在graphics setting許多選項消失了,Unity也用提示框給出了提示。第二,由於我們繞過了默認的pipeline,又沒有提供一個有效的pipeline,因此沒有任何東西被渲染,無論是遊戲窗口、場景窗口還是材質預覽也都不再可用,除了場景窗口還顯示着天空盒。如果你打開 frame debugger(Window / Analysis / Frame Debugger)然後啓用,你會看到遊戲窗口確實沒有任何東西被渲染。

1.3 pipeline instance

要創建一個有效的pipeline,我們需要提供一個實現了IRenderPipeline接口的對象實例負責渲染流程。所以像這樣創建一個類,將他命名爲MyPipeline​​​​​​。

using UnityEngine;
using UnityEngine.Experimental.Rendering;

public class MyPipeline : IRenderPipeline {}

雖然我們可以完全由自己實現IRenderPipeline 接口,但是用抽象類RenderPipeline代替會更方便,這個類已經提供了 IRenderPipeline接口的一些基礎的實現,我們只需在此基礎之上實現剩餘的即可。

public class MyPipeline : RenderPipeline {}

現在我們在MyPipelineAsset類中的InternalCreatePipeline方法中返回一個MyPipeline的實例,這意味着我們在技術上已經返回了一個有效的pipeline,雖然實際上它仍然不會渲染任何東西 。

protected override IRenderPipeline InternalCreatePipeline () {
		return new MyPipeline();
	}

unitypackage 

2、渲染

pipeline對象負責渲染每一幀的畫面。現在unity所有需要渲染的地方都是通過調用該pipeline的Render方法實現的(它把上下文和激活的攝像機做爲參數)。不只是遊戲窗口,還有編輯器的場景窗口以及材質預覽界面。我們要自己來進行合理的配置,找出我們需要渲染什麼,並且以一個正確的順序完成這些任務。

2.1 上下文(Context)

RenderPipeline包含IRenderPipeline接口中定義的Render方法的實現。第一個參數是一個ScriptableRenderContext結構體,作爲渲染上下文,充當本機代碼的外表(?facade) ,第二個參數是一個數組,保存了所有需要渲染的攝像機。

RenderPipeline.Render不會繪製任何東西,但是他會檢查一個pipeline對象是否可以有效的用於渲染,如果不能就會跳出異常,所以我們在自己的類裏重寫這個方法時仍然要調用該方法的基類實現,用以保留檢查的功能。

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 攝像機(Camera)

我們擁有一個攝像機數組,因爲場景中可能存在多個需要被渲染的內容,比如多人分屏、小地圖、後視鏡等都需要設置多個攝像頭。每個攝像頭都需要獨立處理。

我們的pipeline暫不考慮對多重相機的支持。我們簡單的創建一個Render的重載方法用於作用單個相機,讓它渲染天空盒並提交。用它完成每臺相機的提交工作。

void Render (ScriptableRenderContext context, Camera camera) {
    context.DrawSkybox(camera);

    context.Submit();
}

 爲相機數組中的每個元素調用該方法。我使用foreach循環來完成,Unity的pipeline也使用這種方法遍歷相機的。

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如何工作 ...略 

要注意的是,現在相機的方向並不會影響天空盒的渲染,我們將相機組件傳遞給DrawSkybox,但這只是用相機上的clear flags來決定天空盒是否應該被渲染。 

要想正確的渲染天空盒(以及整個場景)我們必須設置視圖投影矩陣。這個變換矩陣結合了相機的位置和方向(視圖矩陣)以及相機的透視或正交投影(投影矩陣)。你可以在frame debugger上看到這個矩陣,這是一些物體繪製時會用到的着色器屬性——unity_MatrixVP。

目前,unity_MatrixVP永遠是不變的。通過 SetupCameraProperties這個方法,我把每個相機各自的屬性應用到上下文中。他會設置視圖投影矩陣以及其他一些屬性。

void Render (ScriptableRenderContext context, Camera camera) {
		context.SetupCameraProperties(camera);

		context.DrawSkybox(camera);

		context.Submit();
	}

現在,天空盒被正確渲染,在遊戲窗口和場景窗口中的顯示都考慮到了攝像機屬性。

2.3 命令緩衝區(Command Buffers)

上下文將實際的渲染操作延遲至我們上交(submit)後,在此之前,我們可以進行配置,並添加命令用於之後的執行。一些任務(如繪製天空盒)可以通過特定的方法執行,但是某些命令就得通過一個單獨的command buffer來間接執行。

初始化一個 CommandBuffer 對象來創建一個命令行緩衝。該類型定義在UnityEngine.Rendering命名空間,因爲command buffers在可編輯渲染管線出現之前就存在了,所以並沒有被包括在實驗性命名空間內。我們在繪製天空盒前創建這樣一個buffer。

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方法告知上下文去執行這個buffer。要記得,這個命令不會立刻執行,這只是將這個命令拷貝到上下文裏的緩衝區,等待submit再執行

		var buffer = new CommandBuffer();
		context.ExecuteCommandBuffer(buffer);

 命令行緩衝區會在Unity引擎的本機上申請空間去存儲他的命令。所以如果不需要這些資源,最好立刻釋放它們。通過調用它的Release方法來實現,這裏我們直接寫在 ExecuteCommandBuffer方法後面。

		var buffer = new CommandBuffer();
		context.ExecuteCommandBuffer(buffer);
		buffer.Release();

執行一個空的 comand buffer不要緊。我們先添加一條指令用來清理我們的渲染目標,確保渲染時不會受到先前繪製的內容的影響。這得通過command buffer來實現,而不是直接使用上下文。

通過調用方法來將一條命令添加到緩衝區。它一共需要三個參數,兩個bool一個color,第一個參數決定是是否清除深度信息,第二個決定是否清除顏色,第三個決定用什麼顏色覆蓋清除。在這裏,我們清除深度信息,不清理顏色並使用Color.clear作爲清除的顏色。

var buffer = new CommandBuffer();
buffer.ClearRenderTarget(true, false, Color.clear);
context.ExecuteCommandBuffer(buffer);
buffer.Release();

 在frame dubugger可以發現這個command buffer已經得到執行,清理了渲染目標,圖中顯示z以及stencil被清理了,Z表示深度緩衝,stencil表示的模板緩衝則是一直會被清理的。

被如何清理應當取決取每臺相機的clear flags以及background color,所以我們用硬編碼來決定如何清理渲染目標。

		CameraClearFlags clearFlags = camera.clearFlags;
		buffer.ClearRenderTarget(
			(clearFlags & CameraClearFlags.Depth) != 0,
			(clearFlags & CameraClearFlags.Color) != 0,
			camera.backgroundColor
		);

clear flag 是怎樣工作的:CameraClearFlags 是由一系列位標識組成的枚舉,每一位的值表示某一特性啓用或關閉、

通過將clear flag與希望知道的位標識,用位與操作符&結合,來提取出對應位的flag,最終結果如果不是0(1&1=1),則設置標誌(true=1)

因爲我們沒有給command buffer設置名字,所以debugger裏顯示的是默認的名字“Unnamed command buffer”。讓我們用相機的名字代替,通過對象初始化器語法,把它傳給buffer的name屬性。

		var buffer = new CommandBuffer {
			name = camera.name
		};

 

對象初始化器的語法...略

2.4剔除(Culling)

我們現在可以渲染天空盒,但是我們放進遊戲場景中的物體還不能被渲染。我們只需要渲染攝像機可以看到的東西即可,並不是每一個物體都需要被渲染。所以在場景裏所有的渲染器(renderer)中,我們剔除那些在相機視錐體外面的渲染器。

渲染器是什麼: 一類附着在遊戲物體上的組件,讓物體可以被渲染成一些東西。 比如說 MeshRenderer組件

 想知道哪些內容可以被剔除,我們需要追蹤多個相機的設置和矩陣,我們可以通過使用結構體ScriptableCullingParameters存儲這些剔除信息。將填充結構體的工作交由靜態方法 CullResults.GetCullingParameters,而不是我們自己完成。該方法將camera作爲輸入參數,並輸出剔除參數。但是該結構體不是通過方法的返回值得到,而是把它當做第二個參數並用out修飾符修飾,讓他成爲一個輸出參數。

	void Render (ScriptableRenderContext context, Camera camera) {
		ScriptableCullingParameters cullingParameters;
		CullResults.GetCullingParameters(camera, out cullingParameters);

		…
	}

爲什麼要用out修飾符 ...略 

除了輸出參數, GetCullingParameters返回一個bool值表示是否可以創建一個有效的剔除參數。並不是所有的相機設置都可以有效,不能用於剔除會導致錯誤的結果,所以如果失敗了我們不渲染任何東西,直接退出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修飾符 ...略 

爲什麼 ScriptableCullingParameters是一個結構體 ...略 

2.5 繪製(Drawing) 

現在我們知道哪些物體對於相機可見,接下來就是繪製這些物體的形狀。我們調用context的DrawRenderers方法來實現,傳入cull.visibleRenderers來告訴他繪製哪些渲染器(Renderer),除此之外,我們還需要提供繪製設置和過濾設置(分別用結構體DrawRendererSettingsFilterRenderersSettings表示),我們把它初始化爲默認值。將繪製設置作爲引用傳入。

		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);

另外,我們也要在繪製設置的構造方法裏傳入camera和 shader pass, camera用於設置排序和剔除層(layer),pass則是來控制哪些shaderpass可以用於渲染。

shader pass通過字符串定義,幷包裝在結構體重。現在讓我們的pipeline只支持無光材質(unlit material),所以我們用unity默認的unlit pass,他被定義爲 SRPDefaultUnlit

		var drawSettings = new DrawRendererSettings(
			camera, new ShaderPassName("SRPDefaultUnlit")
		);

 

不透明(opaque)物體的形狀已經出現了,半透明物體卻沒有顯示出來。但是在frame debugger裏我們可以看到,這些沒顯示的物體也是繪製過的。 

它們確實被繪製了,但是因爲半透明物體的shaderpass不會寫入深度緩衝,所以它們被之後的天空盒渲染給覆蓋了。所以解決的方案就是把半透明物體的渲染時機放在天空盒後面。

首先,在天空盒之前我們要對渲染的物體做出限制,只能渲染不透明物體 。我們通過把過濾設置的renderQueueRange設置爲 RenderQueueRange.opaque來實現我們想要的限制,它包含了0到2500(包括)的渲染隊列。

		var filterSettings = new FilterRenderersSettings(true) {
			renderQueueRange = RenderQueueRange.opaque
		};

(只有不透明物體被渲染)

接下來,我們在渲染天空盒後面,把隊列範圍該爲RenderQueueRange.transparen(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
		);

scene

frame debugger不透明、天空盒、然後纔是半透明

我們通過在天空盒之前渲染不透明物體來避免重複渲染。這些圖形總是出現在天空盒前面,先渲染它們可以避免多餘的工作。因爲不透明物體的shader pass會寫入深度緩衝,而深度緩衝可以用於跳過哪些在之後繪製的卻比現在物體距離更遠的東西。

除了遮蓋天空盒,不透明的渲染器也會相互遮擋。理想狀態下,每個片元只需要把最靠近攝像頭的部分寫入幀緩存。所以爲了儘可能的減少重複渲染,我們應該先繪製最靠近的東西。因此在繪製之前,我們要整理渲染器的順序,通過使用sorting flags 來控制。

繪製設置包含了一個sorting結構,它是DrawRendererSortSettings類型,該類型包含了所有的整理標識。在渲染不透明物體之前,我們把它設爲 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
		);

現在我們的pipeline已經可以正確的渲染無光的不透明和半透明物體了。 

unitypackage 

3 打磨(Polishing)

一個實用的pipeline,能夠正確渲染是其一。還有需要考慮的點,那就是它運行夠快,不會分配不必要的臨時對象和Unity編輯器可以很好的契合。

3.1 內存分配(Memory Allocations)

讓我們測試一下pipeline就內存管理而言是否表現良好,亦或在每一幀都會分配內存,導致頻繁的內存垃圾回收。以Window / Analysis / Profiler 的順序打開profiler,在Hierarchy模式(下方,默認是timeline)中檢查CPU Usage 的數據。雖然您可以在編輯器中以播放模式執行此操作,但通過確保創建開發構建並將其自動附加到分析器來分析構建也是一個好主意(it is also a good idea to profile a build, by making sure you create a development build and having it attach to the profiler automatically,),儘管在這種情況下無法進行深度分析。

以GC Alloc作爲排序項,你將看到每一幀的確有內存被分配。一些不受我們控制,但是另一些被分配的內存字節則是來自我們pipeline的Render方法。

事實證明,剔除操作分配的內存最多。這是因爲雖然CullResults是一個結構,但是其中包含了三個列表(list對象),每次我們new一個cullresult,我們都會爲新列表分配內存。因此,即使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);
		
		…
	}

內存持續分配的另一個來源是我們在每幀使用相機的name屬性。每次我們取得該值,它從本機代碼中獲取名稱數據,這就需要創建一個新的字符串對象。所以我們將命令緩衝區始終命名爲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();

		…
	}

在這些更改後,我們的pipeline不會再每幀都創建臨時對象了。

3.2幀調試器採樣(Frame Debugger Sampling)

我們可以做的另一件事就是優化在幀調試器的顯示。Unity提供的pipeline在幀調試器的的顯示是一系列嵌套的層級事件。但是我們自己的pipeline相關渲染事件卻都顯示在根級別。我們也可以使用開始和結束分析器樣本這兩個命令行緩衝創建我們的層級系統。

在 ClearRenderTarget之前調用BeginSample,在之後調用EndSample。每次採樣都要有開始和結束,而且名字必須相同,除此之外,最好使用和命令行緩衝區一樣的名字來定義這個採樣。命令行緩衝區的名字在很多地方都會用到。

		cameraBuffer.BeginSample("Render Camera");
		cameraBuffer.ClearRenderTarget(true, false, Color.clear);
		cameraBuffer.EndSample("Render Camera");
		context.ExecuteCommandBuffer(cameraBuffer);
		cameraBuffer.Clear();

 

可以看到,包含了清除操作的RenderCamera 層(cameraBuffer.ClearRenderTarget(true, false, Color.clear);)嵌套在了由commandbuffer創建的RenderCamera(cameraBuffer.BeginSample("Render Camera");...End...)中。

我們可以更進一步,將所有和攝像機有關的操作囊括其中。這要求我們將終止採樣指令延遲到我們提交上下文之前。(但這樣的話我們的EndSample命令就添加就在就在執行完命令行緩衝區後了)所以我們必須在之後插入一個額外的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 渲染默認通道(Rendering the Default Pipeline)

因爲我們的通道現在只支持 unlit 着色器,所以使用其他着色器的物體不會被渲染,從而導致它們不可見。雖然這是正確的,但它掩蓋了某些遊戲物體使用了錯誤的着色器這一事實。如果我們可以通過Unity的error着色器來看到它們那就太好了,可以讓他們顯示爲錯誤的洋紅色外形。爲此我們寫一個專門的方法 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作爲正向渲染的第一個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
		);
	}

使用默認着色器的物體現在顯示出來了,也可以在frame debugger裏看到它們了。

 因爲我們的pipeline還不支持正向渲染的pass,所以它們沒有被正確的渲染。由於必要的數據還沒設置,所以依賴光照的一切事物都被當成黑色。相反,我們應該使用error着色器來渲染它們。想要做到,我們就要有一個error着色器。爲此我們添加一個字段。接着在調用 DrawDefaultPipeline之前,如果error之前還沒創建,就創建一個。另外設置材質的 hide flags爲HideFlags.HideAndDontSave ,這樣它就不會顯示在項目窗口,也不會和其他的資源保存在一起了。

	Material errorMaterial;

	…

	void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
		if (errorMaterial == null) {
			Shader errorShader = Shader.Find("Hidden/InternalErrorShader");
			errorMaterial = new Material(errorShader) {
				hideFlags = HideFlags.HideAndDontSave
			};
		}
		
		…
	}

繪製設置有一個選項可以用於在渲染時用指定材質代替原有材質。通過調用SetShaderPassName來實現。它的第一個參數是我們想要覆蓋的參數,第二個參數爲我們指定材質的着色器中用於渲染的pass的序號。error着色器只有一個pass,所以設置序號爲0。

		var drawSettings = new DrawRendererSettings(
			camera, new ShaderPassName("ForwardBase")
		);
		drawSettings.SetOverrideMaterial(errorMaterial, 0);

 使用不受支持材質的物體現在被明確的顯示爲錯誤的顏色。但是這隻對使用unity默認管道的材質有效,也就是使用包含了ForwardBase pass的着色器。對於其他的內置着色器,我們可以用不同的pass來識別,主要是PrepassBaseAlwaysVertexVertexLMRGBM, 和 VertexLM這些。

幸運的是,我們可以調用 SetShaderPassName來給繪製設置添加多個用於篩選的pass。它的第二個參數是pass名。第一個參數是一個控制繪製pass順序的索引,我們不關心這個,所以什麼順序都可以。通過構造函數添加的pass,它的序號被設爲0,所以額外添加的這些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 Code Execution)

Conditional特性定義在System.Diagnostics命名空間,我們可以引用這個命名空間,但不幸的是其中包含的 Debug類型和UnityEngine.Debug相沖突。好在我們只需要用到這個特性,可以通過使用別名來代替使用整個命名空間來避免衝突,把特定類型賦值給一個有效的類型名稱。在這種情況下,我們System.Diagnostics.ConditionalAttribute.把定義爲 Conditional

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
using Conditional = System.Diagnostics.ConditionalAttribute;

爲我們的方法添加該特性。它需要一個指定爲標識符的字符串作爲參數。如果這個標識符在編譯中是定義了的,那麼該方法的調用就正常包含在內。但如果沒有被定義,那麼就忽略此方法的調用(包括其所有參數)就好像DrawDefaultPipeline(context, camera);這串代碼在編譯中不存在一樣。

要僅在編譯Unity編輯器時包含調用,我們仰仗UNITY_EDITOR標識符。

	[Conditional("UNITY_EDITOR")]
	void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
		…
	}

讓我們更近一步,在開發版本development builds也可以正常調用,只在發佈版本時將他排除。要想實現這個,我們要再加一個用 DEVELOPMENT_BUILD 的conditional 。

	[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
	void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
		…
	}

3.5場景窗口的UI(UI in Scene Window) 

還有一件事我們沒考慮到,那就是Unity遊戲內的UI。在場景中添加一個UI元素用於測試。比如,用GameObject / UI / Button創建一個按鈕。他會創建一個帶畫布(canvas)的按鈕,以及一個事件系統。

事實證明,我們不需要做任何事就可以讓UI在遊戲窗口渲染。Unity會幫我們處理。frame debugger表明UI獨立的進行渲染,併疊加在屏幕上。

UI在屏幕空間

 至少,當畫布設置爲在屏幕空間中渲染時就是這種情況。當設置爲在世界空間中渲染時,UI將與其他透明對象一起渲染。

UI在世界空間

雖然UI在遊戲窗口可以正常工作,但是在場景窗口它並沒有顯示出來。在場景窗口,UI總是在世界空間存在,且我們必須手動將它注入場景。要添加UI,我們可以調用靜態方法ScriptableRenderContext.EmitWorldGeometryForSceneView,它需要當前相機作爲參數,該方法必須在剔除前調用。

		if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
			return;
		}

		ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);

		CullResults.Cull(ref cullingParameters, context, ref cull);

但這會導致在遊戲窗口,UI被添加了兩次,爲了防止這種情況,我們必須只在渲染場景窗口時調用這個方法。也就是當相機的cameraType參數CameraType.SceneView. 時。

		if (camera.cameraType == CameraType.SceneView) {
			ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
		}

 這可以正常工作,但僅限編輯器 中,附加條件編譯確保針對發佈時EmitWorldGeometryForSceneView不會編譯進去(編譯時沒有該方法的具體實現 )。這就意味着在我們嘗試發佈時會得到一個編譯錯誤。想讓他再次工作,我們必須使調用EmitWorldGeometryForSceneView的代碼也是有條件的。我們可以把代碼放着#if和#endif聲明中實現。#if聲明需要一個標識符,就像Conditional特性一樣,通過使用UNITY_EDITOR,僅在爲編輯器編譯時纔會包含該代碼。

	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);

		…
	}

 

下一個教程是自定義着色器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章