ウェブコンテンツで3Dを扱える技術としてWebGLがあります。WebGLを利用すればブラウザ上で華やかな表現を実現できます。しかし、コンテンツの内容によっては処理負荷が高くなり、カクつきが生じる場合があります。カクつきはコンテンツの見栄えを損なわせ、作り手の想定とは異なる体験を与えてしまう可能性があります。そのような場合の対策として、本記事ではWebGLのカクつき解消方法をいくつか紹介します。
解説用にカクつきの起こりやすい高負荷なデモを用意しました。本記事で紹介するカクつき解消方法はこのデモで実際に体験できるので、読み進めながら同時に触れておくとイメージがしやすいと思います。
※このデモはThree.js(r141)とTypeScript 4.7とwebpack 5で作成しました。開発環境の構築は記事「最新版TypeScript+Webpackの環境構築まとめ(Three.jsのサンプル付き)」を参考ください。
なぜカクつきが起こるのか
1コマ毎の描画処理が重くfpsを一定に保つことができないためです。fpsとは『Frame Per Second』の略であり、アニメーションの一秒あたりのコマ数の事です。コマ数が多くなるほど滑らかなアニメーションとして見えます。一般的にコンシューマーゲームやPCゲームなどは60fps、つまり1秒あたり60コマでアニメーションされるように作られています。最近のVRでは120fpsが必要とも言われており、滑らかに再生させる重要性は高まっています。
なかでも3D表現をする場合はとくに処理負荷が高く、凝った表現をしようとするとfpsを保つことが難しくなります。WebGLでの3D表現にも同様のことが言えます。
以下の画像は今回のデモの処理負荷の低い状態(60fps)と高い状態(16fps)の1回の描画にかかる処理時間をChromeの開発ツールで比較したものです。
60fpsの場合は16ミリ秒以内で1描画を終えられているのに対し、16fpsの場合は1描画に66ミリ秒もかかってしまっています。さらに60fpsの方は次の処理との間に余裕があり、安定した間隔で処理できていることも分かります。このように1描画にかかる処理時間を減らし安定した間隔で描画できることが、カクつき解消において非常に重要になります。ではどうカクつき対策をしていくべきなのでしょうか?
対策
今回紹介するカクつきの対策方法は以下の3つです。それぞれ順に対策方法の実装方法と注意点を解説します。
- fpsをわざと下げる
canvas
要素の解像度を下げる- カクつきによる動きの遅れを無くす
- 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()
メソッドは2回に一回だけ呼ばれるようになるため描画回数は半分、つまり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の高解像度対応はどこまで行うべきか」で紹介したように、スマートフォンのみならずデスクトップパソコンでもデバイス・ピクセル比の高い端末が増えてきています。
ただし、デバイス・ピクセル比が2よりも大きい端末(例:iPhone Pro等のハイスペックモデルは window.devicePixelRatio
が 3
)では、canvasの面積が広がりすぎて実行時再生負荷が高くなりがちです。デバイス・ピクセル比が2よりも大きい端末では2に下げるというのも対策のひとつです。2
→1
へ変化させると画質の低下が顕著ですが、3
→2
へ下げることは体感的なデメリットは少ないと言えます。
// デバイスピクセル比は上限を2として扱う
const devicePixelRatio = Math.min(2, window.devicePixelRatio);
// Three.jsに適用
renderer.setPixelRatio(devicePixelRatio);
③ カクつきによる動きの遅れを無くす
オススメ度: ★★★★☆ (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® Iris™ Graphics 6100
- MacBook Retina 12インチ / 2015年モデル / Intel® HD Graphics 5300
- VAIO DUO / 2012年モデル / Intel HD Graphics 4000
冒頭のデモの画面左下にGPUの種類を表示しています。みなさんのマシンではなんと表示されていましたか?
おわりに
今回紹介した手法に頼りすぎない作り方ができればベストですが、低スペック端末や大画面サイズの端末への対応を求められる可能性があるので、多く場面で活用できると思います。さらに本記事ではThree.jsを中心に解説しましたが、Pixi.jsも同じ効果を得られるので知っておいて損はありません。仕様検討前やブラッシュアップのタイミングで再読するとスムーズな開発ができると思うので是非参考ください。
※この記事が公開されたのは8年前ですが、11か月前の2024年2月に内容をメンテナンスしています。