BetweenAS3 でやっぱり物理的なイージングをサポートしたい。基本的には時間に基づくトゥイーンしかサポートしていないんだけど、「時間から現在値を算出する関数」と「目的地に着くまでにかかる時間を算出する関数」が導出できれば、組み込むことができる。というわけで、色々やっていたら、なんとなくできた。
今回は、誰もが一度は書いたことがあるであろう、フレームごとに現在値から目的地まで距離の半分ずつ近づく (ゼノンのパラドックスのみたいな) アレについて考えてみる。元コードはこんなイメージ。
function enterFrameHandler():void
{
x = x + (d - x) / 2.0;
}
まあ見覚えあるよね。x が現在値で d が目的地。
まずはじめに、この関数を一般化するところから。開始値を b として、係数 (上のコードでは 2.0 になってる値) を m としたとき、n フレーム目の x の値はどうやったら求まるだろうか?まず何も考えずにプログラム的に書くとこうなる。
x = b;
for (i = 1; i <= n; ++i) {
x = x + (d - x) / m;
}
見ての通り、x に初期値 b を設定し、そのあと、n 回ぶんだけ、先ほどの式を計算しているわけだ。これで、n フレーム目の x の値が求まる。
実はこれを、数学的に記述することができる。n フレーム目の x の値を求める関数を f としよう。すると、f は漸化式というものを使って、以下のように記述することができる。
f{1} = b
f{n + 1} = f{n} + (d - f{n}) / m
※{n} は第 n 項を示すために考えた適当な記法です
漸化式そのものについては詳しくは解説しないけど、なんとなく分かると思う。1 フレーム目の x の値は f(1) で、すなわちb になる。2 フレーム目の x の値は、f(2) = f(1) + (d - f(1)) / m = b + (d - b) / m といったように、ひとつ前の項の関数の結果を元に算出される。関数の再帰なんかをイメージすると分かりやすいか。
さて、漸化式が出来たので、これの一般項を求めると、フレーム数すなわち、現在時間から現在値を算出する関数となる。一般項の求め方なんかとっくの昔に忘れたけど、上の漸化式を次のように変形すると、
f{n + 1} = f{n} + d / m - f{n} / m
f{n + 1} = f{n} - (1 / m) * f{n} + d / m
f{n + 1} = (1 - 1 / m) * f{n} + d / m ... (A)
a{n + 1} = p * a{n} + q という形になり、この一般項の求め方 (超ありがたいネットすばらしい) に当てはめることができる。順番にやっていくと、まず f{n + 1} と f{n} の場所に X を入れ、特性方程式と呼ばれるものを作る。
X = (1 - 1 / m) * X + d / m
これを X について解く。
X - (1 - 1 / m) * X = d / m (1 - (1 - 1 / m)) * X = d / m (1 - 1 + 1 / m) * X = d / m (1 / m) * X = d / m X = (d / m) / (1 / m) X = d
続いて、特性方程式の解を f{n + 1} と f{n} から引き、
(f{n + 1} - d) = (f{n} - d)
漸化式の f{n} の係数を右辺に掛け、新たな漸化式 (B)を作ります。
(f{n + 1} - d) = (1 - 1 / m) * (f{n} - d) ... (B)
ここで試しにこの式を展開してみると、
f{n + 1} - d = (1 - 1 / m) * f{n} - (1 - 1 / m) * d
f{n + 1} - d = (1 - 1 / m) * f{n} - d + d / m
f{n + 1} = (1 - 1 / m) * f{n} + d / m
となり、最初の漸化式 (A) の変形であることが分かります。つまり、最初の漸化式 (A) は新たな漸化式 (B) に変形可能というわけです。
さて、ここで、f{n} - d という数列を、f'{n} と名付けることにします。
f'{n} = (f{n} - d)
この f'{n} を用いて、先ほどの新しい漸化式 (B) を置き換えます。
f'{n + 1} = (1 - 1 / m) * f'{n} ... (C)
すると、この式 (C) は等比数列の漸化式となり、等比数列 a{1} = a1; a{n + 1} = a{n} * r の一般項 a{n} = a1 * Math.pow(r, n - 1) より (Math.pow(m, n) は m の n 乗を求める関数です)、
f'{n} = f'{1} * Math.pow(1 - 1 / m, n - 1)
と書くことができ、f'{n} の一般項が求まります。ここで、f'{n} は (f{n} - d) であるので、それに従い上の式を置き換えると、
f{n} - d = (f{1} - d) * Math.pow(1 - 1 / m, n - 1)
となります。f{1} は b だったのでこれを代入すると、
f{n} - d = (b - d) * Math.pow(1 - 1 / m, n - 1)
整理して、
f{n} = (b - d) * Math.pow(1 - 1 / m, n - 1) + d ... (D)
というわけで、見事一般項 (D) が求まりました。わーぱちぱち。早速、以下のような ActionScript を書いて、合っているか確かめてみましょう。
function f1(b:Number, d:Number, m:Number, n:Number):Number
{
var x:Number = b;
for (var i:Number = 1; i <= n; i += 1.0) {
x = x + (d - x) / m;
}
return x;
}
function f2(b:Number, d:Number, m:Number, n:Number):Number
{
return (b - d) * Math.pow(1 - 1 / m, n - 1) + d;
}
for (var i:uint = 0; i < 10; ++i) {
var n1:Number = f1(10.0, 20.0, 2.0, i);
var n2:Number = f2(10.0, 20.0, 2.0, i + 1);
trace(n1, n2);
}
f1 が最初のループ版、f2 が今求めた式です。それぞれの 10 フレーム目までの値を出力して調べてみましょう。
10 10 15 15 17.5 17.5 18.75 18.75 19.375 19.375 19.6875 19.6875 19.84375 19.84375 19.921875 19.921875 19.9609375 19.9609375 19.98046875 19.98046875
見事に一緒ですね。すばらしー。
さて、これだけでは終わりません。実際、目的に到達するまでどれくらいの時間 (フレーム数) がかかるのかを求める関数も導出する必要があります。先ほどの関数が、値 th になる瞬間を考えます。
th = (b - d) * Math.pow(1 - 1 / m, n - 1) + d
フレーム数は n として与えていたので、これを n について解くことになります。まずは整理して、
th - d = (b - d) * Math.pow(1 - 1 / m, n - 1) (th - d) / (b - d) = Math.pow(1 - 1 / m, n - 1)
Math.pow(a, x) = b という形になったので、ここの指数方程式の解き方 2 に習い、対数を使用して式を変形します。
n - 1 = Math.log(1 - 1 / m, (th - d) / (b - d))
ここで、Math.log(a, x) は底 a に対する x の対数を求める関数を想定しているのですが、残念ながら ActionScript にはそんな関数は無く、Math.log(x) という、自然対数を求める関数しかありません。そこで、底の変換公式を用いて、自然対数から算出出来るよう式を変形します。
n - 1 = Math.log((th - d) / (b - d)) / Math.log(1 - 1 / m)
整理して、
n = Math.log((th - d) / (b - d)) / Math.log(1 - 1 / m) + 1
というわけで、値が th になるまでにかかるフレーム数 n を求める関数を作ることが出来ました。早速 ActionScript で書いてみましょう。
function f3(b:Number, d:Number, m:Number, th:Number):Number
{
return Math.log((th - d) / (b - d)) / Math.log(1 - 1 / m) + 1;
}
trace(f3(10.0, 20.0, 2.0, 19.999));
上の式を用いて f3 という関数を作りました。ここで注意なのですが、x = x + (d - x) / m という式では、x が d に限りなく近づきますが、d になることはありません。というわけで、th に d を指定すると、無限になります (実際関数 f3 は Infinity を返します)。なので、th は終了値限りなく近い値、ここでは 20.0 に限りなく近い 19.999 を指定しています。これを実行すると、
14.287712379547685
となり、訳 14.3 フレームで 19.999 に到達することが分かります。確かめるために f2 に突っ込んで、
trace(f2(10.0, 20.0, 2.0, 14.287712379547685));
確認してみると、
19.999
となり、正しいことが分かります。
以上で、見事「時間から現在値を算出する関数」と「目的地に着くまでにかかる時間を算出する関数」を導出することが出来ました。同時に、BetweenAS3 が物理的なトゥイーンをサポートすることが決定致しました。めでたしめでたし。数学すごい。
サイト内関連記事
この記事へのトラックバック
トラックバックはありません。
TrackBack URL:
http://www.be-interactive.org/trackback.php?id=504

コメントはありません。