歡迎光臨
每天分享高質量文章

利用分層優化 HTML5 畫布渲染

簡介

通常情況下,在玩 2D 游戲或渲染 HTML5 畫布時,需要執行優化,以便使用多個層來構建一個合成的場景。在 OpenGL 或 WebGL 等低級別渲染中,通過逐幀地清理和繪製場景來執行渲染。實現渲染之後,需要優化游戲,以減少渲染的量,所需成本因情況而異。因為畫布是一個 DOM 元素,它使您能夠對多個畫布進行分層,以此作為一種優化方法。

常用的縮寫

  • CSS: Cascading Style Sheets(級聯樣式表)
  • DOM: Document Object Model(文件物件模型)
  • HTML: HyperText Markup Language(超文本標記語言)

本文將探討對畫布進行分層的合理性。瞭解 DOM 設置,從而實現分層的畫布。使用分層進行優化需要各種實踐。本文還將探討一些優化策略的概念和技術,它們擴展了分層方法。

選擇優化策略

選擇最佳優化策略可能很難。在選擇分層的場景時,需要考慮場景是如何組成的。大屏幕上固定物的渲染經常需要重用若干個組件,它們是進行研究的極佳候選人。視差或動畫物體等效果往往需要大量的變化的屏幕空間。在探索您的最佳優化策略時,最好註意這些情況。雖然畫布的分層優化需要採用幾種不同的技術,但在正確應用這些技術後,往往會大幅提升性能。

設置層

在使用分層的方法時,第一步是在 DOM 上設置畫布。通常情況下,這很簡單,只需定義畫布元素,將其放入 DOM 中即可,但畫布層可能需要一些額外的樣式。在使用 CSS 時,成功地實現畫布分層有兩個要求:

  • 各畫布元素必須共存於視區 (viewport) 的同一位置上。
  • 每個畫布在另一個畫佈下面必須是可見的。

圖 1顯示了層設置背後的通用重疊概念。

圖 1. 層示例

設置層的步驟如下:

  1. 將畫布元素添加到 DOM。
  2. 添加畫布元素定位樣式,以便支持分層。
  3. 樣式化畫布元素,以便生成一個透明的背景。

設置畫布重疊堆棧

在 CSS 中創建一個重疊堆棧 (overlay stack) 可能需要少量的樣式。使用 HTML 和 CSS 有許多方法進行重疊。本文中的示例使用一個

標簽來包含畫布。

標簽指定了一個惟一 ID,它將樣式應用於其子 HTML5 畫布元素,如清單 1所示。

清單 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;

}

容器

通過將所有子畫布元素樣式化為使用絕對定位來完成重疊要求。通過選擇讓#viewport使用相對定位,您可以適應未來的發展,因此,應用於子樣式的絕對佈局樣式將會是相對於#viewport容器的樣式。

這些 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 進行測試,並記錄瀏覽器的選項卡記憶體使用情況,您可以看到記憶體使用的明顯趨勢。該測試使用了已經樣式化的

(正如上一節中討論的那樣),並生成了放置在

上的用單一顏色填充的畫布元素。畫布的大小被設定為 1600 x 900 像素,並從 Chrome1 的任務管理器實用程式收集資料。表 1顯示了一個示例。

在 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/




赞(0)

分享創造快樂