Scriptable Render Pipeline
SRP的核心是一堆API集合,使得整個渲染過程及相關配置暴露給用戶,使得用戶可以精確地控制項目的渲染流程。
SRP API為原有的Unity構件提供了一些新的接口(interface):
- Lights
- Materials
- Cameras
- Command Buffers
但是和Unity交互的方式變了。由於性能原因,當自定義SRP時,通常是在使用一組renderers(這里的render應該是指MeshRender之類的),而不是一個。
What is a Render Pipeline
什么是渲染管線?“Render Pipeline”,渲染管線,是許多技術的總稱,用於把物體(Objects,比如三維物體)顯示到屏幕上。概括性講,渲染管線包括:
- Culling
- Rendering Objects
- Post processing
除了上面這些高級概念,渲染管線的每一個環節或者子任務,還可以進一步分解,你可以選擇如何去完成它。例如:可以使用一下方式渲染物體:
- Multi-pass rendering - one pass per object per light 即一個物體被"渲染多次(多個pass)",每一個影響到該物體的light,都在一個單獨的pass中渲染
- Single-pass - one pass per object 即每個物體中一個pass中渲染,也即一個物體只“渲染”一次
- Deferred - Render surface properties to a g-buffer, perform screen space lighting 即先在g-buffer pass中把法線等幾何屬性“渲染”到g-buffer紋理中,然后在Lighting pass中進行屏幕空間的光照計算
當自定義SRP時,上面這些就是需要你做出決策的地方。每種技術都有許多權衡(trade offs)需要考慮,沒有一種技術是完美的。
What is a Scriptable Render Pipeline
如果Render Pipeline是為了完成渲染而需要執行的許多步驟,則Scriptable Render Pipeline是一個可以讓用戶使用Unity的C#腳本控制的pipeline,用於以用戶定義的方式完成渲染。
The Problem (Built-in pipelines存在的問題)
之前Unity已經提供了一些built-in pipelines,一些適合用於手游或者VR游戲(如 forward renderer),另一些用於高級終端游戲上,如主機游戲 (如 deferred renderer)。這些Unity內置的拿來即用的渲染方案具有通用性,“黑盒性”,但也有一些缺陷:
- 只能用於做它們設計好的任務
- 它們被設計的非常具有通用性,功能過於寬泛,意味着它們可以完成任何需求,但沒有任何完美精通或者適用的地方。(But they need to do everything, they are masters at nothing)
- 它們不是那可配置的。(black box with injection points)
- 擴展和修改容易產生錯誤(小的內部改變可能產生大的外部影響)
- 有許多bugs無法修復(因為擅自“修復”可能會破壞項目)。
The Solution (用SRP來解決)
SRP API的出現就是為了解決上面的問題。它把渲染部分從Unity內置的“黑盒”概念 轉變成了用戶可控的每個項目可腳本控制的概念。也就是說,使用SRP,每一個項目都可以有一個符合自己的特色的渲染管線。使用SRP api可以對how rendering進行精細粒度的自定義,從低層到高層(form the low level to the high level)。
SRP 概述
從高層次用戶角度來看,SRP可以分為兩部分:SRP asset和SRP instance。在制作custom rendering pipeline時,兩者都很重要。
SRP Asset
SRP Asset是project asset,表示渲染管線的一個具體配置,例如:
- 是否支持陰影?
- 使用什么級別的shader quality?
- shadow distance值是多少?
- 默認材質配置
用戶想要控制的東西可以保存為配置的一部分,基本上是任何需要序列化的東西。SRP Asset表示SRP的類型(type)和其中的設置(settings)。
SRP Instance
SRP Instance是實際完成渲染的類(class)。當Unity發現SRP被啟用時,Unity會查看當前選擇的SRP Asset並且要求它提供一個渲染實例("rendering instance"),即SRP asset要返回一個實例包含一個'Render'函數。通常這個實例會緩存許多對應SRP asset中的配置信息。
SRP instance表示一個已知的渲染管線配置(pipeline configuration)。從渲染器角度看,調用動作可能如下:
- Clearing the framebuffer
- Performing scene culling
- Rendering sets of objects
- Doing blits from one frame buffer to another
- Rendering shadows
- Applying post process effects
SRP instance表示一個實際將要被實施的渲染(actual rendering)。
SRP Asset
SRP Asset包含一個接口,用戶用它配置渲染管線。當Unity第一次開始渲染時,Unity會調用 SRP Asset上的InternalCreatePipeline
函數,然后,SRP Asset會返回一個可用的SRP instance。
SRP Asset是一個ScriptableObject,這意味着它可以保存為一個project asset(見下面的BasicAssetPipe)。
為了使項目啟用一個SRP asset,需要在Edit>Project Settings>Graphics的Scriptable Render Pipeline Settings欄中選擇你生成的SRP Asset。這樣設置好后,Unity就會使用SRP Asset中的配置進行rendering了,而不再使用standard Unity rendering。
An Simple Asset Example
SRP Asset的職責是包含配置信息和返回一個渲染實例。若SRP Asset上的某個設置發生了變化,則所有由這個Asset創建的實例會被銷毀,並且用新的設置創建新的實例,來進行下一幀的渲染。
下面的代碼是一個簡單的pipeline asset類。它包含一個color,由它返回的實例用來清除屏幕。CreateBasicAssetPipeline
是在Editor下使用的一個工具, 在菜單欄上點SRP-Demo,再點01 - Create Basic Asset Pipeline即可創建一個BasicAssetPipe對應的SRP Asset,它在項目中的路徑為Assets/BasicAssetPipe.asset
。
[ExecuteInEditMode]
public class BasicAssetPipe : RenderPipelineAsset
{
public Color clearColor = Color.green;
#if UNITY_EDITOR
// Call to create a simple pipeline
[UnityEditor.MenuItem("SRP-Demo/01 - Create Basic Asset Pipeline")]
static void CreateBasicAssetPipeline()
{
var instance = ScriptableObject.CreateInstance<BasicAssetPipe>();
UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/BasicAssetPipe.asset");
}
#endif
// Function to return an instance of this pipeline
protected override IRenderPipeline InternalCreatePipeline()
{
return new BasicPipeInstance(clearColor);
}
}
A complete asset example
除了返回實例和保存配置,SRP asset也用於做許多輔助功能,如:
- Default material to use when creating 3d objects
- Default material to use when creating 2d objects
- Default material to use when creating particle systems
- Default material to use when creating terrain systems
The Rendering entry point
SRP asset控制渲染配置,但是最終渲染是由SRP Render Pipeline instance完成的。SRP instance對應的類(class)是渲染邏輯(rendering logic)實際存在的地方。
SRP Instance的最簡單的形式僅僅包含一個Render
函數。Render
函數有兩個參數:
- 一個
ScriptableRenderContext
類型的參數,相當於一個隊列,其中的元素是command buffer,可以把將要完成的渲染操作入隊(enqueue)。 - 一個相機數組
Camera[]
,表示已經啟用的,用於渲染的相機列表。
A basic pipeline
下面這個BasicPipeInstance
類就是上面的BasicAssetPipe
的IRenderPipeline
函數返回的SRP Instance。
public class BasicPipeInstance : RenderPipeline
{
private Color m_ClearColor = Color.black;
public BasicPipeInstance(Color clearColor)
{
m_ClearColor = clearColor;
}
public override void Render(ScriptableRenderContext context, Camera[] cameras)
{
// does not so much yet :()
base.Render(context, cameras);
// clear buffers to the configured color
var cmd = new CommandBuffer();
cmd.ClearRenderTarget(true, true, m_ClearColor);
context.ExecuteCommandBuffer(cmd);
cmd.Release();
context.Submit();
}
}
上面代碼所表示的pipeline僅僅完成了簡單地用給定的顏色(由SRP Asset指定)清除屏幕的操作。注意:
- Unity現有的
CommandBuffers
被用來完成許多操作(此處用於完成ClearRenderTarget
)。 CommandBuffers
是根據於對應的context進行調試的(scheduled)。- “渲染”的最后一步是調用
Submit
,它將(引起)執行所有入隊的command。
RenderPipeline
的Render
函數就是你輸入渲染代碼,來自定義renderer的地方。Culling, Filtering, Changing render targets和Drawing操作都是在這里完成的。這就是你構造渲染器的地方!
SRP使用延遲執行的方式進行渲染。作為用戶,你的任務就是用ScriptableRenderContext
構建一個命令隊列,然后去執行它們。
Culling
Culling(剔出)是指明將要在屏幕上渲染什么的過程。
Unity的Culling過程包含兩類:
- Frustum culling: 視錐體剔出,計算在視錐體內的物體(這些物體會被渲染)。
- Occlusion culling: 遮擋剔出,計算被別的物體遮擋的物體,這些物體將不會被渲染。
渲染開始時,首先要計算需要渲染哪些物體。這涉及到獲取相機,然后完成剔出(cull)操作(從給定相機的角度)。Culling操作返回一個對於給定相機有效的物體(objects和lights)列表,這些物體用於該相機進行后面的渲染步驟。
Culling in SRP
在SRP,用戶通常站在相機的視角來渲染對象。這和Unity built-in rendering一樣。SRP提供了一些有用的Culling相關的API。通常流程如下:
// Create an structure to hold the culling paramaters
ScriptableCullingParameters cullingParams;
//Populate the culling paramaters from the camera
if (!CullResults.GetCullingParameters(camera, stereoEnabled, out cullingParams))
continue;
// if you like you can modify the culling paramaters here
cullingParams.isOrthographic = true;
// Create a structure to hold the cull results
CullResults cullResults = new CullResults();
// Perform the culling operation
CullResults.Cull(ref cullingParams, context, ref cullResults);
cullResults
被用來完成接下來的rendering。
SRP Drawing
經過上面的步驟,該剔出的物體已經被剔出了,現在可把cull results渲染到屏幕上了。
需要提前做一些決策(考慮到以下因素):
- 完成渲染的硬件
- 想要實現的具體樣子(specific look)和風格/感覺(feel),即要實現的效果
- 項目的類型
如,一個2D手游和一個PC上的第一人稱游戲所使用的渲染管線肯定會差異特別大。可能要做下面這些抉擇:
- HDR vs LDR
- Linear vs Gamma
- MSAA vs Post Process AA
- PBR Materials vs Simple Materials
- Lighting vs No Lighting
- Lighting Technique
- Shadowing Technique
目前,我們將要展示一個簡單的沒有光照、可以渲染一些不透明物體的渲染器。
Filtering: Render Buckets and Layers
通常,渲染對象(rendering object)有一個具體的分類:透明的、不透明的、sub surface,或者別的類型。Unity使用隊列表示什么時候一個對象將會被渲染,即相同分類的對象被放在同一個隊列中,這些隊列也被稱為“桶”(bucket)。在SRP中,用戶指定使用哪些桶進行渲染。
除了“桶”的概念,標准的Unity layers也可以被使用。
這提供了額外的過濾能力。
// Get the opaque rendering filter settings
var opaqueRange = new FilterRenderersSettings();
//Set the range to be the opaque queues
opaqueRange.renderQueueRange = new RenderQueueRange()
{
min = 0,
max = (int)UnityEngine.Rendering.RenderQueue.GeometryLast,
};
//Include all layers
opaqueRange.layerMask = ~0;
Draw Settings: How things should be drawn
上面的filtering和culling決定了將要渲染哪些對象。接下來,我們需要決定怎樣渲染它們。SRP提供了許多可配置選項。用於配置的數據結構是DrawRenderSettings
。它允許配置以下選項:
- Sorting - 物體渲染的順序,如:從前到后(front to back)或者從后到前(back to front)。
- Per Renderer flags - Unity應該向shader傳入什么'built in'設置,包括:per object light probes, per object light maps等。
- Rendering flags - 使用哪種batching算法,instancing vs non-instancing。
- Shader Pass - 當前draw call應該使用哪個shader pass
// Create the draw render settings
// note that it takes a shader pass name
var drs = new DrawRendererSettings(myCamera, new ShaderPassName("Opaque"));
// enable instancing for the draw call
drs.flags = DrawRendererFlags.EnableInstancing;
// pass light probe and lightmap data to each renderer
drs.rendererConfiguration = RendererConfiguration.PerObjectLightProbe | RendererConfiguration.PerObjectLightmaps;
// sort the objects like normal opaque objects
drs.sorting.flags = SortFlags.CommonOpaque;
Drawing
現在我們已經有了發起一次draw call所需要的三樣東西:
- Culling results (剔出結果)
- Filtering rules (過濾規則)
- Drawing rules (繪制規則)
下面的代碼發起了一次draw call。在SRP中,你一般不渲染單獨的一個或幾個網格(individual meshes),而是發起一次draw call,一次渲染一大批網格。這能減少執行開銷。
發起一次draw call,需要結合上面我們已經構建的參數。
// draw all of the renderers
context.DrawRenderers(cullResults.visibleRenderers, ref drs, opaqueRange);
// submit the context, this will execute all of the queued up commands.
context.Submit();
這段代碼會把對象渲染到當前綁定的渲染目標上(render target)。也可以通過command buffer來切換不同的渲染目標。
參考:
第一次發表於我的知乎專欄:https://zhuanlan.zhihu.com/p/69046003