はじめに
HTML5 キャンバスは、Apple による試験運用として始まったもので、ウェブ上の 2D 即時モード グラフィックで最も広くサポートされている標準です。現在、多くのデベロッパーが、さまざまなマルチメディア プロジェクト、ビジュアリゼーション、ゲームにこの技術を活用しています。ただし、作成するアプリケーションの複雑さが増すにつれ、デベロッパーは意図せずパフォーマンスの壁にぶつかることがあります。キャンバスのパフォーマンスを最適化するためのさまざまな方法が提案されていますが、この記事では、このドキュメントの一部を、デベロッパーがより簡単に理解できるリソースにまとめることを目的としています。この記事では、すべてのコンピュータ グラフィック環境に適用される基本的な最適化と、キャンバスの実装が改善されるにつれて変更される可能性があるキャンバス固有の手法について説明します。特に、ブラウザ ベンダーがキャンバス GPU アクセラレーションを実装するにつれて、ここで説明したパフォーマンス手法の一部は効果が薄れる可能性があります。必要に応じて、この点について説明します。この記事では、HTML5 キャンバスの使用については説明しません。詳しくは、HTML5Rocks のキャンバス関連の記事、Dive into HTML5 サイトのこの章、MDN キャンバス チュートリアルをご覧ください。
パフォーマンス テスト
急速に変化する HTML5 キャンバスに対応するため、JSPerf(jsperf.com)テストでは、提案された最適化がすべて機能することを確認します。JSPerf は、デベロッパーが JavaScript パフォーマンス テストを記述できるウェブ アプリケーションです。各テストは、達成しようとしている結果(キャンバスの消去など)に焦点を当てており、同じ結果を達成する複数のアプローチが含まれています。JSPerf は、各アプローチを短時間にできるだけ多くの回数実行し、1 秒あたりの反復回数を統計的に有意な数値で提供します。スコアが高いほど良いです。JSPerf のパフォーマンス テストページにアクセスしたユーザーは、ブラウザでテストを実行し、JSPerf が Browserscope(browserscope.org)に正規化されたテスト結果を保存できるようにします。この記事で説明する最適化手法は JSPerf の結果に基づいているため、その手法が現在も適用可能かどうかについては、JSPerf に戻って最新情報を確認してください。これらの結果をグラフとしてレンダリングする小さなヘルパー アプリケーションを作成しました。このアプリケーションはこの記事全体に埋め込まれています。
この記事のパフォーマンス結果はすべて、ブラウザのバージョンに基づいています。これは制限事項となります。ブラウザがどの OS で実行されているか、さらに重要なのは、パフォーマンス テストの実行時に HTML5 キャンバスがハードウェア アクセラレーションされているかどうかが不明であるためです。Chrome の HTML5 キャンバスがハードウェア アクセラレーションされているかどうかを確認するには、アドレスバーで about:gpu
にアクセスします。
オフスクリーン キャンバスに事前レンダリングする
ゲームの作成時によくあるように、複数のフレーム間で類似のプリミティブを画面に再描画する場合は、シーンの大部分を事前レンダリングすることで、パフォーマンスを大幅に向上させることができます。事前レンダリングとは、一時的な画像をレンダリングする別のオフスクリーン キャンバス(またはキャンバス)を使用し、オフスクリーン キャンバスを表示キャンバスにレンダリングすることを意味します。たとえば、マリオが 1 秒あたり 60 フレームで走っている場合を考えてみましょう。マリオの帽子、口ひげ、M をフレームごとに再描画するか、アニメーションを実行する前にマリオをプリレンダリングします。事前レンダリングなし:
// canvas, context are defined
function render() {
drawMario(context);
requestAnimationFrame(render);
}
プリレンダリング:
var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext('2d');
drawMario(m_context);
function render() {
context.drawImage(m_canvas, 0, 0);
requestAnimationFrame(render);
}
requestAnimationFrame
の使用に注目してください。この点については、後のセクションで詳しく説明します。
この手法は、レンダリング オペレーション(上記の例の drawMario
)がコストが高い場合に特に効果的です。テキスト レンダリングは非常にコストの高いオペレーションであり、その良い例です。
ただし、「事前レンダリングされた緩い」テストケースのパフォーマンスは低下します。事前レンダリングを行う場合は、描画する画像に一時キャンバスがぴったり収まるようにすることが重要です。そうしないと、オフスクリーン レンダリングによるパフォーマンスの向上が、大きなキャンバスを別のキャンバスにコピーすることによるパフォーマンスの低下によって相殺されます(これはソース ターゲット サイズの関数として変化します)。上記のテストでは、ぴったりのキャンバスは単に小さくなります。
can2.width = 100;
can2.height = 40;
パフォーマンスが低下する緩い状態と比較すると、
can3.width = 300;
can3.height = 100;
キャンバス呼び出しをまとめてバッチ処理する
描画は負荷の高いオペレーションであるため、長い一連のコマンドで描画状態マシンを読み込み、それらをすべて動画バッファにダンプする方が効率的です。
たとえば、複数の線を描画する場合は、すべての線を含む 1 つのパスを作成し、1 回の描画呼び出しで描画するほうが効率的です。つまり、次のように別々の線を描画するのではなく、
for (var i = 0; i < points.length - 1; i++) {
var p1 = points[i];
var p2 = points[i+1];
context.beginPath();
context.moveTo(p1.x, p1.y);
context.lineTo(p2.x, p2.y);
context.stroke();
}
単一のポリラインを描画すると、パフォーマンスが向上します。
context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
var p1 = points[i];
var p2 = points[i+1];
context.moveTo(p1.x, p1.y);
context.lineTo(p2.x, p2.y);
}
context.stroke();
これは HTML5 キャンバスの世界にも当てはまります。たとえば、複雑なパスを描画する場合は、セグメントを個別にレンダリングするのではなく、すべてのポイントをパスに配置することをおすすめします(jsperf)。
ただし、Canvas にはこのルールに対する重要な例外があります。目的のオブジェクトの描画に関連するプリミティブに小さな境界ボックスがある場合(水平線や垂直線など)、別々にレンダリングするほうが効率的になることがあります(jsperf)。
不要なキャンバス状態の変更を回避する
HTML5 キャンバス要素は、塗りつぶしとストロークのスタイルや、現在のパスを構成する前の点をトラッキングするステートマシンの上に実装されます。グラフィック パフォーマンスを最適化しようとすると、グラフィック レンダリングにのみ焦点を当てがちです。ただし、ステートマシンを操作すると、パフォーマンス オーバーヘッドが発生することもあります。たとえば、複数の塗りつぶし色を使用してシーンをレンダリングする場合は、キャンバス上の配置ではなく色でレンダリングする方が費用対効果が高くなります。ピンストライプ パターンをレンダリングするには、ストライプをレンダリングし、色を変更して、次のストライプをレンダリングします。
for (var i = 0; i < STRIPES; i++) {
context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
context.fillRect(i * GAP, 0, GAP, 480);
}
または、奇数のストライプをすべてレンダリングしてから、偶数のストライプをすべてレンダリングします。
context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}
予想どおり、インターレース アプローチは、状態マシンの変更がコストが高いため、遅くなります。
新しい状態全体ではなく、画面の差分のみのレンダリング
当然のことながら、画面上でレンダリングする内容を減らすと、レンダリングにかかる費用を削減できます。再描画間で増分差のみがある場合は、差分のみ描画することでパフォーマンスを大幅に向上させることができます。つまり、描画前に画面全体を消去するのではなく、
context.fillRect(0, 0, canvas.width, canvas.height);
描画された境界ボックスを記録し、そのボックスのみを消去します。
context.fillRect(last.x, last.y, last.width, last.height);
コンピュータ グラフィックに精通している場合は、この手法を「再描画領域」とも呼ぶことがあります。この手法では、以前にレンダリングされた境界ボックスが保存され、レンダリングごとに消去されます。この手法は、JavaScript の Nintendo エミュレータに関するトークで説明されているように、ピクセルベースのレンダリング コンテキストにも適用されます。
複雑なシーンには複数のレイヤを持つキャンバスを使用する
前述のように、大きな画像の描画はコストがかかるため、可能な限り避けるべきです。オフスクリーン レンダリングに別のキャンバスを使用するだけでなく、前述のプリレンダリング セクションで説明したように、キャンバスを重ねて使用することもできます。フォアグラウンド キャンバスで透明度を使用すると、レンダリング時に GPU を使用してアルファを合成できます。次のように、2 つの絶対配置キャンバスを重ねて設定できます。
<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>
1 つのキャンバスを使用する場合と比べて、フォアグラウンド キャンバスを描画または消去するときに背景を変更する必要がない点が利点です。ゲームやマルチメディア アプリをフォアグラウンドとバックグラウンドに分割できる場合は、別々のキャンバスにレンダリングしてパフォーマンスを大幅に向上させることを検討してください。
人間の認識の不完全さを利用し、背景を 1 回だけレンダリングするか、フォアグラウンド(ユーザーの注意の大部分を占める可能性が高い)と比べて低速でレンダリングすることがよくあります。たとえば、レンダリングするたびにフォアグラウンドをレンダリングし、バックグラウンドは N 番目のフレームごとにレンダリングできます。また、このアプローチは、アプリがこの種の構造で適切に動作する場合、任意の数の合成キャンバスに一般化できます。
shadowBlur を避ける
他の多くのグラフィック環境と同様に、HTML5 キャンバスではプリミティブをぼかすことはできますが、このオペレーションは非常にコストがかかる場合があります。
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);
キャンバスを消去するさまざまな方法を学ぶ
HTML5 Canvas は即時モードの描画パラダイムであるため、シーンはフレームごとに明示的に再描画する必要があります。そのため、キャンバスの消去は、HTML5 キャンバス アプリやゲームにとって基本的に重要なオペレーションです。キャンバスの状態の変更を回避するセクションで説明したように、キャンバス全体を消去することは多くの場合望ましくありませんが、消去する必要がある場合は、context.clearRect(0, 0, width, height)
を呼び出すか、キャンバス固有のハック(canvas.width = canvas.width
)を使用して消去する 2 つの方法があります。執筆時点では、clearRect
は通常、幅のリセット バージョンよりも優れていますが、Chrome 14 では、canvas.width
リセット ハックを使用すると大幅に高速化される場合があります。
このヒントは、基盤となるキャンバスの実装に大きく依存しており、変更される可能性が高いため、注意が必要です。詳細については、キャンバスの消去に関する Simon Sarris の記事をご覧ください。
浮動小数点座標を使用しない
HTML5 キャンバスはサブピクセル レンダリングをサポートしており、オフにすることはできません。整数以外の座標で描画すると、アンチエイリアシングが自動的に使用され、線が滑らかになります。以下は、Seb Lee-Delisle によるサブピクセル キャンバスのパフォーマンスに関する記事から抜粋した視覚効果です。
![サブピクセル](https://web.developers.google.cn/static/articles/canvas-performance/image/sub-pixel-ebd2e5026bd2.png?hl=ja)
スムーズなスプライトが望ましい効果でない場合は、Math.floor
または Math.round
を使用して座標を整数に変換すると、はるかに高速になります(jsperf)。
浮動小数点座標を整数に変換するには、いくつかの巧妙な手法を使用できます。最もパフォーマンスが高い手法は、ターゲット数に 1 を加算し、結果に対してビット単位の演算を実行して小数部分を除去する方法です。
// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;
パフォーマンスの詳細な内訳は、こちら(jsperf)をご覧ください。
キャンバス実装が GPU アクセラレーションに対応し、整数以外の座標をすばやくレンダリングできるようになれば、この種の最適化は不要になります。
requestAnimationFrame
でアニメーションを最適化する
ブラウザでインタラクティブ アプリケーションを実装する場合は、比較的新しい requestAnimationFrame
API を使用することをおすすめします。特定の固定ティックレートでレンダリングするようブラウザに指示するのではなく、レンダリング ルーティンを呼び出し、ブラウザが使用可能になったときに呼び出されるようブラウザに丁寧に依頼します。ページがフォアグラウンドにない場合、ブラウザはレンダリングしないように賢く動作します。requestAnimationFrame
コールバックは 60 FPS のコールバック レートを目指していますが、保証されるわけではありません。そのため、最後のレンダリングから経過した時間を記録する必要があります。次のような形式になります。
var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
var delta = Date.now() - lastRender;
x += delta;
y += delta;
context.fillRect(x, y, W, H);
requestAnimationFrame(render);
}
render();
requestAnimationFrame
のこの使用は、キャンバスだけでなく、WebGL などの他のレンダリング技術にも適用されます。執筆時点では、この API は Chrome、Safari、Firefox でのみ使用できるため、このシムを使用する必要があります。
ほとんどのモバイル キャンバスの実装は遅い
モバイルについてお話ししましょう。残念ながら、執筆時点では、Safari 5.1 を搭載した iOS 5.0 ベータ版のみが、GPU アクセラレーションによるモバイル キャンバスの実装に対応しています。GPU アクセラレーションがないと、モバイル ブラウザには通常、最新のキャンバスベースのアプリケーションに十分な強力な CPU がありません。前述の JSPerf テストの多くは、モバイルではデスクトップと比べて 1 桁もパフォーマンスが低下するため、正常に実行できるクロスデバイス アプリの種類が大幅に制限されます。
まとめ
ここまでの記事では、パフォーマンスの高い HTML5 キャンバスベースのプロジェクトを開発する際に役立つ、包括的な最適化手法について説明しました。新しいことを学んだので、素晴らしい作品を最適化しましょう。最適化するゲームやアプリケーションがない場合は、Chrome 試験運用版と Creative JS でヒントを探してください。
参照
- 即時モードと保持モード。
- HTML5Rocks のその他の キャンバスに関する記事。
- Dive into HTML5 の Canvas セクション。
- JSPerf を使用すると、デベロッパーは JS パフォーマンス テストを作成できます。
- Browserscope は、ブラウザのパフォーマンス データを保存します。
- JSPerfView: JSPerf テストをグラフとしてレンダリングします。
- キャンバスの消去に関するサイモンのブログ投稿と、キャンバスのパフォーマンスに関する章が含まれているサイモンの書籍 HTML5 Unleashed。
- サブピクセル レンダリングのパフォーマンスに関する Sebastian のブログ投稿。
- JS NES エミュレータの最適化に関する Ben の講演。
- Chrome DevTools の新しいキャンバス プロファイラ。