ホーム < ゲームつくろー! < Programming TIPs編

その19 補間関数あれこれ


 ゲーム制作の中では「補間」があちこちに出てきます。補間とは一般に2つの値の間を媒介変数t(=割合t)で表したものです。例えばキャラクタが地点AからBへ移動する時、時間が経過すると少しずつBへ向かいます。この「向かい方」は補間関数によって色々変化させる事ができます。他にも絵をフェードインさせる時とか、エフェクトを動かす時など、実に様々です。そして、この補間関数によってゲームの面白さのかなりな部分が決まってしまうのです。この章では、そんな補間関数をあれこれ見て行きます。



@ 線形補間

 補間で一番単純なのは線形補間です。これは2つの値の間を等速で移動する補間です:

横軸が媒介変数tで、縦軸がそれに対応する値です。グラフを見ると一目瞭然で、線形補間の場合はtと値vは一緒になります。つまり、

です。正に線形(^-^)。この媒介変数tに対するv(t)が「補間関数」です。

 で、このv(t)をどう使うかが本章の肝です。t=0の時の値をs、t=0の時の値をeとすると、補間値I(t)はv(t)を用いて一般に次のように計算されます:

この式はv(t)の形がそのまま補間値Iに反映されます。つまり、線形補間の場合ならsからeに向かって等速に値が変わっていく事になります。



A 2次関数補間

 補間関数はv(0)=0、v(1)=1となるように関数を選ぶのがコツです。曲線的な補間関数としてまず挙げられるのが2次関数補間。これには次の2種類が考えられます:

青いグラフの方は、t=0からvがゆっくりと上昇します。こういうのを「ease-in」と言います。一方赤いグラフの方はv(1)の時に値が徐々に緩やかになります。これは「ease-out」と呼ばれています。「ease」というのが「緩やかに」という意味なので、何となく区別できるかなと思います。

 両方の補間関数はそれぞれ以下のようになります:

上が青い方、下が赤い方です。



B 3次関数補間

 ゲームの補間関数として多分一番実用されているのがこの3次関数補間です。補間の種類としては「ease-in, ease-out」となります。つまり、緩やかにスタートして真ん中で一番速くなり、最後もゆっくりとなる補間です。皆さんがお持ちのゲームを見ると、あらゆる所でこのease-in, ease-outな補間が使われているのが分かると思います:

このような関数は方程式を解く事で得る事ができます。まず、関数は3次関数です。式で言うとv(t)=a*t^3 + b*t^2 + c*t + d です。グラフは原点を通っているのでd=0はすぐに分かります。またt=0とt=1で平らになっていますよね。これは微分係数が0だという事です。v(t)をtで微分した式は v'(t)=3a*t^2 + 2b*t^2 + c となりますが、t=0で傾きゼロなので、c=0もわかります。結局上の式は、v(t)=a*t^3 + b*t^2 という事がわかります。t=1の時にv(t)=1なので、1 = a + b。これが1本目の式。2本目の式は微分式でt=0の時にv'(1)=0(傾きゼロ)なので、0 = 3a + 2b。この2本の式を連立方程式で解けば、a=-2、b=3がわかります。つまり上のグラフは、

という事がわかりました。これ、物凄い使います。



C cos補間

 ease-in, ease-outなもう一つの補間としてcos補間があります。その名の通りcosカーブを使った補間です:

青い方がcos補間で、補間式は次のようになります:

cosは0〜πの間で1〜-1まで数値が動きます。1からそれを引けば0〜2となるので、それを半分に割れば上のようになるというわけです。

 ピンク色の方は3次関数補間です。これを見るとわかるように、両者の間に殆ど違いはありません。3次関数補間の方が若干ease感が無いという感じですが…多分殆どわかりません(^-^;。これなら、普通は計算負荷が軽い3次関数補間を使いますね。



D 補間関数の使い方

 ここまで挙げてきた補間関数を実際にどう使うのか?色々な所に使えるのですが、例えばキャラクタをA地点からB地点に10秒かけて移動させたいという例を見てみましょう。

 A地点の座標をA(Ax, Ay, Az)という3次元ベクトルで表現します。B地点はB(Bx, By, Bz)です。ゲームはupdateメソッドが毎フレーム呼ばれますので、メソッド内で前回のフレームからの差分を取得します。UnityだとTime.deltaTimeがそれに該当するので楽です。

 updateメソッドの中身は例えばこうなります:

void update() {
    elapsedTime += Time.deltaTime;   // 経過時間

    float t = elapsedTime / 10.0f;     // 時間を媒介変数に
    if ( t > 1.0f )
        t = 1.0f;    // クランプ

    float rate = t * t * ( 3.0f - 2.0f * t );   // 3次関数補間値に変換

    Vector3 p = A * ( 1.0f - rate ) + B * rate;   // いわゆるLerp
}

elapsedTimeが経過時間で、それを10秒で割り算すれば媒介変数tが算出できます。tの範囲は通常0〜1の間なので、tが1を超えた場合はクランプします。このtをそのまま使えば線形補間になりますが、それだとキャラクタの動きが機械的なので、上の例では3次関数補間にしています。求めたrateでA地点とB地点を線形補間(Lerp)してあげるとAからBへease in, ease outな動きをしてくれます。

 補間は本当に良く使うので、上のような3次関数補間などを関数化しておくのも手です。例えば、

Vector3::Cube補間
static Vector3 Vector3::cube(
    const Vector3 &A,
    const Vector3 &B,
    float min,
    float max,
    float elapsed
) {
    elapsedTime += Time.deltaTime;   // 経過時間

    float t = ( elapsed - min ) / ( max - min );     // 時間を媒介変数に
    bool isOut = false;
    if ( t < 0.0f ) {
        t = 0.0f; isOuter = true;
    } else if ( t > 1.0f ) {
        t = 1.0f; isOuter = true;
    }

    float rate = t * t * ( 3.0f - 2.0f * t );   // 3次関数補間値に変換

    return A * ( 1.0f - rate ) + B * rate;   // いわゆるLerp
}

こういうcube補間メソッドを作っておくと潰しが利きます。

 他の補間関数も同じように作れますが、違うのはrateを求めている1行だけです。なので、ここの部分だけ外から呼び出すスタイルにするという実装もありです:

Vector3::lerp補間(補間関数を外部から)
static Vector3 Vector3::lerp(
    const Vector3 &A,
    const Vector3 &B,
    float min,
    float max,
    float elapsed,
    float (*func)(float)   // 補間関数
) {
    elapsedTime += Time.deltaTime;   // 経過時間

    float t = ( elapsed - min ) / ( max - min );     // 時間を媒介変数に
    bool isOut = false;
    if ( t < 0.0f ) {
        t = 0.0f; isOuter = true;
    } else if ( t > 1.0f ) {
        t = 1.0f; isOuter = true;
    }

    float rate = (*func)( t );   // 引数の補間値に変換

    return A * ( 1.0f - rate ) + B * rate;   // いわゆるLerp
}

第6引数にfuncが追加されています。これは次のように使います:

float cube( float t ) {
    return t * t * ( 3.0f - 2.0f * t );
}

int main() {
    Vector3 A( 0.0f, 1.0f, 2.0f );
   Vector3 B( 3.0f, 5.0f, 7.0f );
    float elapsed = 3.0f;

    Vector3 pos = Vector3.Lerp( A, B, 0.0f, 10.0f, elapsed, cube );
}

これで2点間を3次関数補間する事が出来ます。線形補間(linear)とか2次関数(easeIn)などを作れば、同じような呼び出しで別の移動を実現できます。これがもう少し進歩すると、いわゆる「Tweener」等の補間ライブラリが作れるわけです。