開發自定義ScriptableRenderPipeline,將DrawCall降低180倍
0x00 前言
大家都知道,Unity在2018版本中正式推出了Scriptable Render Pipeline。我們既可以通過Package Manager下載使用Unity預先建立好的LightWeight Render Pipeline和High Defination Render Pipeline,也可以自己動手建立自定義的Render Pipeline,實現一些符合自己心意的渲染策略。
下面我們先簡單介紹一下自定義SRP的使用方法,之後利用自定義的Render Pipeline來優化一個常見的情景,即渲染半透時由於渲染順序被打亂,從而導致的合批失敗。
0x01 一個簡單的SRP流水線實現
如何自定義一個Scriptable Render Pipeline,Unity有一篇部落格[1]已經做了簡單的介紹。
根據這篇部落格,我們知道,首先要定義一個繼承自UnityEngine.Experimental.Rendering.RenderPipeline
//定義渲染管線邏輯 using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Experimental.Rendering; 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(); } }
這個指令碼的邏輯十分簡單,即使用純色來清屏。ScriptableRenderContext 類的例項context即當前的渲染上下文,儲存了當前的渲染狀態。
有了渲染管線的邏輯,之後我們要做的就是呼叫AssetDatabase.CreateAsset將這個渲染管線儲存為一個Asset,儲存在硬碟上,並將這個Asset賦值給Graphics Setting以啟用該管線。
所以,我們接下來就需要一個能夠被Unity創建出Asset並被序列化儲存的類,在SRP中這個類叫做RenderPipelineAsset。
[ExecuteInEditMode] //定義渲染管線Asset public class BasicAssetPipe : RenderPipelineAsset { public Color clearColor = Color.blue; protected override IRenderPipeline InternalCreatePipeline() { return new BasicPipeInstance(clearColor); } }
這樣,我們就能很方便的創建出一個渲染管線的Asset,和傳統的Scriptable Object一樣,我們可以直接通過Asset來修改其欄位的內容,這裡我們只定義了一個名字是clearColor的欄位。
當然,我們可以建立完Asset之後,再手動給Graphics Setting賦值,也可以直接在指令碼中給Graphics Setting賦值,只需要訪問GraphicsSettings.renderPipelineAsset即可。
using UnityEngine;
using UnityEditor;
using UnityEngine.Rendering;
public class MySRPCreate
{
[MenuItem("Assets/Create/MySRP")]
public static void CreateSRP()
{
var instance = ScriptableObject.CreateInstance<BasicAssetPipe>();
AssetDatabase.CreateAsset(instance, "Assets/MyScriptableRenderPipeline.asset");
GraphicsSettings.renderPipelineAsset = instance;
}
}
ok,開啟相關的選單,點選按鈕,整個Unity的傳統渲染管線就被替換成了我們剛剛自定義的渲染管線——簡單的說,就是一個純色清屏。
0x02 自定義管線,讓DC從3700到20
OK,接下來我們來看一個有趣的場景。這個場景中,我們通過指令碼來生成2種角色,每一種角色的數量有1500名——需要渲染的當然還包括她們的影子。為了儘量減少DrawCall的數量,自然會想到開啟GPU Instance。
這個是Unity的預設渲染管線的渲染成果,可是開啟Frame Debugger我們可以發現渲染的成本高的嚇人,DrawCall數量達到了3700次左右——在打開了GPU Instance的情況下。
檢視一下某次DrawCall的GPU Instance失敗原因,是由於"Objects have different materials"。而檢視相關的DrawCall資料,可以發現2種角色和陰影出現了交替渲染的情況,這樣便導致了materials 不同造成的GPU Instance失敗。
所以接下來我們要做的事情,就是能否自己來對這個場景內的物件進行渲染排序,因為我們希望的是角色和陰影的渲染不要交替出現,所以理想狀態是先把所有的角色面片渲染出來,接下來再來渲染陰影。
在自定義渲染流水線中實際呼叫繪製指令時,我們還會遇到一些別的型別和方法。例如,我們需要先對場景進行裁剪,選出需要被渲染的物件。
在這裡我們會遇到CullResults結構體,以及ScriptableCullingParameters結構體。通過這兩個結構體以及它們所定義的方法,我們可以獲取經過裁剪之後需要被渲染的物件以及燈光資料——分別儲存在CullResults的visibleLights欄位以及visibleRenderers欄位中。
獲取了visibleLights也就是光照資訊之後,我們就可以為我們的管線設定光照資料了。
例如,我們把方向光的顏色傳入到shader的LightColor0變數中,把方向光的方向傳入到shader的WorldSpaceLightPos0變數中。
foreach( var visibleLight in visibleLights)
{
if (visibleLight.lightType == LightType.Directional)
{
Vector4 dir = -visibleLight.localToWorld.GetColumn(2) ;
Shader.SetGlobalVector(ShaderNameHash.LightColor0, visibleLight.finalColor);
Shader.SetGlobalVector(ShaderNameHash.WorldSpaceLightPos0, new Vector4(dir.x,dir.y,dir.z,0.0f) );
break;
}
}
而visibleRenderers中儲存的則是需要被渲染的物件。涉及到物件的渲染,我們顯然需要確定一些渲染設定,在自定義管線中儲存這些設定的是DrawRendererSettings結構體。
一些常見的渲染設定,例如最常見的便是設定所使用的shader——更具體的說是使用的pass,這裡Unity也對Shader的pass名字做了一個簡單封裝,即ShaderPassName結構體,它用來指定我們所使用的shader pass,正確設定後,Unity會在Renderer所使用的shader中尋找指定的pass。
除此之外,如果需要被渲染的物件不是一個,那麼顯然會涉及到一個排序的問題。同樣我們也可以設定DrawRendererSettings結構體的sorting.flags來確定排序規則。可以設定的排序規則,可以檢視這個文件:
https://docs.unity3d.com/ScriptReference/Experimental.Rendering.SortFlags.html
其中有一個叫做SortFlags.OptimizeStateChanges的規則,看上去這個很適合我們的需求,因為它的技能描述是:
Sort objects to reduce draw state changes.
此時visibleRenderers中包括的待渲染物件不僅有角色、還包括四周的牆體、以及角色腳下的陰影面片,所以為了達到先把所有的角色面片渲染出來,接下來再來渲染陰影的目的——也就是說為了規避所謂的穿插問題——我們接下來先把需要渲染的角色過濾出來。此時我們需要另一個結構體來實現過濾的需求——FilterRenderersSettings。FilterRenderersSettings可以按照待渲染物件所在的RenderQueue和layer來篩選真正需要被渲染的物件。
可以看到,角色的渲染佇列設定的3000,也就是transparent。所以我們可以用RenderQueue來進行一次篩選,再使用layer篩選出角色——角色所在的layer叫做Chara。
Ok,到這裡,我們就篩選出了需要被渲染的角色,並且設定好了角色的渲染狀態。最後,我們直接呼叫Draw指令,並把這些設定作為引數傳入Draw即可。
把以上的邏輯封裝為一個方法,在Render中呼叫該方法就可以渲染出所有的角色了。
private void DrawCharacter(ScriptableRenderContext context, Camera camera, ShaderPassName pass,SortFlags sortFlags)
{
var settings = new DrawRendererSettings(camera, pass);
settings.sorting.flags = sortFlags;
var filterSettings = new FilterRenderersSettings(true)
{
renderQueueRange = RenderQueueRange.transparent,
layerMask = 1 << LayerDefine.CHARA
};
context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);
}
這樣,我們就渲染出了3000多個角色——在只用了8個DrawCall的情況下。
第一個小目標達成。
背景牆體,和陰影其實也大同小異,因為我們已經對可能產生穿插渲染的物件做出了區分,先全部渲染角色,再渲染陰影。重點在於分組渲染。渲染牆體、陰影面片的程式碼要做的也便是將牆體、陰影物件過濾出來,進行單獨渲染。
private void DrawBg(ScriptableRenderContext context, Camera camera)
{
var settings = new DrawRendererSettings(camera, basicPass);
settings.sorting.flags = SortFlags.CommonOpaque;
var filterSettings = new FilterRenderersSettings(true)
{
renderQueueRange = RenderQueueRange.opaque,
layerMask = 1 << LayerDefine.BG
};
context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);
}
private void DrawShadow(ScriptableRenderContext context, Camera camera)
{
var settings = new DrawRendererSettings(camera, basicPass);
settings.sorting.flags = SortFlags.CommonTransparent;
var filterSettings = new FilterRenderersSettings(true)
{
renderQueueRange = RenderQueueRange.transparent,
layerMask = 1 << LayerDefine.SHADOW
};
context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);
}
之後,我們只需要再在Render方法中依次呼叫DrawBg和DrawShadow即可。
public override void Render(ScriptableRenderContext context, Camera[] cameras)
{
base.Render(context, cameras);
if (cmd == null)
{
cmd = new CommandBuffer();
}
foreach (var camera in cameras)
{
if (!CullResults.GetCullingParameters(camera, out cullingParams))
continue;
CullResults.Cull(ref cullingParams, context,ref cull);
context.SetupCameraProperties(camera);
cmd.Clear();
cmd.ClearRenderTarget(true, true, Color.black,1.0f);
context.ExecuteCommandBuffer(cmd);
SetUpDirectionalLightParam(cull.visibleLights);
//Draw
DrawCharacter(context, camera, zPrepass, SortFlags.OptimizeStateChanges);
DrawBg(context, camera);
DrawShadow(context, camera);
context.Submit();
}
}
渲染的結果便是:
角色、背景、陰影分別渲染,互不干擾,而DrawCall也從Unity預設的管線中的3700次降低到了使用我們自定義管線的20次。
0x03 小結
利用SRP,我們可以根據專案自身的特點來定製很多有趣的內容,從這個小的演示中我們應該可以體驗到這種靈活性所帶來的效能上的提升。
好了,如果有技術討論的需求,歡迎加群:
Unity官方中文社群群:470161914
Unity官方中文社群②群:629212643
Ref
相關推薦
開發自定義ScriptableRenderPipeline,將DrawCall降低180倍
0x00 前言 大家都知道,Unity在2018版本中正式推出了Scriptable Render Pipeline。我們既可以通過Package Manager下載使用Unity預先建立好的LightWeight Render Pipeline和High Defination Render Pipeline
如何開發自定義標簽
lin lns abcd tro lib case invoke java類 ext 一、簡介 原理:用戶自定義的 jsp 標記。當一個含有自定義標簽的 jsp 頁面被 jsp 引擎編譯成 servlet 時,tag 標簽被轉化成了對一個標簽處理器類的對象的操作。 標簽庫A
開發自定義Mysql連接池
連接池使用第三方包 https://pypi.python.org/pypi/DBUtils tar -zxvf *.tar.gz * python3 setup.py build && python3 setup.py installimport time import py
Vue 開發自定義插件學習記錄 -- 入門
dem isa 我們 isshowing 人的 暴露 doc directive 了解 首先,你需要了解插件實現的基本原理 插件基本原理: 我們都知道用Vue.use註冊插件,那你知道Vue.use(plugin) 幹了什麽? 以下是我對Vue官網的一些摘錄和個人的
利用 JSP 2 提供的 SimpleTagSupport 開發自定義標籤
自定義標籤庫並不是 JSP 2 才出現的,JSP 1.1 版中已經增加了自定義標籤庫規範,自定義標籤庫是一種非常優秀的表現層元件技術。通過使用自定義標籤庫,可以在簡單的標籤中封裝複雜的功能。 為什麼要使用自定義標籤呢?主要是為了取代醜陋的 JSP 指令碼。在 HTML 頁面中插入 JSP 指令碼有
移動端Tap實戰技巧總結以及Vue混合開發自定義Tap
最近在忙的專案是Vue的混合開發,因互動相對複雜,所以也踩了很多坑。在此做一下總結。 1.tap事件的實際應用 在使用tap事件時,老生常談的肯定是點透問題,大多情況下,在有滑屏互動的頁面時,我們會在根節點阻止預設行為以解決事件點透的bug。 阻止預設行為有優點,但也會相對帶來一些問題。 優點: (
Vue 開發自定義外掛學習記錄 -- 入門
首先,你需要了解外掛實現的基本原理 外掛基本原理: 我們都知道用Vue.use註冊外掛,那你知道Vue.use(plugin) 幹了什麼? 以下是我對Vue官網的一些摘錄和個人的理解 Vue.use( plu
android studio 開發自定義按鍵以及基礎動畫
效果圖: 這次主要記錄如何改變button的形狀。 首先在專案app>res>drawable資料夾右鍵new新建 drawable rescource file,然後為drawable檔案命名即可。 然後在新建的drawable xml檔
Allure報告開發自定義外掛
當報告無法滿足當前專案的需求,需要自定義內容來展示在報告中,即需要開發自己的自定義外掛 最終結果 圖 :demo的結果是新增了一個My Tab 目錄欄,(demo未做有意義資料和css樣式) 1.建立一個外掛專案 基本上外掛專案包含2部分 java
基於flume1.7開發自定義Sink元件-一鍵打包
概要 開始 pom檔案 ide使用idea神器,工程組織使用maven,下面是工程的pom檔案: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.a
如何基於Kubernetes開發自定義的Controller_Kubernetes中文社群
繼上次分享Kubernetes原始碼編譯除錯之後,一直想寫些對scheduler,controller-manager,kubelete等元件的深入介紹,今天先介紹下Controller部分,在kubernetes內部提供了大量的controller,比如node controller,pod
Android開發自定義控制元件實現一個圓形進度條【帶數值和動畫】
實現一個如下圖所示的自定義控制元件,可以直觀地展示某個球隊在某個賽季的積分數和勝場、負場、平局數 首先對畫布進行區域劃分,整個控制元件分上下兩部分 上邊是個大的圓環,圓環中間兩行文字,沒什麼難度,選好圓心座標和半徑後直接繪製即可,繪製文字也是如此。 下部分是三個小的圓弧進
安卓開發自定義View之跑馬燈:MarqueeView
*本篇文章已授權微信公眾號 guolin_blog(郭霖)獨家釋出 好久沒寫東西了,感覺有點虛度光陰了,也感覺有點生疏了,剛好最近專案裡面有個跑馬燈的需求,TextView一通設定之後還是出現各種衝突,尤其是當TextView與EditText共存的時
Android開發自定義控制元件實現一個折線圖
實現一個如下圖所示的折線圖 首先是控制元件繪圖區域的劃分,控制元件左邊取一小部分(控制元件總寬度的八分之一)繪製表頭,右邊剩餘的部分繪製表格 確定表格的行列數,首先繪製一個三行八列的網格,設定好行列的座標後開始繪製 /*繪製三條橫線*/ for(int i=0;i&l
SonarQube外掛開發自定義規則(5)新增可配置引數
1、程式碼 public class TXTooMuchIfCheck extends IssuableSubscriptionVisitor { private static fina
Android開發自定義Listview的Adapter基類以及通用ViewHolder的寫法
簡單的寫一個Adapter基類,不用每次寫adapter都呼叫一堆方法。 import android.widget.BaseAdapter; import java.util.ArrayList; import java.util.List; public abstra
如何基於kubernetes開發自定義的Controller
繼上次分享Kubernetes原始碼編譯除錯之後,一直想寫些對scheduler,controller-manager,kubelete等元件的深入介紹,今天先介紹下Controller部分,在kubernetes內部提供了大量的controller,比如node contr
通過Xcode7程式碼實時預覽功能快速開發自定義控制元件(一)
通過程式碼實時預覽功能開發自定義控制元件能夠使開發者能夠直觀的修改控制元件的屬性,達到快速自定義控制元件的效果,我的Xcode版本是7.2正式版,下面開始講述如何通過程式碼實時預覽功能開發自
Android開發自定義圓角帶點選效果的Button
public class AnimationButton extends Button { private int mBackGroundColor = Color.parseColor("#ffffff"); private int normalColo
SonarQube外掛開發自定義規則(4)常用api-其他
1、獲取成員變數型別 @Override public void visitNode(Tree tree) { if (tree instanceof VariableTree) {