Unity CommandBuffer的一些學習整理
1.前言
近期在整理CommandBuffer這塊資料,之前的了解一直較為混亂。
算不上新東西了,但個人覺得有些時候要比加一個攝像機再轉RT廉價一些,至少省了深度排序這些操作。
本文使用兩個例子講解CommandBuffer如何使用,但在此之前稍稍總結一下官方CommandBuffer的案例。
2.官方案例
案例地址如下:
https://blogs.unity3d.com/cn/2015/02/06/extending-unity-5-rendering-pipeline-command-buffers/
文章尾部有Demo下載鏈接。
該demo包含3個例子。
第一個例子BlurryRefraction,和新建攝像機渲染RT類似,在渲染透明對象之前渲染屏幕,並做模糊處理。然後丟給shader轉換到對應的UV空間,轉換的代碼和Grab一樣不做贅述。
第二個例子DeferredCustomLights,這裏燈光的容器模型和第三個例子的貼花容器模型差不多,都是為了空間剔除而建立的模型,有點類似RayMarching,燈光部分直接拿到GBuffer的數據進行繪制。
第三個例子DeferredDecals,和第二個差不多,容器模型直接是方塊,而方塊的投影方式又有點像地形的三方向投影。
3.學習案例
3.1 - 在延遲渲染環境下創建一個standard小球
總的來說坑還是蠻多的,unity的pbr這塊本身和管線有所交互,所以commandBuffer要在光照和GBuffer兩個階段做插入。
但如果是普通的vf shader,放在屏幕特效前做插入即可。
這是完成效果。
那麽從頭開始,首先按照常規思路是在GBuffer之後繪制一個球。
初始代碼:
void OnEnable() { mCacheCommandBuffer = new CommandBuffer(); mCacheCommandBuffer.name = "TestCommandBuffer"; mCacheCommandBuffer.DrawRenderer(testRenderer, testMaterial, 0, -1); Camera.main.AddCommandBuffer(CameraEvent.AfterGBuffer, mCacheCommandBuffer); }
釋放:
void OnDisable() { Camera.main.RemoveCommandBuffer(CameraEvent.AfterGBuffer, mCacheCommandBuffer); mCacheCommandBuffer.Dispose(); }
釋放時Dispose要放在RemoveCommandBuffer之後調用。
DrawRenderer比起DrawMesh多了很多自由度,但缺點是遇到多維子材質會比較棘手。
這裏第四個參數是對應shader的pass,如果填寫-1則所有pass都繪制。
繪制效果如下
(直接畫肯定是有問題的)
打開FrameDebugger看問題,把standard裏所有的pass都繪制了出來這不是想要的。
這裏看了下standard的pass,第三個pass針對的是延遲光照,後面都用pass 3來繪制。
而且還有個問題RT3的自發光信息不正確。
unity的GBuffer中四個RT分別是RT0-漫反射,RT1-高光,RT2-屏幕法線,RT3-自發光。
翻閱了一下standard shader源碼,發現可能是缺失了間接光照信息,而間接光照信息可能沒有正確的初始化
不過發現了這麽一個東西
void fragDeferred( VertexOutputDeferred i, out half4 outGBuffer0 : SV_Target0, out half4 outGBuffer1 : SV_Target1, out half4 outGBuffer2 : SV_Target2, out half4 outEmission : SV_Target3 // RT3: emission (rgb), --unused-- (a) #if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4) , out half4 outShadowMask : SV_Target4 // RT4: shadowmask (rgba) #endif ) { //... }
也就是說可以自己定義輸出的GBuffer,而且也可以指定只輸出某一個GBuffer的RT
void frag( v2f i, out half4 outEmission : SV_Target3 ) { outEmission = 0; }
有時候這個還是蠻管用的,因為在CommandBuffer裏Blit很多通道拷貝不了(應該是我技術不行)。
繪制多加一次:
mCacheCommandBuffer.DrawRenderer(testRenderer, testMaterial, 0, 3); mCacheCommandBuffer.DrawRenderer(testRenderer, testMaterial_fix, 0, -1); Camera.main.AddCommandBuffer(CameraEvent.AfterGBuffer, mCacheCommandBuffer);
解決是解決了,但是和天空盒接觸的地方就會沒有光照。
查看了FrameDebugger,確認是光照部分出了問題,踩了一些坑之後發現在AfterLighting處再繪制一次即可。
void OnEnable() { mCacheCommandBuffer = new CommandBuffer(); mCacheCommandBuffer.name = "TestCommandBuffer"; mCacheCommandBuffer.DrawRenderer(testRenderer, testMaterial, 0, 3); Camera.main.AddCommandBuffer(CameraEvent.AfterGBuffer, mCacheCommandBuffer); mCacheCommandBuffer2 = new CommandBuffer(); mCacheCommandBuffer2.name = "TestCommandBuffer2"; mCacheCommandBuffer2.DrawRenderer(testRenderer, testMaterial, 0, 0); Camera.main.AddCommandBuffer(CameraEvent.AfterLighting, mCacheCommandBuffer2); }
3.2 - 使用CommandBuffer對挖洞模型進行模糊
之前別人做過,覺得這個案例有些意思,自己試了一下。
完成效果如下
這是完成效果,使用Stencil挖洞可以達到模擬半透明的效果,但如果漸變速度較慢時則會造成視覺上的不適。
而通過stencil來標記主角在屏幕中的位置,然後對主角在挖洞的基礎上再做一次高斯模糊可以緩解這種視覺上的不適。
直接在原始模型上做Stencil會導致在shadow階段Stencil數據被清除。
但是先不急著改變CommandBuffer的位置,先切換到正向渲染下看看Stencil不被清除的結果
可以看見即使Stencil生效,挖洞區域的Stencil也被挖掉了。所以必須想另外一個辦法覆蓋這個壞的Stencil。
我的思路是通過DrawRenderer在RenderSkybox之後繪制一個alpha為0的主角Renderer,並且帶有正常Stencil,來覆蓋掉舊的。
也就是需要兩個CommandBuffer
mStencilFixCommandBuffer = new CommandBuffer(); mStencilFixCommandBuffer.name = "StencilFix"; for (int i = 0; i < playerRenderers.Length; i++) { var item = playerRenderers[i]; mStencilFixCommandBuffer.DrawRenderer(item, playerReplaceMaterial, 0, -1); } Camera.main.AddCommandBuffer(CameraEvent.AfterSkybox, mStencilFixCommandBuffer);
Camera.main.AddCommandBuffer(CameraEvent.BeforeReflections, mBlurCommandBuffer);
可以看見這個正確的Stencil已經繪制上去了(當然也可以用這個方法修改深度,GBuffer)
然後就是模糊采樣的問題,在CommandBuffer中你不能插入Lambda的CPU代碼去執行異步內容
所以這裏用幾個RT來回切換做到重復采樣,這裏參考官方CommandBuffer裏的第一個例子,需要用兩個RT來回切換。
CommandBuffer裏新建RT建議這麽用
mBlurTempRT1 = Shader.PropertyToID("BlurTempRT1"); mBlurCommandBuffer.GetTemporaryRT(mBlurTempRT1, -1, -1, 0);
當然這麽用也取不出RT對象,只能通過索引進行操作。
第二和三的參數指定了分辨率,-1為默認值,-2為一半大小分辨率,-3為1/3以此類推。
這裏我遇到了第二個坑,Stencil信息不能通過通道單獨拷貝出來,只有在和CameraTarget進行Blit操作時,才能讀到Stencil信息。
mBlurCommandBuffer.Blit(BuiltinRenderTextureType.CameraTarget, mBlurTempRT1); for (int i = 0; i < sampleNum - 1; i++) { mBlurCommandBuffer.Blit(mBlurTempRT1, BuiltinRenderTextureType.CameraTarget, blurMaterial); mBlurCommandBuffer.Blit(BuiltinRenderTextureType.CameraTarget, mBlurTempRT1); } mBlurCommandBuffer.Blit(mBlurTempRT1, BuiltinRenderTextureType.CameraTarget, blurMaterial);
所以這裏的模糊叠代這麽做(這裏理解的不太清晰,代碼還可以優化)
最後效果也就達到了
就寫到這裏,本來想做一個UI面板的3D展示,後來發現unity pbr的繪制流程實在有點蛋疼。。
所以還是創建一個新相機吧。另外這兩個小測試不太方便附上源碼。
Unity CommandBuffer的一些學習整理