05、モーションウインドウ





04章までは1フレームだけのポーズしか編集できませんでした。
05章ではモーションを作成できるようにし、それを編集するためのウインドウや機能を追加しました。

上のスクリーンショットの左側のウインドウがモーションウインドウです。
モーションウインドウは横軸に時間、縦軸にボーンの名前を表示します。
白い四角はキーです。
モーションウインドウの上部のスライダーで時間を設定し
メインウインドウのX,Y,Zの+−ボタンを押すことで
好きな時間に好きなポーズを設定できます。
キーとキーの間は自動的に補完計算されます。

モーションウインドウ上部の6つのボタンで再生やキー位置への時間の移動や逆方向への再生も出来ます。

メインウインドウの下部にMotionSpeedというスライダーがあります。
これはモーションの再生速度を0.3倍速から3.0倍速の間で変更できるスライダーです。
時間軸がフレーム番号ではなく実数なので、再生速度を変えても60fpsで滑らかに表示されます。

キーの編集は
まずマウスで範囲選択します。選択されるとキーのマークは白い枠線のみになります。
一度に複数ボーン複数時間に渡った複数のキーを選択できます。

選択したらツールウインドウのコピー、対称コピー、ペースト、カット、削除で操作します。
対称コピーは、片側半分のモーションを作ったのち、もう半分にそれと対称のポーズを付けるための機能です。
ですので自分自身のボーンの対称姿勢ではなく、対称ボーンの姿勢の対称姿勢がコピーされます。
対称ボーンとはボーンの末尾に[L], [R]のついたものです。

ショートカットも実装してあります。
モーションウインドウをクリックし、キーを選択したのち
Ctrl + C-->コピー、Ctrl + B-->対称コピー、Ctrl + X-->カット、Ctrl + V-->ペースト、Ctrl + D-->削除、Ctrl + キードラッグ-->キー時間移動
となります。

モーションウインドウは四隅をマウスでドラッグすることにより
大きさを変えられます。
メインウインドウの表示メニューで表示と非表示を切り替えられます。

メインウインドウのモーションメニューで
モーションの新規作成、削除、編集したいモーションの選択が出来ます。


モーションウインドウのGUIのソースはナゾビーフさんが提供してくださいました。

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



05−01、モーションとキーの作成

モーションの作成はモデルデータ読み込み時と、モーションメニューの新規空モーション作成を選んだ時に行います。
モデルを読み込んでいるときは、常に1個はモーションが存在するようにします。
モーションの作成命令はRDBMain.cppのAddMotionです。
この命令からは
s_model->AddMotionが呼ばれ、モーションを構成するキーを格納するデータを作成します。
そしてAddTimeLineでモーションウインドウに存在するキーを表示します。
最後にOnAnimMenuを呼び、メニューにモーション名を追加し選択状態にします。
OnAnimMenuではモーションの切り替え命令s_model->SetCurrentMotionも呼びます。

CModel::AddMotionを見てみましょう。

int CModel::AddMotion( char* srcname, double srcleng, int* dstid )
{
*dstid = -1;
int leng = (int)strlen( srcname );

int maxid = 0;
map<int, MOTINFO*>::iterator itrmi;
for( itrmi = m_motinfo.begin(); itrmi != m_motinfo.end(); itrmi++ ){
MOTINFO* chkmi = itrmi->second;
if( chkmi ){
if( maxid < chkmi->motid ){
maxid = chkmi->motid;
}
}
}
int newid = maxid + 1;


MOTINFO* newmi = (MOTINFO*)malloc( sizeof( MOTINFO ) );
if( !newmi ){
_ASSERT( 0 );
return 1;
}
ZeroMemory( newmi, sizeof( MOTINFO ) );

strcpy_s( newmi->motname, 256, srcname );
newmi->motid = newid;
newmi->frameleng = srcleng;
newmi->curframe = 0.0;
newmi->speed = 1.0;

m_motinfo[ newid ] = newmi;

///////////////
map<int, CBone*>::iterator itrbone;
for( itrbone = m_bonelist.begin(); itrbone != m_bonelist.end(); itrbone++ ){
CBone* curbone = itrbone->second;
if( curbone ){
CallF( curbone->MakeFirstMP( newid ), return 1 );
}
}

*dstid = newid;

return 0;
}

追加したモーションの情報はmap<int, MOTINFO*> m_motinfoに登録します。
モーションは一意に識別できるidと関連付け、そのidでそれぞれのモーションを指定します。
m_motinfoのmapの1個目のintにはこのidを使います。

最初のfor文でしていることは
新しいモーションのidを決める作業です。
既存のidの中で一番大きい数を探し、それに1を足したものを追加するモーションのidにします。

idが決まったら
MOTINFO構造体を作成し、id、フレーム長、現在のフレーム、再生スピードをセットして
m_motinfoに追加します。

MOTINFOはモーションのプロパティです。
実際のモーションのキーはボーンごとに持ちます。

ボーンごとにMakeFirstMP( newid )を呼び出して
モーションの先頭フレームに初期状態のキーを作成します。

この初期状態のキーは別に無くてもOpenRDB的には困りません。
ですがOpenRDBはXファイル出力を視野に入れています。
Xファイルの再生時には最初のフレームにキーが存在しないと
OpenRDBで作った動きと同じ動きを再現できないことがあります。
このため強制的に最初のフレームにキーを作る仕様にしました。

ボーンごとのモーションを保持するメンバは

  //CBoneのモーション格納メンバ
map<int, CMotionPoint*> m_motionkey;
CMotionPoint m_curmp;

です。
map<int, CMotionPoint*> m_motionkeyの最初のintはモーションのidです。
CMotionPoint*は各モーションの先頭のモーションポイント(キー)です。

CMotionPointはm_prevとm_nextを持つ双方向チェインのクラスです。
キーの時刻の小さい順にチェインしていきます。
キーを探すときは常にm_motionkeyの最初のキーから順にたどって探すことになります。
(キーが増えるとこのキーを探す作業に時間がかかるので、今後なんらかの最適化をするかもしれません。)


モーションのキーが追加されるのは
メインウインドウのX+,X-,Y+,Y-,Z+,Z-のオイラー角設定ボタンを押したときです。
ボタンを押したときに指定ボーン、指定時刻にキーが存在しない場合に
キーを新規作成します。

ボタンを押すとRDBMain.cppのAddBoneEulが呼ばれます。

int AddBoneEul( float addx, float addy, float addz )
{
if( !s_model || (s_curboneno < 0) ){
return 0;
}

D3DXVECTOR3 cureul, neweul;

CMotionPoint* newmp = 0;
int existflag = 0;
int calcflag = 1;
_ASSERT( s_model->m_curmotinfo );
newmp = s_model->AddMotionPoint( s_model->m_curmotinfo->motid, s_owpTimeline->getCurrentTime(), s_curboneno, calcflag, &existflag );
if( !newmp ){
_ASSERT( 0 );
return 1;
}

cureul = newmp->m_eul;
neweul.x = cureul.x + addx;
neweul.y = cureul.y + addy;
neweul.z = cureul.z + addz;
newmp->SetEul( neweul );

if( existflag == 0 ){
CBone* curbone = s_model->m_bonelist[ s_curboneno ];
_ASSERT( curbone );
s_owpTimeline->newKey( curbone->m_wbonename, s_owpTimeline->getCurrentTime(), (void*)newmp );
}

return 0;
}

AddBoneEulからCModel::AddMotionPointが呼ばれます。
ここで時刻の値に相当する位置に新しいキーがチェインされます。
すでにその時刻にキーが存在する場合はexistflagに1をセットし、存在するモーションポイントのポインターを返します。

AddBoneEulでは
返り値のキーのオイラー角に指定角度を足し引きします。

05−02、カレントの姿勢の計算

「カレント」とは「現在の」という意味です。
画面に表示する姿勢、つまりカレントの姿勢を計算を始めるのは
OnFrameRenderから呼ばれるCModel::UpdateMatrixです。

CModel::UpdateMatrixからは一番親のボーンから子供方向に向けて再帰的にUpdateMatrixReqが呼ばれます。
UpdateMatrixReqからはボーンごとにCBone::UpdateMatrixが呼ばれます。

CBone::UpdateMatrixの内部で

  CallF( CalcMotionPoint( srcmotid, srcframe, &m_curmp, &existflag ), return 1 );
CallF( m_curmp.UpdateMatrix( wmat, parmat, this ), return 1 );

のように、まずm_curmpに現在時刻のローカルのモーションポイントを計算したものをセットし
そこからm_curmat.UpdateMatrixでグローバルの姿勢を計算します。

CBone::CalcMotionPointは以下のような内容です。


int CBone::CalcMotionPoint( int srcmotid, double srcframe, CMotionPoint* dstmpptr, int* existptr )
{
CMotionPoint* befptr = 0;
CMotionPoint* nextptr = 0;
CallF( GetBefNextMP( srcmotid, srcframe, &befptr, &nextptr, existptr ), return 1 );
CallF( CalcFrameMP( srcframe, befptr, nextptr, *existptr, dstmpptr ), return 1 );

return 0;
}

まずGetBefNextMPで指定時刻の前後のキーを取得します。
もし指定時刻に合致するキーがすでにある場合はbefptrにそのポインタが入り、existflagが1になります。

前後のキーを取得したら
CalcFrameMPでその2つのキーの間を補完計算し、ローカルのカレントの姿勢を計算します。
この補完には現在オイラー角の線形補完方法を取っています。
これは今後追加するであろうスプライン補完をオイラー角で行うために
それと形式を合わせる意味でこうしています。

オイラー角による線形補完よりもクォータニオンの球面線形補完の方がきれいなので
あとでその方法も追加するかもしれません。


05−03、OrgWindowの使い方

OrgWindowのソースはナゾビーフさんが書きました。
実装方法は省略し、簡単な使い方のみ説明します。

OrgWindowはdisp4フォルダのINCLUDE, CPPの中のOrgWindow.h, OrgWindow.cppにまとめられています。

モーションウインドウの親のクラスがOrgWindowです。
そしてキーを表示する部分がOWP_Timelineです。
モーションの再生、ストップなどのボタンを管理するのがOWP_PlayerButtonです。

OrgWindowはRDBMain.cppのInitAppで作成します。
OWP_TimelineはAddTimelineで作成します。

ウインドウから各処理が行われた通知はListenerで受け取ります。
リスナーにはボタンが押されたときやタイムラインをクリックしたときなど
さまざまなときに呼ばれるものが用意されています。

このリスナーにラムダ関数と呼ばれるもので
ユーザー側の処理を定義します。
多くは通知フラグをオンにするという内容を定義しています。

RDBMain.cppのOnFrameMove関数内で
リスナーで設定したフラグをチェックし、オンになっていたら処理を行うようにします。


05−04、クォータニオンからオイラー角への変換

OnFrameMoveでのキーの処理は対称コピー以外は簡単であえて説明するまでもないでしょう。
対称コピーの部分を下に示します。

if( s_symcopyFlag && s_model ){
s_symcopyFlag = false;

s_copyKeyInfoList.erase( s_copyKeyInfoList.begin(), s_copyKeyInfoList.end() );
s_copyKeyInfoList = s_owpTimeline->getSelectedKey();

list<OWP_Timeline::KeyInfo>::iterator itrcp;
for( itrcp = s_copyKeyInfoList.begin(); itrcp != s_copyKeyInfoList.end(); itrcp++ ){
int srcboneno = s_lineno2boneno[ itrcp->lineIndex ];
if( srcboneno >= 0 ){
int symboneno = -1;
int existflag = 0;
CallF( s_model->GetSymBoneNo( srcboneno, &symboneno, &existflag ), return );
if( existflag ){
CMotionPoint symmp;
int existmp = 0;
CallF( s_model->m_bonelist[ symboneno ]->CalcMotionPoint( s_model->m_curmotinfo->motid, itrcp->time, &symmp, &existmp ), return );
CQuaternion symq;
symq.SetParams( 1.0f, 0.0f, 0.0f, 0.0f );
symmp.m_q.CalcSym( &symq );
CMotionPoint* pbef = 0;
CMotionPoint* pnext = 0;
int existcur = 0;
CallF( s_model->m_bonelist[ symboneno ]->GetBefNextMP( s_model->m_curmotinfo->motid, itrcp->time, &pbef, &pnext, &existcur ), return );
D3DXVECTOR3 cpeul;
D3DXVECTOR3 befeul;
if( pbef ){
befeul = pbef->m_eul;
}else{
befeul = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
}
CallF( symq.Q2Eul( befeul, &cpeul ), return );
itrcp->eul = cpeul;
itrcp->tra.x = -symmp.m_tra.x;
itrcp->tra.y = symmp.m_tra.y;
itrcp->tra.z = symmp.m_tra.z;
}else{
CMotionPoint* mpptr = (CMotionPoint*)(itrcp->object);
itrcp->eul = mpptr->m_eul;
itrcp->tra = mpptr->m_tra;
}
}
}
}

まず
s_copyKeyInfoList = s_owpTimeline->getSelectedKey();
で選択しているキー情報をコピー情報保存用のリストに代入します。

後の部分の処理はs_copyKeyInfoListのeul(オイラー角)メンバ, tra(移動成分)メンバに
対称の姿勢を代入するための処理です。

s_model->GetSymBoneNoで対称のボーンを取得します。
対称のボーンとは末尾に[L]の付いているボーンに対しては[R]の付いているボーン、
[R]の付いているボーンに対しては[L]の付いているボーンを指します。

対称ボーンが見つかったら
そのボーンの現在の姿勢をCalcMotionPointで計算します。
そしてその対称な姿勢を計算します。
クォータニオンはCQuaternion::CalcSymで対称なクォータニオンを計算し
移動成分はX成分を-1倍することで計算します。

copyKeyInfoListの回転情報はeulなので
クォータニオンをオイラー角に変換します。
CQuaternion::Q2Eulで変換します。

OpenRDBでのオイラー角はZ軸、X軸、Y軸の順番に回転します。
ですのでQ2Eulではその逆順Y,X,Zの順に軸に一致する回転をさせてオイラー角を求めます。

ポイントは求めたオイラー角をその前のキーのオイラー角に一番近い値に調整することです。
同じクォータニオンに対してオイラー角は何通りも考えられますが
前のキーに一番近い値にすることで一意に求まります。

この調整している部分がCQuaternion::ModifyEulerです。


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

トップページに戻る