Redpoll's 60
第1章 2D空間の基礎

$§$1-9 フレームとフレームレート


A) フレームとフレームレート

前節までのプログラムの実行結果は全て画面内に変化が生じない静止した映像であった。コンピューターグラフィックスにおいて作成される画面内での動きのある映像、すなわち動画は大量の画像を1枚1枚連続的に画面に表示することによって実現されている。動画を構成する、この1枚1枚の画像のことをフレーム(Frame)という。1秒間あたりに画面に表示されるフレーム数をフレームレート(Frame Rate)といい、FPS (Frame Per Seconds)を使って表す。例えば、1秒間に30フレーム表示されるならば30FPS、60フレーム表示されるならば60FPSである。
30FPSであれば、
\[1 / 30 = 0.0333\ldots\]約0.033秒間隔でフレームが切り替わる。
60FPSであれば、
\[1 / 60 = 0.0166\ldots\]約0.017秒間隔でフレームが切り替わる。


B) いくつかの例

図1  4FPS
では実際に、いくつかのフレームレートでオブジェクトを動かすアニメーションを見てみよう。
ここで使う例は、円形オブジェクト Circleが原点から$x = 4$の位置まで1秒間で移動するアニメーションである。まず右図1は、このオブジェクトの運動を4FPSで実行したものである。画像左上の白い数字はフレーム番号を表している。原点から$x = 4$の位置に移動するまでに1秒かかる設定であるから、4FPSであればフレーム番号は Frame 1 から Frame 4 まで変化する(原点から$x = 4$までの移動にフレームが4枚使われている)。

アニメーションを見ればわかる通り、4FPSという映像はなめらかさとはほど遠い(各フレームは約0.25秒間隔で切り替わる)。1枚1枚のフレームがはっきりと見分けられるようでは動画としては不充分である。
各フレームを図2から図5に示す。

図2 Frame1
図3 Frame2

図4 Frame3
図5 Frame4

次に16FPS、60FPSの場合を見てみよう(図6、図7)。

図6  16FPS
図7  60FPS

原点から$x = 4$までの移動に、図6のアニメーションではフレームが16枚使われており、図7のアニメーションでは60枚使われている。16FPSではまだカクカクした感じがあるが、60FPSになるとさすがに流れるように動いている (本節で使わているアニメーションGIFでは、原点から$x = 4$までの移動にかかる時間は正確に1秒ではなく多少の ずれ があることに注意)。

この講義で使われるプログラムは全章を通じて基本的には 30FPS、あるいは 60FPSの設定で実行される。しかし、環境によっては(使用されるマシンの性能によっては)、60FPSを下回る場合も起こり得ることだけは注意しておこう(ただし、文章中のアニメーションGIFのFPSの値はそこまで高くはない)。




実行するプログラムとフレームの関係について以下で具体的に見ていこう。


# Code1
以下のCode1のフレームレートは60FPSで設定している。

[Code1]  (実行結果 図8)
i_frameCount++;
if (Time.time >= i_nextTime)
{
    Debug.Log(Time.time + "  :  " + i_frameCount);
    i_frameCount = 0;
    i_nextTime = Time.time + 1;
}

「Code1のフレームレートが60FPS」という設定は、1秒間にCode1が60回呼ばれるということである。つまり、上の処理が1秒間に60回実行されるのである (フレームレートは本来は、冒頭でも説明したように1秒間あたりのフレーム表示回数のことであるが、ここでは単に1秒間あたりのCode1の実行回数として話を進める)。
1行目の i_frameCount はフレーム数をカウントするためのint型のインスタンス変数である (初期値は$0$)。2行目の Time.time はUnityで用意されているプロパティで、プログラム実行開始からの経過時間を秒で保持している (値はfloat型)。i_nextTimeは、2行目の ifブロックの条件判定用のfloat型のインスタンス変数である。この変数の初期値は$2$であり、これによってプログラム実行後に最初に ifブロックに入るのは実行開始から$2$秒経ってからということになる。
図8 Code1 実行結果
図8は このプログラムの実行結果である。最初に出力されたメッセージ「2.01141 : 101」は、プログラム実行開始から 2.01141秒経過して初めて2行目のifブロックに入り、そのときまでに i_frameCount は101回インクリメントされたことを意味している。すなわち、プログラム実行開始から101フレーム目で初めて経過時間が2秒を越えたわけである。ifブロックに入ると5行目でフレームカウント用の変数 i_frameCountが$0$にリセットされる。さらに、6行目では i_nextTimeTime.time + 1 をセットしている。最初に ifブロックに入ったときの Time.timeの値は上に示されるように「2.01141」であるので、このときの6行目の i_nextTimeの値は「3.01141」となる。これは「次に ifブロックに入るときは Time.timeの値が 3.01141以上になってから、すなわちプログラム実行開始から 3.01141秒経過してから」という設定をしているのである。つまり、次に ifブロックに入るまでに1秒の間隔があり、その間にも このCode1は呼ばれ続けるので1行目の i_frameCount++ は実行される。そして、Time.timeの値が「3.01141」以上になったときに再び ifブロックに入るが、このときに出力されたメッセージ(図8)は「3.019581 : 60」という内容である。このメッセージが意味することは、2回目に ifブロックに入ったのはプログラム実行開始から 3.019581秒後であり、前回 ifブロックに入ったときから約1秒経過している。そして、その間に i_frameCountは60回インクリメントされた、すなわち、約1秒の間にCode1が60回実行されたということである。続いて、3回目に ifブロックに入ったときに出力されたメッセージは「4.027521 : 60」であるが、これは3回目に ifブロックに入ったのはプログラム実行開始から 4.027521秒後であり、2回目に入ったのが 3.019581秒後であったので、やはり、ここでも約1秒の間隔があるが、その間に i_frameCountがインクリメントされた回数が60回であることを示している。それ以降に出力されているメッセージを見ても同じように、ifブロックには約1秒間隔で入り、その間に i_frameCountは60回インクリメントされている。すなわち、これはこのプログラムが60FPSで実行されていることを示すものである。


# Code2
本節 B)の図1は円形オブジェクト Circleが原点から $x = 4$ までの間を1秒間で進むアニメーションであった。そのときのフレームレートは4FPSであり、1秒間に4枚のフレームが表示された。以下のプログラムの実行結果は、このアニメーションと同じ内容になるものである。

[Code2]  (実行結果 図1)
if (Input.GetKeyDown(KeyCode.M))
{
    i_MOVE = true;
    i_frameCount = 0;
}

i_frameCount++;
int nFrames = 4; 
if (i_MOVE)
{
    float mx = (i_frameCount - 1) * (4.0f / (nFrames - 1));
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(mx, 0.0f);
    Circle.SetMatrix(T);
}

if (i_frameCount == nFrames)
{
    i_MOVE = false;
}

このプログラムでは、Mキーを押すと Circleが原点から $x = 4$ までの移動を実行する。
Code2のFPSは4FPSで設定されており、実行結果は図1と同じである。1行目の Input.GetKeyDown(..) は、Unityに用意されているキー操作のためのメソッドであり、ここでの意味は Mキーを押した際にこのメソッドが trueを返し、1行目の ifブロックに入ることになる (キー操作のメソッドに関しては 2-8節にて解説する)。この ifブロックで bool型のインスタンス変数 i_MOVEtrueになるが、これによって9行目の ifブロックに入ることになる。9行目の ifブロックは Circleの移動位置を計算し、その位置に Circleを移動させる処理が記述されている。Code2の場合は、原点から $x = 4$ までに移動する各位置は、上記の図2から図5の位置である。7行目で i_frameCountの値が4になったとき、すなわち Circleの移動を開始してから4フレーム目に Circleは $x = 4$ に到着するが、そのフレームにおいては、16行目の ifブロックの判定が trueとなるので i_MOVEfalseがセットされ、次のフレームからは9行目の ifブロックには入らなくなる、つまり、Circleの移動処理が行われなくなる。しかし、再び Mキーを押すと、1行目の ifブロックに入るので Circleが原点から移動を開始することになる。

図9 Code2のフレームレートの表示
もし、Code2の内容をすべて消去して、代わりに Code1の内容を貼り付けると図9に示されるメッセージが出力される。その内容から、Code2の場合では約1秒の間に4回 i_frameCountがインクリメントされていることがわかる。すなわち、Code2は4FPSで実行されているのである。


なお、本節のプログラムはこの他に Code3、Code4がある。しかし、その内容は Code2と同じものである。異なる点は、Code3は16FPS、Code4は60FPSで設定されている点である。プログラムの変更箇所は1箇所のみで、8行目のローカル変数 nFrames の値が、Code3では 16、Code4では 60となっている (それぞれの実行結果は 図6、図7と同じ内容である)。




C) プログラム実行中のオブジェクトの速度

フレームレートに関して1点追記する。
プログラム実行中のオブジェクトの速度(単位時間あたりの移動量)を指定する方法は大別して2つある。1つは時間(秒)に依存する方法であり、もう1つはフレームレートに依存する方法である。
上で扱った例では「1秒間にCircleを $4$ だけ移動させる」というものであったが、これは時間依存の指定方法である。具体的には、上で見たようにプログラム実行中のフレームレートが4FPSや16FPS、60FPSと違っていても、Circleは(約)1秒の間に $4$ だけ進んだ。

時間依存の指定方法の簡単な例を見てみよう。以下のプログラムは1秒間にオブジェクトを x軸方向に $10$ だけ移動させるものである。
// 1秒間に10だけ進む
i_posX += Time.deltaTime * 10.0f;
THMatrix3x3 T = TH2DMath.GetTranslation3x3(i_posX, 0.0f);
Obj.SetMatrix(T);

i_posX は各フレームにおける Obj の x軸上の移動位置を表すfloat型インスタンス変数であり初期値は $0$ である。毎フレーム少しずつ加算され、1秒ごとに約 $10$ ずつ増加する。
Time.deltaTime は Unityの標準APIに用意されているプロパティで、前回のフレームから今回のフレームまでの経過時間を表す。この経過時間は float型の 秒 で表される。
例えば 60FPSならば毎フレーム約$0.017$秒ごとに実行されるが、このときの各フレームにおける Time.deltaTime の値も約$0.017$である。30FPSならば毎フレーム約$0.033$秒ごとに実行されるが、このときの各フレームにおける Time.deltaTime の値も同じく約$0.033$である。
本節のCircleの例で見たように、時間依存の指定方法ではオブジェクトの単位時間あたりの移動量はフレームレートで変化しない。上のプログラムは時間依存の指定方法であり、オブジェクトを1秒間で $10$ だけ移動させるというものであるが、これはフレームレートが30FPSであっても60FPSであっても120FPSであっても変わらない。フレームレートがどのような値であれ、1秒間の移動量は(約)$10$である。
実際、プログラム2行目の以下の計算において
i_posX += Time.deltaTime * 10.0f;
60FPSならば Time.deltaTime の値は毎フレーム約$0.017$であるから、具体的には
i_posX += 0.017f * 10.0f;
が1秒間に60回実行されるが、このとき i_posX は1秒ごとに約$10$ずつ増加していく ($0.017\times 10.0 \times 60 = 10.2$)。
30FPSならば Time.deltaTime の値は毎フレーム約$0.033$であるから、
i_posX += 0.033f * 10.0f;
が1秒間に30回実行される。60FPSの場合と比べて1秒間に実行されるフレーム数は半分になるが、Time.deltaTime の値が約2倍になっているので、結果として1秒ごとに i_posX が約$10$ずつ増加することは変わらない ($0.033\times 10.0 \times 30 = 9.9$)。
120FPSでも同様である。120FPSの場合、60FPSに比べて1秒間に実行されるフレーム数は2倍になるが、Time.deltaTime の値が約半分になるので結果としては i_posX の1秒あたりの増加量はやはり約$10$である。
すなわち、時間依存の指定方法では異なるフレームレートであっても、単位時間あたりのオブジェクトの移動量は(理論的には)変わらないのである。これは言い換えれば、プログラムを異なるフレームレートで実行しても、オブジェクトの運動はどのフレームレートで見たときにも同じ速さに見えるということである。
また、オブジェクトの運動の記述に物理演算が多用されている場合には時間依存の指定が適している。物理演算では その計算に秒を単位とした経過時間を使うことが多いためである。

もう1つの指定方法であるフレームレートに依存する方法では「1フレームあたり $0.1$ずつ移動」あるいは「毎フレーム $6$°ずつ回転」のように指定する。この指定方法では単位時間あたりのオブジェクトの移動量はフレームレートによって変化する。
例えば、1フレームあたり $0.1$ずつの移動では 60FPSの場合、1秒間の移動量は $6$ であるが、30FPSならば1秒間の移動量は $3$ である。また、1フレームあたり $6$°ずつの回転であれば 60FPSの場合、1秒間に1回転するが、30FPSならば1秒間の回転角度は $180$°である。
つまり、フレームレートに依存する指定方法ではフレームレートが高いほどオブジェクトの運動が速く行われ、フレームレートが低いほどオブジェクトの運動は遅くなっていくのである。

しかし、フレームレートはあらゆるFPSを想定するというよりも、60FPSや30FPS、近頃では120FPSなどのようにある特定のフレームレートに固定して進めていくのが普通である (上でも触れたが、この講義で使うプログラムのフレームレートは 60FPSか 30FPS固定であり、読者が調整する必要はない)。
この講義におけるオブジェクトの速度指定については、必要に応じて時間依存の指定を行うが、主としてはフレームレート依存による指定である。特に解説用の文章などでは「1フレームあたり$0.2$ずつ移動」であるとか「毎フレーム $5$°ずつ回転」のように具体的な数値を使う方が、解説を進めやすい場合が多い (フレームレート依存の指定であれば上記のように $0.2$ や $5$ といった簡単な数値を使えるのである)。












© 2020-2024 Redpoll's 60 (All rights reserved)