初めに
はじめまして。
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の呼び出しとの時間差です。
ループ全体のコードを示します。
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{秒}{フレーム}$$
終わりに
駆け足感のある記事になってしまいました。
初投稿なので間違いがあるかもしれません。もし問題がありましたら連絡ください。