XML Exporter: Reigid Animation

==== 맥스 플러그인 =====

Rigid 애니메이션은 스킨 정보가 없기 때문에 시간의 경과에 따라 지역 행렬을 저장하면된다.

맥스의 시간 개념에 대해 정리하고 넘어간다.

< 맥스에서 시간 >

맥스의 시간의 단위는 tick으로 TiimeValue라는 타입으로 사용하며 1초에 4800tick이다.
맥스의 한 프레임은 GetTicksPerFrame() 간격 만큼 움직이게 된다.
Interval은 맥스 Interface에서 얻어 올수 있다.

Interval   aniRange

시작 프레임:    int startFrame = aniRange.Start() / GetTicksPerFrame();
마지막 프레임: int endFrame = aniRange.End() / GetTicksPerFrame();  
초당 프레임 :   int  fps = GetFrameRate();  
초당 프레임 간격 : int delta = GetTicksPerFrame();

TimeValue start = aniRange.Start()
TimeValue end = aniiRange.End()
애니메이션의 총 프레임 수 : int number_of_frame   = (end - start ) / GetTicksPerFrame();

초당 프레임   int frame_per_second = GetFrameRate();
                                              = 4800 / GetTicksPerFrame()
GetTicksPerFrame() = 4800 / FPS  ----> FPS = 4800 /  GetTicksPerFrame
TicksToSec() 마이크로 센컨드로 1000을 곱한다.

< 바뀐 메서드 >

이전에 CollectINode()를 BuildNode()로 메써드 이름을 변경하였다.
플러그인의 메인루틴은 BuildNode()이다. 아래는 실행 화면이다.

 

< 애니메이션 헤더 정보 모으기 >

구조체 AnimationInfo m_aniInfo에 시작 프레임, 마지막 프레임, FPS, 프레임당 tick을 구한다.
나중에 루트 XmlNode에 WriteHeader( m_pXmlRoot ) 메서드를 이용하여 저장한다.

void Export::GetAnimationInfo()

{

    int   iTick = GetTicksPerFrame();

    Interval range = m_pInterface->GetAnimRange();

 

    m_aniInfo.beginFrame = range.Start() / iTick;

    m_aniInfo.endFrame = range.End() / iTick;

    m_aniInfo.fps = GetFrameRate();

    m_aniInfo.tickPerFrame = iTick;

}

 

<Bone 인지 검사>

애니메이션 데이터는 INode가 본이나 바이패드 일때만 저장한다.

bool Export::IsBone( INode* pNode )

{

    if(pNode == NULL)

        return false;

 

    ObjectState  objectState = pNode->EvalWorldState(0);

    if(objectState.obj == NULL)

        return false;

    if(objectState.obj->ClassID() == BONE_OBJ_CLASSID)  

        return true;

    //include with Helper

    if(objectState.obj->ClassID() == Class_ID(BONE_CLASS_ID, 0))

        return true;

    //include with Helper

    if(objectState.obj->ClassID() == Class_ID(DUMMY_CLASS_ID, 0))

        return false;

 

    Control* control = pNode->GetTMController();

    Class_ID cid = control->ClassID();

    //others biped parts || biped root

    if( cid == BIPSLAVE_CONTROL_CLASS_ID || cid == BIPBODY_CONTROL_CLASS_ID ) 

        return true;

 

    return false;

}

 

<Tick에 따른 애니메이션 지역 좌표 구하기>

1. IsBone()일때만 애니메이션 정보를 저장한다.
2. 애니메이션 데이터는 tickPerFrame 간격으로 저장한다.
3. Tick에따른 지역행렬를 구하기 위해 전역행렬의 역행렬을 곱한다.
4. "Animation" XmlNode를 만들고 애니메이션 지역 행렬을 저장한다.
   "Animation" 노드는 INode에서 온 것이 아니라 Xml로 저장하기 위해서 임의로 만든것이다.

        Matrix3 matLocal;
        Matrix3 matWorld = pNode->GetObjTMAfterWSM( iTick ); 

        if( pParent == NULL )
        {
            matLocal = matWorld;
        }
        else
        {
            Matrix3 matParent = pParent->GetObjTMAfterWSM( iTick );
            matLocal = matWorld * Inverse( matParent );
        }

WriteAnimation() 메서드의 전체 코드이다.

XmlNode* Export::WriteAnimation(INode* pNode, XmlNode* pParentXML)

{

    if( IsBone( pNode ) == false )

        return NULL;

 

    int tickStride = m_aniInfo.tickPerFrame;

    int beginTick = m_aniInfo.beginFrame * tickStride;

    int endTick = m_aniInfo.endFrame * tickStride;

    int aniNum = m_aniInfo.endFrame - m_aniInfo.beginFrame + 1;

 

    INode*  pParent = pNode->GetParentNode();

    base::Matrix* pAniMat = new base::Matrix[aniNum];

 

    int i = 0, iTick = beginTick;

    for( ; iTick <= endTick; iTick += tickStride, ++i)

    {

        Matrix3 matLocal;

        Matrix3 matWorld = pNode->GetObjTMAfterWSM( iTick );

 

        if( pParent == NULL )

        {

            matLocal = matWorld;

        }

        else

        {

            Matrix3 matParent = pParent->GetObjTMAfterWSM( iTick );

            matLocal = matWorld * Inverse( matParent );

        }

 

 

        Matrix3 maxMat = matLocal;

        if( GetLeftHand() )

            RightToLeft( &maxMat, &matLocal );

 

        base::Matrix& mat = pAniMat[i];  mat.Init();    MaxToBaseMatrix( &mat, &maxMat );

    }

 

    XmlNode* pXmlNode = CreateXmlNode( "Animation" );

    pXmlNode->AddAttribute( "Number", ToString( aniNum ).c_str() );

    pParentXML->AttachChild( pXmlNode );

 

    for( int i = 0; i < aniNum; ++i)

    {

        base::Matrix& mat = pAniMat[i];

        pXmlNode->AttachChild( CreateXmlNode( "matrix" ) )->SetData(  ToString( mat ) );

    }

    delete  [] pAniMat;

    return pXmlNode;

}

 

==== 뷰어 ====

< 추가된 함수 >

애니메이션 시간 :
GameTime.h   GameTime.cpp  : GetTickCount()를 API를 이용해 네임스페이스 GameTime을 사용하고 있다.
시간을 전역으로 두면 캐릭터 마다 프레임이 다른 경우의 처리가 애메해진다.  캐릭터 애니메이션 구현이 한바퀴 돌고 나서 다시 작업 하자.

렌더링 루프에서 GameTime::Update()를 실행하여 주기적으로 GameTime::var.nFrame의 값을 업데이트 해준다.

XmlNode 수정 :
XmlNode에 std::string* GetAttribute( const char* szName ) 추가,  이 메서드는 Attribute의 네임으로 값을 리턴해준다.

 

< 애니메이션 헤더, 데이터 로딩 >

INode와 별도로 추가된  XmlNode는 OnUpdate()에서 처리한다. 애니메이션 헤더와 애니메이션 데이터를 처리하는 ConvertAnimationHeader(), ConvertAnimation()이 각각 추가 되었다.

XmlNode시 애니메이션 헤더 데이터는 최상위 노드에 추가 되고, 애니메이션 데이터는 본이 존재했던 노드에 추가 된다.  노드의  멤버 변수는 다음과 같다.

AnimationInfo*       m_pAniInfo; //애니메이션 헤더
int                      m_nAniSize; //애니메이션 리스트 사이즈
D3DXMATRIXA16*  m_aniArray; //애니메이션 배열

 

void XmlMesh::OnUpdate()

{

    if( *GetName() == "mesh" )

        ConvertMeshInfo();   

    else if( *GetName() == "Transform" )

        ConvertTransformInfo();

    else if( *GetName() == "AnimationHeader" )

        ConvertAnimationHeader();

    else if( *GetName() == "Animation" )

        ConvertAnimation();

}

XML을 로딩할 때, 애니메이션 데이터는 상위 노드( m_pParent )에 추가 되어야 한다.
맥스에 익스포트시 "Animation" 노드는 애니메이션 데이터를 저장하기 위해 만든 노드이다.

애니메이션 데이터는 m_aniArray 배열에 m_nAniSize 크기 만큼 로딩하여 변환된다.

void XmlMesh::ConvertAnimation()

{

    SetUseData( false );

    int num = 0;

    std::string* pStr = GetAttribute( "Number" );

    if( pStr )

        num = ToInt( *pStr );

 

    if( num == 0 )

        return;

 

    XmlNodeVector nodeArr;

    FindChild( "matrix", nodeArr );

    if( nodeArr.size() != num )

        return;

 

    XmlMesh* pXmlMesh = (XmlMesh*)m_pParent;

    pXmlMesh->m_nAniSize = num;

    pXmlMesh->m_aniArray = new D3DXMATRIXA16[num];

 

    XmlNodeVector::iterator iter = nodeArr.begin();

    XmlNodeVector::iterator endIter = nodeArr.end();

    for( int index = 0; iter != endIter; ++iter, ++index )

    {

        XmlNode* xmlNode = *iter;

        base::Matrix mat = ToMatrix( *xmlNode->GetData() );       

        ToDXMatrix( pXmlMesh->m_aniArray[index], mat );

    }

}

< XmlMesh의 애니메이션 헤더와 엔진노드를 엔진 노드에 저장 >

XmlMesh::OnXmlToENode()는 로딩된 XmlMesh의 정점, 애니메이션 관련 데이터를 엔진  노드(ENode)를 만들어 엔진 노드에 저장 하는 곳이다.

ENode* XmlMesh::OnXmlToENode( ENode* pParent )

{

    ENode* pNode = NULL;

 

    if( m_vertex.size() > 0 )

    {

        EStaticMesh* pStaticMesh = new EStaticMesh( g_d3d_Device );

        pStaticMesh->CreateVertexBuffer( this );

        pNode = pStaticMesh;

    }

    else

    {

        pNode = new ENode( g_d3d_Device );

    }

 

    if( pParent == NULL )

        pNode->CreateAnimationHeader( this );

 

    pNode->SetName( this->GetName()->c_str() );

    pNode->SetMatrix( m_matLocal );

    if( m_aniArray )

    {

        pNode->CreateAnimation( this );

    }

    return pNode;

}

 

< 월드 행렬 계산 >

ENode::OnCalcWorldMatrix()는 매 프레임, 월드 행렬을 구하는 곳이다. 애니메이션이 배열이 있으면 애니메이션 로컬 행렬을 대입한다.

월드 행렬 = 애니메이션 로컬 행렬 * 부모의 월드 행렬

void ENode::OnCalcWorldMatrix()

{

    if( m_pParent == NULL )

    {

        m_matWorld = m_matLocal;

    }

    else

    {

        if( this->m_nAniSize > 0 )

        {

            int nFrame = GameTime::GetFrame();   

            m_matWorld = m_aniArray[nFrame] * m_pParent->m_matWorld;

        }

        else

            m_matWorld = m_matLocal * m_pParent->m_matWorld;

    }

}

프로젝트
maxProject_rigid_animation.zip
viewer_rigid_animation.zip