ここではキーフレームアニメーションのためのシンプルで柔軟なキーフレーム間の補間アプローチを提案します。
このアプローチでは線形補間、スプライン補間、球面線形補間等の複数のアルゴリズムによる補間を単一のデータストリームに混在させることができ、
シンプルな共通ルーチンによってフレーム毎に補間された値を得ることができます。
またクリティカルな処理は、数回の分岐と数回の加減算のみであり補間関数の複雑さに殆ど依存しません。
■The Principle
2点a,bにおいて2点間をt区間で線形補間する時、(b-a) / tとして増分値を求め、順次加算によって値を求めていくアプローチを我々はよく使います。
これは一次関数を微分し、順次積分することによって値を求めていると考えることができます。
この考えを多次関数に適用します。
つまり有限回微分可能な関数は、有限回の加算によって表現することができるのです。
Hermiteスプラインや、Bezierなどのパラメトリック曲線は3次関数であり、3階微分と3段階の加算によって導出できます。
また球面線形補間(Slerp)については
f(t) = (sin(θ×(1-t)) + sin(θ×t)) / sin(θ)
と定義され、無限微分可能な関数となります。
従って有限回の微分/積分で表現することはできません。
しかし一定区間内における緩やかな変化は6階微分程度で十分に近似することができます。
それでは任意関数を微分することを考えます。
ここでは数値解析的なアプローチによって任意関数を微分します。
一階微分はf(t+Δt)-f(t)
二階微分は(f(t+2Δt)-f(t+Δt)) - (f(t+Δt)-f(t))
となり、これを一般化し実装すると以下のようなコードになります。
//任意関数を微分する
private int difference(double[] dst,double frame,double delta,int degree)
{
double work[] = new double[degree+1];
for (int i=0;i<degree+1;i++)
work[i] = function(frame + i * delta);
for (int i=0;i<degree+1;i++){
dst[i] = work[0];
for (int j=0;j<degree;j++)
work[j] = work[j+1] - work[j];
}
return degree+1;
}
functionは時間tにおける値を返す任意関数であり、degreeは微分する回数、frameは現在の時間、deltaは時間の増加量です。
まず一定時間における値をサンプリングし、そして段階的に差分を取ることで微分を行います。
その結果dst[0]に初期値、dst[1]に一階微分、dst[2]に二階微分した値がそれぞれ入ります。
これを各キーフレームについて適用し、下のようにストリームデータとして出力します。
■次のキーフレームまでのフレーム数
■関数の次数(定数なら0、線形補間なら1、Hermiteなら3 etc.)
■初期値
■一階微分
■二階微分
:
:
■次のキーフレームまでのフレーム数
:
:
:
■Implementation

Applet
Source
以下は補間を行うクラスの全ソースコードです。
reset();で初期化し、nextFrame();で値を補間し、getValue()で補間した値を取得します。
//-----------------------------------------------------------
//パラメータストリーム
//-----------------------------------------------------------
class MtParamStream
{
//コンストラクタ
public MtParamStream(float[] dat)
{
mData = dat;
reset();
}
//最初のフレーム位置に設定する
public void reset()
{
mDegree = 0;
mCount = 0;
mDataPt = 0;
nextFrame();
}
//次のフレームにおける値を補間し計算する
public boolean nextFrame()
{
if ((--mCount) <= 0){
if (mDataPt < mData.length){
//次のキーフレーム情報を取得する
mCount = (int)mData[mDataPt++];
mDegree = (int)mData[mDataPt++];
for (int i=0;i<mDegree+1;i++)
mWork[i] = mData[mDataPt++];
}else{
//最後のキーフレームに到達した
mCount = 0;
mDegree = 0;
return false;
}
}else{
//積分
for (int i=0;i<mDegree;i++)
mWork[i] += mWork[i+1];
}
return true;
}
//現在の値を取得する
public float getValue()
{
return mWork[0];
}
//-----------------------------------------------------------
private float[] mData; //データストリーム
private int mDataPt; //現在のシーク位置
private float[] mWork = new float[16]; //作業用バッファ
private int mCount; //次のフレームまでのカウンタ
private int mDegree; //関数の次数
};