Help us understand the problem. What is going on with this article?

[requestAnimationFrame]ループするときのfpsの求め方とfpsに依存しないアニメーション

初めに

はじめまして。

requestAnimationFrameでループを作るときの、fpsの求め方とfpsに依存しない処理の仕方を記します。

あくまでもオリジナルです。ベストなのかは知りません。
この記事ではrequestAnimationFrameをrAFと略します。長いからね。

fpsについて

fpsとは

fpsはframes per secondの略で、1秒あたりのフレーム数です。
$$\frac{フレーム}{秒}$$
と表せます。

fpsの求め方

rAFが、1秒で何回呼び出されるのかを数えても求められますが、今回は数学的なアプローチで求めます。

$$\frac{フレーム}{秒} = 1 \div \frac{秒}{フレーム}$$
とします。逆数からfpsを求めています。fpsの逆数
$$\frac{秒}{フレーム}$$
はunity等ではdeltaTimeと呼ばれるもので、1フレームあたりの秒数です。

ということでdeltaTimeを求めて逆数にすれば、fpsが求められます。

実際に求める

deltaTimeを求める

1フレームあたりの秒数ということで、前回のrAFの呼び出しとの時間差です。
ループ全体のコードを示します。

loop.js
const loop = (func) => {

  let then = 0;

  const _loop = (timeStamp) => {

    id = requestAnimationFrame(_loop);
    const now = timeStamp * 0.001;
    if (then === 0) {

      then = now;
      return;
    }
    const delta = now - then;

    try {

      func(delta);
    } catch (error) {

      cancelAnimationFrame(id);
      throw new Error('In animationLoop, ' + error);
    }

    then = now;
  };
  let id = requestAnimationFrame(_loop);

  return () => cancelAnimationFrame(id);
};

色々と情報量が多いですが一つずつ。

rAFのコールバックに渡される引数(timeStamp)は、windowが読み込まれた時刻からのミリ秒で、0.001を掛けて秒にします。(変数now)
一回目をスキップするのは、計算に前回の時刻が必要だからです。

現在の時刻から前回の時刻を引いて、1フレームの時間であるdeltaTimeを求めます。

ループ内でエラーが出たらキャンセルします。(これが無いと、毎秒60個のエラーが発生します。)

次回の計算のために現在時刻を保存します。(これが前回の時刻として次回に使用されます)(変数then)

loop関数の返り値は、停止をする関数です。

fpsを求める

const stop = loop((delta) => {

  const fps = 1 / delta;
  console.log(Math.round(fps));
});

ご覧の通り数式そのものでfpsが求められました。適当に丸めて表示しています。

fpsに依存しないように

rAFは環境によって呼び出されるタイミングが異なります。60fpsを前提とした(fpsに依存した)実装では、意図した通りに動作しない場合があります。

<canvas></canvas>
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

等速直線運動

fpsに依存しないためには、1フレームでどのくらい進むのかを計算する必要があります。そこでdeltaTimeを使います。

const velocity = 100;

let x = 0;
let y = canvas.height / 2;

const stop = loop((delta) => {

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  x += velocity * delta;

  if (x >= canvas.width) x -= canvas.width;

  ctx.beginPath();
  ctx.arc(x, y, 20, 0, 2 * Math.PI);
  ctx.fill();
});

こんな感じです。
velocityは秒速です。
$$\frac{ピクセル}{秒}$$
このように、一秒に100ピクセル進みます。
300を超えると300を引くことで、左端に戻るようにしています。

数式にするとイメージしやすいと思います。
xの単位をピクセルとして

$$\frac{ピクセル}{フレーム} = \frac{ピクセル}{秒} \times \frac{秒}{フレーム}$$

ということで、秒速(一秒で何ピクセル進むか)にdeltaTime(1フレームで何秒かかるか)を掛けると1フレームで何ピクセル進むかが求められます。これを座標に足して行くことでfpsに依存せずアニメーションできます。

等加速度直線運動

徐々に加速するときはこのようにします。

const acceleration = 50;
let velocity = 100;

let x = 0;
let y = canvas.height / 2;

const stop = loop((delta) => {

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  velocity += acceleration * delta; 
  x += velocity * delta;

  if (x >= canvas.width) x -= canvas.width;

  ctx.beginPath();
  ctx.arc(x, y, 20, 0, 2 * Math.PI);
  ctx.fill();
});

加速度(一秒間にどのくらい加速するか)にdeltaTime(1フレームに何秒かかるか)を掛けることで、1フレームでどのくらい加速するかを求め、速度に足します。(velocityが定数から変数になったことに注意)

$$\frac{速度}{フレーム} = \frac{速度}{秒} \times \frac{秒}{フレーム}$$

更に、先の等速直線運動と同じように計算することで、等加速度直線運動を実現します。

$$\frac{ピクセル}{フレーム} = \frac{ピクセル}{秒} \times \frac{秒}{フレーム}$$

終わりに

駆け足感のある記事になってしまいました。
初投稿なので間違いがあるかもしれません。もし問題がありましたら連絡ください。

39sho
CoderDojo Aizuに行ってます。 javascriptが一番得意。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away