XML Exporter: 스킨 애니메이션 익스포트

캐릭터의 애니메이션에 PHYSIQUE와 SKIN을 적용한 애니메이션을 구현해 본다.

< 스킨 애니메이션 공식 >

스킨 애니메이션은 각 노드의 행렬과 가중치를 곱한 합으로 정점의 위치를 결정한다.

V = ( (Vorg*M1)*W1 ) + ( (Vorg*M2)*W2 ) + ( (Vorg*M3)*W3 ) …. + ( (Vorg*Mn)*Wn )
   = ∑ Vorg  * Mi * Wi = Vorg ∑ Mi * Wi      ( 여기서 Wi의 합은 1이다.)

Vorg, Mi, Wi 월드 값을 구하여 저장하면 된다.

< BuildBoneWieght >

Export::DoExport()에 BuildBoneWeight()를 추가하였다.

m_pXmlRoot는 XmlNode를 상속한 XmlExp 클래스이다. 정점에 영향을 주는 행렬인 INode의 행렬의 인덱스를 참조하기 위해 XmlNode를 상속한 XmlExp에 행렬 인덱스 m_nIndex를 추가 하였다.

    XmlExp::m_nRef = 0;

    //XML 노드를 만든다.

    BuildINode( pRoot, m_pXmlRoot );

    m_pXmlRoot->BuildBoneWeight();

    WriteHeader( m_pXmlRoot );

    ExportXML( m_strName.c_str() );

< 리깅 적용한 정점(Vorg) 구하기 >

월드로 변환된 정점을 저장한다.
Rigid 모델의 애니메이션과 다른점이 있다면 시작시간(0 timeTick )의 수정자(모디파이어)가 적용된 최종적인 행렬을 적용한다. 수정자가 적용된 행렬은 GetObjTMAfterWSM()을 통해서 구한다. 수정자가 적용되었다는 것은 리깅 작업을 적용한 결과이다.

XmlExp* Export::WriteBaseMesh( INode* pNode, XmlExp* pParentXML )

{

    ...................

 

    Matrix3    matWSkin = pNode->GetObjTMAfterWSM(0);  //Matrix3    matWSkin = pNode->GetObjectTM(0);

 

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

    {

        Point3 v = matWSkin * pMesh->verts[i];  //월드로 변환된 정점을 사용한다.

 

        if( GetLeftHand() )

        {

            float temp = v.y;

            v.y = v.z;

            v.z = temp;

        }

 

        base::Vector3 pos( v.x, v.y, v.z );

        pXmlVertexNode->AttachChild( CreateXmlNode<XmlExp>( "data" ) )->SetData( ToString(pos) );

    }

    ..........

}

 

< 가중치 인덱스와 가중치 구하기 >

BuildBOneWeight은 FindModifier()에 의해 찾은 수정자가 PHYSIQUE이면 WritePhysiqueWeight()를 실행하고  SKIN이면 WriteSkinWeight()을 실행한다.

void XmlExp::BuildBoneWeight()

{

    if( m_pINode )

    {

        Modifier*    pMod = (Modifier*)FindModifier(m_pINode, Class_ID(PHYSIQUE_CLASS_ID_A, PHYSIQUE_CLASS_ID_B) );

        if(pMod)

        {

            WritePhysiqueWeight( pMod );

            return;

        }

 

        pMod = (Modifier*)FindModifier(m_pINode, SKIN_CLASSID);

        if(pMod)

        {

            WriteSkinWeight( pMod );

            return;

        }

    }

    ..............

}

가중치와 노드를 구하는 메서드의 순서는 다음과 같다.

< PHYSIQUE 수정자에 의한 WritePhysiqueWeight() >

1. PHYSIQUE, SKIN 수정자(Modifier) 찾기

2. 수정자에서 IPhysiqueExport 구하기

3. IPhysiqueExport에서 IPhyContextExport 구하기

4. 정점을 Rigid로 바꾸고 블랜딩을 활성화 시킨다.

IPhyContextExport::ConvertToRigid(TRUE), IPhyContextExport::AllowBlending(TRUE)

5. Bone에 영향받는 정점의 갯수를 구한다. (정점은 WriteBaseMesh()에 구한 정점의 갯수와 일치한다.)

IPhyContextExport::GetNumberVertices()

 

정점의 갯수만큼 루프를 반복한다.

6. 정점의 갯수 만큼 루프를 반복 하면서 연결된 INode와 가중치를 구한다.

7. 문맥에서 IPhyVertexExport를 구한다.

8. 문맥에서 구한 IPhyVertexExport::GetVertexType가 RIGID_TYPE이면 영향을 주는 노드와

가중치는 한개이다.

9. IPhyVertexExport로 IPhyBlendedRigidVertex를 구한다.

 

영향을 주는 가중치의 갯수만큼 반복한다.

10. IPhyBlendedRigidVertex:: GetNumberNodes()로 영향을 주는 노드의 갯수만큼 루프를 반복한다.

11. 루프를 돌면서 INode와 가중치를 구한다.

IPhyBlendedRigidVertex::GetNode(), IPhyBlendedRigidVertex::GetWeight()를 구한다.

12. XmlExp::FindID()에 의해 인덱스를 찾는다.

 

< SKIN 수정자에 의한 WriteSkinWeight() >

1. PHYSIQUE, SKIN 수정자(Modifier) 찾기

2. 수정자에서 ISkin 구하기

3. ISkin으로 ISkinContextData를 구한다.

4. Bone에 영향받는 정점의 갯수를 구한다. (정점은 WriteBaseMesh()에 구한 정점의 갯수와 일치한다.)

ISkinContextData::GetNumPoints()

 

정점의 갯수만큼 루프를 반복한다.

5. 정점의 갯수 만큼 루프를 반복 하면서 연결된 INode와 가중치를 구한다.

 

영향을 주는 가중치의 갯수만큼 반복한다.

6. ISkinContextData::GetNumAssignedBones()로 영향을 주는 노드의 갯수만큼 루프를 반복한다.

7. ISkin::GetBone()으로 영향을 주는 노드를 구하고, ISkinContextData::GetBoneWeight()으로 가중치를 구한다.

8. XmlExp::FindID()에 의해 인덱스를 찾는다.

 

< 애니메이션 행렬 저장 : Export::WriteAnimation >

시간별 애니메이션 월드를 구해서 저장한다. 크기 변환이 적용 되지 않도록 NoScale()을 실행해야 한다.
애니메이션 계층구조를 이용하지 않고, 월드변환된 값을 이용한다.

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

{

    ..........

    Matrix3    matPivot = pNode->GetNodeTM(0);

    matPivot.Invert();

 

    int i = 0, iTick = beginTick;

 

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

    {

        Matrix3 matWorld = pNode->GetObjTMAfterWSM( iTick );

        matWorld.NoScale();

 

        //애니메이션 월드 행렬 = Pivot 행렬의 역행렬 * 시간에 대한 노드의 월드 행렬

        Matrix3 matAni = matPivot * matWorld;

 

        Matrix3 maxMat = matAni;

        if( GetLeftHand() )

            RightToLeft( &maxMat, &matAni );

 

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

    }

    ............

}

애니메이션 월드 행렬 = Pivot 행렬의 역행렬 * 시간에 대한 노드의 월드 행렬
Matrix3 matAni = matPivot * matWorld

애니메이션 월드 행렬을 구할때 Pivot 행렬의 역행렬이 필요한 이유는 다음과 같다.

Matrix3 matPivot = pNode->GetNodeTM(0); 
matPivot.Invert();

http://www.gpgstudy.com/forum/viewtopic.php?t=609 참조

tm = pNode->GetNodeTM(0);
tm.Invert();

에서 Invert() 를 하는 이유는 통짜 메쉬 데이터로부터 각 뼈대에 상대적인 위치 벡터를 얻어내기 위한 것입니다. 다시 말씀 드리자면, 통짜 메쉬 데이터는 각각 뼈대에 상대적인 벡터값이 아니라, 모두 한 좌표계를 기준으로 하기 때문에 (예를 들면, 월드 좌표계의 원점을 기준), 뼈대에 대한 상대적인 위치 벡터를 구하려면 뼈대행렬의 역행렬이 필요한 것이지요.

예를 들어, 월드좌표계의 wp 라는 점이 있을 때에 이것을 로컬좌표계의 lp 라는 점으로 표현하자면,

wp = M * lp 

가 됩니다. 이때, M 은 로컬변환행렬이구요. 여기에서는 뼈대 행렬의 의미로 생각하시면 되겠습니다. 즉, 위의 식은 뼈대 행렬을 기준으로 상대적인 위치 벡터 lp 를 뼈대 행렬에 곱하면, 월드 좌표계 상의 메쉬 벡터를 알 수 있다는 것인데, 우리는 월드 좌표계의 메쉬 벡터 wp 는 알고 있지만, lp 를 모르고 있기 때문에,

M^-1 * wp = M^-1 * M * lp
M^-1 * wp = lp

위 식의 양변에 M의 역행렬 M^-1 을 곱해주면, lp 를 구할 수 있는 식이 나오게 되는 거죠. 월드 상의 wp (메쉬 데이터의 정점 벡터)를 알고 있기 때문에 뼈대 좌표계 상의 상대적인 정점 벡터 lp 를 얻기 위해 위와 같이 역행렬이 필요한 겁니다.

프로젝트: maxProject_skin.zip