淺談後處理技術中的各種坑

昨天特效提了一個碎屏的屏幕特效需求,於是在網上找了一下效果(無腦百度),直接複製粘貼就是干,效果是OK沒問題了。

但是發現開了anti-aliasing(抗鋸齒效果)後,圖像上下顛倒了,於是就馬上想到UNITY_UV_STATRS_AT_TOP這個宏,之前大約知道這個是因為3D API差異性引起的問題,也沒太過留意。

查了下相關文檔,發現可以牽扯出不少東西,於是想利用這個東西,來好好探討一下後處理技術中相關的一些知識點。


在問題開始之前,我們先來了解一下,後處理技術實現的過程。個人通俗地說:在所有場景物體渲染完成後,我們通過繪製一個鋪屏的四方形(quad),並將backBuffer里顏色數據拷貝作為_MainTex傳入,調用shader計算,將計算結果寫入bakcBuffer中,最終顯示在屏幕上。所以說,backBuffer是後處理的最終目的地。

在實際的開發中,我們要傳入的紋理不一定是backBuffer裡面的內容,要寫入的目的地也不一定是backBuffer,這中間可能會需要先寫入到指定的緩存中,以便後面更複雜的計算。

在Unity中,Graphic.Blit(source, dest)方法為我們自動實現了這一過程。

這種技術關鍵的兩個步驟是:

1.繪製一個鋪屏的quad。

2.將計算結果寫入到一個buffer中。這就是rendertTarget技術。

關於RenderTarget技術和Rendertexture的概念,這裡有高質量的解析:

Unity3d中渲染到RenderTexture的原理,幾種方式以及一些問題 - leon - CSDN博客

與後處理相關的還有一個重要的方法OnRenderImage(source,dest),包含該方法的腳本掛載在Camera上才會被執行,並且在每個Camera渲染場景完成後都會各自執行一次。不過一般場景不多出現多個Camera的情況,但是對於其機制也要清楚。下圖是4個Camera的渲染順序,根據depth設置排序,黃色為UICamera,並沒有掛載OnRenderImage方法。


問題一:圖像上下顛倒問題

好了,回歸問題本身。

Platform-specific rendering differences

文檔中提及Unity為了兼容D3D,OpenGLES,Metal等不同3D API的特性,內部做了適配,讓開發者不同去考慮這些差異。Unity默認是以OpenGL的標準體系進行描述的:左手坐標系、屏幕坐標系左下角為(0,0)等。為了確保統一性,所有 non-OpenGL的平台的特性,Unity會做出轉換,使得該特性能夠以OpenGL的標準來描述。(現在有點明白Unity是怎麼做到跨平台開發的了)

首先引發問題的根本原因在於RenderTexture坐標系的差異:

OpenGL是從左下角為(0,0)開始,往上遞增。

D3D從左上角為(0,0)開始,往下遞增。

我們知道,Unity是用OpenGL的標準進行描述的,右邊的圖像用左邊的OpenGL坐標系來描述的話,得到的將會是下面這樣一幅顛倒的圖像。

有人可能不明白為什麼換了坐標系,圖像會顛倒。首先我們得理解,紋理本身就是以二維的數組的形式儲存的。從上面的參考圖的網格可以理解,每個像素都有一個明確的數組下標(x,y),數組下標是不變的,但是坐標系會變。比如:在D3D中,像素點(512,0)是在右上角的,但是在OpenGL的坐標系中,就變成右下角了。這就是坐標系變換造成圖像顛倒的原因。

了避免這種顛倒,Unity的做法是:直接將圖像上下翻轉一遍。(具體做法不知道)。但是有些東西會阻礙了這種轉換,MSAA便是其中之一。

經過測試,只有在Rendering Path設置為Forward的情況下,開啟抗鋸齒選項,在OnRenderImage(source,dest)中返回的RenderTexture對象「source」是上下顛倒的。

因為在forward渲染路徑下,採用的抗鋸齒技術為MSAA。查了一下文檔,沒給出MSAA為什麼會引發這個問題的原因,我猜測,大概是因為MSAA是在硬體層面的處理方式,而Unity對RenderTexture的Filp翻轉操作會影響這個過程。這裡如果大大知道為什麼,一定要告訴我(^_^)

請問FXAA、FSAA與MSAA有什麼區別?效果和性能上哪個好? 對多重採樣(MSAA)原理的一些疑問?

所以在non-OpenGL平台,MSAA開啟的情況下,Unity不會對圖像進行Filp翻轉操作。但是由於Unity還是以OpenGL的RenderTexture的坐標系去描述這個_MainTex,所以_MainTex_TexelSize.y為負數。UNITY_UV_STARTS_AT_TOP這個宏其實就判斷圖形api平台是否為non-OpenGL。

// Flip sampling of the Texture: // The main Texture// texel size will have negative Y).#if UNITY_UV_STARTS_AT_TOPif (_MainTex_TexelSize.y < 0) //其xyzw分別對應(1/width,1/height,width,height) uv.y = 1-uv.y;#endif

但是在實際操作中,添加上面這幾句代碼,得到的結果卻是顛倒的,不加反而結果是正確的。那Unity為什麼要我們這樣做呢?這是在耍我們嗎?於是仔細查閱API,發現一句很微妙的話。

If your Image Effect is a simple one (processing one texture at a time) then this does not really matter, because Graphics.Blit takes care of that.

Damn it !!原來是Graphics.Blit偷偷幫我們處理了!

說到Graphic.Blit(),回憶一下它的做法就是先繪製一個鋪滿屏的Quad,然後調用Shader,最後Render到指定的Target中。

經過實踐測試可知,當_MainTex未被正確flip(翻轉)的時候,Graphic.Blit()會幫你自動處理這個過程,方法就是顛倒Quad的UV坐標。繪製的Quad的UV方向會從頂部開始,所以在vertex program中,直接用texcoord去採樣_MainTex得到的結果是正確的。這也是上面那句「processing one texture at a time」的時候,this does not really matter的原因。但是,如果用這個texcoord去採樣_MainTex之外的Texture,就必須要對texcoord.y進行翻轉。

所以可以存兩份uv,一個用來sample因MSAA未被翻轉的_MainTex,另一個用來sample其他texture。

float4 uv = i.texcoord.xyxy;#if UNITY_UV_STARTS_AT_TOPif (_MainTex_TexelSize.y < 0) uv.w = 1-uv.w; //uv.xy:_MainTex; uv.zw:_OtherTex#endif

還有一種類似的情況就是GrabPass。

A similar situation occurs with GrabPass. The resulting render Texture might not actually be turned upside down on Direct3D-like (non-OpenGL-like) platforms. If your Shader code samples GrabPass Textures, use the ComputeGrabScreenPos function from the UnityCG include file.


問題二:後處理技術的效率問題

對於移動平台來說,post effect是一項非常消耗性能的效果。原本我以為是post effect複雜的逐像素運算使得像素填充率成為渲染瓶頸。但是在低端機器的真機測試中發現,在代碼中添加了OnRenderImage(source,dest)方法,但並沒有在裡面做任何處理,機器的幀率直接掉到了一半。

這個真的讓我百思不得琪姐,google也一下也沒得到明確的答案。這裡說一下我的猜測:OnRenderImage(source,dest)中的sourceTexture是從backBuffer中拷貝出來的,而這個拷貝操作是非常耗時的。

既然如此,那麼在低端機器上的優化方式只能是把post effect去掉了,同時可以考慮用Screen Overlap的方式代替一些簡單的屏幕效果。

如果已經確定屏幕特效是渲染瓶頸的時候,我們可以在Profile中分析相關數據。

  1. 減少RenderTexture的數量和大小。具體我們還是要看RenderTexture設置的尺寸和格式,RenderTexture如果作為中途緩存的對象,可以考慮適當縮小其解析度,使用RenderTetxure.GetTemporary和RenderTexture.ReleaseTemporary等等。
  2. 減少Graphics.Blit()的次數。盡量減少Graphic.Blit的不必要的調用,通過material.SetTexture()方法將可以合併計算的效果放在同一個Pass中處理。RendertTexture Swicths這個參數也google了很久,沒找到什麼有用的線索。後面有詳細的研究再補充吧。經過多次試驗觀察:可以了解到,Rendertexture Switchs的數量與Blit的調用次數是保持一致的,destTexture是backBuffer除外。
  3. 從演算法本身入手,根據不同平台做最大限度的計算優化。

但是,在著手優化之前,我們最先應該做的,就是構建一個統一的post effect處理框架


推薦閱讀:

Ray Marching Ocean
計算機圖形學常用術語整理
Unity的立體幾何問題
Unity3D如何將圖片以正確的像素顯示在屏幕上
讓角色半透明:後期模糊(二)

TAG:計算機圖形學 |