계층 구조의 수학적 근거
먼저 계층 구조를 구현하는건 다음의 수식을 따른다고 했다, 여기에 들어갈 값을 구할 수 있어야 계층구조를 구현할 수 있다 차근차근 구해봅시다.

Vworld = Vlocal * Mchild * Mparent                    : 식 1

맥스에서 데이터를 추출하면 가장 쉽게 뽑아낼 수 잇는게 바로 Mtransform이다. 요고는 다음과 같은 용도로 사용된다.

Vworld = Vlocal * Mtransform                             : 식 2

위의 식과 아래의 식을 비교해보자, 일반적으로 맥스에서 추출해낼 수 있는 데이터는 Vworld와 Mtransform의 2가지 값이, 요고로부터 우리는 Vlocal의 값을 구할 수 있다, Vworld = Vlocal * Mtransform을 조금 변형해보자.. 양변에 M-1transform을 곱하면
(주의 : M-1transform : -1은 지수, 고로 역행렬)

Vworld * M-1transform = Vlocal * Mtransform * M-1transform
Vworld * M-1transform = Vlocal * I
Vlocal = Vworld * M-1transform                          : 식 3

즉, 우리는 Vworld에 Mtransform의 역행렬(M-1transform)을 곱해서 Vlocal을 구할 수 있는 것이다. 대부분의 애니메이션은 정점이 Vlocal일 경우를 기준으로 하므로 반드시 Vlocal을 구해야 애니메이션 할 수 있다.

Vlocal을 구했으니, Mtransform을 여러개의 행렬로 분해해야 한다. (Mchild, Mparent등)
쫄지말자 생각보다 쉬우니까요.

그럼 식 1과 식 2를 비교해보면 Vlocal과 Vworld부분이 동일하므로, 다음과 같은 식이 성립된다.

Mtransform = Mchild * Mparent                          : 식 4

여기서 우리는 맥스에서 Mtransform과 Mparent를 구할 수 있으므로 식 4를 변형해보자
(-1 : 지수)

Mtransform * M-1parent = Mchild * Mparent * M-1parent = Mchild * I = Mchild
Mchild = Mtransform * M-1parent                       : 식 5

양변에 M-1parent를 곱하면 Mchild를 구할 수 있는 것이야, 앞으로 Mchild를 Mlocal이라고 부르자.

사실, 이 모든 과정은 맥스스크립트에서 데이터 추출과 동시에 미리 계산되어 출력하도록 만들어져 있지만, 이론적 토대를 알아두기위해 수식을 전개해봤다



맥스 스크립트의 ZUtility.GetLocalVertices() 함수에 Vworld를 Vlocal로 출력하는 기능이 구현되어 있으며, ZUtility.GetLocalTM()에 Mtransform을 Mlocal로 변형하는것이 구현되어 있으므로 참고하자

fn GetLocalVertices obj =
{
varSetVertex = #()

TM = obj.transform
InvTM = inverse TM

for i = 1 to obj.numverts do
{
local vl -- vertex local
local vw -- vertex world
vw = in coordsys world getvert obj i
vl = vw * InvTM
append varSetVertex vl
}
varSetVertex
}

fn GetLocalTM obj =
{
local matLocal = copy obj.transform
if obj.parent != undefined then
{
matLocal = obj.transform * (inverse obj.parent.transform)
}
matLocal
}


시스템 설계
앞으로 개발하려는 캐릭터 애니메이션 엔진을 손쉽게 수정 및 확장할 수 있도록 처음부터 설계를 제대로 해보자.
우선 클래스 다이어그램을 보자

 

ZNode를 맥스로부터 읽어들인 모든 오브젝트들의 최상위 클래스로 두고, 오브젝트를 크게 두가지로 구분한 것이야,

뼈대는 ZBone

메시는 ZMesh

나중에 추가하게 될 스키닝 메시를 생각해서 ZSkinnedMesh 클래스 추가

애니메이션을 위한 ZTrack이라는 클래스도 있는데, 요고는 다음에 구현할거구요

ZNodeMgr 클래스는 ZNode를 관리하는데 사용할 클래스다



실제구현 - ZNodeMgr

제일 먼저 볼 클래스, 

이 클래스는 맥스로 만들어진 XML 파일을 읽은 데이터인 ZCParsedData형을 받아 실제 출력 가능한 형태로 변환하는 역할을 한다

대부분 실제적인 데이터 변환은 각각의 ZNode, ZBone, ZMesh등의 클래스에서 이루어지고, ZNodeMgr은 ZObjectInfo와 ZMaterial 등의 전역 데이터만을 읽어들인 뒤에 각각의 ZCMesh 형들을 어떤 클래스의 객체로 생성해야 적합한가에 대한 판단을 한다.


class ZNodeMgr

{

protected:

ZObjectInfo m_info; /// 현재 노드전체에 대한 정보

vector<ZMaterial> m_materials; /// 재질값 배열

vector<ZNode*> m_nodes; /// 노드들

LPDIRECT3DDEVICE9 m_pDev; /// D3D디바이스

D3DXMATRIXA16 m_matTM; /// 자식 노드 전체에 적용될 TM


public:

ZNodeMgr( LPDIRECT3DDEVICE9 pDev, ZCParsedData* pData );

~ZNodeMgr();


/// TM을 얻어온다

D3DXMATRIXA16* GetTM() { &m_matTM; }


/// TM을 셋팅한다

void SetTM( D3DXMATRIXA16* pTM ) { m_matTM = *pTM; }


/// 애니메이션 행렬을 만든다

int Animate( float fFrame );


/// 노드전체를 그린다.

int Draw();

};


vector<ZNode*> m_nodes로 선언된 것이 이제부터 관리하게 될 전체 오브젝트 배열이다.

외부에서 접근할 수 있는 함수는 생성자를 제외하면 SetTM(), GetTM(), Animate(), Draw()의 4가지 함수 뿐이다


그럼 이게 어떻게 구현되었는지 보자


먼저 ZNodeMgr의 생성자를 보자


ZNodeMgr::ZNodeMgr( LPDIRECT3DDEVICE9 pDev, ZCParsedData* pData )

{

int i;


m_pDev = pDev;

D3DXMatrixIdentity( &m_matTM );


// copy info

m_info.nObjectCount = pData->m_info.nObjectCount;

m_info.nSkinType = pData->m_info.nSkinType;

m_info.nMeshCount = pData->m_info.nMeshCount;

m_info.fAnimationStart = pData->m_info.fAnimationStart;

m_info.fAnimationEnd = pData->m_info.fAnimationEnd;

m_info.nKeyType = pData->m_info.nKeyType;

m_info.BoneTable = pData->m_info.BoneTable;

m_info.strObjectNameTable = pData->m_info.strObjectNameTable;


// copy materials

char drive[_MAX_DRIVE];

char dir[_MAX_DIR];

char fname[_MAX_FNAME];

char ext[_MAX_EXT];

string str;


int size = pData->m_materialTable.size();

m_materials.resize( size );


for( i = 0 ; i < size ; i++ )

{

// 재질의 확산광 속성 설정

D3DCOLORVALUE diffuse = { pData->m_materialTable[i].diffuse.r, pData->m_materialTable[i].diffuse.g, pData->m_materialTable[i].diffuse.b, 0 };

m_materials[i].material.Diffuse = diffuse;


// 재질의 환경광 속성 설정

D3DCOLORVALUE ambient = { pData->m_materialTable[i].ambient.r, pData->m_materialTable[i].ambient.g, pData->m_materialTable[i].ambient.b, 0 };

m_materials[i].material.Ambient = ambient;

// 재질의 반사광 속성 설정

D3DCOLORVALUE specular = { pData->m_materialTable[i].specular.r, pData->m_materialTable[i].specular.g, pData->m_materialTable[i].specular.b, 0 };

m_materials[i].material.Specular = specular;

// 재질의 발산광 속성 설정

D3DCOLORVALUE emissive = { pData->m_materialTable[i].emissive.r, pData->m_materialTable[i].emissive.g, pData->m_materialTable[i].emissive.b, 0 };

m_materials[i].material.Emissive = emissive;


// 반사광의 제곱계수

m_materials[i].material.Power = pData->m_materialTable[i].power;


// 불투명도 값

m_materials[i].opacity = pData->m_materialTable[i].opacity;


// 확산맵 텍스처

m_materials[i].pTex[0] = NULL;

_splitpath( pData->m_materialTable[i].strMapDiffuse.c_str(), drive, dir, fname, ext ); str = fname; str += ext;

D3DXCreateTextureFromFile( pDev, str.c_str(), &m_materials[i].pTex[0] );


// 법선맵 텍스처

m_materials[i].pTex[1] = NULL;

_splitpath( pData->m_materialTable[i].strMapBump.c_str(), drive, dir, fname, ext ); str = fname; str += ext;

D3DXCreateTextureFromFile( pDev, str.c_str(), &m_materials[i].pTex[1] );

// 반사맵 텍스처

m_materials[i].pTex[2] = NULL;

_splitpath( pData->m_materialTable[i].strMapSpecular.c_str(), drive, dir, fname, ext ); str = fname; str += ext;

D3DXCreateTextureFromFile( pDev, str.c_str(), &m_materials[i].pTex[2] );

// 그외의 추가적인 텍스처

m_materials[i].pTex[3] = NULL;

_splitpath( pData->m_materialTable[i].strMapDisplace.c_str(), drive, dir, fname, ext ); str = fname; str += ext;

D3DXCreateTextureFromFile( pDev, str.c_str(), &m_materials[i].pTex[3] );

}


// copy meshes

list<ZCMesh*>::iterator it;

ZNode* pNode;

size = pData->m_meshList.size();

// m_nodes.resize( size );

for( i = 0, it = pData->m_meshList.begin() ; it != pData->m_meshList.end() ; i++, it++ )

{

// 부모가 없는 노드는 0번노드를 부모로 한다.

if( (*it)->m_nParentID == -1 ) 

(*it)->m_nParentID = 0;


// Bone이나 Biped라면 ZBone으로 생성

if( !( (*it)->m_strNodeType.compare( "BoneGeometry" ) ) || 

!( (*it)->m_strNodeType.compare( "Biped_Object" ) ) || 

!( (*it)->m_strNodeType.compare( "Bone" ) ) )

{

pNode = (ZNode*)(new ZBone( pDev, *it ));

}

else // Editable_Mesh라면 ZMesh로 생성

if( !( (*it)->m_strNodeType.compare( "Editable_mesh" ) ) )

{

if( (*it)->m_isSkinned ) // Skin이 있으면 ZSkinnedMesh로 생성

pNode = (ZNode*)(new ZSkinnedMesh( pDev, *it ));

else // Skin이 없으면 ZRigidMesh로 생성

pNode = (ZNode*)(new ZRigidMesh( pDev, *it ));

}

else // 이것도 저것도 아니면 그냥 ZNode로 생성

pNode = new ZNode( pDev, *it );


m_nodes.push_back( pNode );

}

}


길긴하지만 내용은 별거없다

XML문서로부터 읽어들인 pData중에 m_info에 해당하는 전역변수값을 ZNodeMgr::m_info에 복사한다. 그리고 재질 속성을 ZMaterial에 읽어 들인다. 핵심적인 코드는 그 이후에 나오는 노드 복사 루프다
이 부분에서 노드의 종류를 판단하여 속성이 'BoneGeometry', 'Bone', 'Biped_Object'라면, ZBone 클래스의 객체로 생성하고, 'Editable_mesh'라면 ZMesh로 생성한다. 이때 m_isSkinned 속성값을 체크해서 참이면 스키닝이 필요하다고 판단하고 ZMesh로부터 상속받은 ZSkinnedMesh로 생성한다, 스키닝 속성이 없으면 ZRigidMesh로 생성한다. 이런 판단과정에 속하지 않는 모든 값들은 ZNode속성으로 생성해서 메모리에 저장한다. 최종적으로 이렇게 생성된 모든 객체들은 최상위 클래스인 ZNode로 캐스팅하여 ZNodeMgr::m_nodes 배열에 보관해둔다. 상속을 잘 사용한다면, 전체 시스템을 매우 단순화 할 수 있다.

ZNodeMgr::Animate()와 Draw()함수를 보면 상속의 강력함을 다시한번 느낄 수 있을것입니다.

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

for( i = 0 ; i < m_nodes.size() ; i++ )
{
id =  m_nodes[i]->GetParentID();
pTM = m_nodes[id]->GetMatrixTM();
m_nodes[i]->Animate( fFrame, pTM );
}

return 1;
}

int ZNodeMgr::Draw()
{
int i;

for( i = 0 ; i < m_nodes.size() ; i++ )
{

m_pDev->SetMaterial( &m_materials[m_nodes[i]->GetMaterialID()].material );
m_pDev->SetTexture( 0, m_materials[m_nodes[i]->GetMaterialID()].pTex[0] ); 

m_pDev->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ); // 0번 텍스처 스테이지의 확대 필터
m_pDev->SetSamplerState( 1, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ); // 0번 텍스처 스테이지의 확대 필터
m_pDev->SetSamplerState( 0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP );
m_pDev->SetSamplerState( 0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP );
m_pDev->SetTextureStageState( 0, D3DTSS_TEXCOORDINDEX, 0 ); // 0번 텍스처 : 0번 텍스처 인덱스 사용
m_pDev->SetTextureStageState( 1, D3DTSS_TEXCOORDINDEX, 0 ); // 1번 텍스처 : 0번 텍스처 인덱스 사용

m_pDev->SetTextureStageState( 0, D3DTSS_COLOROP,   D3DTOP_MODULATE ); // MODULATE로 섞는다.
m_pDev->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); // 텍스처
m_pDev->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE ); // 정점색
m_pDev->SetTextureStageState( 1, D3DTSS_COLOROP,   D3DTOP_DISABLE );

m_pDev->SetTransform( D3DTS_WORLD, &m_matTM );
m_nodes[i]->Draw();
}

return 1;
}

보다시피 생각보다 훨씬 간단한 구조로 되어있다,
노드의 개수만큼 루프를 돌면서 노드의 Animate() 멤버 함수를 호출할 뿐이다. Draw()역시 마찬가지로 루프를 돌면서 적절한 재질값과 텍스쳐를 설정하고 노드의 Draw() 멤버 함수를 호출한다. 이렇게 간단하게 ZNodeMgr()클래스가 구성될 수 있는 것은 모두 상속과 가상함수 덕분..


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

Direct X - 키 프레임 애니메이션  (0) 2014.10.09
Direct X - 계층구조 2  (0) 2014.09.30
Direct X - 스크립트와 XML파서  (0) 2014.09.05
Direct X - 애니메이션 기법  (0) 2014.09.03
Direct X - 균열 방지  (0) 2014.08.27
Posted by 긍정왕오킹