ease out: x[n] = (1 - r)^n * (x[0] - p) + p
UI 部品のなめらかな移動はゲームに限らず、 iPod などの iOS や Android などの組み込み製品や最近のデスクトップアプリケーションでもみかけるようになった。
数年前、自社の UI フレームワークを移植する仕事をしていたときに、その動きを実現する式が x += (target - x) * rate だと知り、みかけは単純な式がうみだす滑らかな動きにびっくりしたことを覚えている。
先日、 Keijiro's Dev Log — 指数関数を使ったお手軽イーズ・アウト というエントリーをみかけ、あの動きに ease out という名前がついているらしいこと、当時とても悩まされた「フレームレートが変わると操作感が劇的に変わってしまう」ことが言及され、その解決方法が示されていることに軽く興奮した。
この問題を解決するには、上で例示した指数関数を、フレームレートを含んだ形で漸化式に直せばいい。過程は省略するけれど(高校数学の指数関数を思い出して!)こんな感じの式になる。
pos += (target - pos) * (1.0 - exp(-6.0 * deltaTime));
残念なことにこの漸化式にでてくる exp(-6.0 * deltaTime) がどのようにして導きだされるかはわからなかったけれど、一方で「漸化式を直す」という語から漸化式は解くことができるとおもいだし、実際に解いてみた。
結果の式がこのエントリーのタイトル、
x[n] = (1 - r) ^ n * (x[0] - p) + p
である。(r は変化率、 p はターゲット位置)
解いたあとの漸化式は直前の値に依存しなくなるので、たとえば(いくつかのフレームをスキップして)特定のフレームから画面部品の位置を構築しなおすといったことが可能になる。また連続関数とみてフレーム間の絵を挿入することもできるだろう。
(当時、漸化式を解くという発想がなかったため、移動が完了するまでの時間を変化率 r をちまちま変えて動かしては確認し…という作業をしていた自分が恥ずかしい。)
ease out 漸化式の解き方
もともとがプログラミング言語での x += (p - x) * r; という式なので、これを漸化式に直す。
加算代入演算子 += を展開すると x = x + (p - x) * r; になる。プログラミング言語の等号は、数学での等値でなく右辺で左辺を更新する代入、つまり漸化式になる。よって解くべき漸化式は、
x[n + 1] = x[n] + (p - x[n]) * r
だ。これを x について整理すると、
x[n + 1] = (1 - r) * x[n] + r * p
となる。等比数列の形にしたいので両辺の x を X と置いた特性方程式
X = (1 - r) * X + r * p
を解き X - p = 0 (r ≠ 0) を得る。つまり x - p の形で先の漸化式の両辺を整理することができる。
x[n + 1] - p = (1 - r) * (x[n] - p)
ここで y[n] = x[n] - p と置くと、
y[n + 1] = (1 - r) * y[n]
なので y[n] = (1 - r) ^ n * y[0] となり、あらためて y を x に直して整理すことで、
x[n] = (1 - r) ^ n * (x[0] - p) + p
が得られる。
実際の応用で指数関数 (1 - r の n 乗) を都度計算するのはコストが高すぎるので、あらかじめテーブルをつくっておくことになるだろう。 UI 部品が移動完了する時間は二秒もみておけば十分だろうから、 60fps としても 120 エントリーで済む。また大きな精度が要求されるところでもないだろうから 256 倍した整数部、つまり 1 エントリー 1 バイトで…とかケチケチ妄想が膨らむ。ご飯三杯くらいいけそう。