利用Stencil來優化局部後處理特效
我們先拿這個很常見的金棒棒作為例子吧。

只是例子就不要再和我說什麼十字片,廣告牌疊加了。這個效果的實現就是用純色在RT上重繪原模型給一次高斯模糊後疊回原圖。

加了Stencil後計算量下降了一半(為方便對比,我砍掉了後處理流程最後的原圖Blit流程和人物繪製部分,僅顯示了downsample和高斯模糊的量)
原理:
Stencil和ZTest就如同兩兄弟,非常相似,但前者卻常常被忽略。大家都知道,在擁有Early-Z(準確的說是Early-ZS,也就是Z+Stencil)機制的當代GPU里,我們可以通過由前向後繪製,通過ZTest讓被遮擋的物體免於繪製,這是一個最基本的fillrate優化手段。
Stencil本身是一個比較+改寫特定Buff的技術,和ZTest的比較深度+改寫深度其實是一樣的機制,只是擁有更多的變化。所以它也可以通過先改寫Buff,然後讓後面的物體在讀取這個Buff的時候檢測不通過,從而直接跳過像素計算階段。
局部物體使用後處理特效很不合算,就是因為即使只有局部需要計算,GPU也必須計算整個屏幕。如果先用Stencil在屏幕上繪製一個標記區域,就可以將後面的計算限定在小範圍內。

用的是一個簡單的法線延伸OutLine,擴大了棒子的體積。並在繪製過的地方標記Stencil為1
Pass{ CULL OFF ZTest OFF COLORMASK 0 Stencil { Ref 1 Comp NotEqual Pass Replace ReadMask 1 WriteMask 1 } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; }; float _Outline; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float3 norm = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); float2 offset = TransformViewToProjection(norm.xy); o.vertex.xy += normalize(offset) * o.vertex.z * _Outline; UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return 1; } ENDCG}
然後再在之後的後處理流程里,給需要的Pass加上
Stencil{ Ref 1 Comp Equal Pass Keep ReadMask 1 WriteMask 1}
即可。
不過……
1. Stencil只能通過繪製實體來寫入,不能在多張Buff間複製。所以一旦經過downsample就會丟失,而downsample是後處理中非常常見的。
2. 由於這個原因,RT繪製的順序也非常重要,只有作為繪製目標的RT才有獲得Stencil檢測的機會。
3.Stencil在其他RT中的利用,只能通過Graphics.SetRenderTarget(source.colorBuffer, stencil.depthBuffer)來完成,指向一張RT的顏色Buffer,卻同時指向另一張RT的depthBuff,且這兩張RT的解析度必須完全一樣。
具體代碼(普通的Blit是沒法用的,會強制切換RenderTarget):
private void DepthBlit(RenderTexture source, RenderTexture destination, Material mat, int pass, RenderTexture depth){ if (depth == null) { Graphics.Blit(source, destination, mat, pass); return; } Graphics.SetRenderTarget(destination.colorBuffer, depth.depthBuffer); GL.PushMatrix(); GL.LoadOrtho(); mat.mainTexture = source; mat.SetPass(pass); GL.Begin(GL.QUADS); GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(0.0f, 1.0f, 0.1f); GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.1f); GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f, 0.0f, 0.1f); GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(0.0f, 0.0f, 0.1f); GL.End(); GL.PopMatrix();}
紋理大小限制是最麻煩的,其它都還好。實際用法就是,保留保存著Stencil的那張RT,然後在需要的時候掛接在目標RT上。
繪製標記是有代價的,但也可以在繪製正常畫面時順帶繪製,這樣代價就小很多。
在可以用到的時候記得使用即可。
好吧,我知道你們可能會問Bloom本身具體是怎麼做的。Bloom本身Unity內置後處理有一個,但是是全屏的。想做到局部,確實只能單開一張RT來繪製要泛光的物體。
但這就涉及到保留深度緩衝的問題,否則那個物體就必須顯示在最前面而無法被障礙物遮擋。因為同時還要切換RT繪製,唯一的辦法就是剛才的Graphics.SetRenderTarget(source.colorBuffer, stencil.depthBuffer),繪製新RT,同時使用以前的深度緩衝區。
我為了圖簡單,是直接在後處理階段里切換RenderTarget,然後用Graphics.DrawMeshNow來繪製要泛光的物體的,這會導致Mesh無法Batch。想要Batch,必須讓攝像機來繪製這些物體,應該是必須用到CommandBuffer這些東西。
CommandBuffer我也沒怎麼用過,可以試試看。
文件:
http://pan.baidu.com/s/1skIXYzF
推薦閱讀:
※在中國科幻電影人才缺乏時,是否能通過設立電影物理學專業,實現彎道接近國外頂尖水準?
※遊戲後期特效第四發 -- 屏幕空間環境光遮蔽(SSAO)
※如何用Processing做Morphing動畫
