ホーム < ゲームつくろー! < クラス構築編 < 線形補間テンプレートクラスを作ろう!

区間補間テンプレートクラスを作ろう!(仕様編)


 ある地点から別の地点へキャラクタを移動させたり、色を連続的に変化させるなど、「2つのキー値の間の値を算出する」という事が、ゲーム製作において良く見られます。この算出作業の事を一般に「補間」と言います。補間計算は色々な手法がありますが、中でも一番良く使われるのが線形補間です。線形補間は2つのキー値を直線で結ぶ事で、間の値を算出します。線形補間の一般式は次のようになります:


 Interpというのが補間値(Interpolation value)、Vi及びVi+1がキーとなる値です。tは0から1の間を取る割合変数で、0の時はVi、1の時はVi+1になります。この式はアルファブレンドなどでもおなじみの式ですね。補間には他にもスプライン補間やベジェ補間など色々とありますが、殆どの補間が上の式のようにキー値と割合変数で表現されます。

 さて、上式のキー値であるVの「型」は何でしょうか?一般には浮動小数点(float、double)ですが、別にそれに限定しなくても良い気がしませんか?例えばD3DXVECTOR3でもいいはずです。この場合、3次元の点の間の補間になります。D3DXMATRIXでも良いでしょう。それだと2つの行列の線形補間になります。つまり、上式の計算が可能な型であれば、別にどんな型であってもかまわないわけです。もちろん、オリジナルな型でも問題ありません。計算方法が同じで型だけ異なる。これは正にテンプレートがドンピシャリで当てはまるシチュエーションです。どんな型でも補間できてしまうクラスがあったら、その応用性はとてつもなく広いはずです。

 そこでこの章以降で、様々な型の補間をサポートする区間補間テンプレートクラスを作ってみます。ただ全体を通すと結構複雑にな実装になってしまいますので、この章ではまずその仕様から固めてみたいと思います。テンプレートを学んでみたい方もどうぞ(^-^)。



@ STLのコンテナの真似をします

 これから作成するテンプレートクラスはinterpolatingテンプレートと言う名前です。これは、キー値を連続的に登録して、然るべき方法でその補間値を取得できるテンプレートです。こういうテンプレートのお手本はやはりSTL(標準テンプレートライブラリ)です。vectorやlistなどでおなじみのあれです。ここではSTLに習ってvectorやlistと同じ方法で補間値を取得できるような仕様にしてみましょう。

 どういう風に使えると便利であるか?まずは使用方法のイメージからどうぞ:

interpolatingテンプレートクラスの使用方法のイメージ
interpolating< double > ip;   // double型の補間テンプレート
double val;   // 値
double interval;   // 区間距離

// キー値の登録
ip.push_back( val=100.0, interval=0.0 );   // 最初の区間距離は無視されます
ip.push_back( val=300.0, interval=5.0 );
ip.push_back( val=200.0, interval=8.0 );

// 補間値の取得
double val;
interpolating<double>::iterator it;
for(it=ip.begin(); it!=ip.end(); it++)
{
   it.SetUnit( 10.0 );   // 補間単位を設定
   val = (*it);   // 補間値取得
}

// キー値のクリア
ip.clear();


 上の使い方はvectorやlistと殆ど同じです。push_backメソッドでキー値と区間を登録して、イテレータを使って補間値を取得します。イテレータが持つSetUnitメソッドは補間単位距離を設定するメソッドです。これから作るテンプレートは上の実装を完全に満足させます!

 STLのコンテナクラスは他にも沢山のメソッドを持っていますが、私たちが日ごろ使うのは上のプログラムのように、

 ・push_backメソッド
 ・beginメソッド
 ・endメソッド
 ・clearメソッド

くらいではないでしょうか?要は登録して、イテレータで値を取得して、入らなくなったらクリアしてしまう。この位の機能があれば、通常使用で十分事足りるわけです。



A キー値と区間と重みの3つ巴の値の登録

 vectorやlistは登録値をそのまま使用します。一方interpolating補間テンプレートは、キー値と共に「区間距離」と「重み」の3つの値を使うことになります。区間距離というのはどういうことか、下の図で説明します:

 この図は0から出発して、10まで値を変化させているグラフです。この横軸が「区間距離」に相当します。ご覧になってわかりますように、同じ0から10へ変化させるにしても、区間距離が短いX1だと早く変化しますが、区間が長いX2だと緩やかです。この変化の速さを決めるのが区間距離であり、補間計算には絶対に必要になります。

 もう1つ「重み」というのは、各キー値の点ごとに設ける付加情報の事です。補間方法に変化をつけたい場合にこういう変数の存在は重宝するものです。キー値の登録の際には、(値、区間距離、重み)の3つの情報を与える仕様にします。



B 区間の与え方は相対値

 上の図で、X1というのは2つの見方ができます。1つは横軸上の絶対位置と捉える見方、もう1つは前の値からの「相対距離」としてみる見方です。時と場合によりますが、ここでは補間距離は相対値で登録することにします。どちらを選んでも手間は大して変わりません。

 相対値で区間を与える時に考慮する事があります。次の図をご覧下さい:

 左は今の点(赤い点)に対して前の区間からの距離を与えています。一方右の図は、今の点から次の点までの距離を与えています。どちらが良いのか?これは、補間の最後の処理を考えると左の図の方が望ましいと思います。右の図で補間が成立するには、次の点の存在が必要になってしまいます。これは先がいるのかいないのかを常に見る必要に駆られます。左の図の方は、最初の点さえ存在していれば、後の処理は全て統一できます。

 ここまでの仕様をまとめますと、interpolatingテンプレートは、

 ・ STLのコンテナに似た仕様にする(push_back、begin、end、clearメソッドを最低限与える)。
 ・ (value, x, weight)というトリオで値を与える。
 ・ xは前の値からの増分(相対値)で与える。

とします。



B イテレータの仕様

 STLのvectorやlistは基本的に「イテレータ」によって値を取得します。これはデザインパターンにもある有名な設計方法で、値の格納庫(コンテナ)とその値の取得方法(イテレータ)を分けることによって、値の取得の仕方をユーザが選択できるようになります。順方向、逆方向、2個飛び、特定の値のみへのアクセスなど、アクセス方法と言うのは実は沢山あるわけです。

 interpolatingテンプレートにもイテレータを設けます。ただlistなどとは違い、距離Xの増分に対しての補間値を返さなければなりません。つまり、イテレータの内部で補間値を毎回算出する事になります。

 interpolatingのイテレータの仕様としてもう1つ考えておくべき事は、最後の点の処理です。vectorやlistの場合、格納数分だけイテレータを回せば、必ず最後の値にたどり着きます。ところがinterpolatingの場合、増分の与え方によっては最後の点を飛び越してしまう可能性が出てきます。


 上図のように、例えば0.7ずつ進んだとして、最後の部分の帳尻が合わなければ、補間値が存在しないわけです。

 この問題への対処は仕様でまかなう事にします。最後に飛び出した場合は最終点の値を返すようにしましょう。これはある位置から出発したキャラクタが確実に最後の点に到達できるようにするためです。最後の一瞬だけ補間計算が異なる事になりますが、これによる違和感や問題は通常殆ど発生しません。それよりも、最後の点にいない方が違和感ですよね。

 イテレートのもう1つ重要な性質は、イテレートの最後がちゃんとあるということです。listなどでも、

for(it=List.begin(); it!=List.end(); it++)

とendメソッドが返すイテレータと比較する事でイテレートが最後に達したか否かを判定しています。この最後のイテレータは特別なイテレータです。ここではそれを「終端イテレータ」と呼ぶことにしましょう。反復の最後には必ず終端イテレータを返すような仕様にしなければ、ループが終了しません。「反復の最後」というのは、上の図で最後の点を返した後です。最後の点を返した直後ではありません。細かいところですが重要です。

 イテレータは「反復子」であり、補間値そのものではありません。listでイテレータから値を取得する時には、

value = (*it);

とイテレータに間接参照演算子(間接指定演算子)「*」を付けますよね。これはイテレータがこの演算子をオーバーロードしていて、値を返す仕組みになっているためです。イテレータがあたかもポインタであるかのように振舞っているわけです。interpolatingのイテレータもこの仕様に習い、上記の記述で補間値を取得できるようにします。

 イテレータの仕様についてまとめますと、

 ・ イテレートする時にはイテレート単位を与える。
 ・ イテレートの最後ではみ出し時は最後の値を与え、それ以上のイテレートをした場合は終端イテレータを返す。
 ・ イテレータから値を取得するには間接参照演算子*を用いる。

となります。

 キー値の格納方法と補間値の取得方法をここまでで定めました。これで十分でしょうか?いいえいいえ、大切な部分が残っています。それは、どうやって補間するのか?つまり「補間方法の定義」です。



C 補間ストラテジの分離

 補間の方法というのは線形だけではありません。世の中にはスプライン補間、エルミート補間、ベジェ補間など実に様々な補間方法があります。同じ点の情報、同じ単位でのイテレートであっても、補間方法が違えばイテレータが返す補間値も変わってきます。せっかくコンテナと反復子を分離させたのですから、ここはもう一頑張りして「補間方法」も分離させてしまいましょう。そうすれは、線形補間でもスプライン補間でも、好きな補間方法を自由に簡単に扱える夢のようなテンプレートクラスができあがります!

 ここでは、分離させる補間方法を「補間ストラテジ」という名前で総称することにします。補間ストラテジはある特定のメソッドを持つクラス群です。interpolatingテンプレートクラスに対して補間ストラテジクラスを充てることで、使いたい補間方法を指定することになります。イテレータで補間値を計算する際に、その計算方法をすべて補間ストラテジクラスに委譲することになります。このことから、補間ストラテジクラスのメソッドは完全に統一させなけらばなりません。

 一般に、補間に必要な情報は「最低限必要な点の数」「点の位置」そして「点1つ1つに与えられる付加情報(重みなど)」です。例えば線形補間の場合、2点があれば補間可能です。3次スプライン補間の場合は3点などです。この必要な点の数は補間ストラテジがちゃんと知っていますのでNeddPointNumメソッドのような必要点の数を返すメソッドを設ける事が可能です。点さえ与えられれば補間が出来るわけですから、補間ストラテジは静的な点の配列を持てます。その配列に点を置いてもらってCalcメソッドを呼べば、補間点が自動的に計算される。そういう仕様にします。計算の際に必要な付加情報(例えばAdobe Illustratorなどにみられる「制御バー」などが付加情報に相当)は点と一緒に与えます。この辺りは実装をご覧になった方が分かりやすいと思います。



 大まかな仕様はこれで固まりました。基本的な使い方は一番最初に示したプログラムイメージに尽きます。決して難しい使用方法ではありません。ここまでに固めた仕様を満たした場合、次のような凝った使い方が可能になります:

interpolatingテンプレートクラスの使用方法のイメージ
// D3DXMATRIXを線形補間する補間テンプレートクラスを作成
//  D3DXMATRIX : 行列
//  double : 距離の型
//  double : 重みの型
//  CSplineInterp : スプライン補間ストラテジクラス

interpolating< D3DXMATRIX, double, double, CSplineInterp > ip;

D3DXMATRIX val[3];   // 値
double interval;   // 区間距離

// キー値(行列)の登録
ip.push_back( val[0], interval=0.0 );   // 最初の区間距離は無視されます
ip.push_back( val[1], interval=10.0 );
ip.push_back( val[2], interval=10.0 );

// 補間値の取得
D3DXMATRIX val;
interpolating< D3DXMATRIX, double, double, CSplineInterp >::iterator it;
for(it=ip.begin(); it!=ip.end(); it++)
{
   it.SetUnit( 1.0 );   // 補間単位を設定
   val = (*it);   // 補間値取得
}

// キー値のクリア
ip.clear();

 上のプログラムはD3DXMATRIX型の点の間を3次スプライン関数で結び、その補間点を取得する実装です。宣言の方法が最初のプログラムと違うだけで、値の入れ方、イテレータからの値の取得方法は全く同じである点に注目してください。点の型はD3DXVECTOR3でも、独自の型でも何でも可能です(演算子の定義は必要になります)。こういうことが自由に可能なるという事の有用さは、言わずもがなですよね。

 さて次の章からは、いよいよこの仕様を満たすテンプレートを作っていきます。