FlashのStage3Dや、WebGLの登場によってブラウザ上でも高度な3D表現ができるようになり、ウェブコンテンツの表現の幅が広がりました。しかし、高速と言ってもコンテンツの内容によっては処理負荷が高くなり、カクつきが生じる場合があります。カクつきはコンテンツの見栄えを損なわせ、作り手の想定とは異なる体験を与えてしまう可能性があります。そのような場合の対策として、本記事ではWebGLのカクつき解消方法をいくつか紹介します

解説用にカクつきの起こりやすい高負荷なデモを用意しました。本記事で紹介するカクつき解消方法はこのデモで実際に体験できるので、読み進めながら同時に触れておくとイメージがしやすいと思います。

※このデモはThree.js(r86)とTypeScript 2.4webpack 3で作成しました。開発環境の構築は記事「最新版TypeScript+Webpackの環境構築まとめ(Three.jsのサンプル付き)」を参考ください。

なぜカクつきが起こるのか

1コマ毎の描画処理が重くfpsを一定に保つことができないためです。fpsとは『frame per second』の略であり、アニメーションの一秒あたりのコマ数の事です。コマ数が多くなるほど滑らかなアニメーションとして見えます。一般的にコンシューマーゲームやPCゲームなどは60fps、つまり1秒あたり60コマでアニメーションされるように作られています。最近のVRでは120fpsが必要とも言われており、滑らかに再生させる重要性は高まっています。

中でも3D表現をする場合は特に処理負荷が高く、凝った表現をしようとするとfpsを保つ事が難しくなります。WebGLでの3D表現にも同様の事が言えます。

以下の画像は今回のデモの処理負荷の低い状態(60fps)と高い状態(16fps)の1描画にかかる処理時間をChromeの開発ツールで比較したものです。

160623_webgl_fps_chrome

60fpsの場合は16ミリ秒以内で1描画を終えられているのに対し、16fpsの場合は1描画に66ミリ秒もかかってしまっています。さらに60fpsの方は次の処理との間に余裕があり、安定した間隔で処理できていることも分かります。このように1描画にかかる処理時間を減らし安定した間隔で描画できることが、カクつき解消において非常に重要になります。ではどうカクつき対策をしていくべきなのでしょうか?

対策

今回紹介するカクつきの対策方法は以下の3つです。それぞれ順に対策方法の実装方法と注意点を解説します。

  1. fpsをわざと下げる
  2. canvas要素の解像度を下げる
  3. カクつきによる動きの遅れを無くす
  4. GPUの種類によって処理負荷を減らす

① fpsをわざと下げる

オススメ度: ★★☆☆☆ (2)

本来目指すべきは60fpsですが、最大fpsをわざと60fpsよりも少なくなるように制御する方法です。1秒間に描画すべきコマ数を減らすことで描画負荷を下げ、fpsを安定させやすくなります。

実装方法

以下のコードは60fpsから30fpsに落とす例です。

let frame = 0;

function tick() {
  requestAnimationFrame(tick);

  // フレーム数をインクリメント
  frame++;

  // フレーム数が2で割り切れなければ描画しない
  if(frame % 2 == 0) { return; }

  // 描画
  renderer.render(scene, camera);
}

tick();

requestAnimationFrame()は渡された関数を60fpsになるように実行させる関数です。requestAnimationFrame()関数によりtick()関数が毎フレーム呼び出されますが、render()メソッドは二回に一回だけ呼ばれるようになるため描画回数は半分、つまり30fpsに制御できます。

デモで確認したい場合は画面右上のパネルからfps30にチェックボックスを選択すると30fpsに切替えられます。

注意点

fpsが落ちてしまうとスピード感や臨場感損なわれ、開発者の意図にそぐわないものになることがあります。そのため、素早い動きの多いアクションゲームやレースゲームなどのコンテンツには適していません。スピード感などを重視しないコンテンツであれば30fpsでも十分アニメーションとして成立するので内容によって使い分けましょう。

② canvas要素の解像度を下げる

オススメ度: ★★★☆☆ (3)

canvas要素の解像度を下げ、1コマにかかる描画負荷を下げる方法です。
iPhoneのRetinaディスプレイや、パソコンの4Kディスプレイなどデバイス・ピクセル比が高い端末でWebGLコンテンツを表示させた場合、カクつきを起こしやすくなります。デバイス・ピクセル比とは、画像の1つのドットに対して使用するピクセルの数との比です。つまり、デバイス・ピクセル比が高いほど1コマを表示させるまで処理負荷が多いためカクつきが生まれます。

canvas要素の解像度を下げるこの方法を使うことでデバイス・ピクセル比の高い端末でも負荷が上がらないように制御できます。

実装方法

以下のコードはThree.jsのTHREE.Rendererオブジェクトが持つsetPixelRatio()メソッドを使ってcanvas要素の解像度をデバイス・ピクセル比を1:1に変更しています。

renderer.setPixelRatio(1);

この処理を追加しておけば、デバイス・ピクセル比の高い端末でも1ドットを1ピクセルで表示するので処理負荷は統一されます。

デモの右上パネルのpixelRatioの数を変更することで実際に比較できます。

注意点

canvas要素の解像度が下がるのでぼけた見た目になります。見栄えとして悪くならない程度にしておきましょう。

記事「HTML5 CanvasとWebGLの高解像度対応はどこまで行うべきか」で紹介したように、最近はデバイス・ピクセル比の高い端末が増えてきているので、解像度を下げるのは極力避けたいものですが…。

③ カクつきによる動きの遅れを無くす

オススメ度: ★★★★☆ (4)

フレームベースのアニメーションの場合、fpsが落ちるとアニメーションもその分遅れてしまいます。その遅れた分のアニメーションを詰める処理を加える方法です。

例えば60fpsで1秒間にx軸上を+100px移動するアニメーションの場合、fpsが30になると1秒間に+50pxしか進まないことになります。そうならないよう移動時に遅れ分の比較係数をかけて+100pxになるよう調整します。

fpsが多少低くなってしまってもアニメーションが進むべきところまで進むので、カクつきによる違和感が大幅に解消されます。

実装方法

以下のコードは毎フレーム比較係数を更新し、メッシュの移動値に反映させている例です。

let time = 0;
let timeRatio = 1;

function updateTimeRatio() {
  const lastTime = time;
  if(lastTime > 0) {
    // 1フレーム当たりの時間(ミリ秒)
    const FPS_60_SEC = 1000 / 60;
    // 差分時間をセット
    const dTime = new Date().getTime() - lastTime;
    // FPS60との比較係数をセット
    timeRatio = dTime / FPS_60_SEC;
  }
  // 現在時間をセット
  time = new Date().getTime();
}

function tick() {
  requestAnimationFrame(tick);
  // 比較係数を更新
  updateTimeRatio();

  // メッシュを移動
  mesh.position.x += 10 * timeRatio;

  // 描画
  renderer.render(scene, camera);
}

tick();

デモで確認したい場合は、画面右上のパネルからtimeRatioModeにチェックボックスを選択すると比較係数が反映されたアニメーションに切替えられます。

注意点

フレーム毎に移動させている場所全てに比較係数をかける必要があります。複数のオブジェクトをアニメーションさせている場合は特に手間になります。漏れがあるとその箇所のアニメーションが遅れることになるので注意ください。

④GPUの種類によって処理負荷を減らす

オススメ度: ★★★★★ (5)

GPUは種類によって性能が大きく異なります。特にデスクトップやノートパソコンのオンボードのGPUではWebGLの性能が極端に低いです。あまり知られていないテクニックですが、GPUのドライバの種類を判定して、性能の低いドライバの場合のみデバイスピクセル比を下げるのがいいでしょう。

ドライバーの文字列に「Intel」が含まれているときはオンボードのGPUなので、このときに下げると効果的です。

▼GPUのドライバーの名前を取得するJavaScriptのコード

const canvas = document.createElement('canvas');
let gl;
let renderer;
try {
  gl = canvas.getContext('experimental-webgl');

  //ドライバー情報を取得
  const ext = gl.getExtension('WEBGL_debug_renderer_info');

  if (!ext) {
    renderer = '';
  }
  else {
    renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
  }
}
catch (e) {
  // WebGL未対応の場合
  gl       = null;
  renderer = '';
}
// ドライバの種類を出力
console.log(renderer);

例えば、オンボードのGPUを積んでいるマシンには次のようなものがあります。Retinaモデルが多いですが、GPUが弱いので処理負荷の高いWebGLはあまり性能がでません。

  • MacBook Pro Retina 13インチ / 2013年モデル / Intel Iris OpenGL Engine
  • MacBook Pro Retina 13インチ / 2015年モデル / Intel(R) Iris(TM) Graphics 6100
  • MacBook Retina 12インチ / 2015年モデル / Intel(R) HD Graphics 5300
  • VAIO DUO / 2012年モデル / Intel HD Graphics 4000

冒頭のデモの画面左下にGPUの種類を表示しています。みなさんのマシンではなんと表示されていましたか?

おわりに

今回紹介した手法に頼りすぎない作り方ができればベストですが、低スペック端末や大画面サイズの端末への対応を求められる可能性があるので、多く場面で活用できると思います。さらに本記事ではThree.jsを中心に解説しましたが、Pixi.jsCreateJSProcessing.jsでも同じ効果を得られるので知っておいて損はありません。仕様検討前やブラッシュアップのタイミングで再読するとスムーズな開発ができると思うので是非参考ください。