先說明一下,fanvas是筆者在企鵝公司開發的,即將開源的flash轉canvas工具。
臟區重繪(dirty rectangle)並不是一門新鮮的技術了,這在最早2D游戲誕生的時候就已經存在。
復雜的術語或概念就不多說,簡單說,臟區重繪就是每一幀繪制圖形界面的時候,只重新繪制有變化的區域,而不是全屏刷新。很明顯,這肯定能帶來性能的提升。
舉個例子,看下邊兩個圖:
假設這里是動畫的連續2幀,那么從第一幀到第二幀,其實變化的只有蝴蝶的區域。那么所謂的臟區就是兩個圖片的紅色框之和,要把上一幀的蝴蝶擦掉,然后把新區域的蝴蝶位置也擦掉,接着才能繪制新的背景和蝴蝶。這相比整屏重繪,重繪的面積小了幾十倍,由於canvas 2d使用的是CPU處理,那么相應地,CPU處理的像素個數就少了很多倍,順理成章,動畫的效率就會提高。
看起來非常簡單,大概來說,只需要2步:
1、找出這一幀變化的矩形區域;
2、利用canvas的api實現臟區重繪。
但是,問題來了,怎么計算變化區域呢?canvas又是否提供了現成的接口呢?我們拿上述蝴蝶的例子,逐步來看看。
首先,我們看關於臟區的計算。
如果動畫非常簡單,沒有使用“顯示列表”,所有圖案都是一層繪制的,那么“也許”繪制者,也就是開發者了,可能會知道蝴蝶的位置,然后手工指定重繪的區域。呃。。。等等,好像有點什么問題,不可能每次都手工指定重繪的區域!!!
再看看Fanvas里的情況,Fanvas采用了顯示列表,把圖案拆分為多個元件,元件和元件之間以“顯示列表”的形式組織起來,這參考了Flash的技術。這里,蝴蝶被封裝為一個Shape,蝴蝶在畫面飛舞,抽象為Shape在父元件中移動、旋轉。最初,在Shape中繪制蝴蝶的時候,可能占據的矩形區域是(x:0,y:0,width:100,height:50),這里參考的是Shape內部的坐標系(還沒放到舞台上)。然后,蝴蝶被添加到舞台上時,需要位移和旋轉,例如做了(x:400,y:100)的位移,和旋轉了60度。這時候如何計算新的矩形呢?
這個過程其實就是局部坐標系映射到全局坐標系的問題,涉及到矩陣計算,可以參考我之前寫的文章,這里就不多說了。http://km.oa.com/articles/show/238103。另外,提一下,這里其實還有一個難點,初始繪制時(x:0,y:0,width:100,height:50),這個矩形是如何計算得到的呢?如果繪制的是一個圖片,那當然好計算;如果是一系列的矢量線條,這個就略麻煩了,不過這個不在這里討論了,因為Fanvas是Flash導出Canvas動畫,在導出的時候Flash自帶了這個矩陣信息。
上述的計算都在一個前提情況下:我們已知蝴蝶是唯一一個變化的元件,但在實際動畫過程中,如何自動識別變化的內容呢?
要從動畫的原理說起,動畫過程無非分為4種操作:
1. 新建一個元件(例如蝴蝶),添加到舞台上;
2. 移動、旋轉、放縮原有的元件;
3. 刪除已有的元件;
4. 修改元件的遮罩關系,這點有點特殊,如果對flash動畫不熟悉的同學可能不大理解,不過不重要,我們知道有這回事就可以了,不影響文章的繼續閱讀。
那么,在Fanvas中,我們就需要對上述4種情況分別處理。
1. 新建:只有1個臟矩形,就是這個元件本身;
2. 移動/旋轉/放縮:元件上一幀的矩形區域是臟區,新一幀的矩形區域也是臟區;
3. 刪除:跟新建情況一樣;
4. 遮罩變化:跟2一樣。
理清楚這些細節之后,如何實現就比較好辦了,無非就是每一幀繪制前把臟區列表情況,然后計算出所有臟區矩形,再開始繪制。
接着,我們再來看第二步,canvas如何具體操作,是否有臟區重繪接口?
其實,canvas並沒有真正的臟區重繪接口,不過有一個clip,這個一般用於實現遮罩,不過也可以取巧的用來實現臟區重繪。經筆者測試,簡單使用clip雖然性能優化不是太明顯,但還是有20%的提升的。再復雜一些,當然大家可以自行根據臟區列表,重寫每個元件的繪制方法,自行實現臟區重繪,不過筆者估計啊,js寫這么多邏輯,最終還是吃力不討好。
我們來看看代碼:
for (var i = 0; i < dirtyRectList.length; i++) { var rect = dirtyRectList[i]; ctx.clearRect(rect.x, rect.y, rect.width, rect.height); } ctx.beginPath(); for (var i = 0; i < dirtyRectList.length; i++) { var rect = dirtyRectList[i]; ctx.rect(rect.x, rect.y, rect.width, rect.height); } ctx.clip();
相信變量名已經很明顯的暴露了自己的用途,大家應該明白,實現臟區重繪非常簡單,只需要在全部繪制前加那么一段clip,搞掂。
不過啊,這里可有一個很大的坑,估計有同學也知道。
正如上圖所示,會出現一些1px白線或者沒清干凈的bug,尤其是舞台本身有拉伸的情況下,這種bug更明顯。經過筆者多次摸索,大概搞清楚了,主要就是臟區要算仔細(如果舞台有拉伸,很容易算出來有1、2px差別),畫面要等比例拉伸,另外就是清除和重繪時,大方點,給1px的放寬。
最后變成:
for (var i = 0; i < dirtyRectList.length; i++) { var rect = dirtyRectList[i]; ctx.clearRect(rect.x-1, rect.y-1, rect.width+2, rect.height+2); } ctx.beginPath(); for (var i = 0; i < dirtyRectList.length; i++) { var rect = dirtyRectList[i]; ctx.rect(rect.x-1, rect.y-1, rect.width+2, rect.height+2); } ctx.clip();
至此,Fanvas臟區重繪的秘密就徹底曝光了。。。
最后來看看實際的效果(第一張是沒有使用臟區重繪,第二張使用臟區重繪):
當然,這並不是每個動畫都有效果,因為一些動畫本來就大范圍變化,所以Fanvas針對這些情況也做了兼容處理,如果發現臟區太多或者面積太大,就繼續使用原來的全屏重繪。