簡介
通常情況下,在玩 2D 遊戲或渲染 HTML5 畫布時,需要執行最佳化,以便使用多個層來構建一個合成的場景。在 OpenGL 或 WebGL 等低階別渲染中,透過逐幀地清理和繪製場景來執行渲染。實現渲染之後,需要最佳化遊戲,以減少渲染的量,所需成本因情況而異。因為畫布是一個 DOM 元素,它使您能夠對多個畫布進行分層,以此作為一種最佳化方法。
常用的縮寫
- CSS: Cascading Style Sheets(級聯樣式表)
- DOM: Document Object Model(檔案物件模型)
- HTML: HyperText Markup Language(超文字標記語言)
本文將探討對畫布進行分層的合理性。瞭解 DOM 設定,從而實現分層的畫布。使用分層進行最佳化需要各種實踐。本文還將探討一些最佳化策略的概念和技術,它們擴充套件了分層方法。
選擇最佳化策略
選擇最佳最佳化策略可能很難。在選擇分層的場景時,需要考慮場景是如何組成的。大螢幕上固定物的渲染經常需要重用若干個元件,它們是進行研究的極佳候選人。視差或動畫物體等效果往往需要大量的變化的螢幕空間。在探索您的最佳最佳化策略時,最好註意這些情況。雖然畫布的分層最佳化需要採用幾種不同的技術,但在正確應用這些技術後,往往會大幅提升效能。
設定層
在使用分層的方法時,第一步是在 DOM 上設定畫布。通常情況下,這很簡單,只需定義畫布元素,將其放入 DOM 中即可,但畫布層可能需要一些額外的樣式。在使用 CSS 時,成功地實現畫布分層有兩個要求:
- 各畫布元素必須共存於視區 (viewport) 的同一位置上。
- 每個畫布在另一個畫佈下面必須是可見的。
圖 1顯示了層設定背後的通用重疊概念。
圖 1. 層示例
設定層的步驟如下:
- 將畫布元素新增到 DOM。
- 新增畫布元素定位樣式,以便支援分層。
- 樣式化畫布元素,以便生成一個透明的背景。
設定畫布重疊堆疊
在 CSS 中建立一個重疊堆疊 (overlay stack) 可能需要少量的樣式。使用 HTML 和 CSS 有許多方法進行重疊。本文中的示例使用一個
清單 1. 畫布定位樣式
#viewport {
/**
* Position relative so that canvas elements
* inside of it will be relative to the parent
*/
position: relative;
}
#viewport canvas {
/**
* Position absolute provides canvases to be able
* to be layered on top of each other
* Be sure to remember a z-index!
*/
position: absolute;
}
容器
這些 HTML5 畫布元素的順序也很重要。可以按元素出現在 DOM 上的順序進行順序管理,也可以按照畫布應該顯示的順序來樣式化 z-index 樣式,從而管理順序。雖然並非總是如此,但其他樣式可能也會影響渲染;在引入額外的樣式(比如任何一種 CSS 轉換)時要小心。
透明的背景
透過使用重疊可見性來實現層技術的第二個樣式要求。該示例使用這個選項來設定 DOM 元素背景顏色,如清單 2所示。
清單 2. 設定透明背景的樣式表規則
canvas {
/**
* Set transparent to let any other canvases render through
*/
background-color: transparent;
}
將畫布樣式化為擁有一個透明背景,這可以實現第二個要求,即擁有可見的重疊畫布。現在,您已經構造了標記和樣式來滿足分層的需要,所以您可以設定一個分層的場景。
分層方面的考慮因素
在選擇最佳化策略時,應該註意使用該策略時的所有權衡。對 HTML5 畫布場景進行分層是一個側重於執行時記憶體的策略,用於獲得執行時速度方面的優勢。您可以在頁面的瀏覽器中增加更多的權重,以獲得更快的幀速率。一般來說,畫布被視為是瀏覽器上的一個圖形平面,其中包括一個圖形 API。
透過在 Google Chrome 19 進行測試,並記錄瀏覽器的選項卡記憶體使用情況,您可以看到記憶體使用的明顯趨勢。該測試使用了已經樣式化的
在 Google Chrome 的 Task Manager 中,您可以看到某個頁面所使用的記憶體量(也稱為 RAM)。Chrome 也提供 GPU 記憶體,或者是 GPU 正在使用的記憶體。這是常見資訊,如幾何形狀、紋理或計算機將您的畫布資料推送到螢幕可能需要的任何形式的快取資料。記憶體越低,放在計算機上的權重就會越少。雖然目前還沒有任何確切的數字作為依據,但應始終對此進行測試,確保您的程式不會超出極限,並使用了過多的記憶體。如果使用了過多的記憶體,瀏覽器或頁面就會因為缺乏記憶體資源而崩潰。GPU 處理是一個遠大的程式設計追求,已超出本文的討論範圍。您可以從學習 OpenGL 或查閱 Chrome 的檔案(請參閱參考資料)開始。
表 1. 畫布層的記憶體開銷
層數 記憶體 GPU 記憶體
0 30.0 11.9
1 37.6 28.9
1 37.6 28.9
2 49.0 46.6
3 52.2 59.6
8 58.4 98.0
16 65.0 130
32 107 187
在表 1中,隨著在頁面上引入和使用了更多的 HTML5 畫布元素,使用的記憶體也越多。一般的記憶體也存線上性相關,但每增加一層,記憶體的增長就會明顯減少。雖然這個測試並沒有詳細說明這些層對效能帶來的影響,但它確實表明,畫布會嚴重影響 GPU 記憶體。一定要記得在您的標的平臺上執行壓力測試,以確保平臺的限制不會導致您的應用程式無法執行。
當選擇更改某個分層解決方案的單一畫布渲染週期時,需考慮有關記憶體開銷的效能增益。儘管存在記憶體成本,但這項技術可以透過減小每一幀上修改的畫素數量來完成其工作。
下一節將說明如何使用分層來組織一個場景。
對場景進行分層:遊戲
在本節中,我們將透過重構一個滾動平臺跑步風格的遊戲上的視差效果的單畫布實現,瞭解一個多層解決方案。圖 2顯示了遊戲檢視的組成,其中包括雲、小山、地面、背景和一些互動物體。
圖 2. 合成遊戲檢視
在遊戲中,雲、小山、地面和背景都以不同的速度移動。本質上,背景中較遠的元素移動得比在前面的元素慢,因此形成了視差效果。為了讓情況變得更為複雜,背景的移動速度會足夠慢,它每半秒鐘才重新渲染一次。
通常情況下,好的解決方案會將所有幀都清除並重新渲染螢幕,因為背景是一個影象並且在不斷變化。在本例中,由於背景每秒只需變化兩次,所以您不需要重新渲染每一幀。
目前,您已經定義了工作區,所以可以決定場景的哪些部分應該在同一個層上。組織好各個層之後,我們將探討用於分層的各種渲染策略。首先,需要考慮如何使用單個畫布來實現該解決方案,如清單 3所示。
清單 3. 單畫布渲染迴圈的偽程式碼
/**
* Render call
*
* @param {CanvasRenderingContext2D} context Canvas context
*/
function renderLoop(context)
{
context.clearRect(0, 0, width, height);
background.render(context);
ground.render(context);
hills.render(context);
cloud.render(context);
player.render(context);
}
像清單 3中的程式碼一樣,該解決方案會有一個render函式,每個遊戲迴圈呼叫或每個更新間隔都會呼叫它。在本例中,渲染是從主迴圈呼叫和更新每個元素的位置的更新呼叫中抽象出來。
遵循 “清除到渲染” 解決方案,render會呼叫清除背景關係,並透過呼叫螢幕上的物體各自的render函式來跟蹤它。清單 3遵循一個程式化的路徑,將元素放置到畫布上。雖然該解決方案對於渲染螢幕上的物體是有效的,但它既沒有描述所使用的所有渲染方法,也不支援任何形式的渲染最佳化。
為了更好地詳細說明物體的渲染方法,需要使用兩種型別的物體物件。清單 4顯示了您將使用和細化的兩個物體。
清單 4. 可渲染的Entity偽程式碼
var Entity = function() {
/**
Initialization and other methods
**/
/**
* Render call to draw the entity
*
* @param {CanvasRenderingContext2D} context
*/
this.render = function(context) {
context.drawImage(this.image, this.x, this.y);
}
};
var PanningEntity = function() {
/**
Initialization and other methods
**/
/**
* Render call to draw the panned entity
*
* @param {CanvasRenderingContext2D} context
*/
this.render = function(context) {
context.drawImage(
this.image,
this.x – this.width,
this.y – this.height);
context.drawImage(
this.image,
this.x,
this.y);
context.drawImage(
this.image,
this.x + this.width,
this.y + this.height);
}
};
清單 4中的物件儲存物體的影象、x、y、寬度和高度的實體變數。這些物件遵循 JavaScript 語法,但為了簡潔起見,僅提供了標的物件的不完整的偽程式碼。目前,渲染演演算法非常貪婪地在畫布上渲染出它們的影象,完全不考慮遊戲迴圈的其他任何要求。
為了提高效能,需要重點註意的是,panning渲染呼叫輸出了一個比所需影象更大的影象。本文忽略這個特定的最佳化,但是,如果使用的空間比您的影象提供的空間小,那麼請確保只渲染必要的補丁。
確定分層
現在您知道如何使用單一畫布實現該示例,讓我們看看有什麼辦法可以完善這種型別的場景,並加快渲染迴圈。要使用分層技術,則必須透過找出物體的渲染重疊,識別分層所需的 HTML5 畫布元素。
重繪區域
為了確定是否存在重疊,要考慮一些被稱為重繪區域的不可見區域。重繪區域是在繪製物體的影象時需要畫布清除的區域。重繪區域對於渲染分析很重要,因為它們使您能夠找到完善渲染場景的最佳化技術,如圖 3所示。
圖 3. 合成遊戲檢視與重繪區域
為了視覺化圖 3中的效果,在場景中的每個物體都有一個表示重繪區域的重疊,它跨越了視區寬度和物體的影象高度。場景可分為三組:背景、前景和互動。場景中的重繪區域有一個彩色的重疊,以區分不同的區域:
- 背景 – 黑色
- 雲 – 紅色
- 小山 – 綠色
- 地面 – 藍色
- 紅球 – 藍色
- 黃色障礙物 – 藍色
對於除了球和障礙物以外的所有重疊,重繪區域都會橫跨視區寬度。這些物體的影象幾乎填滿整個螢幕。由於它們的平移要求,它們將渲染整個視區寬度,如圖 4所示。預計球和障礙物會穿過該視區,並且可能擁有透過物體位置定義的各自的區域。如果您刪除渲染到場景的影象,只留下重繪區域,就可以很容易地看到單獨的圖層。
圖 4. 重繪區域
初始層是顯而易見的,因為您可以註意到互相重疊的各個區域。由於球和障礙物區域改寫了小山和地面,所以可將這些物體分組為一層,該層被稱為互動層。根據遊戲物體的渲染順序,互動層是頂層。
找到附加層的另一種方法是收集沒有重疊的所有區域。佔據視區的紅色、綠色和藍色區域並沒有重疊,並且它們組成了第二層——前景。雲和互動物體的區域沒有重疊,但因為球有可能跳躍到紅色區域,所以您應該考慮將該物體作為一個單獨的層。
對於黑色區域,可以很容易地推斷出,背景物體將會組成最後一層。填充整個視區的任何區域(如背景物體)都應視為填充整個層中的該區域,雖然這對本場景並不適用。在定義了我們的三個層次之後,我們就可以開始將這層分配給畫布,如圖 5所示。
圖 5. 分層的遊戲檢視
現在已經為每個分組的物體定義了層,現在就可以開始最佳化畫布清除。此最佳化的標的是為了節省處理時間,可以透過減少每一步渲染的螢幕上的固定物數量來實現。需要重點註意的是,使用不同的策略可能會使影象獲得更好的最佳化。下一節將探討各種物體或層的最佳化方法。
渲染最佳化
最佳化物體是分層策略的核心。對物體進行分層,使得渲染策略可以被採用。通常,最佳化技術會試圖消除開銷。正如表 1所述,由於引入了層,您已經增加了記憶體開銷。這裡討論的最佳化技術將減少處理器為了加快遊戲而必須執行的大量工作。我們的標的是尋找一種減少要渲染的空間量的方法,並盡可能多地刪除每一步中出現的渲染和清除呼叫。
單一物體清除
第一個最佳化方法針對的是清除空間,透過只清除組成該物體的螢幕子集來加快處理。首先減少與區域的各物體周圍的透明畫素重疊的重繪區域量。使用此技術的包括相對較小的物體,它們填充了視區的小區域。
第一個標的是球和障礙物物體。單一物體清除技術涉及到在將物體渲染到新位置之前清除前一幀渲染該物體的位置。我們會引入一個清除步驟到每個物體的渲染,並儲存物體的影象的邊界框。新增該步驟會修改物體物件,以包括清除步驟,如清單 5所示。
清單 5. 包含單框清除的物體
var Entity = function() {
/**
Initialization and other methods
**/
/**
* Render call to draw the entity
*
* @param {CanvasRenderingContext2D} context
*/
this.render = function(context) {
context.clearRect(
this.prevX,
this.prevY,
this.width,
this.height);
context.drawImage(this.image, this.x, this.y);
this.prevX = this.x;
this.prevY = this.y;
}
};
render函式的更新引入了一個常規drawImage之前發生的clearRect呼叫。對於該步驟,物件需要儲存前一個位置。圖 6顯示了物件針對前一個位置所採取的步驟。
圖 6. 清除矩形
您可以為每個物體建立一個在更新步驟前被呼叫的clear方法,實現此渲染解決方案(但本文將不會使用clear方法)。您還可以將這個清除策略引入到PanningEntity,在地面和雲物體上新增清除,如清單 6所示。
清單 6. 包含單框清除的PanningEntity
var PanningEntity = function() {
/**
Initialization and other methods
**/
/**
* Render call to draw the panned entity
*
* @param {CanvasRenderingContext2D} context
*/
this.render = function(context) {
context.clearRect(
this.x,
this.y,
context.canvas.width,
this.height);
context.drawImage(
this.image,
this.x – this.width,
this.y – this.height);
context.drawImage(
this.image,
this.x,
this.y);
context.drawImage(
this.image,
this.x + this.width,
this.y + this.height);
}
};
因為PanningEntity橫跨了整個視區,所以您可以使用畫布寬度作為清除矩形的大小。如果使用此清除策略,則會為您提供已為雲、小山和地面物體定義的重繪區域。
為了進一步最佳化雲物體,可以將雲分離為單獨的物體,使用它們自己的重繪區域。這樣做會大幅減少在雲重繪區域內要清除的螢幕空間量。圖 7顯示了新的重繪區域。
圖 7. 具有單獨重繪區域的雲
單一物體清除策略產生的解決方案可以解決像本例這樣的分層畫布遊戲上的大多數問題,但仍然可以對它進行最佳化。為了尋找針對該渲染策略的極端情況,我們假設球會與三角形碰撞。如果兩個物體碰撞,物體的重繪區域就有可能發生重疊,並建立一個不想要的渲染構件。另一個清除最佳化,更適合於可能會碰撞的物體,它也將有益於分層。
臟矩形清除
若沒有單一清除策略,臟矩形清除策略可以是一個功能強大的替代品。您可以對有重繪區域的大量物體使用這種清除策略,這種物體包括密集的粒子系統,或有小行星的空間遊戲。
從概念上講,該演演算法會收集由演演算法管理的所有物體的重繪區域,併在一個清除呼叫中清除整個區域。為了增加最佳化,此清除策略還會刪除每個獨立物體產生的重覆清除呼叫,如清單 7所示。
清單 7.DirtyRectManager
var DirtyRectManager = function() {
// Set the left and top edge to the max possible
// (the canvas width) amd right and bottom to least-most
// Left and top will shrink as more entities are added
this.left = canvas.width;
this.top = canvas.height;
// Right and bottom will grow as more entities are added
this.right = 0;
this.bottom = 0;
// Dirty check to avoid clearing if no entities were added
this.isDirty = false;
// Other Initialization Code
/**
* Other utility methods
*/
/**
* Adds the dirty rect parameters and marks the area as dirty
*
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
*/
this.addDirtyRect = function(x, y, width, height) {
// Calculate out the rectangle edges
var left = x;
var right = x + width;
var top = y;
var bottom = y + height;
// Min of left and entity left
this.left = left < this.left left : this.left;
// Max of right and entity right
this.right = right > this.right right : this.right;
// Min of top and entity top
this.top = top < this.top top : this.top;
// Max of bottom and entity bottom
this.bottom = bottom > this.bottom bottom : this.bottom;
this.isDirty = true;
};
/**
* Clears the rectangle area if the manager is dirty
*
* @param {CanvasRenderingContext2D} context
*/
this.clearRect = function(context) {
if (!this.isDirty) {
return;
}
// Clear the calculated rectangle
context.clearRect(
this.left,
this.top,
this.right – this.left,
this.bottom – this.top);
// Reset base values
this.left = canvas.width;
this.top = canvas.height;
this.right = 0;
this.bottom = 0;
this.isDirty = false;
}
};
將臟矩形演演算法整合到渲染迴圈,這要求在進行渲染呼叫之前呼叫清單 7中的管理器。將物體新增到管理器,使管理器可以在清除時計算清除矩形的維度。雖然管理器會產生預期的最佳化,但根據遊戲迴圈,管理器能夠針對遊戲迴圈進行最佳化,如圖 8所示。
圖 8. 互動層的重繪區域
- 幀 1 – 物體在碰撞,幾乎重疊。
- 幀 2 – 物體重繪區域是重疊的。
- 幀 3 – 重繪區域重疊,並被收集到一個臟矩形中。
- 幀 4 – 臟矩形被清除。
圖 8顯示了由針對在互動層的物體的演演算法計算出的重繪區域。因為遊戲在這一層上包含互動,所以臟矩形策略足以解決互動和重疊的重繪區域問題。
作為清除的重寫
對於在恆定重繪區域中動畫的完全不透明物體,可以使用重寫作為一項最佳化技術。將不透明的點陣圖渲染為一個區域(預設的合成操作),這會將畫素放在該區域中,不需要考慮該區域中的原始渲染。這個最佳化消除了渲染呼叫之前所需的清除呼叫,因為渲染會改寫原來的區域。
透過在之前的渲染的上方重新渲染影象,重寫可以加快地面物體。也可以透過相同的方式加快最大的層,比如背景。
透過減少每一層的重繪區域,您已經有效地為層和它們所包含的物體找到最佳化策略。
結束語
對畫布進行分層是一個可以應用於所有互動式實時場景的最佳化策略。如果想利用分層實現最佳化,您需要透過分析場景的重繪區域來考慮場景如何重疊這些區域。一些場景是具有重疊的重繪區域的集合,可以定義層,因此它們是渲染分層畫布的良好候選。如果您需要粒子系統或大量物理物件碰撞在一起,對畫布進行分層可能是一個很好的最佳化選擇。
原文出處:IBM developerworks
http://www.ibm.com/developerworks/cn/web/wa-canvashtml5layering/