XNA – RenderTarget & AlphaBlend (上)

完全出乎我意料的,AlphaBlend真是有夠麻煩的…
終於,找到原因(應該),也順利解決了問題(似乎)!
嘛~結果是我要的就好了咩XDDD

AlphaBlend會出問題的原因在於「算法」和「認知」的差異。
對,或許你腦中的混和方程式其實是錯的,書上寫的甚至也是錯誤的
(也許)
如果不想被我誤導,請先瀏覽一下參考文章:
Drawing problems with RenderTarget2D
SpriteBatch::DrawString() into RenderTarget2D odd effect
Premultiplied alpha
Alpha Blending (Part 1)
Alpha Blending, Part 2
Alpha Blending, Part 3




先來看看預設的計算公式:

FinalColor = (SourceColor * SourceBlend) + (DestinationColor * DestinationBlend);

其中:
FinalColor – 最終顏色。
SourceColor – 來源顏色。
DestinationColor – 目標顏色。
SourceBlend – 混合時,來源的混合方式。預設為「SourceAlpha」,也就是來源顏色的透明值。
DestinationBlend – 混合時,目標的混合方式。預設為「InverseSourceAlpha」,也就是來源顏色透明值的反差。

也就是…

FinalColor.rgb = (SourceColor.rgb* SourceColor.a) + (DestinationColor.rgb * (1 – SourceColor.a));

最終結果的顏色 = 來源提供的量(依照透明值) + 目標提供的量(依照透明值)。
看似合情合理,卻和我們直覺的混合有所不同,舉個簡單的例子。
假設我們將一顆純紅且半透明(1, 0, 0, 0.5)的顏色,畫到一個純黑且完全透明(0, 0, 0, 0)的顏色上。
那麼,直覺上來說,結果應該就是來源的顏色rgb(0.5, 0, 0)吧?
接著來看看實際的計算過程:

FinalColor = (SourceColor* SourceColor.a) + (DestinationColor * (1 – SourceColor.a));
FinalColor = ((1, 0, 0) * 0.5) + ((0, 0, 0) * (1 – 0.5));
FinalColor = ((1, 0, 0) * 0.5) + ((0, 0, 0) * (0.5));
FinalColor = (0.5, 0, 0) + (0, 0, 0);
FinalColor = (0.5, 0, 0);

到這裡,毫無破綻,確實符合裡想的結果。
所以,當你直接把圖片畫在Backbuffer上是完全看不出異樣的。



現在,我們把上面的顏色畫在某個畫布(RenderTarget)上,我們在將畫布畫到BackBuffer。
首先,先把上半部沒有明確列出來的Alpha計算補完。
在「SeparateAlphaBlendEnabled」開啟之前(預設為關閉),Alpha的混合是參照rgb的混合方式。
也就是依照
SourceAlpha和InverseSourceAlpha計算,先來看看預設的計算結果:

FinalColor.a = (SourceColor.a * SourceColor.a) + (DestinationColor.a* (1 – SourceColor.a));
FinalColor.a = (0.5 * 0.5) + (0 * (1 – 0.5));
FinalColor.a = (0.5 * 0.5) + (0 * 0.5);
FinalColor.a = (0.25) + (0);
FinalColor.a = 0.25;

很顯然的,這個透明值是錯誤的,也就是一切錯誤的根源…
後續畫到BackBuffer的步驟就省略了,其
結果應該不難想像。
既然如此,我想我們需要將SeparateAlphaBlendEnabled打開,這是為了讓透明值獨立計算。
分離後的透明值計算公式如下:

FinalColor.a = (SourceColor.a * AlphaSourceBlend) + (DestinationColor.a * AlphaDestinationBlend);

其中:
AlphaSourceBlend – 來源透明質的混合方式。預設為One。
AlphaDestinationBlend – 目標透明質的混合方式。預設為Zero。

OK,來走一次計算過程吧!

FinalColor.a = (SourceColor.a * AlphaSourceBlend) + (DestinationColor.a * AlphaDestinationBlend);
FinalColor.a = (SourceColor.a * One) + (DestinationColor.a * Zero);
FinalColor.a = (0.5 * 1) + (0 * 0);
FinalColor.a = (0.5) + (0);
FinalColor.a = 0.5;

Great!!正確的結果。
那麼,我們在經過一層呢?這次畫在純藍的些許透明(0, 0, 1, 0.8)的顏色上~

FinalColor.a = (0.5 * 1) + (0.8 * 0);

…?目標的透明值呢?它天生是個M,就該拋棄它嗎?
而且,如果我們選擇拋棄它,顯然是個極大的錯誤!
那麼,是要讓兩個透明值相加?還是相乘呢?
哪一種方式全看個人需求,但是請看看這篇的參考文章,就跟著用相乘吧XDDD
將AlphaSourceBlend設為One,
AlphaDestinationBlend設為InverseSourceAlpha。

再走一次計算流程吧!

FinalColor.a = (SourceColor.a * One) + (DestinationColor.a * InverseSourceAlpha);
FinalColor.a = (0.5 * 1) + (0.8 * (1 – 0.5));
FinalColor.a = (0.5 * 1) + (0.8 * 0.5);
FinalColor.a = (0.5) + (0.4);
FinalColor.a = 0.9;

嗯~看起來似乎正確了!要來幾層都沒問題啦!



本來想寫得簡單一些,沒想到還是打了這大串
就此暫時打住,剩下的留到下篇補完吧!
另外,如果有錯誤的地方麻煩告知一下,小弟手殘眼殘腦殘也不是五天六天的事情了

–Next

XNA – RenderTarget

Render Targets」,簡而言之就是畫布。
其中,一直都在使用的Backbuffer就是其中一個RenderTarget。
對於組合較於複雜的畫面,這是一個重要且必要的技術…
最簡單的範例就是「視窗」,每個視窗內都有或多或少的物件要繪製,對整個視窗做特效也就只需要動到該視窗專用的RenderTarget即可。
再者就是很常見的多Viewport也就一定要用啦~!
進階的就像是即時產生紋理圖之類的。

使用RenderTarget的基本流程:

 // 保留原本的Target
 RenderTarget2D oriTarget = GraphicsDevice.GetRenderTarget(0) as RenderTarget2D;
 GraphicsDevice.SetRenderTarget(0, MyTarget);
 GraphicsDevice.Clear(Color.White);
 // …繪製…
 // 設回原來的Target
 GraphicsDevice.SetRenderTarget(0, oriSpace);



關於使用
RenderTarget時,如果有牽涉到AlphaBlend,會產生莫名其妙的紫色的問題,目前還沒找到真正的解法。
目前可以應急的方式為:
「截取GraphicsDeviceManager的OnPreparingDeviceSettings事件,將RenderTargetUsage設為PreserveContents

{
 // …
 GraphicDeviceManager.PreparingDeviceSettings += OnPreDevSet;
 // …
}
private void OnPreDevSet(object sender, PreparingDeviceSettingsEventArgs e)
 {
  e.GraphicsDeviceInformation.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PreserveContents;
 }

關於RenderTargetUsage可參考「Rendertarget changes in XNA Game Studio 2.0」。
XNA中的RenderTarget」裡面提到,PreserveContents在PC上的效能會比DiscardContents來得好。
這樣的話,如果只需要在PC上執行,就直接使用
PreserveContents吧!
但如果在X360上呢?這真是個好問題,希望有解法出現。

目前推測應該就是Contents的問題,或許XNA是在繪製每個東西的時候都會比照辦理吧!(炸)

相關連結:
[2.0] SetRenderTarget causes a purple background.
RenderTargetUsage.PreserveContents on Xbox
Purple Sprite Halo
Screen Manager

–End