XML Exporter: Texture Rendering (1차 완성)

맥스 플러그인 : 텍스쳐 Export

텍스쳐 좌표를 익스포트 해서 텍스쳐를 렌더링 한다.

맥스에서 텍스쳐 좌표와 정점 좌표는 1:1이 아니다. 정점 인덱스 리스트와 별도로 텍스쳐 좌표도 인덱스 리스트로 관리하여 공유하고 있다.

그래서 맥스에서는 텍스쳐 좌표 인덱스 리스트가 별도로 존재한다. 일반적으로 텍스쳐 정점이 메쉬의 정점갯수보다 많다.  정점은 한 개인데 정점을 공유하는 삼각형 두 개가 텍스쳐가 다르면 텍스쳐 좌표는 메쉬의 정점보다 많아 질것이다.  아래 그림을 보고 이해하는게 더 쉬울 것이다.

< 정점 Face 인덱스와 텍스쳐 Face 인덱스의 관계 >

  a b c a b c
정점 face
인덱스
V0 V1 V3 V1 V2 V3

텍스쳐
face
인덱스

T0 T1 T2 T3 T4 T7

위의 그림에서 V0, V1, V2, V3은 정점 좌표의 face 인덱스이고, T1, T2, T3, T4, T5, T7은 텍스쳐 좌표 인덱스이다. 위의 그림처럼 텍스쳐 좌표 face 인덱스에 맞춰 정점, 노멀, 애니메이션 블랜딩 값을 조정 할 필요가 있다.

XmlExp::WriteTextureCoordinate()메소드에서 텍스쳐가 포함된 정점을 익스포트 하는 것은 다음의 순서로 진행된다.

1. 텍스쳐 Face 인덱스 리스트와 텍스쳐 좌표를 배열 pTvFace, pUV로 가져 온다..
2. 텍스쳐 Face 인덱스 리스트를 오름차순으로 정렬하면서 중복된 데이터를 제거한다.
3. 텍스쳐 Face 인덱스, 정점 Face 인덱스, UV 좌표를 하나의 그룹으로 하여 배열 uvFaceArr을 만든다.
4. uvFaceArr의  텍스쳐 인덱스 값으로 pTvFace의 인덱스 값을 수정한다.
5. uvFaceArr을 기준으로 정점 리스트를 다시 만든다.
6. pTvFace를 새로운 정점 Face 인덱스로 정한다.
7. uvFaceArr의 정점 인덱스로 블랭딩 리스트를 새롭게 구성한다.

1. 텍스쳐 Face 인덱스 리스트와 텍스쳐 좌표를 배열 pTvFace, pUV로 가져 온다..
pTvFace에 삼각형 인덱스 리스트를 저장한다.
PUV에  텍스쳐 좌표를 저장한다.

    base::Index3* pTvFace = new base::Index3[ numFace ];   

    base::Vector2* pUV = new base::Vector2[ numTVerts ];

 

    //1. 텍스쳐 좌표 Face 인덱스 리스트와 텍스쳐 좌표를 배열 pTvFace, pUV로 가져 온다..

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

    {

        base::Index3& index = pTvFace[i];

        index.a = pMesh->tvFace[i].t[0];

        index.b = pMesh->tvFace[i].t[1];

        index.c = pMesh->tvFace[i].t[2];

 

        if( GetLeftHand() )

        {

            index.b = pMesh->tvFace[i].t[2];

            index.c = pMesh->tvFace[i].t[1];

        }

    }

 

    for(int n=0; n < numTVerts; ++n)

    {

        UVVert t = pMesh->tVerts[n];

        pUV[n].x = t.x;

        pUV[n].y = 1.f - t.y;    // DX의 UV에 맞게 수정

    }


2. 텍스쳐 Face 인덱스 리스트를 오름차순으로 정렬하면서 중복된 데이터를 제거한다.
텍스쳐 좌표 삼각형 리스트에서 중복된 정점, 빈 인덱스를 제거하기 하기 위해  맵 tvFaceMap에 저장한다.

    mapTvFace tvFaceMap;

    for(int n = 0; n < numFace; ++n)

    {

        tvFaceMap.insert( mapTvFace::value_type( pTvFace[n].a,  m_mesh.pFace[n].a) );

        tvFaceMap.insert( mapTvFace::value_type( pTvFace[n].b,  m_mesh.pFace[n].b) );

        tvFaceMap.insert( mapTvFace::value_type( pTvFace[n].c,  m_mesh.pFace[n].c) );

    }

3. 텍스쳐 Face 인덱스, 정점 Face 인덱스, UV 좌표를 하나의 그룹으로 하여 배열 uvFaceArr을 만든다.
정렬된 tvFaceMap을 벡터 uvFaceArr에 옮긴다.  이때, 텍스쳐 좌표도 같이 저장한다.
나중에 uvFaceArr을 이용하여 정점 리스트, 노멀값 리스트를 구할 것이다.

    mapTvFaceIter    iter        = tvFaceMap.begin();

    mapTvFaceIter    endIter        = tvFaceMap.end();

 

    std::vector<UVIndex> uvFaceArr;

    for(; iter != endIter; ++iter )

    {

        int nTIndex = iter->first;

        int nVIndex = iter->second;

 

        float u = pUV[nTIndex].x;

        float v = pUV[nTIndex].y;

 

        uvFaceArr.push_back( UVIndex(nTIndex, nVIndex, u, v));

    }

4. uvFaceArr의  텍스쳐 인덱스 값으로 pTvFace의 인덱스 값을 수정한다.
중간에 사용되지 않는 인덱스가 제거된다.

    for(int n=0; n < uvFaceArr.size(); ++n)

    {

        int tIndex = uvFaceArr[n].ti;

 

        for(int j = 0; j < numFace; ++j)

        {

            if(tIndex == pTvFace[j].a)   

                pTvFace[j].a = n;

            if(tIndex == pTvFace[j].b)   

                pTvFace[j].b = n;

            if(tIndex == pTvFace[j].c)   

                pTvFace[j].c = n;

        }

    }

5. uvFaceArr을 기준으로 정점 리스트를 다시 만든다.
기존의 정점, 노멀값을 삭제 후,  uvFaceArr의 값으로 정점, 노멀값을 생성한다. 이때, UV 값도 저장한다.

    int    nNewVtxSize =  (int)uvFaceArr.size();

    base::Vector3* pNewPos = new base::Vector3[nNewVtxSize];

    base::Vector3* pNewNor = new base::Vector3[nNewVtxSize];

    base::Vector2* pNewUV = new base::Vector2[nNewVtxSize];

 

    for(int n = 0; n < nNewVtxSize; ++n)

    {

        int faceIndex = uvFaceArr[n].vi;       

        base::Vector2 uv = uvFaceArr[n].uv;

 

        pNewPos[n] = m_mesh.pPos[ faceIndex ];

        pNewNor[n] = m_mesh.pNormal[ faceIndex ];

        pNewUV[n] = uv;

    }

 

    SAFE_DELETE_ARRAY( m_mesh.pPos );

    SAFE_DELETE_ARRAY( m_mesh.pNormal );

    SAFE_DELETE_ARRAY( m_mesh.pFace );

6. pTvFace를 새로운 정점 Face 인덱스로 정한다.

    m_mesh.pFace = pTvFace; 

    m_mesh.pPos = pNewPos;

    m_mesh.pNormal = pNewNor;

    m_mesh.pUV = pNewUV;

    m_mesh.numVertex = nNewVtxSize;

7. uvFaceArr의 정점 인덱스로 블랭딩 리스트를 새롭게 구성한다.

    BlendVector* pNewBone = new BlendVector[nNewVtxSize];

    BlendVector* pOldBone = m_mesh.pBoneWeight;

 

    for(int n=0; n < nNewVtxSize; ++n)

    {

        int oldIndex = uvFaceArr[n].vi;   

 

        if( oldIndex >= m_mesh.numBoneVertex )

            continue;

 

        BlendVector* pCurrBone = &pOldBone[ oldIndex ];   

 

        BlendVector::iterator iter = pCurrBone->begin();

        BlendVector::iterator endIter = pCurrBone->end();

 

        for(; iter != endIter; ++iter )

        {

            BlendData data = { (*iter).index, (*iter).weight };

            pNewBone[n].push_back( data );

        }

    }

 

    uvFaceArr.clear();

    SAFE_DELETE_ARRAY( pOldBone );

 

    m_mesh.pBoneWeight = pNewBone;

    m_mesh.numBoneVertex = nNewVtxSize;

 

< 텍스쳐 좌표를 익스포트 하기까지 큰 흐름 >

1. DoExport에서 BuildTextureCoordinate()를 호출한다.

int Export::DoExport(...)

{

    ......

    m_pXmlRoot->BuildTextureCoordinate();

    ......

}

2. BuildTextureCoordinate()를 재귀 호출로 실행하면서 WriteTextureCoordinate()가 true이면 XML을  이전 "mesh" 노드는 삭제후 XML을 새롭게 만든다.
텍스쳐 좌표를 익스포트 할 때, 정점을 다시 생성하기 위해 XmlExp 클래스의 MeshData m_mesh에 정점 구조체 관련 값이 저장 되어 있다.

void XmlExp::BuildTextureCoordinate()

{

    if( this->GetName() && *GetName() == "mesh" )

    {

        if( WriteTextureCoordinate() )

        {

            DetachMeshChild();

            XmlExp* pXmlExp = (XmlExp*)GetParent();

            WriteMesh( pXmlExp->GetINode() );

            return;

        }

    }

 

    XmlNode::ChildList* pChildList = &m_child;

    XmlNode::ChildList::iterator iter = pChildList->begin();

    XmlNode::ChildList::iterator endIter = pChildList->end();

    for( ; iter != endIter; ++iter )

    {

        XmlExp* pChildXml = (XmlExp*)*iter;

        pChildXml->BuildTextureCoordinate();

    }

}

 

뷰어 : 텍스쳐

< 텍스쳐 좌표와 재질 정보 로딩 >

XmlMesh 클래스에는 텍스쳐 UV와 텍스쳐 파일 이름을 얻어오는 FetchTexureCoordinate(), FetchMaterial() 메소드가 추가 되었다.

void XmlMesh::ConvertMeshInfo()

{

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

    //텍스쳐 UV 값 읽기

    XmlNode* pUVXmlNode = FindFirstChild( "TextureUV" );

    if( pUVXmlNode  == NULL )

        return;

    pXmlMesh = (XmlMesh*)pUVXmlNode;

    pXmlMesh->FetchTexureCoordinate( GetParent()->m_vertexArr );

 

    //메트리얼 값 읽기

    XmlNode* pMaterialXmlNode = FindFirstChild( "material" );

    if( pMaterialXmlNode  == NULL )

        return;

    pXmlMesh = (XmlMesh*)pMaterialXmlNode;

    pXmlMesh->FetchMaterial( &GetParent()->m_material );

}

< 정점 구조체와 재질 정보 >

정점 구조체에 텍스쳐 uv를 읽어오기 위해 uv를 추가하고, FVF에 D3DFVF_TEX1도 추가한다.

EShaderBoneMesh 클래스에는 텍스쳐 정보를 추가한다.
std::string     m_diffuseMap;
LPDIRECT3DTEXTURE9  m_pTexture;

struct ShaderBlendVertex

{

    D3DXVECTOR3    p;            //메시의 정점

 

    FLOAT        weight[3];        // BLEND WEIGHT

    BYTE        boneIndex[4];    // MATRIX Index

 

    D3DXVECTOR3        n;            //메시의 노멀값

    D3DXVECTOR2        uv;            //텍스쳐 UV값

 

    ShaderBlendVertex() : p(0,0,0), n(0, 0, 0), uv(0, 0)

    {

        boneIndex[0] = boneIndex[1] = boneIndex[2] = boneIndex[3] = 0;

        weight[0] = weight[1] = weight[2] = 0;

    }

 

    enum {    FVF = (D3DFVF_XYZB4 | D3DFVF_LASTBETA_UBYTE4 | D3DFVF_NORMAL | D3DFVF_TEX1 ),    };

};

< 정점 버퍼 생성 >

정점 구조체의 UV값을 셋팅하고 정점의 텍스쳐 이름을 설정한다.

int EShaderBoneMesh::CreateVertexBuffer( XmlNode* pXmlNode )

{

    ..........

    int n = m_nVertex = (int)xmlVx.size();

    ..........

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

    {

        //텍스쳐 좌표

        m_pMeshVerts[i].uv.x = xmlVx[i].t.x;

        m_pMeshVerts[i].uv.y = xmlVx[i].t.y;

 

    }

    ..........

    //텍스쳐 이름

    if( pXmlMesh->m_material.diffuseMap.empty() == false )

    {

        this->m_diffuseMap =  pXmlMesh->m_material.diffuseMap;

    }

   ..........

}

< 렌더링 >

텍스쳐를 렌더링 하기 위해 D3DRS_FILLMODE 렌더 상태를 D3DFILL_WIREFRAME에서 D3DFILL_SOLID로 바꾼다.렌더링시 텍스쳐가 로딩 안되었으면 텍스쳐 로딩 후 설정한다.

int EShaderBoneMesh::OnRender()

{

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

    m_pDevice->SetRenderState( D3DRS_FILLMODE, D3DFILL_SOLID);

    ............. // 셰이더 설정

    if( this->m_diffuseMap.empty() == false )

    {

        hr = S_OK;

        if( m_pTexture == NULL )

        {

            char szTexture[MAX_PATH];

            sprintf_s( szTexture, MAX_PATH, "data\\%s", this->m_diffuseMap.c_str() );

            hr = D3DXCreateTextureFromFile( m_pDevice, szTexture, &m_pTexture );

        }

        if( m_pTexture )

            m_pDevice->SetTexture(0, m_pTexture );

    }

    .........  // 메시 렌더링

}

<앞으로 과제>

기본적인 캐릭터 출력은 되었다. 맥스 플러그인과 뷰어의 남은 과제를 정리해 본다.

맥스 버전 2011로 업그레이드 및, UI 수정
XML 최적화 가능성 조사후 최적화
캐릭터 익스포트시 월드 값 대신 로컬 값 사용하도록 변경 가능한지 점검
셰이더에 라이팅 정보 추가
기타 등등~~~~~~

실행 화면이다. 예제는 혜지원의 김종민님의 3dsMax Game Design의 예제를 이용하였다.  이미지는 걷고 있는 모습이다. 아직 셰이더에 라이팅 정보는 들어가 있지 않다.

맥스 플러그인 : maxProject_texture.zip
맥스 뷰어: viewer_texture.zip