チュートリアル / プラグインを作ってみよう!ゲーム開発のためのツール製作講座
第7回:Softimage編 アニメーション、スキニング、シェイプ、カスタムパラメータの取得
- ゲーム
- コラム
- スクリプト・API
- チュートリアル
- 上級者
- 中級者
前回でモデルの形状と質感まで取得できるようになりました。今回は残っている部分を扱っていこうと思います。こちらが今回のサンプルです。
[AJ_SISDK_201201.zip]
まずアニメーションの情報を取得してみましょう。DCCツール上のアニメーションは大きく2つに分けられます。ファンクションカーブが存在するものと、そうでないものです。
ファンクションカーブが存在するもの:
●手付けモーション
●モーションキャプチャ のデータ
ファンクションカーブが存在しないもの:
●エクスプレッション
●コンストレイン
●物理シミュレーション
●その他
今回の実装ではファンクションカーブの有無に関わらず、アニメーションしているものはすべて出力するという仕様にしました。そのためアニメーションしているすべての要素の全フレームの値を出力しています。
それでは実際にアニメーションを出力する部分を説明します。最初にアニメーションが設定されているフレームの範囲を取得します。フレーム数の情報はプロジェクトの"Play Control"というプロパティが持っています。この中の”Frame In”, “Frame Out”というパラメータに開始フレームと終了フレームが入っています。
まずApplicationクラスからApplication::GetActiveProject()関数で現在有効なプロジェクトを取得します。それからProject::GetProperties()関数で"Play Control"プロパティを取得します。
プロパティの"In"と"Out"パラメータから開始フレームと終了フレームを取得します。Explorer上の名前とパラメータ名が違うので注意してください。
今回の実装では全フレームのアニメーションを出力しますが、一部の範囲のアニメーションを出力したいこともあります。その場合はエクスポーターのオプションで出力する範囲を指定することになるでしょう。
出力するフレームの範囲がわかったので、この間でループを回してアニメーションを出力します。ここではフレームの範囲に注意しましょう。たとえば1フレームから100フレームまでアニメーションが定義されている場合、アニメーションは「100フレーム」まで存在します。最後のフレームも含まれるので忘れないようにしてください。
それでは実際にノードのTransformアニメーションを取得してみましょう。以前のコラムでは省略しましたが、KinematicState::GetTransform()関数にフレーム数を渡すと、そのフレーム数のTransform情報が取得できます。
CTransformationからスケールなどの各要素を取得する方法は以前のコラムで説明したのでここでは省略します。
次にパラメータのアニメーションを取得します。今回の実装では、マテリアルやジオメトリの情報を取得する過程でアニメーションしているパラメータを見つけたら、その時点でリストに登録して、あとでまとめてアニメーション情報を取得しています。パラメータがアニメーションしているかどうかはParameter::IsAnimated()で調べます。
最初の説明では省略しましたが、Parameter::GetValue()関数の引数にフレーム数を与えると、そのフレーム数のパラメータの値が取得できます。
GetParameterValue()関数の場合は第2引数にフレーム数を指定できます。
ここまでの説明ではGetTransform()やGetValue()の引数にフレーム数を渡すと、そのフレームの値が取得できると書いていますが、実は値が正しく取得できないケースが存在します。コンストレインやエクスプレッションなどを使用しているシーンでは、実際にSoftimage上でフレーム数を変更しないと、これらの要素が評価されず正しい値を取得することができません。この問題に対処するために、今回のサンプルではSoftimage上でフレーム数を実際に変更しながらアニメーションの値を取得するようになっています。フレーム数をSoftimage上で実際に変更するとエクスポートの時間が長くなってしまうので、必要ない場合は外したほうがよいでしょう。
アニメーションの次はスキニング情報の取得方法を説明します。エクスポーターを作る場合、スキニング情報の取得は比較的難しい部分です。スキニングの正しい計算方法を理解するとデータの取得と値の検証が容易になるので、まずはスキニングの原理から説明していきます。
スキニングの説明の前に用語を2つだけ説明します。Softimageではスキニングのことを「エンベロープ」と呼びます。意味はスキニングとほとんど同じです。またポリゴンメッシュとノードに対してスキニングの設定を行うことを「バインドする」と言います。バインドのほうは他のDCCツールでも使われるので覚えておくとよいでしょう。
それでは下の図を使ってスキニングの原理を説明していきます。
左側はメッシュにスキニングを設定した直後の状態です。左側の状態からボーンを動かしたのが右の状態です。ここで左側のポリゴンメッシュのワールド座標での頂点位置をPolyVertex(World,Bind), 右側をPolyVertex(World,Anim)、左側のボーンのマトリックスをBoneMatrix(World,Bind), 右側をBoneMatrix(World,Anim)としてみましょう。
このときエクスポーターが出力するポリゴンの位置情報はPolyVertex(World,Bind)とPolyVertex(World,Anim)のどちらでしょうか? またマトリックスはBoneMatrix(World,Bind)とBoneMatrix(World,Anim)のどちらを出力するべきでしょうか? そしてゲームエンジン上ではどのような計算を行うべきでしょうか?
ここでのポイントは、ポリゴンメッシュはスキニング情報を割り当てられた(バインドされた)状態を基準として、そこからボーンが動かされることによってポリゴンメッシュの変形が行われるということです。バインドされた時点のポリゴンメッシュの形状を出力しておいて、これをスキニングが割り当てられたときのボーンと動かされたボーンとの差分だけ変形します。こうするとスキニングによって変形された結果のポリゴンメッシュの頂点位置が計算できます。
さらに考えなければいけないのがポリゴンメッシュ自体のマトリックスです。スキニングの付いたポリゴンメッシュは変形すると同時に、自分自身のマトリックスによって移動します。このマトリックスを考慮しておかないと、エクスポートしたモデルがゲームエンジン上で違う場所に表示されてしまいます。
ここではポリゴンメッシュのマトリックスをそれぞれPolyMatrix(World,Bind)、PolyMatrix(World,Anim)とします。先ほどまではポリゴンメッシュの頂点位置をワールド座標で考えていましたが、これをポリゴンメッシュ自体のマトリックスとローカル座標に分解しておきます。
スキニングを割り当てられた状態のポリゴンメッシュのローカル座標での形状をPolyVertex(Local,Bind)、動いた後のものをPolyVertex(Local,Anim)とすると以下の式になります。
これを先ほどの式に当てはめると以下の式になります。これが最終的なスキニングの計算式です。
この式が正しいか調べるために、ポリゴンメッシュの座標がスキニングした状態から一切動いていない場合を考えてみましょう。このとき、バインド時とアニメーション時のポリゴンメッシュ自身のマトリックスは等しくなります。
式の両辺からポリゴンメッシュのマトリックスを取り除くと以下の式になります。
さらにボーンがスキニングした状態から動いていない場合は以下の式が単位マトリックスになります。
これを先ほどの式に代入すると以下の結果になります。
ボーンもポリゴンメッシュも動いていない場合、ポリゴンメッシュは変形していません。よってこの結果は正しいと言えそうです。
ここまででエクスポートする要素が判明しました。
●バインド時のポリゴンメッシュのローカル座標での位置情報
●バインド時のポリゴンメッシュのワールド座標
●バインド時のボーンのマトリックス(ワールド座標)
これに加えて以下の情報も必要です。
●頂点単位のウェイト値
●スキニング対象のノードの名前
実際にはノードは親子構造になっているので、エクスポーターではノードの座標をローカル座標で出力して、ランタイム側でワールド座標に変換して使います。
実際にSoftimageから必要なデータを取得する前にテストデータを作成しましょう。まず適当なポリゴンメッシュを作成して「スケルトン>2Dチェインを描く」を選び、骨構造を作成します。それからポリゴンメッシュを選んで「エンベロープ>エンベロープの設定」を選びます。アイコンが変わるので、スキニングを設定したいノードをExplorer上で左クリック選択して、右クリックで終了します。ノードを動かしてポリゴンメッシュが変形するのを確認してください。
スキニング用のテストデータを作る時のポイントは、ポリゴンメッシュをできるだけシンプルにすることです。最初のテストデータは4角形ポリゴン一枚がよいでしょう(右下の画像)。最初から円柱(左下の画像)を作ってしまうとデバッグが大変です。ポリゴン数が多い上に、位置情報を見ただけではどこの部分かすぐにわからないからです。テストデータは可能な限り小さくするのがプラグイン作成のコツです。
テストデータができたので、実際にスキニングの情報を取得していきましょう。スキニングの情報はEnvelopeクラスに格納されています。まずX3DObjectクラスからGetEnvelopes()関数を使ってEnvelopeクラスを取得します。メッシュにスキニングの設定が行われていない場合はEnvelopeクラスを取得することはできません。基本的にはX3DObjectにエンベロープは一つしか付かないので最初のエンベロープだけ調べればよいでしょう。
Envelope::GetDeformers()でウェイト対象のノード一覧が取得できます。戻り値はCRefArrayなので適当なクラスにキャストします。今回は座標情報を取得したいのでX3DObjectにキャストしています。ノードの名前はGetName()で取得できます。
これでスキニングの対象となるノードが取得できました。次に頂点ごとのウェイト値を取得しましょう。ウェイトの情報はEnvelope::GetDeformerWeights()関数で取得できます。この関数は引数にウェイトを取得したいターゲットのX3DObjectクラスを指定します。
この関数の戻り値は頂点ごとのウェイト値(パーセント)の配列で、0から100の値をとります。COLLADAではウェイト情報を0から1の範囲で扱うので、エクスポーターの実装では0.01を乗算して格納しています。下の表は四角形ポリゴン、ボーン2本の場合のウェイトの値の例です。ボーンが2本なので2つ合わせると100になります。
頂点のウェイトの情報は頂点数と同じ数存在します。ターゲットとなっているノード数がN、頂点数をMとすると、頂点のウェイト情報はN x M 存在することになります。これはかなり大きな数ですが、実際には頂点ウェイトの対象として使われていないノードも沢山存在します。無駄なデータは出力しないほうがよいので、あらかじめウェイト値をすべて調べて、全部0のノードは出力しないようにしましょう。今回のエクスポーターではその処理を行っているので、詳細はそちらを参照してください。
GetDeformerWeights()関数で取得したウェイトの値はWeight Editor(下の画像)でチェックできます。Weight Editorは「エンベロープ>ウェイトの編集」で開きます。Weight Editorでは「スキニング対象のノードの名前」と「頂点ごとのウェイトの値」を表の形で見ることができます。このWeight Editorはデバッグのときに大変便利です。
今度はスキニング対象のノードからバインド時の座標を取得してみましょう。スキニング対象となっているX3DObjectクラスのGetStaticKinematicState()関数を呼ぶと、StaticKinematicStateクラスを取得することができます。StaticKinematicStateクラスはオブジェクトの基本ポーズを持っているクラスです。StaticKinematicStateクラスもKinematicStateクラスと同じようにGetTransform()関数を持っているので、同様に座標情報を取得します。ポリゴンメッシュのバインド時の座標も同様に取得することができます。
以上でスキニングの情報がすべて取得できました。ゲーム開発においてはSoftimageの骨構造が問題になるケースがあるので、少し補足説明をしたいと思います。Softimageの骨構造は「ルート」「ボーン」「エフェクタ」の3つから構成されます。骨が一つだけの場合でも必ずこの3種類のノードが作られてしまいます。この3種類のノードをすべてエクスポートしてランタイム側で再生すると、アニメーションとスキニングの計算負荷が増えてしまうので、どうにかしてノードの個数を減らす必要があります。ここでは2つの方法を紹介したいと思います。
最初の方法は「ルート」「ボーン」「エフェクタ」の「ボーン」だけを使う方法です。「ルート」と「エフェクタ」にはアニメーションと頂点ウェイトを設定できなくなりますが、ノードの数は最低限に抑えられます。この場合、モデルを作成するアーティストが間違えて「ルート」と「エフェクタ」に頂点ウェイトを設定してしまうと正常に動作しないデータになってしまうので注意が必要です。
もう一つの方法はすべての骨構造をNullで置き換えてしまう方法です。SoftimageではNullをスキニングの対象に使えるので、エクスポートするすべての骨構造をNullで置き換えれば必要最低限のノードにすることができます。モーションを作成する場合には、別にSoftimageの骨構造を作っておき、その骨構造とNullをコンストレインして使います。こちらの方法はNullに置き換える手間がかかりますが、その後の間違いが減るというメリットがあります。
スキニングの次はシェイプの情報を取得しましょう。スキニング同様、シェイプも比較的扱いが難しい部分です。Softimage上でのシェイプは「Shape Manager」で見ることができます。メニューの「表示>アニメーション>Shape Manager」からShape Managerを表示できます(下の画像)。
左側のシェイプを選択すると、右側に表示される形状が切り替わります。「アニメート」のタブに切り替えて、各シェイプのウェイトを変化させると、形状をブレンドすることができます。このように複数の形状をブレンドさせて、ポリゴンメッシュの形状を変化させるのがシェイプと呼ばれる機能です。このシェイプの機能は大半のDCCツールに用意されています。
このシェイプをランタイム側で実装するためには、各シェイプの名前と頂点情報が必要です。シェイプ形状の計算に必要な頂点情報は位置と法線です。頂点カラーやUV座標はすべてのシェイプで同じなので、2つ目以降のシェイプは位置と法線だけ持っていればよいことになります。実際には接線と従法線もシェイプによって変形するのですが、ゲームエンジンによってはその計算を省略することがあります。今回のエクスポーターでは接線と従法線は最初の形状だけ出力することにしています。このあたりはゲームエンジンの実装に合わせて対応してください。
シェイプの基本形状はジオメトリ情報のところで出力しているので、ここでは各シェイプの形状の出力方法について説明します。まずシェイプの名前を取得しましょう。シェイプの名前はCGeometryAccessor::GetShapeKeys()から取得できます。ShapeKeyの配列が返ってくるので、そこから名前を取得します。
次に各シェイプの頂点情報を取得します。頂点情報を直接取得する方法もありますが、今回はSoftimage上のシェイプ形状を実際に切り替えながら頂点情報を出力する実装になっています。
まずSoftimageのGUI上でシェイプを手動で切り替えてみましょう。Explorerからシェイプを設定したポリゴンメッシュの中身を表示すると"Cluster Shape Combiner"というオペレータが見つかります。このオペレータを開いて"結果の表示"のチェックを外して、"シェイプをソロに"の項目を変更すると、Softimage上のシェイプ形状が切り替わります。この状態で各シェイプの頂点情報を取得すればよいわけです。
今度はこの一連の操作をプログラム側から行ってみましょう。まずX3DObjectから先ほどの"Cluster Shape Combiner"オペレータを取得します。スクリプト用の名前は"clustershapecombiner"なので、そのフルネームを作成してからCRef::Set()を使ってOperatorクラスへの参照を取得しています。ここで出てきたOperatorクラスはSoftimage上のオペレータに相当するクラスです。
"Cluster Shape Combiner"オペレータの"ShowResult"パラメータの値をfalseにしてから、"SoloIndex"パラメータの値を切り替えます。”SoloIndex”の値は0がオリジナルの形状で、シェイプの形状は1から始まります。
シェイプで変形されたジオメトリ情報を取得する場合は、PolygonMesh::GetGeometryAccessor()関数の引数にsiConstructionModePrimaryShapeを渡します。これを忘れるとシェイプで変形していないオリジナルの形状を取得してしまうので注意してください。
これでシェイプごとの頂点情報が取得できました。ここで使用したSoloIndexの番号は、CGeometryAccessor::GetShapeKeys()から取得したシェイプの順番と一致しています。ShapeManagerでシェイプを切り替えるとスクリプトエディタにログが出るので確認してみてください。シェイプの名前を取得する方法はいくつかありますが、他の方法ではSoloIndexと順番が一致しないので注意しましょう。
これで各シェイプの情報が取得できました。シーンによってはスキニングとシェイプが両方適用されていることもあります。この場合はスキニングを一度無効化してからシェイプの形状を取得します。
まず初めにSoftimageのGUI上でスキニングを無効化してみましょう。Explorerでスキニング適用済みのポリゴンメッシュの中身を表示すると"Envelope Operator"という名前のOperatorが見つかります。このOperatorを開いてミュートにチェックを入れるとスキニングが一時的に無効になります。
今度はこの作業をプログラム側から実行してみます。X3DObject::GetEnvelopes()でEnvelopeの一覧を取得して、"Envelope Operator"という名前のEnvelopeを探します。そのEnvelopeの"mute"パラメータにParameter::PutValue()でtrueを設定するとスキニングが無効になります。スキニングを「無効」にするときに「true」を指定するので間違えないようにしてください。シェイプ形状の取得が終わった後は元の値に戻します。
[AJ_SISDK_201201.zip]
まずアニメーションの情報を取得してみましょう。DCCツール上のアニメーションは大きく2つに分けられます。ファンクションカーブが存在するものと、そうでないものです。
ファンクションカーブが存在するもの:
●手付けモーション
●モーションキャプチャ のデータ
ファンクションカーブが存在しないもの:
●エクスプレッション
●コンストレイン
●物理シミュレーション
●その他
今回の実装ではファンクションカーブの有無に関わらず、アニメーションしているものはすべて出力するという仕様にしました。そのためアニメーションしているすべての要素の全フレームの値を出力しています。
それでは実際にアニメーションを出力する部分を説明します。最初にアニメーションが設定されているフレームの範囲を取得します。フレーム数の情報はプロジェクトの"Play Control"というプロパティが持っています。この中の”Frame In”, “Frame Out”というパラメータに開始フレームと終了フレームが入っています。
まずApplicationクラスからApplication::GetActiveProject()関数で現在有効なプロジェクトを取得します。それからProject::GetProperties()関数で"Play Control"プロパティを取得します。
Application app;
Property playControl;
playControl = app.GetActiveProject().GetProperties().GetItem( L"Play Control" );
Property playControl;
playControl = app.GetActiveProject().GetProperties().GetItem( L"Play Control" );
プロパティの"In"と"Out"パラメータから開始フレームと終了フレームを取得します。Explorer上の名前とパラメータ名が違うので注意してください。
const int inFrame = playControl.GetParameterValue( L"In" );
const int outFrame = playControl.GetParameterValue( L"Out" );
const int outFrame = playControl.GetParameterValue( L"Out" );
今回の実装では全フレームのアニメーションを出力しますが、一部の範囲のアニメーションを出力したいこともあります。その場合はエクスポーターのオプションで出力する範囲を指定することになるでしょう。
出力するフレームの範囲がわかったので、この間でループを回してアニメーションを出力します。ここではフレームの範囲に注意しましょう。たとえば1フレームから100フレームまでアニメーションが定義されている場合、アニメーションは「100フレーム」まで存在します。最後のフレームも含まれるので忘れないようにしてください。
for( int frame=inFrame; frame<=outFrame; frame++ ) {
/* frameのアニメーションを出力 */
}
/* frameのアニメーションを出力 */
}
それでは実際にノードのTransformアニメーションを取得してみましょう。以前のコラムでは省略しましたが、KinematicState::GetTransform()関数にフレーム数を渡すと、そのフレーム数のTransform情報が取得できます。
MATH::CTransformation transformation;
transformation = x3DObject.GetKinematics().GetLocal().GetTransform( frame );
transformation = x3DObject.GetKinematics().GetLocal().GetTransform( frame );
CTransformationからスケールなどの各要素を取得する方法は以前のコラムで説明したのでここでは省略します。
次にパラメータのアニメーションを取得します。今回の実装では、マテリアルやジオメトリの情報を取得する過程でアニメーションしているパラメータを見つけたら、その時点でリストに登録して、あとでまとめてアニメーション情報を取得しています。パラメータがアニメーションしているかどうかはParameter::IsAnimated()で調べます。
Parameter param = shader.GetParameter( "diffuse" );
if( param.IsAnimated() ) {
/* アニメーションしているのでリストに登録 */
}
if( param.IsAnimated() ) {
/* アニメーションしているのでリストに登録 */
}
最初の説明では省略しましたが、Parameter::GetValue()関数の引数にフレーム数を与えると、そのフレーム数のパラメータの値が取得できます。
const float value = parameter.GetValue( frame );
GetParameterValue()関数の場合は第2引数にフレーム数を指定できます。
const float value = parameter.GetParameterValue( L"red", frame );
ここまでの説明ではGetTransform()やGetValue()の引数にフレーム数を渡すと、そのフレームの値が取得できると書いていますが、実は値が正しく取得できないケースが存在します。コンストレインやエクスプレッションなどを使用しているシーンでは、実際にSoftimage上でフレーム数を変更しないと、これらの要素が評価されず正しい値を取得することができません。この問題に対処するために、今回のサンプルではSoftimage上でフレーム数を実際に変更しながらアニメーションの値を取得するようになっています。フレーム数をSoftimage上で実際に変更するとエクスポートの時間が長くなってしまうので、必要ない場合は外したほうがよいでしょう。
for( int frame=inFrame; frame<=outFrame; frame++ ) {
// 取得するフレームに変更
setCurrentFrame( frame );
/* frameのアニメーションを出力 */
}
// 取得するフレームに変更
setCurrentFrame( frame );
/* frameのアニメーションを出力 */
}
アニメーションの次はスキニング情報の取得方法を説明します。エクスポーターを作る場合、スキニング情報の取得は比較的難しい部分です。スキニングの正しい計算方法を理解するとデータの取得と値の検証が容易になるので、まずはスキニングの原理から説明していきます。
スキニングの説明の前に用語を2つだけ説明します。Softimageではスキニングのことを「エンベロープ」と呼びます。意味はスキニングとほとんど同じです。またポリゴンメッシュとノードに対してスキニングの設定を行うことを「バインドする」と言います。バインドのほうは他のDCCツールでも使われるので覚えておくとよいでしょう。
それでは下の図を使ってスキニングの原理を説明していきます。
左側はメッシュにスキニングを設定した直後の状態です。左側の状態からボーンを動かしたのが右の状態です。ここで左側のポリゴンメッシュのワールド座標での頂点位置をPolyVertex(World,Bind), 右側をPolyVertex(World,Anim)、左側のボーンのマトリックスをBoneMatrix(World,Bind), 右側をBoneMatrix(World,Anim)としてみましょう。
このときエクスポーターが出力するポリゴンの位置情報はPolyVertex(World,Bind)とPolyVertex(World,Anim)のどちらでしょうか? またマトリックスはBoneMatrix(World,Bind)とBoneMatrix(World,Anim)のどちらを出力するべきでしょうか? そしてゲームエンジン上ではどのような計算を行うべきでしょうか?
ここでのポイントは、ポリゴンメッシュはスキニング情報を割り当てられた(バインドされた)状態を基準として、そこからボーンが動かされることによってポリゴンメッシュの変形が行われるということです。バインドされた時点のポリゴンメッシュの形状を出力しておいて、これをスキニングが割り当てられたときのボーンと動かされたボーンとの差分だけ変形します。こうするとスキニングによって変形された結果のポリゴンメッシュの頂点位置が計算できます。
PolyVertex(World,Anim) = PolyVertex(World,Bind) * BoneMatrix(World,Anim) * Inverse( BoneMatrix(World,Bind) )
さらに考えなければいけないのがポリゴンメッシュ自体のマトリックスです。スキニングの付いたポリゴンメッシュは変形すると同時に、自分自身のマトリックスによって移動します。このマトリックスを考慮しておかないと、エクスポートしたモデルがゲームエンジン上で違う場所に表示されてしまいます。
ここではポリゴンメッシュのマトリックスをそれぞれPolyMatrix(World,Bind)、PolyMatrix(World,Anim)とします。先ほどまではポリゴンメッシュの頂点位置をワールド座標で考えていましたが、これをポリゴンメッシュ自体のマトリックスとローカル座標に分解しておきます。
スキニングを割り当てられた状態のポリゴンメッシュのローカル座標での形状をPolyVertex(Local,Bind)、動いた後のものをPolyVertex(Local,Anim)とすると以下の式になります。
PolyVertex(World,Bind) = PolyVertex(Local,Bind) * PolyMatrix(World,Bind)
PolyVertex(World,Anim) = PolyVertex(Local,Anim) * PolyMatrix(World,Anim)
PolyVertex(World,Anim) = PolyVertex(Local,Anim) * PolyMatrix(World,Anim)
これを先ほどの式に当てはめると以下の式になります。これが最終的なスキニングの計算式です。
PolyVertex(Local,Anim) * PolyMatrix(World,Anim) =
PolyVertex(Local,Bind) * PolyMatrix(World,Bind) * BoneMatrix(World,Anim) * Inverse( BoneMatrix(World,Bind) )
PolyVertex(Local,Bind) * PolyMatrix(World,Bind) * BoneMatrix(World,Anim) * Inverse( BoneMatrix(World,Bind) )
この式が正しいか調べるために、ポリゴンメッシュの座標がスキニングした状態から一切動いていない場合を考えてみましょう。このとき、バインド時とアニメーション時のポリゴンメッシュ自身のマトリックスは等しくなります。
PolyMatrix(World,Bind) = PolyMatrix(World,Anim)
式の両辺からポリゴンメッシュのマトリックスを取り除くと以下の式になります。
PolyVertex(Local,Anim) =
PolyVertex(Local,Bind) * BoneMatrix(World,Anim) * Inverse( BoneMatrix(World,Bind) )
PolyVertex(Local,Bind) * BoneMatrix(World,Anim) * Inverse( BoneMatrix(World,Bind) )
さらにボーンがスキニングした状態から動いていない場合は以下の式が単位マトリックスになります。
BoneMatrix(World,Anim) * Inverse( BoneMatrix(World,Bind ) )
これを先ほどの式に代入すると以下の結果になります。
PolyVertex(Local,Anim) = PolyVertex(Local,Bind)
ボーンもポリゴンメッシュも動いていない場合、ポリゴンメッシュは変形していません。よってこの結果は正しいと言えそうです。
ここまででエクスポートする要素が判明しました。
●バインド時のポリゴンメッシュのローカル座標での位置情報
●バインド時のポリゴンメッシュのワールド座標
●バインド時のボーンのマトリックス(ワールド座標)
これに加えて以下の情報も必要です。
●頂点単位のウェイト値
●スキニング対象のノードの名前
実際にはノードは親子構造になっているので、エクスポーターではノードの座標をローカル座標で出力して、ランタイム側でワールド座標に変換して使います。
実際にSoftimageから必要なデータを取得する前にテストデータを作成しましょう。まず適当なポリゴンメッシュを作成して「スケルトン>2Dチェインを描く」を選び、骨構造を作成します。それからポリゴンメッシュを選んで「エンベロープ>エンベロープの設定」を選びます。アイコンが変わるので、スキニングを設定したいノードをExplorer上で左クリック選択して、右クリックで終了します。ノードを動かしてポリゴンメッシュが変形するのを確認してください。
スキニング用のテストデータを作る時のポイントは、ポリゴンメッシュをできるだけシンプルにすることです。最初のテストデータは4角形ポリゴン一枚がよいでしょう(右下の画像)。最初から円柱(左下の画像)を作ってしまうとデバッグが大変です。ポリゴン数が多い上に、位置情報を見ただけではどこの部分かすぐにわからないからです。テストデータは可能な限り小さくするのがプラグイン作成のコツです。
テストデータができたので、実際にスキニングの情報を取得していきましょう。スキニングの情報はEnvelopeクラスに格納されています。まずX3DObjectクラスからGetEnvelopes()関数を使ってEnvelopeクラスを取得します。メッシュにスキニングの設定が行われていない場合はEnvelopeクラスを取得することはできません。基本的にはX3DObjectにエンベロープは一つしか付かないので最初のエンベロープだけ調べればよいでしょう。
CRefArray envelopes = x3DObject.GetEnvelopes();
if( envelopes.GetCount() > 0 ) {
Envelope envelope( envelopes[ 0 ] );
...
}
if( envelopes.GetCount() > 0 ) {
Envelope envelope( envelopes[ 0 ] );
...
}
Envelope::GetDeformers()でウェイト対象のノード一覧が取得できます。戻り値はCRefArrayなので適当なクラスにキャストします。今回は座標情報を取得したいのでX3DObjectにキャストしています。ノードの名前はGetName()で取得できます。
CRefArray deformers = envelope.GetDeformers();
for( int i=0; i<deformers.GetCount(); i++ ) {
X3DObject targetX3DObject( deformers[ i ] );
CString targetName = targetX3DObject.GetName();
}
for( int i=0; i<deformers.GetCount(); i++ ) {
X3DObject targetX3DObject( deformers[ i ] );
CString targetName = targetX3DObject.GetName();
}
これでスキニングの対象となるノードが取得できました。次に頂点ごとのウェイト値を取得しましょう。ウェイトの情報はEnvelope::GetDeformerWeights()関数で取得できます。この関数は引数にウェイトを取得したいターゲットのX3DObjectクラスを指定します。
CDoubleArray weights = envelope.GetDeformerWeights( targetX3DObject );
この関数の戻り値は頂点ごとのウェイト値(パーセント)の配列で、0から100の値をとります。COLLADAではウェイト情報を0から1の範囲で扱うので、エクスポーターの実装では0.01を乗算して格納しています。下の表は四角形ポリゴン、ボーン2本の場合のウェイトの値の例です。ボーンが2本なので2つ合わせると100になります。
頂点のウェイトの情報は頂点数と同じ数存在します。ターゲットとなっているノード数がN、頂点数をMとすると、頂点のウェイト情報はN x M 存在することになります。これはかなり大きな数ですが、実際には頂点ウェイトの対象として使われていないノードも沢山存在します。無駄なデータは出力しないほうがよいので、あらかじめウェイト値をすべて調べて、全部0のノードは出力しないようにしましょう。今回のエクスポーターではその処理を行っているので、詳細はそちらを参照してください。
GetDeformerWeights()関数で取得したウェイトの値はWeight Editor(下の画像)でチェックできます。Weight Editorは「エンベロープ>ウェイトの編集」で開きます。Weight Editorでは「スキニング対象のノードの名前」と「頂点ごとのウェイトの値」を表の形で見ることができます。このWeight Editorはデバッグのときに大変便利です。
今度はスキニング対象のノードからバインド時の座標を取得してみましょう。スキニング対象となっているX3DObjectクラスのGetStaticKinematicState()関数を呼ぶと、StaticKinematicStateクラスを取得することができます。StaticKinematicStateクラスはオブジェクトの基本ポーズを持っているクラスです。StaticKinematicStateクラスもKinematicStateクラスと同じようにGetTransform()関数を持っているので、同様に座標情報を取得します。ポリゴンメッシュのバインド時の座標も同様に取得することができます。
CTransformation transform = x3DObject.GetStaticKinematicState().GetTransform();
以上でスキニングの情報がすべて取得できました。ゲーム開発においてはSoftimageの骨構造が問題になるケースがあるので、少し補足説明をしたいと思います。Softimageの骨構造は「ルート」「ボーン」「エフェクタ」の3つから構成されます。骨が一つだけの場合でも必ずこの3種類のノードが作られてしまいます。この3種類のノードをすべてエクスポートしてランタイム側で再生すると、アニメーションとスキニングの計算負荷が増えてしまうので、どうにかしてノードの個数を減らす必要があります。ここでは2つの方法を紹介したいと思います。
最初の方法は「ルート」「ボーン」「エフェクタ」の「ボーン」だけを使う方法です。「ルート」と「エフェクタ」にはアニメーションと頂点ウェイトを設定できなくなりますが、ノードの数は最低限に抑えられます。この場合、モデルを作成するアーティストが間違えて「ルート」と「エフェクタ」に頂点ウェイトを設定してしまうと正常に動作しないデータになってしまうので注意が必要です。
もう一つの方法はすべての骨構造をNullで置き換えてしまう方法です。SoftimageではNullをスキニングの対象に使えるので、エクスポートするすべての骨構造をNullで置き換えれば必要最低限のノードにすることができます。モーションを作成する場合には、別にSoftimageの骨構造を作っておき、その骨構造とNullをコンストレインして使います。こちらの方法はNullに置き換える手間がかかりますが、その後の間違いが減るというメリットがあります。
スキニングの次はシェイプの情報を取得しましょう。スキニング同様、シェイプも比較的扱いが難しい部分です。Softimage上でのシェイプは「Shape Manager」で見ることができます。メニューの「表示>アニメーション>Shape Manager」からShape Managerを表示できます(下の画像)。
左側のシェイプを選択すると、右側に表示される形状が切り替わります。「アニメート」のタブに切り替えて、各シェイプのウェイトを変化させると、形状をブレンドすることができます。このように複数の形状をブレンドさせて、ポリゴンメッシュの形状を変化させるのがシェイプと呼ばれる機能です。このシェイプの機能は大半のDCCツールに用意されています。
このシェイプをランタイム側で実装するためには、各シェイプの名前と頂点情報が必要です。シェイプ形状の計算に必要な頂点情報は位置と法線です。頂点カラーやUV座標はすべてのシェイプで同じなので、2つ目以降のシェイプは位置と法線だけ持っていればよいことになります。実際には接線と従法線もシェイプによって変形するのですが、ゲームエンジンによってはその計算を省略することがあります。今回のエクスポーターでは接線と従法線は最初の形状だけ出力することにしています。このあたりはゲームエンジンの実装に合わせて対応してください。
シェイプの基本形状はジオメトリ情報のところで出力しているので、ここでは各シェイプの形状の出力方法について説明します。まずシェイプの名前を取得しましょう。シェイプの名前はCGeometryAccessor::GetShapeKeys()から取得できます。ShapeKeyの配列が返ってくるので、そこから名前を取得します。
CRefArray shapeKeys = ga.GetShapeKeys();
for( int i=0; i<shapeKeys.GetCount(); i++ ) {
ShapeKey shapeKey( shapeKeys[ i ] );
CString name = shapeKey.GetName();
}
for( int i=0; i<shapeKeys.GetCount(); i++ ) {
ShapeKey shapeKey( shapeKeys[ i ] );
CString name = shapeKey.GetName();
}
次に各シェイプの頂点情報を取得します。頂点情報を直接取得する方法もありますが、今回はSoftimage上のシェイプ形状を実際に切り替えながら頂点情報を出力する実装になっています。
まずSoftimageのGUI上でシェイプを手動で切り替えてみましょう。Explorerからシェイプを設定したポリゴンメッシュの中身を表示すると"Cluster Shape Combiner"というオペレータが見つかります。このオペレータを開いて"結果の表示"のチェックを外して、"シェイプをソロに"の項目を変更すると、Softimage上のシェイプ形状が切り替わります。この状態で各シェイプの頂点情報を取得すればよいわけです。
今度はこの一連の操作をプログラム側から行ってみましょう。まずX3DObjectから先ほどの"Cluster Shape Combiner"オペレータを取得します。スクリプト用の名前は"clustershapecombiner"なので、そのフルネームを作成してからCRef::Set()を使ってOperatorクラスへの参照を取得しています。ここで出てきたOperatorクラスはSoftimage上のオペレータに相当するクラスです。
CRef ref;
ref.Set( x3DObject.GetFullName() + L".polymsh.clustershapecombiner" );
Operator clusterShapeCombiner( ref );
ref.Set( x3DObject.GetFullName() + L".polymsh.clustershapecombiner" );
Operator clusterShapeCombiner( ref );
"Cluster Shape Combiner"オペレータの"ShowResult"パラメータの値をfalseにしてから、"SoloIndex"パラメータの値を切り替えます。”SoloIndex”の値は0がオリジナルの形状で、シェイプの形状は1から始まります。
Parameter showResult = shapeCombiner.GetParameter( L"ShowResult" );
showResult.PutValue( false );
Parameter soloIndex = shapeCombiner.GetParameter( L"SoloIndex" );
for( int i=0; i<shapeCount; i++ ) {
// シェイプ形状を切り替える
soloIndex.PutValue( i+1 );
/* 形状を出力 */
...
}
// 元に戻す
showResult.PutValue( true );
showResult.PutValue( false );
Parameter soloIndex = shapeCombiner.GetParameter( L"SoloIndex" );
for( int i=0; i<shapeCount; i++ ) {
// シェイプ形状を切り替える
soloIndex.PutValue( i+1 );
/* 形状を出力 */
...
}
// 元に戻す
showResult.PutValue( true );
シェイプで変形されたジオメトリ情報を取得する場合は、PolygonMesh::GetGeometryAccessor()関数の引数にsiConstructionModePrimaryShapeを渡します。これを忘れるとシェイプで変形していないオリジナルの形状を取得してしまうので注意してください。
これでシェイプごとの頂点情報が取得できました。ここで使用したSoloIndexの番号は、CGeometryAccessor::GetShapeKeys()から取得したシェイプの順番と一致しています。ShapeManagerでシェイプを切り替えるとスクリプトエディタにログが出るので確認してみてください。シェイプの名前を取得する方法はいくつかありますが、他の方法ではSoloIndexと順番が一致しないので注意しましょう。
これで各シェイプの情報が取得できました。シーンによってはスキニングとシェイプが両方適用されていることもあります。この場合はスキニングを一度無効化してからシェイプの形状を取得します。
まず初めにSoftimageのGUI上でスキニングを無効化してみましょう。Explorerでスキニング適用済みのポリゴンメッシュの中身を表示すると"Envelope Operator"という名前のOperatorが見つかります。このOperatorを開いてミュートにチェックを入れるとスキニングが一時的に無効になります。
今度はこの作業をプログラム側から実行してみます。X3DObject::GetEnvelopes()でEnvelopeの一覧を取得して、"Envelope Operator"という名前のEnvelopeを探します。そのEnvelopeの"mute"パラメータにParameter::PutValue()でtrueを設定するとスキニングが無効になります。スキニングを「無効」にするときに「true」を指定するので間違えないようにしてください。シェイプ形状の取得が終わった後は元の値に戻します。
CRefArray envelopes = x3DObject.GetEnvelopes();
const int num = envelopes.GetCount();
for( int i=0; i<num; i++ ) {
Envelope envelope = envelopes[ i ];
if( envelope.GetName() == L"Envelope Operator" ) {
Parameter paramMute = envelope.GetParameter( L"mute" );
if( paramMute.IsValid() ) {
// スキニングを無効化する
paramMute.PutValue( true );
}
}
}
const int num = envelopes.GetCount();
for( int i=0; i<num; i++ ) {
Envelope envelope = envelopes[ i ];
if( envelope.GetName() == L"Envelope Operator" ) {
Parameter paramMute = envelope.GetParameter( L"mute" );
if( paramMute.IsValid() ) {
// スキニングを無効化する
paramMute.PutValue( true );
}
}
}
- 1
- 2