03、mikotoデータを読み込んでFKでポーズをつける


この章では02章で計算したボーン影響度をもとに
モデルデータをボーン変形して表示します。



コンボボックスでボーンを選び、XYZのそれぞれの増減ボタンを押すとボーンを曲げることが出来ます。


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


###############################

03−01、頂点フォーマット

ボーン変形のために頂点フォーマットを変えます。
頂点フォーマットの作成はCDispObj::CreateDeclで行います。

int CDispObj::CreateDecl()
{
    D3DVERTEXELEMENT9 vdecl[] = {
    //pos[4]
    { 0, 0, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },

    //normal[3]
    { 0, 16, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0 },

    //uv
    { 0, 28, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 },

    //weight[4]
    { 1, 0, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BLENDWEIGHT, 0 },

    //boneindex[4]
    { 1, 16, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BLENDINDICES, 0 },


    D3DDECL_END()
    };

    HRESULT hr;
    hr = m_pdev->CreateVertexDeclaration( vdecl, &m_dispvdecl );
    if( hr != D3D_OK ){
        _ASSERT( 0 );
        return 1;
    }

    return 0;
}


新たにweight[4](ブレンド率)とboneindex[4](ボーンの番号)が加わりました。
[4]となっているのは1つの頂点に付き最大4個のボーンの影響をブレンドできるようにするためです。
weight[0]とboneidex[0]、weight[1]とboneindex[1]、... のように
1つのボーンについて同じ添え字のweightとboneindexが対応します。

注目すべきは追加したDECLのエントリーの{}の最初の数字、つまりストリーム番号が1になっていることです。
それ以前に定義したものはストリーム番号が0です。

DirectXでは頂点の定義に複数のストリームを使うことが出来ます。
ストリームとは簡単に言えばバッファです。
1つの頂点の定義に複数のバッファを使えるのです。

ここでは、pos[4], normal[3], uvからなるバッファ(m_VB)とweight[4], boneindex[4]からなるバッファ(m_InfB)
の2つのバッファで頂点を定義します。

こうすることの利点は描画の種類ごとにバッファをすべて作り直すことなく
バッファの組み合わせでさまざまな描画に対応できることにあります。

描画の際には
CDispObj::RenderNormalのように

m_pdev->SetVertexDeclaration( m_dispvdecl );
m_pdev->SetStreamSource( 0, m_VB, 0, sizeof(PM3DISPV) );
m_pdev->SetStreamSource( 1, m_InfB, 0, sizeof(PM3INF) );
m_pdev->SetIndices( m_IB );
hres = m_pdev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST,
    0,
    0,
    m_pm3->m_optleng,
    currb->startface * 3,
    curnumprim
    ); 


のようにSetStreamSourceで複数のストリームをシェーダーに渡します。


03−02、モーションポイント

ボーンごとの姿勢はCMotionPointに格納します。

class CMotionPoint
{
    ...関数メンバ省略...

public:
    D3DXVECTOR3 m_eul;
    CQuaternion m_q;
    D3DXVECTOR3 m_tra;
    D3DXMATRIX m_mat;//親の影響を受けていないマトリックス
    D3DXMATRIX m_totalmat;//親の影響を受けているマトリックス
    D3DXMATRIX m_worldmat;//ワールド変換と親の影響を受けたマトリックス
};


モーションポイントは、回転をm_eulとm_qに格納します。
m_eulはオイラー角です。Z,X,Y軸の順に回転させる仕様にします。
m_qはクォータニオンです。
m_traに移動成分を格納します。

m_mat, m_totalmap, m_worldmatに回転と移動から算出した姿勢行列を格納します。

このCMotionPointは将来的にはモーション用のクラスを作ってそこに格納します。
今は1フレームだけの操作しか扱わないので暫定的にCBoneに1個ずつ作成します。

行列の計算方法を説明します。
まず自分のボーンについてだけの姿勢行列m_matを計算します。

int CMotionPoint::MakeMat( CBone* srcbone )
{
    D3DXMATRIX befrotmat, aftrotmat, rotmat, tramat;
    D3DXMatrixIdentity( &befrotmat );
    D3DXMatrixIdentity( &aftrotmat );
    D3DXMatrixIdentity( &rotmat );
    D3DXMatrixIdentity( &tramat );

    befrotmat._41 = -srcbone->m_vertpos[ BT_PARENT ].x;
    befrotmat._42 = -srcbone->m_vertpos[ BT_PARENT ].y;
    befrotmat._43 = -srcbone->m_vertpos[ BT_PARENT ].z;

    aftrotmat._41 = srcbone->m_vertpos[ BT_PARENT ].x;
    aftrotmat._42 = srcbone->m_vertpos[ BT_PARENT ].y;
    aftrotmat._43 = srcbone->m_vertpos[ BT_PARENT ].z;

    rotmat = m_q.MakeRotMatX();

    tramat._41 = m_tra.x;
    tramat._42 = m_tra.y;
    tramat._43 = m_tra.z;

    m_mat = befrotmat * rotmat * aftrotmat * tramat;

    return 0;
}



ボーンの位置を原点に戻すような移動がbefrotmatで
rotmatはm_qに対応する回転行列で
原点をボーン位置に移動する行列がaftrotmatです。

この3つのセットでボーン位置を中心とした回転を表します。

これらを順番に掛け算して
最後に移動変換のtramatを掛けてボーンのローカルの姿勢の完成です。


ボーンのローカル姿勢が計算できたら
親の姿勢(m_totalmat)を

m_totalmat = m_mat * *parmat;


のように掛けます。
親のtotalmatが確定してからではないと子供の姿勢が計算できないことになります。
ですので行列の計算は一番親から子供の方向に再帰的に計算していくことになります。

m_totalmatが出来たら
モデル全体の移動回転を掛け算してm_worldmatを計算します。
このm_worldmatが頂点のワールド座標を計算する際の行列になります。

ボーンのワールド変換行列は一時的にCModel::m_bonemat[MAXBONENUM]に格納し
シェーダーのg_mWMatArray[43]定数に転送します。

これはCModel::SetShaderConstで行っています。

int CModel::SetShaderConst()
{
    map<int, CBone*>::iterator itrbone;
    int setcnt = 0;
    for( itrbone = m_bonelist.begin(); itrbone != m_bonelist.end(); itrbone++ ){
        int boneno = itrbone->first;
        CBone* curbone = itrbone->second;
        if( curbone && (boneno >= 0) && (boneno < MAXBONENUM) ){
            m_bonemat[boneno] = curbone->m_curmp.m_worldmat;
        }
    }

    HRESULT hr;
    hr = g_pEffect->SetMatrixArray( g_hmWMatArray, m_bonemat, MAXBONENUM );
    if( hr != D3D_OK ){
        _ASSERT( 0 );
        return 1;
    }

    return 0;
}



03−03、シェーダー

頂点シェーダーは以下のようになります。
シェーダーファイルはMedia/Shader/Ochakko.fxです。

...

float4x4 g_mWMatArray[43] : WORLDMATRIXARRAY;

...


VS_OUTPUT RenderSceneVS( float4 vPos : POSITION,
float3 vNormal : NORMAL,
float2 vTexCoord0 : TEXCOORD0,
float4 bweight : BLENDWEIGHT,
float4 bindices : BLENDINDICES,
uniform int nNumLights )
{
VS_OUTPUT Output;
float4 wPos;
 
float4x4 finalmat = 0;
finalmat += g_mWMatArray[bindices.x] * bweight.x;
finalmat += g_mWMatArray[bindices.y] * bweight.y;
finalmat += g_mWMatArray[bindices.z] * bweight.z;
finalmat += g_mWMatArray[bindices.w] * bweight.w;

wPos = mul( vPos, finalmat );
Output.Position = mul( wPos, g_mVP );
wPos /= wPos.w;

float3 wNormal;
wNormal = normalize(mul(vNormal, (float3x3)finalmat)); // normal (world space)

float3 totaldiffuse = float3(0,0,0);
float3 totalspecular = float3(0,0,0);
float calcpower = g_power * 0.1f;

for(int i=0; i<nNumLights; i++ ){
float nl;
float3 h;
float nh;
float4 tmplight;

nl = dot( wNormal, g_LightDir[i] );
h = normalize( ( g_LightDir[i] + g_EyePos - wPos.xyz ) * 0.5f );
nh = dot( wNormal, h );

totaldiffuse += g_LightDiffuse[i] * max(0,dot(wNormal, g_LightDir[i]));
totalspecular += ((nl) < 0) || ((nh) < 0) ? 0 : ((nh) * calcpower);
}

Output.Diffuse.rgb = g_diffuse.rgb * totaldiffuse.rgb + g_ambient + g_emissive;
Output.Diffuse.a = g_diffuse.a;

Output.Specular = g_specular * totalspecular;

Output.TextureUV = vTexCoord0;

return Output;
}



finalmatに4つのボーンのワールド行列をブレンドします。
そしてwPosに頂点のワールド座標を計算します。
wPosをwで割るのはwを無視してxyzだけを使用する際に必要です。
(確かユークリッド空間とかその辺の話です。)

2つしかボーンの影響を受けていない場合は
CDispObjで頂点データをセットする際に、余ったボーンのweightに0がセットされています。

いつも4つ分計算して無駄じゃないかと思うかもしれませんが
シェーダーにif文を入れると環境によってはfpsがガタ落ちします。

影響ボーンの数の種類分、頂点シェーダーの関数を定義するか
もしくはすべてにおいて4つ分計算するかが妥当なところです。


03−04、FKのユーザーインターフェース

UIにコンボボックスとボタンを追加しました。
どちらもCDXUTDialogを使います。

コンボボックスはCDXUTDialog::AddComboBox, ボタンはCDXUTDialog::AddButtonで作成します。
どちらもユーザーの操作はコールバック関数OnGUIEventで反映させます。

コンボボックスを選んだときは、カレントの操作用ボーン番号g_curbonenoをセットし
ボタンを押したときにオイラー角を変更して
CModel::SetBoneEulを呼びます。

変更された姿勢はOnFrameRenderから呼ばれるUpdateMatrix, SetShaderConstでシェーダーデータに反映され
描画されます。


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

トップページに戻る