BeInteractive!

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

この記事へのコメント

コメントはありません。

コメント書き込み:

カテゴリ

タグ

アーカイブ