原文 : https://catlikecoding.com/unity/tutorials/custom-srp/custom-render-pipeline/
強烈建議對照原文看,我很有可能會翻譯出問題,而且原文的代碼會比這邊更清楚(更改的地方有高亮)
文中的連接都是原文的連接, 還沒翻譯, 翻譯後可能會換成翻譯後的連接.
本文翻譯未經作者允許(沒問), 我水平非常有限, 也是剛開始學, 就邊學邊翻譯了.
文中不重要的部分就直接用機器翻譯了, 拿不準的地方會附上原文(我的英文水平很差)
自定義渲染管線
- 創建 render pipeline asset 和 render pipeline。
- 渲染相機的視圖。
- 執行剔除,過濾和排序。
- 對不透明, 透明, 無效 的pass進行分離 。
- 多個相機的處理。
這是關於創建自定義可編程渲染管道系列教程的第一部分。 它涵蓋了最初創建的最基礎的渲染管線,我們會在後面的教程中對其進行擴展.
本教程使用Unity 2019.2.6f1製作。
關於其他SRP系列(機翻)
作者還有另一個教程系列,涉及可腳本化的渲染管道,但其中使用了實驗性SRP API,該API僅適用於Unity 2018.該系列適用於Unity 2019及更高版本。 本系列文章採用了一種不同的,更現代的方法,但是將涵蓋許多相同的主題。 如果您不想等到這個系列趕上了它,那麼完成2018年系列仍然很有用。
新的渲染管線
無論要渲染任何物體,Unity必須決定在什麼地方、什麼時候、用什麼設置繪製什麼形狀。在渲染的效果很多的時候,這可能會非常複雜。光線、陰影、透明度、圖像效果、體積效果等等都必須按照正確的順序處理才能得到最終的圖像。這就是渲染管道的作用。
在過去,Unity只支持一些內置的渲染方式。Unity 2018引入了可編程渲染管線(簡寫爲RP),這使得我們可以做任何我們想做的事情,同時仍然能夠依靠Unity來完成一些基本的步驟,比如剔除。Unity 2018還添加了兩個使用這種新方法制作的實驗性RP:輕量級RP(Lightweight RP)和高清晰度RP(High Definition RP)。在Unity 2019年,輕量級RP不再處於實驗階段,並在Unity 2019.3中被重新命名爲通用RP( Universal RP)
通用RP註定會取代作爲缺省值的當前舊RP。通用RP可以適配各種情況,而且非常容易擴展。本系列將從頭創建整個RP,而不是修改該RP。
項目設置
在Unity 2019.2.6或更高版本中創建新的3D項目。 我們將創建自己的管道,因此不要選擇RP項目模板之一。打開項目後,您可以轉到程序包管理器(package manager)並刪除所有不需要的包。 在本教程中,我們將僅使用Unity UI包來繪製UI,因此可以把這個包留下來。
我們將只使用線性顏色空間,但Unity 2019.2仍然使用gamma空間作爲默認值。通過 Edit -> Project Settings -> Player -> Other Settings -> Color Space 設爲 Linear。
原圖
在默認場景中放一些物體,材質在下面三種中隨便挑選:mix of standard 、unlit opaque 、transparent。 Unlit/Transparent的着色器只對紋理有效,所以這裏給出了一個UV球體貼圖。
我在測試場景中放了幾個立方體,它們都是不透明的。 紅色的與 Standard 着色器一起使用,綠色和黃色的與 Unlit/Color 着色器一起使用。 藍色球體使用 Standard 材質球,“渲染模式”(Rendering Mode)設置爲“透明(Transparent)”,而白色球體使用 Unlit/Transparent 着色器。
原圖
本節做了:
- 建立新項目,用默認的模板(3D)即可
- 刪除 無用的包,最好保留UGUI
- 顏色空間 設爲 線性
- 建立測試場景,用如下材質:
- Standard
- Standard (Transparent)
- Unlit/Color
- Unlit/Transparent
渲染管線資產
當前,Unity使用默認渲染管線。 要用自定義渲染管線替換,我們首先必須創建一個對應的資產類型。我們將使用與Unity通用RP大致相同的文件夾結構。創建 Custom RP 文件夾,並在其下面創建 Runtime 子文件夾。創建一個新的c#腳本,名爲 CustomRenderPipelineAsset。
原圖
RP資產需要繼承在命名空間 UnityEngine.Rendering 中的 RenderPipelineAsset 類
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipelineAsset : RenderPipelineAsset {}
RP資產的主要目的是讓Unity獲得一個負責渲染的RP實例。資產本身只是一個句柄和存儲設置的地方。我們還沒有任何設置,所以我們所要做的就是給Unity一個方法來獲得我們的管道對象實例,這是通過重寫抽象方法 CreatePipeline 完成的。該方法返回一個 RenderPipeline 實例。但是我們還沒有定義自定義的RP類型,所以先返回null。
CreatePipeline 方法是使用protected訪問修飾符定義的,這意味着只有定義該方法的類(即RenderPipelineAsset)及其擴展類才能訪問它。
protected override RenderPipeline CreatePipeline () {
return null;
}
現在,我們想在菜單中創建這個資產。 爲此,請將CreateAssetMenu特性添加到 CustomRenderPipelineAsset 上。
[CreateAssetMenu]
public class CustomRenderPipelineAsset : RenderPipelineAsset { … }
這將在“Asset / Create”菜單中放置一個條目。 讓我們保持整潔並將其放在“Rendering”子菜單中。 通過將屬性的menuName屬性設置爲Rendering / Custom Render Pipeline,可以做到這一點。 可以在屬性類型之後直接在圓括號中設置此屬性。
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset { … }
用我們剛剛的新菜單項創建資產, 然到 Graphics 項目設置中,然後把資產放到 Scriptable Render Pipeline Settings 選項中。
原圖
替換默認RP改變了幾件事。 首先,Graphcis面板中少了很多選項。 其次,我們禁用了默認RP而不提供有效的替換,因此不再呈現任何內容。 遊戲窗口,場景窗口和材質預覽不再起作用。 如果您通過“窗口” /“分析” /“框架調試器”打開框架調試器並啓用它,您將看到在遊戲窗口中確實沒有繪製任何內容
本節做了:
- 建立如圖文件夾結構(應該不是必須的),建立 CustomRenderPipelineAsset 類
- 繼承 RenderPipelineAsset
- 重寫 CreatePipeline,先返回null
- 將 CustomRenderPipelineAsset 添加到菜單項
- 用菜單項創建 CustomRenderPipelineAsset 實例
- 將該實例設置到 Scriptable Render Pipeline Settings 項(在Project Setting 的Graphics中)
渲染管線實例
創建一個CustomRenderPipeline
類,將其與CustomRenderPipelineAsset
放在同一個文件夾中。這個類型將作爲前面RP資產所返回的類型,因此必須繼承RenderPipeline
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline {}
RenderPipeline
定義了一個受保護的抽象方法Render
,我們必須重寫這個方法來創建一個真正的渲染管線。Render
會接收兩個參數:ScriptableRenderContext
和Camera
數組。我們先讓這個方法是空着的。
protected override void Render (
ScriptableRenderContext context, Camera[] cameras
) {}
現在改變前面的CustomRenderPipelineAsset.CreatePipeline
方法,返回一個CustomRenderPipeline
實例。這個樣我們就有了一個有效的最最基礎的渲染管線,雖然它沒有渲染任何東西。
protected override RenderPipeline CreatePipeline () {
return new CustomRenderPipeline();
}
本節做了:
- 新建類
CustomRenderPipeline
- 繼承
RenderPipeline
- 重寫
Render
- 修改
CustomRenderPipelineAsset.CreatePipeline
返回CustomRenderPipeline
的實例
渲染
Unity在每幀都會調用RP實例上的Render函數。通過傳入一個環境結構體變量來與本地引擎進行交流,我們可以用這個變量來進行渲染。還會傳入一個組相機,因爲場景中可能會有多個相機被激活了。RP的職責是要按照確定的順序將這些相機渲染出來。
相機渲染器(Camera Renderer)
每個攝像機都是獨立渲染的。因此,我們將把這個責任轉交給專門渲染一個攝像機的新類,而不是用CustomRenderPipeline來渲染所有的攝像機。將新類命名爲CameraRenderer,並給它一個帶有環境和攝像機參數的公有函數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;
}
}
讓CustomRenderPipeline
創建一個渲染器實例,然後循環使用它來渲染所有的相機。
CameraRenderer renderer = new CameraRenderer();
protected override void Render (
ScriptableRenderContext context, Camera[] cameras
) {
foreach (Camera camera in cameras) {
renderer.Render(context, camera);
}
}
我們的相機渲染器大致相當於通用RP的腳本渲染器。這種方法對於每個攝像機用不同的渲染方法時會很方便,例如一個用第一人稱視圖和另一個用3D地圖疊加(3D map overlay),或者一個用前向渲染,一個用延遲渲染。但是現在我們將用同樣的方式渲染所有的相機。
本節做了:
- 創建
CameraRenderer
類 - 其帶有一個 公有的
Render
函數,接收ScriptableRenderContext
和Camera
作爲參數 - 將這兩個參數存儲到私有字段中
- 在
CustomRenderPipeline.Render
中創建一個實例,對每個相機循環調用這個實例的Render
函數來將渲染任務分派出去。
繪製天空盒
CameraRenderer.Render
的職責是繪製相機可以看到的所有幾何圖形。用一個單獨的DrawVisibleGeometry方法來分離那個特定的任務,這樣更清晰。我們首先讓它繪製默認的skybox,這可以通過在context
(前文的環境,後面都用context)上以攝像機作爲參數調用DrawSkybox
來完成。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
DrawVisibleGeometry();
}
void DrawVisibleGeometry () {
context.DrawSkybox(camera);
}
天空盒並沒有出現。 這是因爲我們對上下文發出的命令是實際上只是被緩存下來了,並沒有直接被執行。 我們必須通過在context
上調用Submit
來執行所有被緩存下來的工作。 讓我們在單獨的Submit
方法中執行此操作,此方法在DrawVisibleGeometry
之後調用。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
DrawVisibleGeometry();
Submit();
}
void Submit () {
context.Submit();
}
現在skybox就出現了,無論是在Scene還是在Game窗口。當你運行遊戲的時候,您還可以在frame debugger中看到它的條目。它被列爲Camera.RenderSkybox,在它下面有一個Draw Mesh條目,它代表了實際的 draw call。這對應於Game窗口的渲染信息。框架調試器不會報告在其他窗口(如Scene窗口)中的渲染信息。
原圖
注意,當前相機的方向不會影響skybox的渲染。我們將相機傳遞給DrawSkybox
,但這只是通過相機的clear flags來控制是否應該繪製skybox。
爲了正確渲染天空盒以及整個場景,我們必須設置變換矩陣(view-projection matrix)。 這個矩陣將攝像機的位置和方向(視圖矩陣view matrix)與攝像機的透視或正投影(投影矩陣projection matrix)結合在一起(即投影*視圖)。 在着色器中稱爲unity_MatrixVP
,這是編寫shader時是用到的屬性之一。 選擇一個draw call後,可以在frame debugger的ShaderProperties部分中檢查此矩陣。
現在,unity_MatrixVP
矩陣總是相同的。我們需要通過SetupCameraProperties
方法,將攝像機的屬性應用到context。這樣就配置了矩陣以及其他一些屬性。在調用DrawVisibleGeometry
之前,聲明一個新方法Setup
執行此操作。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
Setup();
DrawVisibleGeometry();
Submit();
}
void Setup () {
context.SetupCameraProperties(camera);
}
本節做了:
- 通過
context.DrawSkybox(camera)
繪製天空盒 - 通過
context.Submit
提交操作 - 通過
context.SetupCameraProperties(camera)
配置context的信息
命令緩衝
context
會將渲染延遲,直到我們提交它。在此之前,我們對其進行配置並向其添加命令。一些任務,如繪製skybox,可以通過特定的方法直接發出命令,但其他命令必須通過單獨的命令緩衝區對象發出。我們需要用緩衝區來繪製場景中的其他幾何體。
爲了獲得緩衝區,我們必須創建一個新的CommandBuffer
實例。我們只需要一個緩衝區,所以默認情況下只爲CameraRenderer
創建一個緩衝區,並在字段中存儲對它的引用。還要給緩衝區起一個名字,這樣我們就可以在frame debugger中識別它。叫做 “Render Camera” 就可以。
const string bufferName = "Render Camera";
CommandBuffer buffer = new CommandBuffer {
name = bufferName
};
我們可以使用命令緩衝區注入profiler的採樣,這些採樣將同時顯示在profiler和frame debugger中。 這是通過在適當的位置調用BeginSample
和EndSample
來完成的,在本例中,這是在Setup
和Submit
的開頭。 兩種方法都必須提供相同的名稱,我們可以直接用緩衝區的名稱。
void Setup () {
buffer.BeginSample(bufferName);
context.SetupCameraProperties(camera);
}
void Submit () {
buffer.EndSample(bufferName);
context.Submit();
}
要執行緩衝區,請以緩衝區爲參數在context上調用ExecuteCommandBuffer
。 這會把命令從緩衝區複製而不是彈出(複製後刪除)到context中。如果要重置緩衝區,我們必須在之後明確地執行該操作。 因爲執行和清除總是一起完成的,所以寫一個同時執行這兩步操作的方法會很方便。
void Setup () {
buffer.BeginSample(bufferName);
ExecuteBuffer();
context.SetupCameraProperties(camera);
}
void Submit () {
buffer.EndSample(bufferName);
ExecuteBuffer();
context.Submit();
}
void ExecuteBuffer () {
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
}
現在 Camera.RenderSkyBox 採樣就在 Render Camera 採樣之內了。
原圖
本節做了:
- 創建
CommandBuffer
對象,併爲其起名字 - 通過 該對象的
BeginSample
與EndSample
方法 進行採樣,並設置採樣的名字(這裏設爲與該對象相同的名字) - 通過
context.ExecuteCommandBuffer
執行該對象中緩衝的操作 - 通過 該對象的
Clear
方法清空對象中的操作。
清理渲染目標
無論我們繪製什麼,最終都會將圖像繪製到相機的渲染目標中,默認情況下,渲染目標是幀緩衝區(譯者的理解:大概就是指顯卡里的幀緩衝區,發送給顯示器的那片內存),但也可以是渲染紋理(render texture)。 渲染目標中的東西不會被自動清除,這可能會干擾我們本次渲染的圖像。 爲了保證正確的渲染,我們必須清理渲染目標。 這是通過調用命令緩衝區中的ClearRenderTarget
方法來完成的,這一步在Setup
方法中執行。
void Setup () {
buffer.BeginSample(bufferName);
buffer.ClearRenderTarget(true, true, Color.clear);
ExecuteBuffer();
context.SetupCameraProperties(camera);
}
原圖
現在,frame debugger將顯示“Draw GL”條目以進行清除操作,該條目嵌套另一個“Render Camera”中。 發生這種情況是因爲ClearRenderTarget
自動使用命令緩衝區的名稱將清除內容包裝在一個採樣中。在開始我們自己的樣本之前,清理渲染目標,就可以消除多餘的嵌套。 實際上會產生兩個相鄰的“Render Camera”區域,而這會被自動合併爲一個。
void Setup () {
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(bufferName);
//buffer.ClearRenderTarget(true, true, Color.clear);
ExecuteBuffer();
context.SetupCameraProperties(camera);
}
“Draw GL”條目代表使用“Hidden/InternalClear”着色器繪製全屏四邊形並寫入渲染目標,這不是清除目標的最有效方法。 之所以使用這種方法,是因爲我們在設置相機屬性之前,就先進行了清除。 如果我們交換這兩個步驟的順序,則可以更快速清除。
void Setup () {
context.SetupCameraProperties(camera);
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(bufferName);
ExecuteBuffer();
//context.SetupCameraProperties(camera);
}
現在我們看到“Clear (color+Z+stencil)”,這表示顏色和深度緩衝區都被清除。???(最後一句我沒看明白,原文如下 Z represents the depth buffer and the stencil data is part the same buffer. 不過這應該不影響主題)。
本節做了:
- 用
buffer.ClearRenderTarget(true, true, Color.clear)
清理渲染目標 - 調整到正確的順序
剔除
我們現在只能看到skybox,而無法看到放入場景中的任何物體。我們不會繪製每一個對象,而是隻渲染那些對攝像機可見的對象。我們從場景中所有帶有 renderer 組件的對象中,剔除掉那些落在攝像機視錐體之外的對象。
找出可以剔除的內容需要我們跟蹤多個相機設置和矩陣,爲此我們可以使用ScriptableCullingParameters
結構體。 除了手動爲這個結構體中的字段賦值,我們還可以在相機上調用TryGetCullingParameters
。 它返回一個bool值,表示取值是否成功,因爲一些退化的相機設置可能會失敗。 爲了得到真正需要的數據,我們需要一個輸出參數。 我們在返回成功或失敗的新方法Cull
中執行此操作。
bool Cull () {
ScriptableCullingParameters p
if (camera.TryGetCullingParameters(out p)) {
return true;
}
return false;
}
當變量作爲輸出參數時,可以內聯變量聲明:
bool Cull () {
//ScriptableCullingParameters p
if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
return true;
}
return false;
}
在Render
中的Setup
之前調用Cull
,如果失敗則中止。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
if (!Cull()) {
return;
}
Setup();
DrawVisibleGeometry();
Submit();
}
實際的剔除是通過調用context
的Cull
來完成的,這會生成CullingResults
結構體變量。 如果成功,請在Cull
中執行此操作,然後將結果存儲在字段中。(刪掉了c#語法相關的解釋)
CullingResults cullingResults;
…
bool Cull () {
if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
cullingResults = context.Cull(ref p);
return true;
}
return false;
}
本節做了:
- 用camera.TryGetCullingParameters獲得執行剔除所需要的信息
- 利用context.Cull,接收剛剛獲得的信息進行剔除,並得到剔除的結果
繪製幾何體
一旦我們知道了那些物體是可以看到的,就可以繼續渲染這些東西。 這是通過在context
上調用DrawRenderers
完成的,並以剔除結果作爲參數,這會告訴它需要繪製哪些renderer。 除此之外,我們必須提供繪圖設置和過濾設置。 兩者都是結構體DrawingSettings
和FilteringSettings
,我們將先用它們的默認構造函數。 兩者都必須通過引用傳遞。在DrawVisibleGeometry
中繪製天空盒之前執行此操作。
void DrawVisibleGeometry () {
var drawingSettings = new DrawingSettings();
var filteringSettings = new FilteringSettings();
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
context.DrawSkybox(camera);
}
我們看不到任何東西,因爲我們還必須指出允許使用哪種着色器。 由於在本教程中我們僅支持Unlit着色器,因此我們必須獲取“SRPDefaultUnlit”的着色器標籤ID,我們可以將其緩存在靜態字段中。
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
將這個id作爲DrawingSettings
構造函數的第一個參數,一個新的SortingSettings
結構值作爲第二個參數。將攝像機傳遞給SortingSettings
的構造函數,因爲它用於確定是否應用正交排序或基於距離的排序(不確定怎麼翻譯,也不知道這是啥,原文是orthographic or distance-based sorting)。
void DrawVisibleGeometry () {
var sortingSettings = new SortingSettings(camera);
var drawingSettings = new DrawingSettings(
unlitShaderTagId, sortingSettings
);
…
}
除此之外,還必須指出我們要渲染哪些渲染隊列。通過把RenderQueueRange.all
設置到FilteringSettings的構造函數,來渲染所有的渲染隊列。
var filteringSettings = new FilteringSettings(RenderQueueRange.all);
原圖
原圖
目前我們僅繪製了使用Unlit着色器的可見對象。 所有的draw call用都在frame debugger中列出,並在“RenderLoop.Draw”分組下。 透明物體發生了一些奇怪的事情,但是讓我們首先看一下對象的繪製順序。 frame debugger顯示了這一點,您可以通過依次選擇一個或使用箭頭鍵逐步完成繪製調用。
演示視頻:遍歷frame debugger的每個條目
繪製的順序是隨意的。我們可以通過設置SortingSettings
的criteria
屬性來強制指定繪製順序。我們用SortingCriteria.CommonOpaque
的順序。
var sortingSettings = new SortingSettings(camera) {
criteria = SortingCriteria.CommonOpaque
};
演示視頻:Opaque排序方式
原圖
對象現在可以或多或少地從前向後繪製,這對於不透明對象非常理想(譯者:沒明白,對於不透明物體也並不理想,現在與剛剛的區別是先繪製了不透明物體,然後才繪製了透明物體,與前後似乎無關)。如果某些東西在其他東西后面繪製,它的被擋住的片元(fragment)就可以被跳過,這加快了渲染速度。common opaque排序選項還考慮了其他一些條件,包括渲染隊列和材質。
本節做了:
- 利用相機,創建
SortingSettings
結構體,criteria
字段設爲SortingCriteria.CommonOpaque
- 創建
ShaderTagId
對象,傳入“SRPDefaultUnlit”獲取要渲染的shader的ID - 利用這兩個對象創建
DrawingSettings
結構體 - 創建
FilteringSettings
結構體,傳入RenderQueueRange.all
,表示要渲染的渲染隊列 - 利用
context.DrawRenderers
進行渲染,需要上節的剔除結果,剛剛創建的的DrawingSettings
與FilteringSettings
分別繪製不透明和透明幾何圖形
我們從frame debugger上可以看到,的確繪製了透明對象,但是天空盒的繪製覆蓋掉了一部分透明對象,這些透明對象的特點是他們的後面都沒有不透明對象。 天空盒是在繪製了所有幾何體的之後才繪製的,應當可以跳過所有被擋住的片元,但是它卻沒有跳過被透明幾何體擋住的片元。 發生這種情況是因爲透明着色器不會寫入深度緩衝區。 因此透明幾何體不會擋住任何東西,因爲我們需要看到它們後面的物體。 解決方案是先繪製不透明的對象,然後繪製天空盒,然後再繪製透明的對象。
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
然後,在繪製天空盒之後,再次調用DrawRenderers
。 但是在此之前,請將渲染隊列範圍更改爲RenderQueueRange.transparent
。 並把SortingSettings
的criteria
更改爲SortingCriteria.CommonTransparent
,然後再次設置DrawingSettings
的sortingSettings
。 這將反轉透明對象的繪製順序。
context.DrawSkybox(camera);
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
演示視頻:不透明,天空盒,透明
原圖
爲什麼要反轉渲染順序
由於透明對象不會寫入深度緩衝區,因此將它們從前到後進行排序沒有任何性能優勢。 但是,當多個透明對象重疊在一起的時候,必須將它們從後向前繪製以正確的融合效果。
不幸的是,從後到前的排序也不能保證正確的混合,因爲排序是針對整個對象的,並且僅考慮了對象的位置。 自身重疊或者較大的透明對象仍會產生不正確的結果。 有時可以通過將幾何形狀切割成較小的部分來解決。(《Unity Shader 入門精要》裏有更詳細的解釋)
本節做了:
- 分離出兩個
DrawRenderers
,分別在DrawSkybox
的之前與之後 - 之前的將
FilteringSettings
的參數改爲RenderQueueRange.opaque
,其他不變。這樣就正確渲染了不透明物體 - 之後的將
SortSettings
的criteria
改爲SortingCriteria.CommonTransparent
- 再將
FilteringSettings
的renderQueueRange
參數改爲RenderQueueRange.transparent
。這個參數應當就是構造函數傳入的參數。這樣就正確渲染了透明物體。
編輯模式中的渲染
我們的RP目前只能正確地繪製Unlit對象。我們可以做一些事情來提高在Unity編輯器中使用它的體驗。
繪製舊版着色器
因爲我們的RP僅支持Unlit着色器的pass,所以不會渲染使用pass的對象。 儘管這是正確的,但它沒有對場景中某些對象使用錯誤着色器做出提示。 因此,無論如何,還是分開渲染它們。
如果有人要從默認的Unity項目開始,然後再切換到我們的RP,那麼他們的場景中的物體可能帶有錯誤的着色器。 爲了覆蓋所有Unity的默認着色器,我們需要這些標籤的ID:Always,ForwardBase,PrepassBase,Vertex,VertexLMRGBM和VertexLM。 我們可以把這些ID存儲在靜態數組中。
static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};
在繪製了支持的幾何體之後,我們遍歷所有這些不受支持的pass,創建一個新的方法對其進行繪製。 由於這些無效的pass,無論如何結果都是錯誤的,因此我們不在乎其他設置。 我們可以通過FilteringSettings.defaultValue屬性獲取默認過濾設置。
public void Render (ScriptableRenderContext context, Camera camera) {
…
Setup();
DrawVisibleGeometry();
DrawUnsupportedShaders();
Submit();
}
…
void DrawUnsupportedShaders () {
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
);
var filteringSettings = FilteringSettings.defaultValue;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
}
我們可以通過在drawing settings
上調用SetShaderPassName
來繪製多個pass,把繪製順序(draw order index )和shader tag的ID作爲參數傳入即可。 對數組中的所有的pass遍進行此操作。從第二個開始,因爲在構造圖形設置時我們已經設置了第一個。
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
);
for (int i = 1; i < legacyShaderTagIds.Length; i++) {
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}
原圖
我們把使用標準着色器的對象繪製了出來,但它們現在是純黑色的,因爲我們的RP還沒有爲其設置所需的着色器屬性。
本節做了:
- 取得Unity默認着色器的Shader Tag Id。有這些默認着色器:Always,ForwardBase,PrepassBase,Vertex,VertexLMRGBM和VertexLM
- 用
drawingSettings.SetShaderPassName
把這些Shader ID 都加到同一個DrawingSettings
中。 - 在新函數
DrawUnsupportedShaders
中用默認設置繪製這些着色器。
錯誤材質
爲了清楚地表明哪些對象使用了不受支持的着色器,我們將使用Unity的錯誤shader對其進行繪製。 使用該着色器作爲參數構造一個新材質,我們可以通過調用Shader.Find
,並以“Hidden/InternalErrorShader”作爲參數來查找。 爲了不在每幀上都創建一個新的材質,我們可以通過靜態字段緩存材質。 然後將其分配給圖形設置的overrideMaterial
屬性。
static Material errorMaterial;
…
void DrawUnsupportedShaders () {
if (errorMaterial == null) {
errorMaterial =
new Material(Shader.Find("Hidden/InternalErrorShader"));
}
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
) {
overrideMaterial = errorMaterial
};
…
}
原圖
現在所有物體都被顯示出來了,而且錯誤的材質也有明顯的顯示。
本節做了:
- 創建一個材質,通過
Shader.Find
找到着色器"Hidden/InternalErrorShader" - 將這個材質存儲到靜態變量中。
- 將
DrawingSettings
的overrideMaterial
設爲這個材質
局部類
繪製無效對象對開發很有用,但不適用於已發佈的應用程序。因此,讓我們將CameraRenderer
的所有編輯器代碼放在一個單獨的局部類中。首先複製原始CameraRenderer腳本資產,並將其重命名爲CameraRenderer.editor。
原圖
然後將原始CameraRenderer轉換爲一個局部類,並從其中刪除標記數組、錯誤材質和DrawUnsupportedShaders
方法。
public partial class CameraRenderer { … }
另一個部分類文件只包含我們剛剛刪除的內容
using UnityEngine;
using UnityEngine.Rendering;
partial class CameraRenderer {
static ShaderTagId[] legacyShaderTagIds = { … };
static Material errorMaterial;
void DrawUnsupportedShaders () { … }
}
編輯器部分的內容僅需要存在於編輯器中,因此使其以UNITY_EDITOR爲條件。
partial class CameraRenderer {
#if UNITY_EDITOR
static ShaderTagId[] legacyShaderTagIds = { … }
};
static Material errorMaterial;
void DrawUnsupportedShaders () { … }
#endif
}
但是,此時構建將會失敗,因爲另一部分包含了對DrawUnsupportedShaders
的調用,該調用現在僅應當在編輯器中存在。 爲了解決這個問題,我們也使該方法成爲局部的。 爲此,我們在運行時的文件中聲明這個方法(而不進行實現),並在方法簽名前面加上partial
,這與抽象方法聲明類似。 我們可以在類定義的任何部分中實現這個函數,因此將其放在編輯器部分中。 方法的實現也必須標記爲partial。
partial void DrawUnsupportedShaders ();
#if UNITY_EDITOR
…
partial void DrawUnsupportedShaders () { … }
#endif
}
構建的編譯現在就可以通過了。編譯器將所有調用了沒有實現的分部方法的地方都刪掉。
本節做了:
- 將CameraRenderer標爲部分類
- 將剛剛所有渲染錯誤材質的代碼,放到新的部分類中
- 在原來的類中 聲明
DrawUnsupportedShaders
,並標記爲分部函數 - 在新部分類中 將
DrawUnsupportedShaders
標記爲分部函數
繪製Gizmos
目前我們的RP不繪製Gizmos,無論是在場景窗口還是在遊戲窗口(如果它們在遊戲窗口中被啓用了)。
原圖
我們可以通過調用UnityEditor.Handles.ShouldRenderGizmos
來檢查是否應繪製小控件。 如果是這樣,我們必須使用相機作爲參數在上下文中調用DrawGizmos
,再加上第二個參數來指示應繪製哪個Gizmo子集。 有兩個子集,分別用於圖像效果前後。 由於我們目前不支持圖像效果,因此我們將同時調用兩者。 在僅用於編輯器的新方法DrawGizmos
中執行此操作。
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
partial class CameraRenderer {
partial void DrawGizmos ();
partial void DrawUnsupportedShaders ();
#if UNITY_EDITOR
…
partial void DrawGizmos () {
if (Handles.ShouldRenderGizmos()) {
context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
}
}
partial void DrawUnsupportedShaders () { … }
#endif
}
應在其他所有內容之後繪製小控件。
public void Render (ScriptableRenderContext context, Camera camera) {
…
Setup();
DrawVisibleGeometry();
DrawUnsupportedShaders();
DrawGizmos();
Submit();
}
本節做了
- 通過
Handles.ShouldRenderGizmos()
判斷是否繪製Gizmos - 調用
context.DrawGizmos
繪製Gizmos,第一個參數是相機,第二個參數表示在後效果之前還是之後繪製。這裏我們不支持後效果,我們繪製兩次。
繪製UnityUI
另一件需要我們注意的事情是Unity的UI部分。例如,通過GameObject / UI / button添加一個按鈕來創建一個簡單的UI。它會出現在遊戲窗口,而不是場景窗口。
原圖
frame debugger告訴我們UI是單獨呈現的,而不是由RP呈現的。
原圖
至少,當Canvas組件的“渲染模式”設置爲“屏幕空間-覆蓋”時,這是默認情況。 將其更改爲“屏幕空間-相機”並將主相機用作其“渲染相機”將使其成爲透明幾何體的一部分。
原圖
UI在場景窗口中渲染時始終使用世界座標,這就是爲什麼它通常會變得非常大的原因。 而且,儘管我們可以通過場景窗口編輯UI,但不會繪製它。
在爲scene窗口渲染時,我們必須通過使用相機作爲參數調用ScriptableRenderContext.EmitWorldGeometryForSceneView
,將UI顯式添加到世界幾何體中。 在僅限編輯器的新方法PrepareForSceneWindow
中執行此操作。 當場景攝像機的cameraType屬性等於CameraType.SceneView時,將使用場景攝像機進行渲染。
partial void PrepareForSceneWindow ();
#if UNITY_EDITOR
…
partial void PrepareForSceneWindow () {
if (camera.cameraType == CameraType.SceneView) {
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
}
因爲這可能會給場景添加幾何圖形,所以必須在剔除之前完成。
PrepareForSceneWindow();
if (!Cull()) {
return;
}
本節做了:
- 將UI的渲染模式設置爲 “Screen Space - Camera ”(似乎默認也可以)
- 通過
camera.cameraType == CameraType.SceneView
判斷當前相機是否爲場景相機 - 如果是,就通過
ScriptableRenderContext.EmitWorldGeometryForSceneView
將UI添加到scene中。 - 這些要在Cull之前完成。
多相機
場景中可能有一個以上的活動攝像機。 如果是這樣,我們必須確保它們一起工作。
兩個相機
每個攝像機都有一個Depth值,默認主攝像機爲-1。 它們以增加的深度順序進行渲染。 要看到這一點,請複製主攝像機,將其重命名爲“Secondary Camera”,並將其“深度”設置爲0。最好再給它換個標籤,因爲MainCamera僅應由單個攝像機使用。
現在,場景被渲染兩次。 由於渲染目標在兩者之間被清除,因此生成的圖像仍然相同。 frame debugger顯示了這一點,但是由於合併了具有相同名稱的相鄰樣本範圍,因此最終只有一個“Render Camera”。
如果每個攝像機都有自己的採樣,那就更清楚了。 爲此,請添加僅編輯器的PrepareBuffer方法,該方法使緩衝區的名稱與相機的名稱相同。
partial void PrepareBuffer ();
#if UNITY_EDITOR
…
partial void PrepareBuffer () {
buffer.name = camera.name;
}
#endif
這在PrepareForSceneWindow()
之前執行
PrepareBuffer();
PrepareForSceneWindow();
處理不同的Buffer名
儘管幀調試器現在爲每個攝像機顯示了一個單獨的示例層次結構,但是當我們進入播放模式時,Unity的控制檯將充滿消息,警告我們BeginSample和EndSample計數必須匹配。 因爲我們爲樣本及其緩衝區使用了不同的名稱而感到困惑。 除此之外,每次訪問攝像機的name屬性時,我們最終都會分配內存,因此我們不想在構建的時候這樣做。
爲了解決這兩個問題,我們將添加SampleName
字符串屬性。 如果在編輯器中,則將它與緩衝區的名稱一起設置在PrepareBuffer
中,否則,它只是Render Camera字符串的常量別名。
#if UNITY_EDITOR
…
string SampleName { get; set; }
…
partial void PrepareBuffer () {
buffer.name = SampleName = camera.name;
}
#else
const string SampleName = bufferName;
#endif
在Setup
和Submit
中的開始結束採樣操作中,傳入這個SampleName(而不是原來的bufferName)
void Setup () {
context.SetupCameraProperties(camera);
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(SampleName);
ExecuteBuffer();
}
void Submit () {
buffer.EndSample(SampleName);
ExecuteBuffer();
context.Submit();
}
我們可以通過檢查Profiler(通過Window / Analysis / Profiler打開)看到差異。 切換到“Hierarchy”模式,然後按“ GC Alloc”列進行排序。 您將看到一個兩次調用GC.Alloc的條目,總共分配了100個字節,這是由檢索攝像機名稱引起的。 在更下方,您將看到這些名稱顯示爲示例:“Main Camera”和“Secondary Camera”。
接下來,在啓用了Development Build和Autoconnect Profiler的情況下進行構建。 播放構建,並確保已連接探查器並進行記錄。 在這種情況下,我們沒有獲得100字節的內存分配,而是獲得了一個“Render Camera”樣本。
其他的48字節是什麼?
這是我們無法控制的相機陣列。 它的大小取決於要渲染的相機數量。
爲了清楚地知道,我們僅在編輯器中而不是在發佈的版本中分配內存,我們可以通過將獲取相機名字這一步包裝在名爲“Editor Only”的探查器樣本中。 在這種情況下,我們需要從UnityEngine.Profiling
命名空間調用Profiler.BeginSample
和Profiler.EndSample
。 只有BeginSample
需要傳遞名稱。
using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.Rendering;
partial class CameraRenderer {
…
#if UNITY_EDITOR
…
partial void PrepareBuffer () {
Profiler.BeginSample("Editor Only");
buffer.name = SampleName = camera.name;
Profiler.EndSample();
}
#else
string SampleName => bufferName;
#endif
}
本節做了:
- 創建一個屬性,在非編輯器模式下是bufferName,編輯器模式會是相機的名字
- 在
PrepareForSceneWindow()
之前插入一個僅編輯器模式的新方法PrepareBuffer
- 在方法中將
buffer.name
與SampleName
賦值爲camera.name
- 用
Profiler.BeginSample
與Profiler.EndSample
將這句話包起來,並將“Editor Only”作爲參數傳給Profiler.BeginSample
圖層
攝像機也可以配置爲僅在某些圖層上看到事物。 這可以通過調整其剔除蒙版來完成。 要查看該效果,讓我們將所有使用標準着色器的對象移動到“ Ignore Raycast ”層。
原圖
從主攝像機的剔除蒙版中排除該層。
原圖
使它成爲第二臺相機唯一能看到的圖層。
原圖
本節做了:
- 把所有用了標準材質物體的層級設爲 Ignore Raycast
- 從主相機的Culling Mask中排除IgnoreRaycast
- 輔助相機的Culling Mask只有IgnoreRaycast
清理標記(Clear Flags)
我們可以通過調整要渲染的第二個攝像機的清除標誌來合併兩個攝像機的結果。 它由CameraClearFlags枚舉定義,我們可以通過相機的clearFlags
屬性獲得它的清除標誌。 清除之前,請在Setup中執行此操作:
void Setup () {
context.SetupCameraProperties(camera);
CameraClearFlags flags = camera.clearFlags;
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(SampleName);
ExecuteBuffer();
}
CameraClearFlags枚舉定義四個值。 從1到4依次是Skybox, Color, Depth, Nothing。 這些標誌實際上不是獨立的,而是說,在前面的也要把後面的一併清除(比如標誌位是Color時也會清除深度緩衝(Depth),但不會清除天空盒(Skybox))。除了最後一種,所有情況下都必須清除深度緩衝區。
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth, true, Color.clear
);
當標誌設置爲Color時,我們只需要清除顏色緩衝區,因爲在Skybox的情況下,無論如何我們最終都會在context.DrawSkybox
中重新繪製所有內容。
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth,
flags == CameraClearFlags.Color,
Color.clear
);
如果要清除爲純色,則必須使用相機的背景色。 但是,由於我們要在線性顏色空間中進行渲染,因此必須將該顏色轉換爲線性空間,因此最終需要使用camera.backgroundColor.linear
。 在所有其他情況下,顏色都無關緊要,因此我們可以使用Color.clear就足夠了。
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth,
flags == CameraClearFlags.Color,
flags == CameraClearFlags.Color ?
camera.backgroundColor.linear : Color.clear
);
由於Main Camera是第一個渲染的攝像機,因此其“清除標誌”應設置爲“ Skybox”或“Color”。 啓用幀調試器後,我們總是從清除緩衝區開始,但這在一般情況下時不能保證的。
Secondary Camera的清除標誌確定如何組合兩個攝像機的渲染。 對於天空盒或彩色盒,以前的結果將完全替換。 如果僅清除深度,則輔助攝影機將正常渲染,但不會繪製天空盒,因此以前的結果將顯示爲背景。 當什麼都不清除時,深度緩衝區將保留,因此會產生遮擋關係,就像它們是由同一臺攝像機繪製的一樣。 但是,前一臺攝像機繪製的透明對象沒有深度信息,因此會像之前繪製天空盒時候所做的那樣被繪製。
原圖
原圖
原圖
通過調整攝像機的Viewport Rect,還可以將渲染區域縮小到整個渲染目標的一小部分。 其餘渲染目標不受影響。 在這種情況下,將使用“Hidden/InternalClear”着色器進行清除。 模板緩衝區用於將渲染限制在視口區域。
原圖
請注意,每幀渲染多個攝像機意味着必須同時進行多次剔除,設置,排序等操作。 每個唯一視角使用一臺攝像機通常是運行時效率最高的方法。
本節做了:
- 通過
camera.clearFlags
得到相機的清理標識 - 當
flags <= CameraClearFlags.Depth
時,清理深度緩存 - 當
flags == CameraClearFlags.Color
時,清理顏色緩存 - 若
flags == CameraClearFlags.Color
,則將清理顏色設爲camera.backgroundColor.linear
,其中.linear
代表將顏色轉換到線性顏色空間。否則設爲默認的Color.clear
下一篇教程是 Draw Calls