Graphics/DirectX

Direct X - 스키닝

MOLOKINI 2014. 10. 21. 12:07

스키닝

계층구조 + 뼈대 애니메이션을 한단계 진보시킨 기법이다.

메시를 뼈대와 결합시킬 때 생성하는 행렬을 어떻게 만드느냐 하는 것이 핵심


설계

 


그 아래 세개의 클래스는 ZSWSkinnedMesh, ZFFSkinnedMesh, ZVSSkinnedMesh는 스키닝을 지원하는 방식에 따라서 나눈 것.


ZSWSkinnedMesh : 소프트웨어적으로 CPU연산을 통한 스키닝 방식

ZFFSkinnedMesh : Direct3D의 매트릭스 팔레트를 사용한 API 고정함수 방식

ZVSSkinnedMesh : 아직 안만들었는데, 정점셰이더를 사용해서 만들 예정





구현 - ZSkinnedMesh


class ZSkinnedMesh : public ZMesh

{

protected:

vector<int> m_idxBones; /// 메시에 영향을 미치는 뼈대의 인덱스

vector<D3DXMATRIX>* m_pMatPalette;


public:

ZSkinnedMesh( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );

~ZSkinnedMesh();

// 최초에 한번 불린다.

void SetMatrixPalette( vector<D3DXMATRIX>* pPal ) { m_pMatPalette = pPal; }


vector<int>& GetBones() { return m_idxBones; }

virtual int Draw( D3DXMATRIX* pTM ) { return 1; }

};


스키닝은 대부분 뻐대를 기반으로 만들어지게 되기 때문에 스키닝되는 메시는 반드시 뼈대에 대한 정보를 갖고 있어야 한다.

그래서 두개의 변수가 추가되었다.

하나는 현재 메시에 영향을 주눈 뼈대의 인덱스 정보

나머지 하나는 스키닝에 사용될 행렬 정보

이 스키닝 행렬을 만드는 것은 기존의 변환행렬을 만드는것과는 약간 다른 방식이 적용된다.

이에 대해서는 ZBone::Animate() 함수에서 자세히 설명한다.


매트릭스 팔레트는 ZNodeMgr에 소속된 모든 노드들이 공유해야 할 정보이므로 ZNodeMgr에 선언되어 있다. 그래서 SetMatrixPalette()함수를 통해 ZNodeMgr에 선언된 매트릭스 팔레트에 접근할 수 있도록 했다.



구현 - ZSWSkinnedMesh


class ZSWSkinnedMesh : public ZSkinnedMesh

{

protected:

/// 소프트웨어 스키닝을 위해서 정점버퍼와 똑같은 정점배열을 한 카피 보관해 둔다.

ZSWSkinnedVertex* m_pvtx; /// skinning 정점 배열

ZRigidVertex* m_pvtxDraw; /// rigid 정점 배열

// 소프트웨어 스키닝을 위해서 인덱스버퍼와 똑같은 인덱스배열을 한 카피 보관해 둔다.

void* m_pidx; /// index는 16비트, 32비트 2가지를 지원해야

/// 하므로 void형 포인터를 사용했다

D3DFORMAT m_fmtIdx; /// D3DFMT_INDEX16 or D3DFMT_INDEX32

int _CreateVIB( ZCMesh* pMesh );

void _ApplyPalette( D3DXMATRIX* pTM );


public:

ZSWSkinnedMesh( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );

~ZSWSkinnedMesh();


virtual int Draw( D3DXMATRIX* pTM );

};


ZSWSkinnedVertex는 다음과 같이 선언되어 있다.
struct ZSWSkinnedVertex
{
  D3DXVECTOR3 p;
  float b[4];
  DWORD i[4];
}

가중치 값 4개(b[4])와 뼈대의 인덱스 4개(i[4])가 선언되어있다. 이렇게 선언된 ZSWSkinnedVertex는 출력에는 사용되지 않고 오직 뼈대와의 가중치 연산에만 사용되며, 실제 출력은 ZRigidVertex가 담당한다.

이제 정점을 생성하는 과정을 살펴보자

int ZSWSkinnedMesh::_CreateVIB( ZCMesh* pMesh )
{
int i, j;
set<int> idxBones;

m_nVerts = pMesh->m_vtxFinal.size();
m_dwFVF = ZRigidVertex::FVF;
m_pvtx = new ZSWSkinnedVertex[m_nVerts];
m_pvtxDraw = new ZRigidVertex[m_nVerts];

m_bboxMax = D3DXVECTOR3( pMesh->m_vtxFinal[0].p.x, pMesh->m_vtxFinal[0].p.y, pMesh->m_vtxFinal[0].p.z );
m_bboxMin = D3DXVECTOR3( pMesh->m_vtxFinal[0].p.x, pMesh->m_vtxFinal[0].p.y, pMesh->m_vtxFinal[0].p.z );

for( i = 0 ;  i < m_nVerts ; i++ )
{
m_pvtx[i].p.x = pMesh->m_vtxFinal[i].p.x;
m_pvtx[i].p.y = pMesh->m_vtxFinal[i].p.y;
m_pvtx[i].p.z = pMesh->m_vtxFinal[i].p.z;
m_pvtx[i].b[0] = pMesh->m_vtxFinal[i].b[0];
m_pvtx[i].b[1] = pMesh->m_vtxFinal[i].b[1];
m_pvtx[i].b[2] = pMesh->m_vtxFinal[i].b[2];
m_pvtx[i].b[3] = 1.0f - ( m_pvtx[i].b[0] + m_pvtx[i].b[1] + m_pvtx[i].b[2] );
for( j = 0 ; j < 4 ; j++ )
{
m_pvtx[i].i[j] = ((pMesh->m_vtxFinal[i].i>>(j*8)) & 0xff);
idxBones.insert( m_pvtx[i].i[j] );
}

m_pvtxDraw[i].n.x = pMesh->m_vtxFinal[i].n.x;
m_pvtxDraw[i].n.y = pMesh->m_vtxFinal[i].n.y;
m_pvtxDraw[i].n.z = pMesh->m_vtxFinal[i].n.z;
m_pvtxDraw[i].t.x = pMesh->m_vtxFinal[i].t.x;
m_pvtxDraw[i].t.y = pMesh->m_vtxFinal[i].t.y;

// 맥스스크립트로 얻어지는 경계상자는 이상할 경우가 많아서 직접 계산한다
if( m_pvtx[i].p.x > m_bboxMax.x ) m_bboxMax.x = m_pvtx[i].p.x;
if( m_pvtx[i].p.y > m_bboxMax.y ) m_bboxMax.y = m_pvtx[i].p.y;
if( m_pvtx[i].p.z > m_bboxMax.z ) m_bboxMax.z = m_pvtx[i].p.z;
if( m_pvtx[i].p.x < m_bboxMin.x ) m_bboxMin.x = m_pvtx[i].p.x;
if( m_pvtx[i].p.y < m_bboxMin.y ) m_bboxMin.y = m_pvtx[i].p.y;
if( m_pvtx[i].p.z < m_bboxMin.z ) m_bboxMin.z = m_pvtx[i].p.z;
}

// 값이 겹치면 안되므로 STL의 set컨테이너를 사용한다
for( set<int>::iterator it = idxBones.begin() ; it != idxBones.end() ; it++ )
{
m_idxBones.push_back( *it );
}

idxBones.clear();

// 인덱스 생성
D3DCAPS9 caps;
m_pDev->GetDeviceCaps( &caps );
m_nTriangles = pMesh->m_idxFinal.size();

// 인덱스가 32비트 인덱스를 지원하면 32비트 인덱스 버퍼 생성
if( caps.MaxVertexIndex > 0x0000ffff )
{
m_pidx = (void*)(new Index3i[m_nTriangles]);
DWORD* pW = (DWORD*)m_pidx;
m_fmtIdx = D3DFMT_INDEX32;
for( i = 0 ; i < m_nTriangles ; i++ )
{
*(pW+0) = pMesh->m_idxFinal[i].i[0];
*(pW+1) = pMesh->m_idxFinal[i].i[1];
*(pW+2) = pMesh->m_idxFinal[i].i[2];
pW += 3;
}
}
else // 아니라면 16비트 인덱스로 인덱스 버퍼 생성
{
m_pidx = (void*)(new Index3w[m_nTriangles]);
WORD* pW = (WORD*)m_pidx;
m_fmtIdx = D3DFMT_INDEX16;
for( i = 0 ; i < m_nTriangles ; i++ )
{
*(pW+0) = (WORD)pMesh->m_idxFinal[i].i[0];
*(pW+1) = (WORD)pMesh->m_idxFinal[i].i[1];
*(pW+2) = (WORD)pMesh->m_idxFinal[i].i[2];
pW += 3;
}
}

return 1;
}

루프를 돌면서 pMesh->m_vtxFinal의 값을 복사한다.
이때, 인덱스와 가중치를 복사하는 과정을 잘 살펴보도록 하자. 중간에 STL의 set<int> idxBones를 선언해서 메시에서 사용중인 뼈대의 인덱스 값을 중복되는 값 없이 구해놓는다. 이 값이 ZSkinnedMesh::m_idxBones 값이 된다.

정점 정보를 모두 복사했으면 인덱스 정보를 복사하는데, 이때 그래픽카드가 16비트 이상의 인덱스를 지원하면 32비트 인덱스로 생성하도록 자동 판단하고있다. 이 방법은 편리하기는 하지만 메모리의 낭비가 심하므로, 실제로는 이렇게 하지 않고 메시의 크기에 따라서 판단하는것이 훨씬 효율적일 것이다.

이제 생성된 정점과 인덱스를 기반으로 스키닝 연산을 해보자

int ZSWSkinnedMesh::Draw( D3DXMATRIX* pTM )
{
_ApplyPalette( pTM );
m_pDev->SetFVF( m_dwFVF );
m_pDev->DrawIndexedPrimitiveUP( D3DPT_TRIANGLELIST, 0, m_nVerts, m_nTriangles, m_pidx, m_fmtIdx, m_pvtxDraw, sizeof(ZRigidVertex) );
return 1;
}

Draw()함수의 본체가 간단한걸 보면, _ApplyPalette() 함수에서 매트릭스 팔레트 스키닝을 하고있음을 알 수 있다.

void ZSWSkinnedMesh::_ApplyPalette( D3DXMATRIX* pTM )
{
int i, j;
DWORD idx;
D3DXMATRIXA16 m;
D3DXVECTOR3 v;

for( i = 0 ; i < m_nVerts ; i++ )
{
// 정점을 0으로 초기화
m_pvtxDraw[i].p = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );

for( j = 0 ; j < 4 ; j++ )
{
idx = m_pvtx[i].i[j]; // 뼈대의 인덱스
m = m_matTM * ((*m_pMatPalette)[idx]); // TM_bone행렬
D3DXVec3TransformCoord( &v, &m_pvtx[i].p, &m ); // V_world(n) = V_local(n) * TM_bone(idx)
m_pvtxDraw[i].p += ( v * m_pvtx[i].b[j] ); // V_world = V_world(0)+ ... +V_world(3)
}
}
}

생각보다 함수의 구조가 간단하다
이 함수를 차근차근 분석해보면, 결국 전체 정점의 개수만큼 루프를 돌면서 각각의 정점마다 4개씩의 가중치연산을 한다. 이때, 영향을 받는 뼈대의 인덱스에 기반해서 매트릭스 팔레트로부터 스킨행렬을 얻어오고, 이 행렬과 정점을 곱한 값을 가중치만큼 더해주는 것이다.

이걸 수식으로하면
Vworld = Vlocal * M[index0] * W0 + Vlocal * M[index1] * W1 + 
             Vlocal * M[index2] * W2 + Vlocal * M[index3] * W3

ZSWSkinnedMesh에 구현된 함수는 이게 전부
그럼 ZFFSkinnedMesh는?


구현 - ZFFSkinnedMesh

class ZFFSkinnedMesh : public ZSkinnedMesh
{
protected:
int _CreateVIB( ZCMesh* pMesh );
void _ApplyPalette( D3DXMATRIX* pTM );
public:
ZFFSkinnedMesh( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );
~ZFFSkinnedMesh();

virtual int Draw( D3DXMATRIX* pTM );
};

클래스 선언이 ZSWSkinnedMesh 보다 간단하다, 실제 구현함수도 그럴까?

int ZFFSkinnedMesh::_CreateVIB( ZCMesh* pMesh )
{
int i , j;
int idx;
VOID* pV; // 정점버퍼 lock했을때 얻어오는 값
VOID* pI; // 인덱스버퍼 lock했을때 얻어오는 값
set<int> idxBones;

m_dwFVF = ZSkinnedVertex::FVF;
m_nVerts = pMesh->m_vtxFinal.size();;
m_bboxMax = D3DXVECTOR3( pMesh->m_vtxFinal[0].p.x, pMesh->m_vtxFinal[0].p.y, pMesh->m_vtxFinal[0].p.z );
m_bboxMin = D3DXVECTOR3( pMesh->m_vtxFinal[0].p.x, pMesh->m_vtxFinal[0].p.y, pMesh->m_vtxFinal[0].p.z );

for( i = 0 ;  i < m_nVerts ; i++ )
{
// 인덱스 값을 set에 저장해둔다
for( j = 0 ; j < 4 ; j++ )
{
idx = ( ( pMesh->m_vtxFinal[i].i >> (j*8) ) & 0x000000ff );
idxBones.insert( idx );
}

// 맥스스크립트로 얻어지는 경계상자는 이상할 경우가 많아서 직접 계산한다
if( pMesh->m_vtxFinal[i].p.x > m_bboxMax.x ) m_bboxMax.x = pMesh->m_vtxFinal[i].p.x;
if( pMesh->m_vtxFinal[i].p.y > m_bboxMax.y ) m_bboxMax.y = pMesh->m_vtxFinal[i].p.y;
if( pMesh->m_vtxFinal[i].p.z > m_bboxMax.z ) m_bboxMax.z = pMesh->m_vtxFinal[i].p.z;
if( pMesh->m_vtxFinal[i].p.x < m_bboxMin.x ) m_bboxMin.x = pMesh->m_vtxFinal[i].p.x;
if( pMesh->m_vtxFinal[i].p.y < m_bboxMin.y ) m_bboxMin.y = pMesh->m_vtxFinal[i].p.y;
if( pMesh->m_vtxFinal[i].p.z < m_bboxMin.z ) m_bboxMin.z = pMesh->m_vtxFinal[i].p.z;
}

// 값이 겹치면 안되므로 STL의 set컨테이너를 사용한다
for( set<int>::iterator it = idxBones.begin() ; it != idxBones.end() ; it++ )
{
m_idxBones.push_back( *it );
}

idxBones.clear();

// 정점 버퍼 생성
m_pDev->CreateVertexBuffer( m_nVerts * sizeof(ZSkinnedVertex), 0, m_dwFVF, D3DPOOL_DEFAULT, &m_pVB, NULL );
m_pVB->Lock( 0, m_nVerts * sizeof(ZSkinnedVertex), (void**)&pV, 0 );
memcpy( pV, &pMesh->m_vtxFinal[0], m_nVerts * sizeof(ZSkinnedVertex) );
m_pVB->Unlock();

D3DCAPS9 caps;
m_pDev->GetDeviceCaps( &caps );
m_nTriangles = pMesh->m_idxFinal.size();

// 인덱스가 32비트 인덱스를 지원하면 32비트 인덱스 버퍼 생성
if( caps.MaxVertexIndex > 0x0000ffff )
{
m_pDev->CreateIndexBuffer( m_nTriangles * sizeof(Index3i), 0, D3DFMT_INDEX32, D3DPOOL_DEFAULT, &m_pIB, NULL );
m_pIB->Lock( 0, m_nTriangles * sizeof(Index3i), (void**)&pI, 0 );
DWORD* pW = (DWORD*)pI;
for( i = 0 ; i < m_nTriangles ; i++ )
{
*(pW+0) = pMesh->m_idxFinal[i].i[0];
*(pW+1) = pMesh->m_idxFinal[i].i[1];
*(pW+2) = pMesh->m_idxFinal[i].i[2];
pW += 3;
}
m_pIB->Unlock();
}
else // 아니라면 16비트 인덱스로 인덱스 버퍼 생성
{
m_pDev->CreateIndexBuffer( m_nTriangles * sizeof(Index3w), 0, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &m_pIB, NULL );
m_pIB->Lock( 0, m_nTriangles * sizeof(Index3w), (void**)&pI, 0 );
WORD* pW = (WORD*)pI;
for( i = 0 ; i < m_nTriangles ; i++ )
{
*(pW+0) = (WORD)pMesh->m_idxFinal[i].i[0];
*(pW+1) = (WORD)pMesh->m_idxFinal[i].i[1];
*(pW+2) = (WORD)pMesh->m_idxFinal[i].i[2];
pW += 3;
}
m_pIB->Unlock();
}

return 1;
}

앞서 살펴본 ZSWSkinnedMesh와 거의 동일함을 알 수 있다.
다만 ZSWSkinnedMesh는 CPU연산을 해야하기 때문에 정점버퍼나 인덱스버퍼를 만들지 않고 직접 배열을 사용하고 있는것이 다를 뿐이다.
ZFFSkinnedMesh는 Direct3D의 가속기능을 사용할 것이므로 정점버퍼와 인덱스버퍼를 만들어야한다.

int ZFFSkinnedMesh::Draw( D3DXMATRIX* pTM )
{
_ApplyPalette( pTM );

m_pDev->SetRenderState( D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE );
// blend weight는 4개(오타 아님! 4개!)
m_pDev->SetRenderState( D3DRS_VERTEXBLEND, D3DVBF_3WEIGHTS );

m_pDev->SetStreamSource( 0, m_pVB, 0, sizeof(ZSkinnedVertex) );
m_pDev->SetFVF( m_dwFVF );
m_pDev->SetIndices( m_pIB );
m_pDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, m_nVerts, 0, m_nTriangles );
    m_pDev->SetRenderState( D3DRS_VERTEXBLEND, D3DVBF_DISABLE );
    m_pDev->SetRenderState( D3DRS_INDEXEDVERTEXBLENDENABLE, FALSE );
return 1;
}

Draw()함수가 ZSWSkinnedMesh에 비해 약간 복잡하다, 그러나 매트릭스 팔레트 블렌딩을 사용하기 위해서 Direct3D의 렌더링 상태를 바꿔주는 함수 호출이 추가되었을 뿐 나머지는 일반 DrawIndexedPrimitive()와 동일함을 알 수 있을 것이다.

이제 매트릭스 팔레트에 행렬을 세팅하는 함수를 보자

void ZFFSkinnedMesh::_ApplyPalette( D3DXMATRIX* pTM )
{
D3DXMATRIX m;
int size = m_idxBones.size();
int idx;

for( int i = 0 ; i < size ; i++ )
{
idx = m_idxBones[i];
m = m_matTM * ((*m_pMatPalette)[idx]);
m = m * *pTM; // NodeManager의 TM행렬
m_pDev->SetTransform( D3DTS_WORLDMATRIX(idx), &m );
}
}

방금 가중치와 행렬연산을 통해서 직접 최종 정점의 좌표를 구했지만, 알다시피 매트릭스 팔레트는 스키닝 행렬을 매트릭스 팔레트에 등록해주기만 하면 자동으로 연산해주는 시스템이다.
소스를 보면 영향을 받는 뼈대의 개수만큼 루프를 돌면서 뼈대의 스키닝 행렬을 매트릭스 팔레트에 등록하고있다. 이 과정 중간에 m = m * *pTM 연산을 하게되는데, 이 연산은 ZSWSkinnedMesh에는 없던 연산이야, pTM행렬은 ZNodeMgr 행렬의 변환행렬로 모든 노드들에 적용되어야 하는 행렬이다, 그렇기 때문에 ZNodeMgr::Draw() 함수에서 다음과 같이 기본행렬을 세팅하고 ZNode::Draw() 함수들을 호출한다.
  
   m_pDev->SetTransform( D3DTS_WORLDMATRIX(idx), &m );

ZSWSkinnedMesh의 Draw()함수는 직접 가중치 연산을 해서 지역위치(local vertex)를 구하고 출력시에, 이 행렬이 곱해져 최종위치(world vertex) 연산이 되지만, 우리가 사용하는 매트릭스 팔레트에서는 이 행렬의 영향을 받지 않는다, 따라서 처음부터 정점이 최종위치로 가도록 행렬을 구성해야한다.
그래서 우리가 직접 ZNodeMgr의 변환행렬을 곱해주는 것이다.

이제 준비는 끝났다, 스킨행렬만 만들면 모든게 완벽한데 글쎄


변경 - ZBone

class ZBone : public ZNode
{
protected:
D3DXMATRIXA16 m_matSkin; /// Skin TM 행렬
public:
ZBone( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );
~ZBone();

D3DXMATRIXA16* GetMatrixSkin() { return &m_matSkin; }
D3DXMATRIXA16* Animate( float frame, D3DXMATRIXA16* pParent );

/// 메시를 그린다
virtual int Draw( D3DXMATRIX* pTM );
};

ZBone의 변화된 부분이 눈에 띈다, 전혀 생소한 변수인 m_matSkin이라는 스키닝 전용 행렬이 선언되었어, 여기에 이 행렬값을 얻어내는 GetMatrixSkin()이라는 함수도 추가되었다, 게다가 ZNode::Animate()가 아닌 ZBone::Animate()함수가 새롭게 선언되었다. 요 Animate() 함수에서 모든 비밀이 풀린다.

D3DXMATRIXA16*  ZBone::Animate( float frame, D3DXMATRIXA16* pParent )
{
D3DXMATRIXA16* pmatAni;
D3DXMATRIXA16 mWI; // world inverse

// 애니메이션 행렬을 만든다.
pmatAni = m_pTrack->Animate( frame );
m_matTM = m_matLocal * *pmatAni;

// 만약 pos키값이 없으면 local TM의 좌표를 사용한다
if( pmatAni->_41 == 0.0f && pmatAni->_42 == 0.0f && pmatAni->_43 == 0.0f )
{
m_matTM._41 = m_matLocal._41;
m_matTM._42 = m_matLocal._42;
m_matTM._43 = m_matLocal._43;
}
else // pos키값을 좌표값으로 적용한다(이렇게 하지 않으면 TM의 pos성분이 두번적용된다)
{
m_matTM._41 = pmatAni->_41;
m_matTM._42 = pmatAni->_42;
m_matTM._43 = pmatAni->_43;
}

m_matTM = m_matTM * *pParent;

// mWI = inverse(m_matWorld)
D3DXMatrixInverse( &mWI, NULL, &m_matWorld );
// skin = inverse(M_world) * M_ani * M_parent
m_matSkin = mWI * m_matTM;
// D3DXMatrixIdentity( &m_matSkin );
return &m_matSkin;
}

ZNode::Animate()와 비교해서 대체 어떤 차이가 있는 걸까..? 차이가 안보일 정도로 비슷해 보인다, 그러나 소스를 자세히 살펴보면 근소한 차이를 발견할 수 있다

일반 애니메이션의 변환행렬은 다음과 같다.
Mtransform = Mlocal * Mani * Mparent

그러나 스킨행렬은,,,,
Mskin = M^-1world * (Mlocal * Mani * Mparent) = M^-1world * Mtransform

즉, 변환행렬의 제일 앞에 Mworld의 역행렬인 M^-1world를 곱해주는 것이다,
이것의 의미는 무엇일까?
메시는 기본적으로 자신의 지역 좌표계(로컬 좌표계)를 기준으로 만들어져있다. 그러나 뼈대로부터 영향을 받으려면 메시의 좌표계가 뼈대를 기준으로 한 좌표계로 변환되어야 한다. Mworld라는 것은 현재 뼈대가 월드좌표계로 변환되기 위해서 필요한 행렬이다.
그렇다면 M^-1world를 정점에 곱하면? 그렇다, 정점의 좌표가 뼈대의 지역좌표계로 변환되는 것이다. 여기서 애니메이션과 관련된 모든 변환을 거쳐서 최종 정점의 좌표가 되는 것이다.

이제 우리는 스킨행렬까지 구했으니, 준비가 끝났다
마지막으로, ZNodeMgr에서 스킨행렬을 사용할 수 있도록 수정해보자.


변경 - ZNodeMgr

protected:
                                           :
vector<D3DXMATRIX> m_matPalette;
                                           :

ZNodeMgr 클래스는 매트릭스 팔레트로 사용될 m_matPalette 배열이 선언된 것이 유일한 변경 사항이다.

int ZNodeMgr::Animate( float fFrame )
{
int i;
int id;
int size;
D3DXMATRIXA16 m;
D3DXMATRIXA16* pTM;

if( fFrame > m_info.fAnimationEnd ) fFrame = m_info.fAnimationEnd;
if( fFrame < m_info.fAnimationStart ) fFrame = m_info.fAnimationStart;

// 먼저 뼈대(bone)들의 애니메이션 행렬을 만들어 둔다
size = m_info.BoneTable.size();
for( i = 0 ; i < size ; i++ )
{
id =  m_nodes[m_info.BoneTable[i]]->GetParentID();
pTM = m_nodes[id]->GetMatrixTM();
m_nodes[m_info.BoneTable[i]]->Animate( fFrame, pTM );
pTM = ((ZBone*)m_nodes[m_info.BoneTable[i]])->GetMatrixSkin();
// 스키닝을 위해 스킨행렬을 매트릭스 팔레트에 등록
m_matPalette[i] = *((D3DXMATRIX*)pTM); 
}

// 뼈대가 아닌 것들의 애니메이션 행렬을 만든다
size = m_info.MeshTable.size();
for( i = 0 ; i < size ; i++ )
{
id =  m_nodes[m_info.MeshTable[i]]->GetParentID();
pTM = m_nodes[id]->GetMatrixTM();
m_nodes[m_info.MeshTable[i]]->Animate( fFrame, pTM );
}

return 1;
}

ZNodeMgr::Animate() 함수는 기본적으로 동일하지만, 뼈대 노드들의 Animate()를 호출한 뒤 GetMatrixSkin() 함수를 통해서 얻은 스키닝 행렬들을 매트릭스 팔레트에 등록해 놓는것이 다르다. 이거말고는 나머지는 같다.



스키닝을 지원하는 방식은 맥스를 사용할 경우 바이패드(biped) + 피지크(physique) 모디파이어 방식과 본(bone) + 스킨(skin) 모디파이어 방식이 많이 사용된다.
인간형 캐릭터의 경우 바이패드를 사용하면 애니메이션이 쉽기 때문에 많이 사용되지만, 본은 좀 더 풍부한 표현이 가능하기 때문에, 앞으로의 3D 게임 발전방향으로 볼 때 본에 대한 지원이 상당히 중요해질 것이다

그리고 이 엔진은 학습용이라서 실무용으로 쓰기는 어렵다ㅠㅠ
왜냐면 데이터간의 결합도가 높기 때문
예를 들면, 캐릭터 100개 출력할라면 ZNodeMgr클래스가 100개 있어야된다는 것....




'Graphics > DirectX' 카테고리의 다른 글

Direct X - 키 프레임 애니메이션  (0) 2014.10.09
Direct X - 계층구조 2  (0) 2014.09.30
Direct X - 계층구조  (0) 2014.09.16
Direct X - 스크립트와 XML파서  (0) 2014.09.05
Direct X - 애니메이션 기법  (0) 2014.09.03