HTML5 Canvas のパフォーマンスの改善

はじめに

HTML5 キャンバスは、Apple による試験運用として始まったもので、ウェブ上の 2D 即時モード グラフィックで最も広くサポートされている標準です。現在、多くのデベロッパーが、さまざまなマルチメディア プロジェクト、ビジュアリゼーション、ゲームにこの技術を活用しています。ただし、作成するアプリケーションの複雑さが増すにつれ、デベロッパーは意図せずパフォーマンスの壁にぶつかることがあります。キャンバスのパフォーマンスを最適化するためのさまざまな方法が提案されていますが、この記事では、この本文の一部を、デベロッパーにとって理解しやすいリソースに統合することを目的としています。この記事では、すべてのコンピュータ グラフィック環境に適用される基本的な最適化と、キャンバスの実装が改善されるにつれて変更される可能性があるキャンバス固有の手法について説明します。特に、ブラウザ ベンダーがキャンバス GPU アクセラレーションを実装するにつれて、ここで説明したパフォーマンス手法の一部は効果が薄れる可能性があります。必要に応じて、この点について説明します。この記事では、HTML5 キャンバスの使用方法は説明しません。詳しくは、HTML5Rocks のキャンバス関連の記事Dive into HTML5 サイトのこの章MDN キャンバス チュートリアルをご覧ください。

パフォーマンス テスト

急速に変化する HTML5 キャンバスに対応するため、JSPerfjsperf.com)テストでは、提案された最適化がすべて機能することを確認します。JSPerf は、デベロッパーが JavaScript パフォーマンス テストを作成できるウェブ アプリケーションです。各テストは、達成しようとしている結果(キャンバスのクリアなど)に重点を置き、同じ結果を実現する複数のアプローチを含んでいます。JSPerf は、各アプローチを短時間にできるだけ多くの回数実行し、1 秒あたりの反復回数を統計的に有意な数値で提供します。スコアが高いほど良いです。JSPerf のパフォーマンス テストページにアクセスしたユーザーは、ブラウザでテストを実行し、JSPerf が Browserscopebrowserscope.org)に正規化されたテスト結果を保存できるようにします。この記事で説明する最適化手法は JSPerf の結果に基づいているため、その手法が現在も適用されるかどうかについて最新情報を確認できます。これらの結果をグラフとしてレンダリングする小さなヘルパー アプリケーションを作成しました。このアプリケーションはこの記事全体に埋め込まれています。

この記事のパフォーマンス結果はすべて、ブラウザのバージョンに基づいています。これは制限事項となります。ブラウザがどの OS で実行されているか、さらに重要なのは、パフォーマンス テストの実行時に HTML5 キャンバスがハードウェア アクセラレーションされているかどうかが不明であるためです。Chrome の HTML5 キャンバスがハードウェア アクセラレーションされているかどうかを確認するには、アドレスバーで about:gpu にアクセスします。

画面外のキャンバスにプリレンダリングする

ゲームの作成時によくあるように、複数のフレーム間で類似のプリミティブを画面に再描画する場合は、シーンの大部分を事前レンダリングすることで、パフォーマンスを大幅に向上させることができます。事前レンダリングとは、一時的な画像をレンダリングする別のオフスクリーン キャンバス(またはキャンバス)を使用し、オフスクリーン キャンバスを表示キャンバスにレンダリングすることを意味します。たとえば、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();
}

以下のように、1 つのポリラインを描画する方がパフォーマンスが向上します。

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) を呼び出すか、キャンバス固有のハックを使用してそれを実行するかの 2 つの方法があります。執筆時点では、clearRect は通常幅リセット バージョンよりも優れていますが、場合によっては Chrome の canvas.width ハッキング 14 を使用すると大幅に速くなります。canvas.width = canvas.width

このヒントは、基盤となるキャンバスの実装に大きく依存しており、変更される可能性が高いため、注意が必要です。詳細については、キャンバスの消去に関する Simon Sarris の記事をご覧ください。

浮動小数点座標を使用しない

HTML5 キャンバスはサブピクセル レンダリングをサポートしており、これをオフにする方法はありません。整数以外の座標で描画すると、自動的にアンチ エイリアスを使用して線が滑らかになります。以下は、Seb Lee-Delisle によるサブピクセル キャンバスのパフォーマンスに関する記事から抜粋した視覚効果です。

サブピクセル

スムーズなスプライトが目的の効果でない場合は、Math.floor または Math.round を使用して座標を整数に変換すると、はるかに高速になります(jsperf)。

浮動小数点座標を整数に変換するには、いくつかの巧妙な手法を使用できます。最もパフォーマンスの高い手法は、ターゲット数値に半分を加算し、その結果に対してビット演算を実行して小数部分を除去することです。

// 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 でのみ使用可能です。こちらの shim を使用してください。

ほとんどのモバイル キャンバスの実装は遅い

モバイルについてお話ししましょう。残念ながら、執筆時点では、Safari 5.1 を搭載した iOS 5.0 ベータ版のみが、GPU アクセラレーションによるモバイル キャンバスの実装に対応しています。GPU アクセラレーションがないと、モバイル ブラウザには通常、最新のキャンバスベースのアプリケーションに十分な強力な CPU がありません。前述の JSPerf テストの多くは、モバイルではパソコンに比べてパフォーマンスが桁違いに悪く、正常に実行できるクロスデバイス アプリの種類が大幅に制限されます。

まとめ

ここまで、パフォーマンスの高い HTML5 キャンバスベースのプロジェクトを開発する際に役立つ、包括的な最適化手法について説明しました。ここまでで 新しいことを学んだところで 素晴らしいクリエイティブを最適化していきましょうまた、最適化できるゲームやアプリケーションがない場合は、Chrome ExperimentsCreative JS を参考にしてアイデアを得ましょう。

参照