실제구현 - ZNode


ZNode 클래스 선언부를 보도록 하자

class ZNode
{
protected:
enum NODETYPE { NODE_NODE, NODE_BONE, NODE_MESH }; // 현재 지원하는 노드의 종류

int m_nNodeType; /// 노드의 종류
int m_nObjectID; /// 오브젝트 ID
int m_nParentID; /// 부모의ID
int m_nMaterialID; /// 재질의 ID
D3DXVECTOR3 m_bboxMax; /// 바운딩 박스의 최대값(max)
D3DXVECTOR3 m_bboxMin; /// 바운딩 박스의 최소값(min)
D3DXMATRIXA16 m_matLocal; /// local TM 행렬(불변)
D3DXMATRIXA16 m_matAni; /// animation 행렬(변화)

/// m_matTM = m_matAni * m_matLocal * (mom's m_matTM) * (grandmom's m_matTM) * ...
D3DXMATRIXA16 m_matTM; /// 최종 TM 행렬(변화)

LPDIRECT3DDEVICE9 m_pDev; /// D3D디바이스
public:
ZNode( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );
~ZNode();

/// 메시의 ID값을 얻는다
int GetObjectID() { return m_nObjectID; }

/// 부모메시의 ID값을 얻는다
int GetParentID() { return m_nParentID; }

int GetMaterialID() { return m_nMaterialID; }

/// 현재의 TM값을 얻는다
D3DXMATRIXA16* GetMatrixTM() { return &m_matTM; }

/// 경계상자의 값을 얻는다
void GetBBox( D3DXVECTOR3* pMin, D3DXVECTOR3* pMax )
{ *pMin = m_bboxMin; *pMax = m_bboxMax; }

/// ZTrack으로부터 m_matAni를 구해서 m_matTM을 만든다
D3DXMATRIXA16* Animate( float fFrame, D3DXMATRIXA16* pParentTM );

/// 노드를 그린다.
virtual int Draw() { return 1; }

/// 경계상자를 그린다
virtual int DrawBBox() { return 1; }
};



ZNode는 최상위 클래스답게 멤버 변수도 멤버 함수도 전체 클래스중에서 제일 많다. 그러나 대부분은 Get...() 계열의 인라인함수들이고, 실제로 구현된 함수는 생성자와 소멸자 Animate()함수 세개다. 그럼 생성자를 보자.

ZNode::ZNode( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh )
{
m_pDev = pDev;
m_nNodeType = NODE_NODE;
m_nObjectID = pMesh->m_nObjectID;
m_nParentID = pMesh->m_nParentID;

m_bboxMax.x = pMesh->m_bboxMax.x;
m_bboxMax.y = pMesh->m_bboxMax.y;
m_bboxMax.z = pMesh->m_bboxMax.z;
m_bboxMin.x = pMesh->m_bboxMin.x;
m_bboxMin.y = pMesh->m_bboxMin.y;
m_bboxMin.z = pMesh->m_bboxMin.z;
m_nMaterialID = pMesh->m_nMaterialID;

// 기본 local TM복사
memcpy( &m_matLocal, &pMesh->m_tm, sizeof m_matLocal );
D3DXMatrixIdentity( &m_matAni );
D3DXMatrixIdentity( &m_matTM );
}

보다시피 생성자의 주된 역할은 경계상자를 만들거나 ID값을 보관하고 각종 행렬을 초기화하는 간단한 일이다

그리고 Animate() 함수는,,

D3DXMATRIXA16* ZNode::Animate( float fFrame, D3DXMATRIXA16* pParentTM )
{
m_matTM = m_matLocal * m_matAni * *pParentTM;
return &m_matTM;
}

현재 우리는 아무런 애니메이션 처리를 하지 않기 때문에 간단하게 계층 구조를 구현하는 행렬을 만들고 있음을 알 수 있다.
계층구조행렬 공식 기억하구있지요?

Vworld = Vlocal * Mlocal * Mparent

지금은 애니메이션이 구현되어있지 않기 때문에, m_matAni는 그냥 단위행렬이다


실제구현 - ZMesh

class ZMesh : public ZNode
{
protected:
BOOL m_isSkinned; /// 스키닝이 적용되는 메시인가?

DWORD m_nVerts; /// 전체 정점의 개수
DWORD m_nTriangles; /// 그려질 삼각형의 개수
DWORD m_dwFVF; /// 정점의 fvf값
LPDIRECT3DVERTEXBUFFER9 m_pVB; /// 정점 버퍼
LPDIRECT3DINDEXBUFFER9 m_pIB; /// 인덱스 버퍼
protected:
/// 정점배열과 버퍼를 생성한다
virtual int _CreateVIB( ZCMesh* pMesh ) { return 1; }
public:
ZMesh( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );
~ZMesh();
/// 메시를 그린다
virtual int Draw();

/// 경계상자를 그린다
virtual int DrawBBox();
};

ZMesh는 ZNode로부터 상속받는다.
ZMesh의 주된 역할은 ZSkinnedMesh와 ZRigidMesh를 위한 인터페이스와 기초적인 멤버변수 선언에 있다.

정점버퍼와 인덱스버퍼에 대한 선언은 여기에서 이루어지지만, 실제로 정점 버퍼와 인덱스버퍼는 ZMesh로 부터 상속된 ZSkinnedMesh와 ZRigidMesh에서 생성된다.

ZMesh의 실제적인 구현은 생성자 뿐이다.

ZMesh::ZMesh( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh ) : ZNode( pDev, pMesh )
{
m_isSkinned = pMesh->m_isSkinned ? TRUE : FALSE;
m_nNodeType = NODE_MESH;

m_pVB = NULL;
m_pIB = NULL;
}

ZMesh의 생성자는 먼저 ZNode의 생성자를 호출한다, 그리고 스키닝 여부를 기록하고, m_nNodeType을 NODE_MESH로 설정한다. 이때, ZNode의 생성자에서는 m_nNodeType을 NODE_MODE로 생성하지만, ZMesh의 생성자가 ZNode보다 나중에 불리기 때문에 결과적으로 m_nNodeType은 NODE_MESH값을 갖게 된다.


실제구현 - ZBone

class ZBone : public ZNode
{
public:
ZBone( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );
~ZBone();
/// 메시를 그린다
virtual int Draw();
virtual int DrawBBox();
};

좀 심하다 싶을정도로 간단하다, 그러나 당연한것이야
ZBone은 현재 아무런 역할을 하지 않기 때문이야.

요고는 생성자
ZBone::ZBone( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh ) : ZNode( pDev, pMesh )
{
m_nNodeType = NODE_BONE;
}


실제구현 - ZRigidMesh

class ZRigidMesh : public ZMesh
{
private:
// 정점버퍼와 똑같은 정점배열을 한 카피 보관해 둔다.
vector<ZRigidVertex> m_vtx; /// skinning없는 정점 배열
// 인덱스버퍼와 똑같은 인덱스배열을 한 카피 보관해 둔다.
vector<Index3w> m_idx; /// 인덱스 배열

int _CreateVIB( ZCMesh* pMesh );
public:
ZRigidMesh( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh );
~ZRigidMesh();

int Draw();
int DrawBBox();
};

ZRigidMesh는 ZMesh로부터 상속받는다, 현재는 생성자에서 정점버퍼와 인덱스버퍼를 생성하고, Draw()에서 메시를 그려주는 기능만 구현하면 된다.

ZRigidMesh::ZRigidMesh( LPDIRECT3DDEVICE9 pDev, ZCMesh* pMesh ) : ZMesh( pDev, pMesh )
{
_CreateVIB( pMesh );
}

ZRigidMesh::_CreateVIB( ZCMesh* pMesh )
{
int i;
int size;
VOID* pV; // 정점버퍼 lock했을때 얻어오는 값
VOID* pI; // 엔딕스버퍼 lock했을때 얻어오는 값

size = pMesh->m_vtxFinal.size();
m_dwFVF = ZRigidVertex::FVF;
m_vtx.resize( size );
m_nVerts = 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 < size ; i++ )
{
m_vtx[i].p.x = pMesh->m_vtxFinal[i].p.x;
m_vtx[i].p.y = pMesh->m_vtxFinal[i].p.y;
m_vtx[i].p.z = pMesh->m_vtxFinal[i].p.z;
m_vtx[i].n.x = pMesh->m_vtxFinal[i].n.x;
m_vtx[i].n.y = pMesh->m_vtxFinal[i].n.y;
m_vtx[i].n.z = pMesh->m_vtxFinal[i].n.z;
m_vtx[i].t.x = pMesh->m_vtxFinal[i].t.x;
m_vtx[i].t.y = pMesh->m_vtxFinal[i].t.y;

// 맥스스크립트로 얻어지는 경계상자는 이상할 경우가 많아서 직접 계산한다
if( m_vtx[i].p.x > m_bboxMax.x ) m_bboxMax.x = m_vtx[i].p.x;
if( m_vtx[i].p.y > m_bboxMax.y ) m_bboxMax.y = m_vtx[i].p.y;
if( m_vtx[i].p.z > m_bboxMax.z ) m_bboxMax.z = m_vtx[i].p.z;
if( m_vtx[i].p.x < m_bboxMin.x ) m_bboxMin.x = m_vtx[i].p.x;
if( m_vtx[i].p.y < m_bboxMin.y ) m_bboxMin.y = m_vtx[i].p.y;
if( m_vtx[i].p.z < m_bboxMin.z ) m_bboxMin.z = m_vtx[i].p.z;
}
// 정점 버퍼 생성
m_pDev->CreateVertexBuffer( size * sizeof(ZRigidVertex), 0, m_dwFVF, D3DPOOL_DEFAULT, &m_pVB, NULL );
m_pVB->Lock( 0, size * sizeof(ZRigidVertex), (void**)&pV, 0 );
memcpy( pV, &m_vtx[0], size * sizeof(ZRigidVertex) );
m_pVB->Unlock();


// 인덱스 생성
size = pMesh->m_idxFinal.size();
m_idx.resize( size );
m_nTriangles = size;
for( i = 0 ; i < size ; i++ )
{
m_idx[i].i[0] = (WORD)pMesh->m_idxFinal[i].i[0];
m_idx[i].i[1] = (WORD)pMesh->m_idxFinal[i].i[1];
m_idx[i].i[2] = (WORD)pMesh->m_idxFinal[i].i[2];
}

// 인덱스 버퍼 생성
    m_pDev->CreateIndexBuffer( size * sizeof(Index3w), 0, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &m_pIB, NULL );
    m_pIB->Lock( 0, size * sizeof(Index3w), (void**)&pI, 0 );
memcpy( pI, &m_idx[0], size * sizeof(Index3w) );
    m_pIB->Unlock();


return 1;
}

좀 기네?
그러나 하는일은 m_vtx와 m_idx에 값을 읽어들이고, 이 값들을 기반으로 정점버퍼와 인덱스버퍼를 생성하는게 전부다

Draw()함수를 보자

int ZRigidMesh::Draw()
{
m_pDev->MultiplyTransform( D3DTS_WORLD, &m_matTM );
m_pDev->SetStreamSource( 0, m_pVB, 0, sizeof(ZRigidVertex) );
m_pDev->SetFVF( m_dwFVF );
m_pDev->SetIndices( m_pIB );
m_pDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, m_nVerts, 0, m_nTriangles );
return 1;
}

현재 ZNode의 변환행렬 (m_matTM)을 세팅하고, ZMesh의 정점버퍼와 인덱스버퍼를 가지고 DrawIndexedPrimitive()함수로 메시를 그리는 것이다

그러나 이 함수가 전체 소스중에 유일하게 메시를 그리는 부분이다


실제구현 - ZSkinnedMesh
요고는 ZRigidMesh와 동일하게 만들어져있다
ZSkinnedMesh에 대한 정확한 구현은 스키닝을 다루면서 할거니까 우선 넘어가도록 하겠다.


실제구현 - main.cpp
지금까지 만들어낸 ZNodeMgr, ZNode, ZMesh, ZBone, ZRigidMesh, ZSkinnedMesh등의 클래스들을 실제로 사용해보자, 제일 먼저 할 일은 XML로부터 데이터를 로딩하는것

#include "ZCParser.h"
#include "ZCParsedData.h"
#include "ZNodeMgr.h"

ZNodeMgr* g_pNodeMgr = NULL; // 노드를 관리하는 Node Manager클래스

이렇게 ZCParser와 ZCParsedData, ZNodeMgr이 선언된 헤더파일을 #include하고 ZNode

실제로 XML파일을 읽고, ZNodeMgr 객체를 생성하는것은 InitObject()함수에서 담당한다.

HRESULT InitObjects()
{
ZCParser* pParser = NULL; // 파서객체
ZCParsedData* pData = NULL; // 파싱데이터 객체

g_pCamera = new ZCamera; // 카메라 초기화
g_pLog = new ZFLog( ZF_LOG_TARGET_WINDOW ); // 로깅객체 초기화

pParser = new ZCParser(); // 파서 초기화
pData = new ZCParsedData(); // 파싱데이터 초기화
if( !pParser->Parse( pData, FNAME_XML ) ) return E_FAIL; // 파서 작동!

list<ZCMesh*>::iterator it; // STL반복자 선언
for( it = pData->m_meshList.begin() ; it != pData->m_meshList.end() ; it++ )
{
g_pLog->Log( "-------------------------------------" );
g_pLog->Log( "ID:[%d,%s]", (*it)->m_nObjectID, pData->m_info.strObjectNameTable[(*it)->m_nObjectID].c_str() );
g_pLog->Log( "Parent:[%d,%s]", (*it)->m_nParentID, (*it)->m_nParentID == -1 ? "undefined" : pData->m_info.strObjectNameTable[(*it)->m_nParentID].c_str() );
g_pLog->Log( "Vertex:[%d]", (*it)->m_vertex.size() );
g_pLog->Log( "Index:[%d]", (*it)->m_triindex.size() );
g_pLog->Log( "MergedVertex:[%d]", (*it)->m_vtxFinal.size() );
g_pLog->Log( "Skin:[%s]", (*it)->m_isSkinned ? "true" : "false" );
g_pLog->Log( "-------------------------------------" );
}

g_pNodeMgr = new ZNodeMgr( g_pd3dDevice, pData ); //노드 매니저 생성

S_DEL( pData ); // 파서객체 삭제
S_DEL( pParser ); // 파싱데이터 객체 삭제

return S_OK;
}

그 중에서도 다음의 세줄이 XML파일을 열어서 파싱을 하는 핵심 함수다

pParser = new ZCParser(); // 파서 초기화
pData = new ZCParsedData(); // 파싱데이터 초기화
if( !pParser->Parse( pData, FNAME_XML ) ) return E_FAIL; // 파서 작동!


파싱이 끝나면 ZNodeMgr 객체를 생성한다

g_pNodeMgr = new ZNodeMgr( g_pd3dDevice, pData ); //노드 매니저 생성

이제 우리는 생성된 ZNodeMgr 객체를 조작만 하면 된다

VOID Render()
{
    /// 후면버퍼와 Z버퍼 초기화
    g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(200,200,200), 1.0f, 0 );
g_pd3dDevice->SetRenderState( D3DRS_FILLMODE, g_bWireframe ? D3DFILL_WIREFRAME : D3DFILL_SOLID );

/// 애니메이션 행렬설정
Animate();
    /// 렌더링 시작
    if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )
    {
/// 렌더링 종료
g_pNodeMgr->Draw(); // 노드 그리기
g_pd3dDevice->EndScene();
    }

    /// 후면버퍼를 보이는 화면으로!
    g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}

렌더링을 담당하는 함수에서 g_pNodeMgr->Draw()를 호출한다. 이것으로 모든 것이 끝이다. 내부적인 행렬연산이나 DrawPrimitive()등은 g_pNodeMgr객체 내부에서 전부 이루어진다. 종료 직전에 g_pNodeMgr 객체만 메모리에서 제거해주면 된다

void DeleteObjects()
{
/// 등록된 클래스 소거
S_DEL( g_pNodeMgr );
S_DEL( g_pCamera );
S_DEL( g_pLog );
}

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

Direct X - 스키닝  (0) 2014.10.21
Direct X - 키 프레임 애니메이션  (0) 2014.10.09
Direct X - 계층구조  (0) 2014.09.16
Direct X - 스크립트와 XML파서  (0) 2014.09.05
Direct X - 애니메이션 기법  (0) 2014.09.03
Posted by 긍정왕오킹