目次

WebGLFundamentals.org

WebGLのアニメーション

この記事はWebGLの連載シリーズの続きである。 この連載は「WebGLの基本」から始まり、 前回は「三次元でのカメラ処理」について説明した。 これらについて学んでいなければ、先にそちらを読んでおくことをお勧めする。

WebGLでアニメーションするにはどうしたらよいだろうか?

JavaScriptでアニメーションするためには、「映像のどこかを変更して再描画する」 ということを「継続的にやる」必要がある。 実際のところ、これはWebGLに限定した話ではない。

以前書いたサンプルのひとつを例としてアニメーションを実装するとしたら、このようになる。

*var fieldOfViewRadians = degToRad(60);
*var rotationSpeed = 1.2;

*requestAnimationFrame(drawScene);

// シーンを描画する
function drawScene() {
*  // 各フレームごとに回転量を少しだけ増加する
*  rotation[1] += rotationSpeed / 60.0;

  ...
*  // 次のフレームの描画のために再度drawSceneを呼び出す。
*  requestAnimationFrame(drawScene);
}

これを実際に動かすとこのようになる。

一見よさそうに見えるが、このやり方には隠れた問題がある。 上のコードではrotationSpeed / 60.0としている。 60.0で割っているのは、ブラウザがrequestAnimationFrameの処理を1秒あたり60回実行できることを 想定しているためである。一般的なブラウザ環境はこの想定を満たしているはずだ。

しかし、この想定が常に有効とは限らない。 ユーザーは旧型のスマートフォンのような処理速度が遅い機器を使っているかも知れない。 また、OS上でブラウザ以外の重たいプログラムを実行しているかも知れない。 「ブラウザは1秒に60フレーム描画できる」という想定が正しくない状況はいくらでも考えられる。 もしかしたら、2020年の世界ではあらゆるマシンが毎秒240フレーム処理しているかも知れない。 ユーザーがゲーマーで、高性能CRTモニターを秒間90フレーム表示できる設定にしているかも知れない。

この問題がどういう意味か、目で見てわかるようなサンプルを用意してみた。

この例では、どの'F'も同じ速度で回転することが期待されている。 中央の'F'は、「期待した速度」且つ「フレームレート非依存の実装」で回転している。 左右の'F'はいずれも、擬似的に「実行環境が1/8の速度しかない状況」を再現している。 左の'F'は、「フレームレートに依存した実装」、 右の'F'は、「フレームレートに依存しない実装」となっている。

左の場合、フレームレートが60より小さくなることを想定していないため、 期待した速度よりも遅い速度で回っていることに注目してほしい。 一方、右の場合は、フレームレートは同じく1/8となっているが、期待した速度、つまり、 フルスピードで回転している中央のFと同じ速度で回っている。

アニメーションを「フレームレート非依存にする」ということは、 「あるフレームと次のフレームの間にかかる時間」を計算して、 その時間を元に「画面に表示される物体をどれくらい動かすか」を計算する、ということである。

まず、この時間を知る必要がある。幸いなことにrequestAnimationFrameを使えば、 この時間を「ページをロードしてからコールバックするまでにかかった時間」として知ることができる。

私は時間を秒単位で扱うのがわかりやすいと思うが、 requestAnimationFrameは時間をミリ秒(1秒の1/1000)単位で扱うので、 これに0.001を掛けている。

以上のことから、フレームとフレームの間の小さな経過時間(deltaTime)はこのようにして求められる。

*var then = 0;

requestAnimationFrame(drawScene);

// シーンを描画する。
*function drawScene(now) {
*  // 時間の単位をミリ秒から秒に変換する。
*  now *= 0.001;
*  // 現在時刻から、前回のフレームの時刻を引く。
*  var deltaTime = now - then;
*  // 次回のフレームで利用するために、現在時刻を記憶しておく。
*  then = now;

   ...

「フーレム間に何秒かかったか」がdeltaTimeとして得られたので、次はこれを元にして 画面上で「『1秒あたりどの程度(何単位)』動くべきか」について考えることができる。 今回の場合「1秒あたり、rotationSpeedを、1.2単位動かす」ことにしている。 動きの単位は好きなように決めればよいが、 今回は「1秒あたり1ラジアン回転する動きを1単位とする」ことにしたので、 1.2なら「1秒あたり1.2ラジアン回転する」、という意味になる。 1.2ラジアンは1/5回転に相当するので、表現を変えれば「一回りに5秒ほどかけて回転する」という意味になる。 フレームレートに関わらず「一回りに5秒ほどかけて回転する」のである。

*    rotation[1] += rotationSpeed * deltaTime;

以上を実装した結果がこれである。

低速のマシンでないと、最初のサンプルとこのサンプルの動きの違いは見てもわからないかも知れない。 しかし、フレームレートに依存しない実装を心がけておかないと、一部のユーザーは 作り手が意図したものとはまったく違ったものを体験する可能性がある、ということだ。

次回は「テクスチャーの使い方」についての講義となる予定だ。

setIntervalやsetTimeoutは禁止!

過去にJavaScriptでアニメーションするプログラムを書いた経験がある読者は、 描画関数を呼び出す際にsetIntervalsetTimeoutといったAPIを 使ったことがあると思う。

アニメーションを実現する目的では、 setIntervalsetTimeoutには共通した問題がふたつある。 ひとつめは、これらのAPIは「ブラウザの描画と関係なく実行される」という点である。 これらのAPIは、ブラウザがフレームを書き始めるタイミングと関係なく実行されるので、 ユーザー環境のマシンと同期のしようがないない。 setIntervalsetTimeoutを使って秒間60フレームの想定で 描画しようとしているのにマシンが別のフレームレートで動作していた場合、 「同期ずれ」が起こる可能性がある。

もうひとつの問題は「ブラウザはsetIntervalsetTimeoutが どういう目的で使用されているか知ることができない」という点である。 これらのAPIを使う限り「選択したタブが最前面にない」などの理由でページが非表示になっていても、 コードが実行中であっても、ブラウザはそのことを知ることができない。 setIntervalsetTimeoutが メール受信や新規Tweetの確認のような「非表示の状況でも実行し続けてほしい用途」 で使われているかも知れない。が、ブラウザーはそれを知ることができない。 「新規メッセージを確認する」のは数秒に1回実行するのが適切だろうが、 「WebGLで1000オブジェクトを描画する」のには適切ではないだろう。 同様に「非表示状態のタブ上のページでアニメーションを実行し続ける」のは適切ではない。 ユーザーのマシンにDoS攻撃をするようなものである。

requestAnimationFrameはこのふたつの問題を解決する。 このAPIを使えば画面の同期に合わせてアニメーションが描画されるし、 ページが表示されていない時はそもそも実行されない。

質問? stackoverflowで質問(英語).
問題点/バグ? githubでissueを作成.
comments powered by Disqus