2015年1月30日 星期五

C# - PictureBox 繪圖處理閃爍的方法

動畫這一門技術,運用的是多數圖片不斷播放的原理,讓視覺產生一連串畫面不斷閃爍而造成會動的效果。程式,就是要去處理這一連串動作而寫出的方法。


相信有很多人有玩過遊戲,或者卡通電影動畫這類的
(沒有童年的話快去補完你的人生先~XD)

假設要在表單上進行圖片的繪製,但是在實際的測試中發現了問題,那就是重繪的時候會發生閃爍,或者在程式繪製動畫的高頻率刷新的時候,也會產生閃爍,這時要用到的解決辦法,是對動畫進行雙緩衝(Double Buffering)處理。



在要了解雙緩衝這個名詞之前,先來探討下為什麼重繪的時候會發生閃爍:
動畫的原理就是利用了人眼的視覺殘留(Visual staying)現象,當一副畫面進入眼睛產生影像後,並不會立刻消失,而是仍會保留一小段時間,於是當連續的圖像以很高的速度切換的時候,人眼會看到動態的影響,而不是處於切換中的單個圖像。
這個過程可以下圖1:












將這每一張的圖片串成一連串的播放動作,就會產生下面的結果:
(有某有感覺在跑的港覺-(拿刀狀ing),說~~~)























當這三幅圖片以一定頻率直接切換的時候,人們就會看到貌似是在跑步的動態。

想要更詳細的了解動畫可以參考--->>>維基百科



那麼為什麼依據這個原理來程式設計繪製動畫的時候會出現閃爍呢?

假設不加任何處理,就在畫布C上進行繪圖,那麼電腦的處理過程是這樣的:

Step 1: 將C以背景色填充(也就是清除C上現有的內容)
Step 2: 在C上按照要求繪製新的畫面

那麼這樣的過程會對動畫產生怎樣的影響呢?請看下圖2:










能看出和圖1的差別嗎?Step1相當於在原本連續的動畫中嵌入了空白的畫面,這個空白的畫面由於和人眼中原本殘留的圖像反差非常大,所以便會破壞視覺殘留產生的動畫,給人的感覺就是,這個動畫在不停的閃爍。
於是我們知道消除Step 1這個過程帶來的影響,就能夠避免在繪製的時候發生閃爍。如果直接把Step 1略過是個好的方式嗎?如果只是單純的略過Step 1,那麼動畫就會變成這樣:












在視覺上就成了一個拖著殘像尾巴的動畫,像下面的情況:

















(當然這也可以當做一種影像處理的效果,例如想做出像飛影的殘像拳這樣XDDDDD)


上面的圖就能了解雙緩衝是怎樣防止閃爍的。
假如我們希望在螢幕S上展示動畫,首先我們需要在記憶體中建立一個虛擬的畫布C,然後我們所有的繪圖操作都在C上進行,當繪製動畫的一幀完畢後,我們啪唧~把C直接往S上一拍,這樣就既不會出現拖尾巴,也不會出現Refresh時的短暫空白了,如下圖所示,下方的畫面就代表那塊虛擬的畫布:

















實際應用的時候還是會遇到一些問題,這些問題涉及到C#本身表單的繪製機制,當在pictureBox上繪圖的時候,特別是pictureBox還存在背景圖片的時候,就會遇到問題:

// 初始化圖片
Bitmap image = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);

// 初始化影像
Graphics g = Graphics.FromImage(image);

// 給圖區域碼 Begin
// 實作區
// 給圖區域碼 End
pictureBox1.CreateGraphics().DrawImage(image, 0, 0);

圖像g是透明的,所以在g上繪製後,貼在pictureBox上,背景圖片還是會展示出來,但是問題也就來了,由於pictureBox的繪製機制問題,如果我在pictureBox上貼一張透明的圖,將透明的圖片貼上去,那其實和直接略掉剛才所說的Step 1一樣!如果想要將之前的內容去除,就不得不再使用pictureBox1.Refresh()方法,而這樣的話,顯然會導致閃爍,那麼該怎麼辦呢?
一般寫程式時會用下面幾種方式,比如將表單的Double Buffered屬性設置為true,或者通過繼承或者反射機制,將pictureBox的Double Buffered屬性設置為true,但是經過實驗,這些方法都會有些問題。
其實原理就是,這個問題所遇到的障礙就是不能影響pictureBox的背景圖的展示,所以為何不把pictureBox的背景圖片也提取出來,作為底層圖像呢?
下面是原理範例:

// 初始化畫板
Bitmap image = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);

// 獲取背景層
Bitmap bg = (Bitmap)pictureBox1.BackgroundImage;

// 初始化圖像
Bitmap canvas = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);

// 初始化圖形面板
Graphics g = Graphics.FromImage(image);
Graphics gb = Graphics.FromImage(canvas);

// 繪圖部分 Begin
// ... ...
// 繪圖部分 End

gb.DrawImage(bg, 0, 0); // 先繪製背景層
gb.DrawImage(image, 0, 0); // 再繪製繪畫層

pictureBox1.BackgroundImage = canvas; // 設置為背景層
pictureBox1.Refresh();
pictureBox1.CreateGraphics().DrawImage(canvas, 0, 0);

注意標註的地方,就是添加的部分。
pictureBox的Refresh()方法不會影響其背景層,所以將最後合成的畫布直接貼在背景層上,這樣再Refresh()就不會產生閃爍了,同時,由於系統會自動重繪背景層,所以在視窗最小化或者被遮擋過後,繪製的圖像也不會消失,這樣一來,閃爍問題就被解決了!

在撰寫像是繪圖引擎這種機制,其實都會套用上面較基本的方法,例如Unity(各家繪圖演算法不盡相同,各有長短)。試想如果只是幾張圖,就會產生這麼大的閃爍狀況,那更多的影像特效,沒有處理好,其實非常耗電腦運算的效能,上面只是比較簡單的觀念,最重要的還是要勤練功,找到屬於自己的演算模式。

其實以我曾經開發過遊戲的經驗來說,閃爍其實也是一種寫程式的技巧,也可以運用在寫引擎這件事情上,但有些時候還是取決於電腦的效能,有時候考量的層面很多,俗話說的好,一個技術做到越後面,腦袋就會越來越爆炸,我想說什麼呢?? 就是扯遠了XDDDDD。

-註-
雙緩衝(double buffering):

雙重的圖形輸出記憶體,以動態的方式配合螢幕輸出的速率。當一組記憶體正在讀出的時候,另一組則在寫入,如此輪流讀寫的方式使得螢幕可以即時地更新畫面資料。



-雲遊山水為知已逍遙一生而忘齡- 電腦神手

沒有留言:

張貼留言