14、モーフィング





14章でモーフィングアニメ機能が付きました。
モーフ形状の設定はメタセコイアで行います。

モーフのオブジェクトの名前の付け方はKeynoteと同じにしました。
モーフベースには普通に名前を付けます。
そしてターゲットには「elem:ターゲット名-ベース名」のように名前を付けます。

ベースとターゲットは同じ頂点数、同じ面数、同じ頂点順序でないといけません。
マテリアルについては特殊です。
ベースとターゲットはマテリアル配置(マテリアルごとの面の組)が同一でなければなりません。
つまり面の組は同じでないといけませんが、その面の組にどのマテリアルを割り当てるかは自由です。

モーフは頂点座標、マテリアル、法線、UVについて行います。

モーフ形状を設定したmqoを読み込むと上図のようにモーフウインドウにベースの数だけラインが出来ます。
ツールウインドウのモーフキー作成ボタンを押してベースごとにキーを作成します。
キーを右クリックするとターゲット名とそのブレンド率の一覧のメニューが出ます。
メニューでブレンド率を設定したいターゲット名を選択すると、ブレンド率設定ウインドウが出ます。
ブレンド率設定ダイアログのスライダーを動かして、ブレンド率を設定します。

ブレンド率はターゲットごとに設定し、エントリーを削除することも出来ます。
フレーム0のキーにはブレンド率0のエントリーが作ってありますが
新たに作成したキーには、自分で設定しないとエントリーは出来ません。

モーフアニメと姿勢アニメは同じモーションに格納されます。
つまりモーション名、フレーム長は共有ということになります。

モーフアニメはプロジェクト保存時にモーションファイル*.qubに保存されます。

モーフ形状が確認しづらいときは、Ctrl + Jキーを押すとボーンマークが消えます。
もう一度押すとまた表示されます。

Media/MorphTest/MorphTest.rdbにモーフアニメのテストデータを同梱しています。

####### 2012/05/23 6:00 プログラムと解説を修正
モーフの無駄を省いて高速化。
毎フレームCDispObj::CreateDispObjしていたのをやめて、CDispObj::CopyDispVを新設してそれを呼ぶようにしました。
つまりメモリの削除アロケートし直しを省略し、必要データだけコピーするようにしました。

####### 2012/05/28 12:40 プログラムの修正


ウインドウ上部の3色のスプライトをドラッグしてボーンの回転、移動を行えるようにしました。
(回転か移動かはメインウインドウの「回転IK」「移動IK」ラジオボタンで切り替えます。)

ボーン移動モードのときはマニピュレータのリングを表示しないようにしました。

Ctrlキーを押しながらクリックすると、ボーンは選択されずにマニピュレータが選択されるようにしました。
(Ctrlを押していないとボーンの選択が優先されます。)


####### 2012/05/30 8:35 プログラム修正
マニピュレータのZ(青)をドラッグ時の動作を、Zスプライトドラッグ時の動作と同じにしました。

保存時の保存場所フォルダの初期値をデスクトップにしました。



--------  ダウンロード -------
ソースは下のページからダウンロードしてください。
OpenRDBダウンロードページ




########## 以下 プログラム解説

14−1、モーフ形状のピックアップ

モーフ形状のピックアップはオブジェクト名でモーフ用の形状かどうかを判断して行います。

モーフ形状にはベース形状とターゲット形状があります。
ベース形状とは変形前の基準となる形状です。
ターゲット形状とは変形した後の形状です。
通常はベース1個に対し、複数のターゲットを定義します。

モーフ形状の名前のつけ方の説明をします。
メタセコイアのプラグインのkeynoteの命名法を採用しました。
ベース形状は通常の表示オブジェクトと同じように普通に名前を付けます。
ターゲット形状の名前は、「elem:ターゲット名-ベース名」のようになります。

ピックアップ用の関数はmqofile.cppのLoadMQOFile_aftの最後のほうで呼び出します。
ターゲットのピックアップをCMQOFile::PickUpMorph()で行い、ベースのピックアップをCModel::SetMorphBaseMap()で行います。

PickUpMorphで名前にelemの付いたオブジェクトを探し、
ベース名で指定されたCMQOObjectのメンバのm_morphobjというmapに登録します。

SetMorphBaseMapで
m_objectをすべて検索し、m_morphobjの登録数が1以上のものを探して見つかったら
m_mbaseobjectというmapに登録します。


14−2、モーフキー

モーフアニメには姿勢アニメとは違うフレームにキーを作成できるように
CMorphKeyという専用のモーフキークラスを作りました。

クラス定義は以下のようです。

class CMorphKey
{
public:
CMorphKey( CMQOObject* baseptr );
~CMorphKey();

int InitMotion();

int SetBlendWeight( CMQOObject* targetobj, float srcweight );
int SetBlendAllZero();
int SetBlendOneExclude( CMQOObject* targetobj );
int DeleteBlendWeight( CMQOObject* targetobj );

int NormalizeWeight();

int AddToPrev( CMorphKey* addmp );
int AddToNext( CMorphKey* addmp );
int LeaveFromChain( int srcmotid, CMQOObject* ownerobj );

private:
int InitParams();
int DestroyObjs();

public:
double m_frame;
CMQOObject* m_baseobj;
map<CMQOObject*, float> m_blendweight;

CMorphKey* m_prev;
CMorphKey* m_next;
};

注目すべきメンバは、map<CMQOObject*, float> m_blendweightです。
このm_blendweightにターゲットごとのブレンド率を格納します。
mapにしたのはブレンド率のエントリーを作るキーを選べるようにしたかったからです。
つまりキーごとにエントリーのあるターゲットとエントリーのないターゲットが出来ます。

モーフキーも姿勢のキー(CMotionPoint)と同様に双方向のリンクを持ちます(m_prevとm_next)。
モーフキーはフレーム番号が小さい順にリンクされるようにします。
そして一番最初のキーは、ベース形状のCMQOObjectのm_morphkeyにセットします。

m_morphkeyはモーションIDとモーフキーのmapです。
モーションIDは姿勢のモーションIDと同じものです。


14−3、モーフブレンド計算

モーフのブレンド計算はCModel::UpdateMatrixから呼ばれるUpdateMorph関数で行います。
CModel::UpdateMorphはベースオブジェクト(CMQOObject)のUpdateMorphを呼び出します。

CMQOObject::UpdateMorphは以下のようです。

int CMQOObject::UpdateMorph( LPDIRECT3DDEVICE9 pdev, int hasbone, int srcmotid, double srcframe, map<int, CMQOMaterial*>& srcmaterial )
{
if( !m_pm3 ){
return 0;
}

CMorphKey curmk( this );
CallF( CalcCurMorphKey( srcmotid, srcframe, &curmk ), return 1 );
CallF( CalcMorph( &curmk, srcmaterial ), return 1 );

if( !m_dispobj ){
_ASSERT( 0 );
return 1;
}
CallF( m_dispobj->CopyDispV( m_pm3 ), return 1 );

return 0;
}

まずCalcCurMorphKeyで現在のモーションフレームの各ターゲットのブレンド率を計算します。
次にそのブレンド率を使ってCalcMorphで座標、マテリアル、法線、UVのブレンド計算をします。
最後にCopyDispVでブレンド結果をビデオメモリの頂点データに転送します。
つまりモーフのブレンドはボーン変形前の状態で行い、ブレンド結果にシェーダーでボーン変形を施して表示することになります。

この際、どこの処理が重いかを知っておくと使い方の参考にもなると思います。
ブレンドは頂点数xブレンド率0以外のターゲット数に比例して重くなります。
しかし最近のCPUではハイポリでない限りそれほど問題にはならないと思います。

重いのはCPUで計算した結果をビデオメモリに転送する部分です。
つまりCreateDispObjが重い処理です。


では各処理の詳細を見てみましょう。
まずはCalcCruMorphKeyです。

int CMQOObject::CalcCurMorphKey( int srcmotid, double srcframe, CMorphKey*
      dstmk )
{
map<int, CMQOObject*>::iterator itrtarget;
for( itrtarget = m_morphobj.begin(); itrtarget != m_morphobj.end(); itrtarget++ ){
CMQOObject* curtarget = itrtarget->second;
if( curtarget ){
CMorphKey* pbef = 0;
CMorphKey* pnext = 0;
int existflag = 0;
CallF( GetBefNextMK( srcmotid, srcframe, curtarget, &pbef, &pnext, &existflag ), return 1 );
CallF( CalcFrameMK( curtarget, srcframe, pbef, pnext, existflag, dstmk ), return 1 );
}
}
return 0;
}

ターゲットごとにブレンド率を求めます。
そのためにターゲットごとに現在のフレームの前後のキーをGetBefNextMKで探します。
そしてその前後のキーからCalcFrameMKでブレンド率を計算します。

ブレンド計算をするCalcMorph関数を見てみましょう。

int CMQOObject::CalcMorph( CMorphKey* curkey, map<int, CMQOMaterial*>&
      srcmaterial )
{
//モーフのブレンドを計算し、結果をbasepm3->m_dispvに格納する。

CPolyMesh3* basepm3 = m_pm3;

if( !m_pm3->m_dispv ){
_ASSERT( 0 );
return 1;
}

//dispvの初期化
int baseoptv;
for( baseoptv = 0; baseoptv < basepm3->m_optleng; baseoptv++ ){
PM3DISPV* curv = basepm3->m_dispv + baseoptv;
N3P* curn3p = basepm3->m_n3p + baseoptv;
curv->pos.x = (m_pointbuf + curn3p->pervert->vno)->x;
curv->pos.y = (m_pointbuf + curn3p->pervert->vno)->y;
curv->pos.z = (m_pointbuf + curn3p->pervert->vno)->z;
curv->pos.w = 1.0f;
curv->normal = curn3p->pervert->smnormal;
curv->uv = curn3p->pervert->uv[0];
}

int matcnt;
for( matcnt = 0; matcnt < basepm3->m_optmatnum; matcnt++ ){
CMQOMaterial* curmat = (basepm3->m_matblock + matcnt)->mqomat;
map<int,CMQOMaterial*>::iterator itrmat;
itrmat = m_morphmaterial.find( curmat->materialno );

CMQOMaterial* dstmat = 0;
if( itrmat == m_morphmaterial.end() ){
dstmat = new CMQOMaterial();
_ASSERT( dstmat );
*dstmat = *curmat;
m_morphmaterial[ dstmat->materialno ] = dstmat;
}else{
dstmat = itrmat->second;
*dstmat = *curmat;
}
}

map<int, CMQOObject*>::iterator itrtarget;
for( itrtarget = m_morphobj.begin(); itrtarget != m_morphobj.end(); itrtarget++ ){
CMQOObject* curtarget = itrtarget->second;
if( curtarget ){
CPolyMesh3* targetpm3 = curtarget->m_pm3;

map<CMQOObject*, float>::iterator itrbw;
itrbw = curkey->m_blendweight.find( curtarget );

float blendw;
if( itrbw == curkey->m_blendweight.end() ){
blendw = 0.0f;
}else{
blendw = itrbw->second;
}

if( blendw == 0.0f ){
continue;//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}

for( matcnt = 0; matcnt < basepm3->m_optmatnum; matcnt++ ){
CMQOMaterial* basematerial = (basepm3->m_matblock + matcnt)->mqomat;

int facecnt = 0;
int basefaceno;
for( basefaceno = (basepm3->m_matblock + matcnt)->startface; basefaceno <= (basepm3->m_matblock + matcnt)->endface; basefaceno++ ){
CMQOMaterial* targetmaterial;
int baseorgfaceno;
int targetfaceno;
baseorgfaceno = (basepm3->m_n3p + basefaceno * 3)->perface->faceno;
targetfaceno = targetpm3->m_faceno2optfaceno[ baseorgfaceno ];

N3P* basen3p = basepm3->m_n3p + basefaceno * 3;
N3P* targetn3p = targetpm3->m_n3p + targetfaceno * 3;

if( facecnt == 0 ){
targetmaterial = GetMaterialFromNo( srcmaterial, targetn3p->perface->materialno );
if( !targetmaterial ){
_ASSERT( 0 );
return 1;
}

CMQOMaterial* setmat = m_morphmaterial[ basematerial->materialno ];
if( !setmat ){
_ASSERT( 0 );
return 1;
}

CMQOMaterial tmptargetmat = *targetmaterial * blendw;
CMQOMaterial tmpbasemat = *basematerial * blendw;

setmat->col.x += tmptargetmat.col.x - tmpbasemat.col.x;
setmat->col.y += tmptargetmat.col.y - tmpbasemat.col.y;
setmat->col.z += tmptargetmat.col.z - tmpbasemat.col.z;
setmat->col.w += tmptargetmat.col.w - tmpbasemat.col.w;
setmat->dif += tmptargetmat.dif - tmpbasemat.dif;
setmat->amb += tmptargetmat.amb - tmpbasemat.amb;
setmat->emi += tmptargetmat.emi - tmpbasemat.emi;
setmat->spc += tmptargetmat.spc - tmpbasemat.spc;
setmat->power += tmptargetmat.power - tmpbasemat.power;

setmat->dif4f.x += tmptargetmat.dif4f.x - tmpbasemat.dif4f.x;
setmat->dif4f.y += tmptargetmat.dif4f.y - tmpbasemat.dif4f.y;
setmat->dif4f.z += tmptargetmat.dif4f.z - tmpbasemat.dif4f.z;
setmat->dif4f.w += tmptargetmat.dif4f.w - tmpbasemat.dif4f.w;
setmat->amb3f.x += tmptargetmat.amb3f.x - tmpbasemat.amb3f.x;
setmat->amb3f.y += tmptargetmat.amb3f.y - tmpbasemat.amb3f.y;
setmat->amb3f.z += tmptargetmat.amb3f.z - tmpbasemat.amb3f.z;
setmat->emi3f.x += tmptargetmat.emi3f.x - tmpbasemat.emi3f.x;
setmat->emi3f.y += tmptargetmat.emi3f.y - tmpbasemat.emi3f.y;
setmat->emi3f.z += tmptargetmat.emi3f.z - tmpbasemat.emi3f.z;
setmat->spc3f.x += tmptargetmat.spc3f.x - tmpbasemat.spc3f.x;
setmat->spc3f.y += tmptargetmat.spc3f.y - tmpbasemat.spc3f.y;
setmat->spc3f.z += tmptargetmat.spc3f.z - tmpbasemat.spc3f.z;
setmat->sceneamb.x += tmptargetmat.sceneamb.x - tmpbasemat.sceneamb.x;
setmat->sceneamb.y += tmptargetmat.sceneamb.y - tmpbasemat.sceneamb.y;
setmat->sceneamb.z += tmptargetmat.sceneamb.z - tmpbasemat.sceneamb.z;
setmat->sceneamb.w += tmptargetmat.sceneamb.w - tmpbasemat.sceneamb.w;

setmat->glowmult[0] += tmptargetmat.glowmult[0]- tmpbasemat.glowmult[0];
setmat->glowmult[1] += tmptargetmat.glowmult[1]- tmpbasemat.glowmult[1];
setmat->glowmult[2] += tmptargetmat.glowmult[2]- tmpbasemat.glowmult[2];
}

PM3DISPV* dstdispv = basepm3->m_dispv + basefaceno * 3;

int fcnt;
for( fcnt = 0; fcnt < 3; fcnt++ ){
(dstdispv + fcnt)->pos.x += ( (targetpm3->m_pointbuf + (targetn3p + fcnt)->pervert->vno)->x - (basepm3->m_pointbuf + (basen3p + fcnt)->pervert->vno)->x ) * blendw;
(dstdispv + fcnt)->pos.y += ( (targetpm3->m_pointbuf + (targetn3p + fcnt)->pervert->vno)->y - (basepm3->m_pointbuf + (basen3p + fcnt)->pervert->vno)->y ) * blendw;
(dstdispv + fcnt)->pos.z += ( (targetpm3->m_pointbuf + (targetn3p + fcnt)->pervert->vno)->z - (basepm3->m_pointbuf + (basen3p + fcnt)->pervert->vno)->z ) * blendw;

(dstdispv + fcnt)->normal += ( (targetn3p + fcnt)->pervert->smnormal - (basen3p + fcnt)->pervert->smnormal ) * blendw;
(dstdispv + fcnt)->uv += ( (targetn3p + fcnt)->pervert->uv[0] - (basen3p + fcnt)->pervert->uv[0] ) * blendw;
}

facecnt++;
}
}
}
}
return 0;
}

この関数でブレンド計算をして、その結果をベースのCPolyMesh3::m_dispv(頂点情報)とベースのCMQOObject::m_morphmaterial(材質情報)に格納します

まず関数の最初でm_dispvとm_morphmaterialをベースの情報で初期化します。
そしてターゲットごとにベースとターゲットとの差異にブレンド率をかけた情報を、初期化した情報(ベースの情報)に足し算していきます。

気をつけないといけないのは
ベースとターゲットではマテリアルの面の組は同じですがマテリアルは異なる可能性があることです。
頂点情報が格納されているCPolyMesh3::m_n3pはマテリアル番号でソートされます。
そのためベースとターゲットではn3pの頂点の順番が異なる可能性があります。

これを解決するためにCPolyMesh3::m_faceno2optfacenoを使います。
これはソート前のn3pの面番号からソート後の面番号を参照する表です。
m_faceno2opefaceno[ソート前の面番号]で参照した値がソート後の面番号になるのです。
これを使ってベースとターゲットで同じ頂点を参照しつつブレンドするようにします。


14−4、モーフGUIの概略

GUIに関してはさらっとだけ説明します。
モーフ用のタイムラインは姿勢のタイムラインとは独立したものを作ります。
構造的にはあまり変わりません。

モーフ用のタイムライン用のメンバーには姿勢用のタイムラインのメンバ名にMをつけたものを使用しました。

姿勢用と違う部分はキーを右クリックした処理を付け加えた点です。
右クリックしたイベントを通知する関数を登録できるようにしました。

RDBMain.cppのAddTimeLineのs_owpMTimeline->setCallPropertyListenerの部分が
モーフキーを右クリックしたときのリスナー関数登録部分です。

右クリックした位置にキーが存在する場合は
そのキーのターゲットとブレンド率の一覧のメニューをクリック部分に出します。
このメニューはCRMenuMainクラスです。

そしてメニューでターゲット名を選択すると
そのターゲットのブレンド率を設定するためのウインドウOrgWindow* s_morphWndを表示します。
s_morphWndにはスライダーが付いていて、これをスライドさせることでブレンド率を設定できます。


オープンソースのトップに戻る

トップページに戻る