簡介
通常情況下,在玩 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 有許多方法進行重疊。本文中的示例使用一個<div>標籤來包含畫布。<div>標籤指定了一個惟一 ID,它將樣式應用於其子 HTML5 畫布元素,如清單 1所示。
清單 1. 畫布定位樣式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#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; } |
容器<div>通過將所有子畫布元素樣式化為使用絕對定位來完成重疊要求。通過選擇讓#viewport使用相對定位,您可以適應未來的發展,因此,應用於子樣式的絕對佈局樣式將會是相對於#viewport容器的樣式。
這些 HTML5 畫布元素的順序也很重要。可以按元素出現在 DOM 上的順序進行順序管理,也可以按照畫布應該顯示的順序來樣式化 z-index 樣式,從而管理順序。雖然並非總是如此,但其他樣式可能也會影響渲染;在引入額外的樣式(比如任何一種 CSS 轉換)時要小心。
透明的背景
通過使用重疊可見性來實現層技術的第二個樣式要求。該示例使用這個選項來設定 DOM 元素背景顏色,如清單 2所示。
清單 2. 設定透明背景的樣式表規則
1 2 3 4 5 6 |
canvas { /** * Set transparent to let any other canvases render through */ background-color: transparent; } |
將畫布樣式化為擁有一個透明背景,這可以實現第二個要求,即擁有可見的重疊畫布。現在,您已經構造了標記和樣式來滿足分層的需要,所以您可以設定一個分層的場景。
分層方面的考慮因素
在選擇優化策略時,應該注意使用該策略時的所有權衡。對 HTML5 畫布場景進行分層是一個側重於執行時記憶體的策略,用於獲得執行時速度方面的優勢。您可以在頁面的瀏覽器中增加更多的權重,以獲得更快的幀速率。一般來說,畫布被視為是瀏覽器上的一個圖形平面,其中包括一個圖形 API。
通過在 Google Chrome 19 進行測試,並記錄瀏覽器的選項卡記憶體使用情況,您可以看到記憶體使用的明顯趨勢。該測試使用了已經樣式化的<div>
(正如上一節中討論的那樣),並生成了放置在<div>上的用單一顏色填充的畫布元素。畫布的大小被設定為 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. 單畫布渲染迴圈的虛擬碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * 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虛擬碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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. 包含單框清除的實體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
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顯示了由針對在互動層的實體的演算法計算出的重繪區域。因為遊戲在這一層上包含互動,所以髒矩形策略足以解決互動和重疊的重繪區域問題。
作為清除的重寫
對於在恆定重繪區域中動畫的完全不透明實體,可以使用重寫作為一項優化技術。將不透明的點陣圖渲染為一個區域(預設的合成操作),這會將畫素放在該區域中,不需要考慮該區域中的原始渲染。這個優化消除了渲染呼叫之前所需的清除呼叫,因為渲染會覆蓋原來的區域。
通過在之前的渲染的上方重新渲染影像,重寫可以加快地面實體。也可以通過相同的方式加快最大的層,比如背景。
通過減少每一層的重繪區域,您已經有效地為層和它們所包含的實體找到優化策略。
結束語
對畫布進行分層是一個可以應用於所有互動式實時場景的優化策略。如果想利用分層實現優化,您需要通過分析場景的重繪區域來考慮場景如何重疊這些區域。一些場景是具有重疊的重繪區域的集合,可以定義層,因此它們是渲染分層畫布的良好候選。如果您需要粒子系統或大量物理物件碰撞在一起,對畫布進行分層可能是一個很好的優化選擇。
下載
描述 | 名字 | 大小 |
---|---|---|
文章原始碼 | HTML5CanvasRenderingSource.zip | 3KB |
參考資料
學習
- Google Chrome Task Manager:瞭解如何使用 Task Manager(工作管理員)獲得在 Google Chrome 中執行的特定程式的詳細資訊,或者強制關閉行為異常的選項卡或應用程式。
- GPU Accelerated Compositing in Chrome:獲得有關在 Chrome 中的硬體加速合成的實現背景和細節。
- Parallax:在 Wikipedia 上閱讀有關視差效果的更多資料。
- “使用 HTML5 canvas 繪製精美的圖形“(developerWorks,2011 年 2 月):瞭解如何使用一個簡單而強大的 HTML 元素 Canvas 來增強您的 Web 頁面。
- HTML5 Canvas:觀看此演示,它側重於畫布 API 的使用,並向您演示如何繪製一個很簡單的動畫。
- “HTML5 基礎知識,第 4 部分:最後的完善:Canvas 元素“(developerWorks,2011 年 7 月):閱讀這個對 HTML5 canvas 元素的介紹。其中的幾個示例可以說明所包括的函式。
- Canvas Pixel Manipulation:觀看此演示,它是管理畫布以開發有效的視覺資產的一個很好的示例,它來自 Safari Dev Center。
- WHATWG:探索該社群,它由使用 W3C 來調優 HTML5 的開發人員組成。
- Canvas 教程:檢視這個來自 Mozilla 開發人員的教程和演示。
- HTML5 Canvas 參考:瀏覽 W3Schools.com 有用的練習,幫助您掌握畫布知識。
- jQuery Events API:瞭解用於註冊行為的方法,使其在使用者與瀏覽器互動時生效。
- developerWorks Web 開發專區:查詢涵蓋多個基於 Web 的解決方案的文章。參閱Web 開發技術庫,獲得各種技術文章和技巧、教程、標準以及 IBM Redbooks。
- developerWorks 技術活動和網路廣播:隨時關注這些領域中的技術。
- developerWorks 點播演示:那裡提供了面向初學者的產品安裝和設定演示,以及面向經驗豐富的開發人員的高階功能演示。
- Twitter 上的 developerWorks:立即加入,關注 developerWorks tweet。
獲得產品和技術
- OpenGL:獲得最新的驅動程式。
- jQuery:下載流行的 JavaScript Library,可以簡化 HTML 文件遍歷、事件處理、動畫和 Ajax 互動並實現快速 Web 開發。
- Modernizr:獲得此開源 JavaScript 庫,幫助您構建下一代的 HTML5 和 CSS3 驅動的 Web 站點。
- Kibo:下載流行的專門為快速跨瀏覽器的鍵盤事件處理編寫的庫。
- IBM 產品評估版本:下載或在 IBM SOA Sandbox 中探索線上試用版本,並動手使用來自 DB2、Lotus、Rational、Tivoli 和 WebSphere 的應用程式開發工具和中介軟體產品。
討論
- developerWorks 社群:探索由開發人員推動的部落格、論壇、組和維基,並與其他 developerWorks 使用者進行交流。