home lecture link bbs blame

◈Max Plugin◈

플러그인(Plugin)의 뜻을 찾아보면 프로그램에 없던 새 기능을 추가하기 위해 끼워 넣는」부가적인 프로그램으로 자체적으로는 실행능력은 없지만 특정한 프로그램 속에서 함께 실행되어 기능을 발휘한다고 되어 있습니다.

Autodesk 3D Max(이하 MAX: 맥스) Plugin은 사용자의 목적에 맞게 맥스 프로그램의 기능을 보강하기 위해서 만든 프로그램입니다. 윈도우 운영체제에서 MAX용 플러그인의 구현은 DLL (Dynamic Linking Library)입니다. 따라서 원래 프로그램의 형식과 윈도우 DLL의 표준에 맞게 프로그램을 만든다면 플러그인을 만들 수 있습니다.

맥스 플러그인은 아무거나 만들 수 있는 것이 아니라 맥스에서 지원되는 것만 만들 수 있습니다. 어떤 플러그인을 만들 것인지는 Visual Studio에서 맥스 플러그인 만드는 Wizard로 확인할 수 있으며 이 중에서 "File Export"가 우리가 만들고자 하는 플러그인 입니다.

"File Export" 플러그인은 게임에서 필요한 맥스 파일의 일부를 꺼내오는 것입니다. 물론 다 꺼내오면 좋겠지만 그렇게 하기는 힘들고 설령 다 가져온다 하더라도 맥스 프로그램과 게임 프로그램의 자료 해석 차이로 화면에 보이는 모습은 약간의 차이가 있을 수 있습니다. 좋은 "File Export" 플러그인은 맥스 화면과 게임 화면의 차이를 줄이고 최대한 일치시키는 것이라 할 수 있습니다.

보통 경력자들도 쓸만한 "File Export" 플러그인을 만드는데 거의 4~6개월의 시간이 소요됩니다. 이것은 맥스와 D3D는 같은 데이터라도 처리하는 방식이 다르고 구체적인 맥스의 내용을 모르는 상태에서 작업을 하기 때문입니다. 또한 맥스 프로그램의 화면에서는 정상적인 모습으로 나타나지만 Exporting을 하면 미처 처리하지 못한 데이터로 인해 원치 않는 모습이 종종 나타납니다. 따라서 완벽한 플러그인을 만들기까지 너무 많은 시간이 소요된다면 회사로서도 비용의 문제가 발생하기 때문에 버그를 피하는 방법을 그래픽 담당자들과 논의를 하고 그래픽 작업의 순서를 결정합니다.

 

이 장은 완벽하지 않지만 그래도 어느 정도 쓸 만한 플러그인을 만들어보는 것이 목적입니다. 모든 맥스 데이터에 대해서 데이터를 Export한다고는 할 수 없고 그러한 문제들은 독자의 목으로 남겨 놓겠습니다. 또한 3D 맥스가 2010 버전까지 출시되었지만 여기서는 8.0기준으로 플러그인을 만들겠습니다. 그것은 맥스의 가격이 500만원이 넘습니다. 보통 교육기관이나 기업체에서 소프트웨어가 새로 출시되면 학생들 또는 개발자들을 위해 새로운 소프트웨어를 공급하는 것이 일반적이지만 개발자 월급 보다 훨씬 비싼 소프트웨어를 업그레이드 한다는 것은 회사 운영상 맞지 않기 때문에 대부분 이전 버전을 그대로 사용하는 경향이 많습니다. 프로그래머의 입자에서 바라보아도 맥스 버전이 올라가도 필요한 SDK 내용은 거의 그대로인 것이 대부분입니다.

8.0을 선택한 주된 이유는 가장 많이 사용되는 버전이기도 하지만 이전에는 맥스에서 애니메이션 작업을 할 때 Physique를 주로 사용했는데 8.0 이후에는 스키닝(Skinning)이 지원되어 이 방식으로 작업을 많이 한다고 합니다. 스키닝으로 애니메이션을 작업하면 그래픽과 게임에서 일치하지 않는 버그 찾기도 수월하다고 합니다.

 

 

5.1 SDK

5.1.1 SDK 설치와 VC Project

맥스는 용량이 크므로 Program Files에 설치하기 보다는 하드를 분할해서 C 대신 D E에 설치하는 것이 좋습니다. 저는 "D:\_3dsMax8" 에 설치 했습니다.

맥스 SDK는 맥스와 따로 설치됩니다. 다음과 같이 install CD가 있으면 SDK를 설치합니다. 저는 "D:\_3dsMax8\maxsdk"에 설치했습니다.

 

<MAX SDK 설치>

 

SDK를 설치하고 나서 맥스 SDK가 설치된 폴더 예를 들어 "D:\_3dsMax8\maxsdk\howto"에 가면 "3dsmaxPluginWizard" 폴더가 있습니다. 이 폴더의 "Readme.txt"파일을 읽어보면 이 위저드는 "Visual Studio 7.1"용 위저드 임을 알리고 있습니다.

 

또한 Installing 2번을 보면 "3dsmaxPluginWizard.vsz" 파일을 열어서 다음 부분을 고치라고 합니다.

Param="ABSOLUTE_PATH = [Absolute Path Location of 3dsmaxPluginWizard Root Directory]"

 

이것은 Visual Studio에서 위저드를 실행할 때 맥스 SDK의 위저드를 실행할 수 있는 절대 경로를 설정하라는 것으로 그림처럼 "dsmaxPluginWizard.vsz" 파일이 있는 경로를 복사해서 다음과 이 만듭니다.

 

Param="ABSOLUTE_PATH = [D:\_3dsMax8\maxsdk\howto\3dsmaxPluginWizard]"

 

또한 경로 이름에 '\'를 추가하지 말라고 합니다. (그냥 탐색기에 있는 경로를 붙이면 문제 없습니다.)

< dsmaxPluginWizard.vsz 파일의 경로 복사>

 

다음으로 Installing 3번에 보면

 

3dsmaxPluginWizard.ico

3dsmaxPluginWizard.vsdir

3dsmaxPluginWizard.vsz

 

파일 3개를 Visual Studio가 설치된 폴 더 밑에 "Visual Studio 폴더\Vc7\vcprojects Vc7\vcprojects" 폴더에 복사하라고 하는데 다음과 같이 하면 됩니다.

 

<Visual Studio 위저드 프로젝트 설치>

 

이렇게 하면 위저드 설치는 끝이 납니다. Visual Studio의 새 프로젝트를 실행하면 다음 그림처럼 맥스 플러그인 위저드 선택 아이콘이 있습니다.

 

<맥스 플러그인 위저드 실행>

 

확인 버튼을 누르면 어떤 플러그인을 만들 것인지 선택 화면이 나옵니다. "File Export"를 선택합니다.

 

<플러그인 종류 선택: "File Exporter">

 

다음 창에 Plugin Detail 창이 있습니다. 그냥 넘어갑니다. 그 다음 창에는 맥스 SDK가 설치된 폴더, 플러그인이 컴파일 되면 복사될 경로, 맥스의 실행 경로를 지정하는 것이 나옵니다. 맥스는 실행 파일이 있는 하위 폴더 "plugins" 에 있는 플러그인들을 실행할 때 전부 올립니다. 따라서 이곳에 자동으로 복사하도록 설정하는 것이 좋습니다. 이것이 설정이 안되면 매 번 플러그인을 이곳에 복사해 놓아야 합니다.

 

<SDK 헤더 파일 Path, 플러그인 출력 위치, 맥스 실행 파일 경로 설정>

 

Finish 버튼을 누르면 위저드 코드가 만들어집니다. 2005의 경우에는 프로젝트가 Visual Studio에 올라오지 않을 것입니다. 이것은 이 위저드가 2003 버전에 맞추어져 있기 때문입니다. Visual Studio를 닫고 앞서 만든 maxProject1.vcproj 를 다시 실행하면 변환 마법사가 진행이 됩니다. 마법사를 진행해도 다음과 같은 에러가 발생할 것입니다.

 

"XML 구문을 분석하는 동안 다음과 같은 오류가 발생했습니다. 파일: E:\_Document\_vc\maxProject1\maxProject1.vcproj : 86 : 22 오류 메시지: 부모 요소 'Configuration'의 콘텐츠 모델에 따르면 'IntelOptions'() 예기치 않은 요소입니다. …"

 

당황하지 말고 메모장으로 vcproj 파일을 열어서 다음 부분을 과감히 삭제 합니다. 다시 Visual Studio를 실행하고 변환 마법사를 진행하면 무사히 프로젝트가 올라 옵니다.

 

<프로젝트 옵션 설정>

 

여기까지 했으면 다 된 것 같은데 안타깝게도 프로젝트를 빌드 하면 에러가 10개 정도 만들어 집니다. 이것은 Visual Studio 2005 버전이 이전 버전보다 C++ 표준을 엄격히 따르다 보니 만들어진 문제들입니다. 찾아서 해결하면 됩니다.

 

첫 번째 에러는 d:\_3dsmax8\maxsdk\include\manipulator.h(463) 파일의 ManipExport Invalidate() { mValid = NEVER; }에서 나왔군요.

2005 이전에는 함수의 반환 형을 적지 않으면 int형으로 컴파일러가 인식했고 반환을 지정하지 않아도 Warning만 만들어지고 컴파일은 됐습니다. 코드의 내용상으로 봐서 반환은 없어 void가 맞겠지만 이전에는 int형이었으므로 Invalidate 앞에 int를 적어 줍니다. ManipExport int Invalidate() { mValid = NEVER; }

 

또 다른 에러는

"e:\_document\_vc\maxproject1\maxproject1.cpp(48):error C2065: 'themaxProject1'" 에서 만들어 졌습니다.

 

"void* Create(BOOL loading = FALSE) { return &themaxProject1; }" 으로 작성되어 있는 부분을 프로젝트의 이름과 같은 클래스의 인스턴스를 만드는 코드로 전환합니다.

 

void* Create(BOOL loading = FALSE) { return new maxProject1(); }

 

그 다음의 maxProject1ObjClassDesc 클래스 코드부터 콜백(Call Back) 함수 maxProject1OptionsDlgProc() 전까지 필요 없는 코드이므로 전부 삭제해 버립니다. 대략 80~ 224 라인이 될 것 같습니다. 이렇게 지우면 클래스는 maxProject1, maxProject1ClassDesc 두 개만 남아 있을 것입니다. 다시 컴파일 하면 이번에는 리소스 파일(rc)에서 에러가 만들어질 것입니다. 에러 부분은 지웁니다.

 

다시 빌드 하면 이번에는 "DllEntry.obj : error LNK2019: "class ClassDesc2 * __cdecl GetmaxProject1ObjDesc" 에러가 발생합니다. 이것은 앞서 maxProject1ClassDesc 클래스를 지웠기 때문입니다.

 

DllEntry.cpp16라인 "extern ClassDesc2* GetmaxProject1ObjDesc();"

부분과 59라인 "case 1: return GetmaxProject1ObjDesc();"을 삭제합니다.

 

다시 빌드 하면 링크 에러 "error LNK2001: "public: virtual class Manipulator * __thiscall maxProject1ClassDesc"가 만들어집니다.

클래스 maxProject1ClassDesc 안의 "BOOL IsManipulator() { return TRUE; }" 부터

"Manipulator* CreateManipulator(RefTargetHandle hTarget, INode* pNode);"까지 전부 지웁니다.

 

이렇게 지우고 나면 maxProject1ClassDesc 클래스는 다음과 같은 함수만 남아 있을 것입니다.

 

class maxProject1ClassDesc : public ClassDesc2 {

public:

        int            IsPublic() { return TRUE; }

        void *         Create(BOOL loading = FALSE) { return new maxProject1(); }

        const TCHARClassName() { return GetString(IDS_CLASS_NAME); }

        SClass_ID      SuperClassID() { return SCENE_EXPORT_CLASS_ID; }

        Class_ID       ClassID() { return maxProject1_CLASS_ID; }

        const TCHAR*   Category() { return GetString(IDS_CATEGORY); }

        const TCHAR*   InternalName() { return _T("maxProject1"); }

        HINSTANCE      HInstance() { return hInstance; }

};

 

솔루션을 다시 빌드하면 "plugins" 폴더에 "maxProject1.dle" 파일이 만들어 집니다. 연습 삼아 코드가 동작 하는지 다음과 같이 생성자 소멸자, 그리고 Ext() 함수에 메시지 박스를 띄워 놓고()브레이크 포인트를 설정(F9 )합니다.

 

maxProject1::maxProject1()

{

      MessageBox(NULL, "Create maxProject1", "Message", 0);

}

 

maxProject1::~maxProject1()

{

      MessageBox(NULL, "Destory maxProject1", "Message", 0);

}

 

const TCHAR *maxProject1::Ext(int n)

{             

      MessageBox(NULL, "Call maxProject1 Exporter", "Message", 0);

        return _T("");

}

 

디버그 모드로 실행하면 다음과 같이 창이 올라오고 여기에 맥스 프로그램의 위치를 찾아서 넣으면 맥스가 실행이 됩니다.

 

<Visual Studio에서 플러그인 동작으로 위해 맥스 실행: mxp00_maxProject1.zip >

 

디버딩 정보 없음 창이 나와도 ""를 선택하십시오.

아무 도형이나 대충 그리고 다음 그림과 같이 Export를 선택해서 실행하면 설정한 브레이크 포인터가 동작하고 간단한 메시지 창이 올라오는 것을 확인할 수 있습니다.

 

<플러그인 실행>

 

 

5.1.2 다른 프로젝트 수정

다른 사람이 작성한 플러그인 프로젝트를 수정하고 싶을 때도 있습니다. 예를 들어 "mxp00_mymax0.zip" LcMax라는 프로젝트로 모두 바꾼다고 합시다. 먼저 해 야할 일은 파일 "mymax.*"로 되어 있는 이름을 "LcMax.*"로 바꾸는 것입니다.

 

<파일 이름 바꾸기>

 

다음으로 editplus 등의 편집기를 이용해서 그림의 모든 파일을 열어서 "mymax"라는 이름을  전부 LcMax로 바꾸어 주어야 합니다.

<프로젝트 내용 전부 바꾸기>

 

이렇게 파일 이름과 프로젝트의 내용을 바꾸고 나서 다음과 같은 Class_ID의 값을 찾아서 바꾸어야 합니다.

 

#define …_CLASS_ID   Class_ID(0x…, 0x…)

 

Class_ID 값은 Guid Number 를 직접 조합해서 만들어도 되고 맥스 SDK가 설치된 하위 폴더 help 아래 gencid.exe("맥스 SDK 폴더\help\gencid.exe") 파일이 있습니다. 이것을 실행하고 New Class ID 버튼을 누르고 Copy To Clipboard 버튼을 누른 다음 Class_ID 위에 "Ctrl+V" 키를 누르면 새로운 값이 붙여집니다.

다음으로 확장자를 반환하는 Ext(int n) 함수도 자신이 정한 이름에 맞게 고칩니다.

 

const TCHAR *LcMax::Ext(int n)

{

        return _T("acm");

}

 

빌드 후 실행하고 Menu à "File" à "Export…" 를 진행 하면 다음 그림처럼 Export할 준비가 되어 있음을 볼 수 있습니다.

 

<다른 프로젝트를 바꾸어 사용할 때: mxp01_lcmax.zip>

 

 

5.1.3 ASE 프로젝트 수정

맥스 SDK가 설치된 폴더에서 "samples" 폴더에는 다양한 플러그인 예제들이 있습니다. 이중에서 "맥스 SDK 폴더\samples\import_export\asciiexp" 에는 여러분에게 친숙한 ASE 파일을 만드는 예제가 있습니다. "asciiexp.vcproj" 프로젝트를 열고 변환을 시도하면 다음과 같은 에러에 대한 메시지를 볼 수 있습니다.

 

"구문을 분석하는 동안 다음과 같은 오류가 발생했습니다."

"파일: D:\_3dsMax8\maxsdk\samples\import_export\asciiexp\asciiexp.vcproj : 204 : 22 오류 메시지: 부모 요소 'Configuration'의 콘텐츠 모델에 따르면 'IntelOptions'() 예기치 않은 요소입니다."

 

이 메시지지는 전에도 본적이 있는 메시지 입니다. asciiexp.vcproj 파일을 메모장이나 Edit plus등으로 열어서 해당 열을 삭제합니다.

 

"맥스 SDK 폴더\help\ gencid.exe"를 실행해서 새로운 ID asciiexp.cpp파일의 상단에 대략 25라인에 있는 Class_ID 를 새로운 아이디로 바꾸어 줍니다.

 

asciiexp.cpp파일의 135 라인 근처의 Ext(int n) 함수의 반환을 ASE와 구분하기 위해서 다음과 같이 asd로 바꾸어 줍니다.

 

const TCHAR * AsciiExp::Ext(int n)

{

        switch(n) {

        case 0:

               return _T("asd");

        }

        return _T("");

}

 

마지막으로 프로젝트의 플러그인이 출력할 폴더를 변경합니다. 메뉴 à 프로젝트 à "asciiexp" 속성을 열어서

"..\..\..\..\maxsdk\plugin\asciiexp.dle" 부분을 "..\..\..\..\plugins\asciiexp.dle" 으로 변경합니다.

"maxsdk\plugin" 그대로 두면 컴파일 한 후에 다시 플러그인을 옮겨야 합니다.

 

<수정된 ase: 출력 경로 변경>

 

리소스 뷰를 열어 IDD_ASCIIEXPORT_DLG에 테스트로 다음과 같이 버튼을 추가해 봅시다.

 

<리소스 뷰: 다이얼로드 수정>

 

컴파일 한 다음 실행하고 나서 맥스에서 파일이 Export가 되는지 간단한 박스 한 개를 그립니다. 그리고 나서 메뉴 à 파일 à Export 를 선택하면 ASD를 선택할 수 있고 다음과 같이 저장해 봅니다.

 

<Export 파일 저장>

 

이렇게 하면 이전에 리소스 뷰에서 수정한 다이얼로그 화면이 나타나고 OK 버튼을 누르면 확장자가 asd가 붙은 파일이 생성이 됩니다.

 

"맥스 폴더\stdplugs" 에는 기본적으로 제공되는 맥스 플러그인이 있습니다. 여기에 3ds ase도 같이 있습니다. 이들을 다른 곳으로 옮겨 놓으면 Export 선택에서 지워집니다.

 

만약 asciexp 프로젝트를 다른 곳으로 옮겨서 사용하려면 속성을 열어서 C/C++ 일반 à 추가 포함 디렉토리가 상대적으로 설정되어있는 것을 절대적으로 바꾸어 줍니다.

 

"..\..\..\include" à "맥스 SDK 폴더\include"

 

링커 à 일반 à "출력 파일 위치"를 변경합니다.

 

"..\..\..\..\plugins\asciiexp.dle" à "D:\_3dsMax8\plugins\asciiexp.dle"

 

링커 à 입력 à "추가 종속성"에 라이브러리를 추가합니다.

 

"odbc32.lib odbccp32.lib comctl32.lib"

à "odbc32.lib odbccp32.lib comctl32.lib core.lib geom.lib maxutil.lib mesh.lib"

 

리소스 à 일반 à "추가 포함 디렉토리"를 변경합니다.

 

"..\..\..\include" à "맥스 SDK 폴더\include"

 

솔루션 탐색기에서 Libraries를 전부 삭제합니다. 이 라이브러리들은 링커의 라이브러리 추가 종속성에 이미 포함시켰습니다.

"맥스 폴더\plugins" 에서 이전에 연습으로 작성한 플러그인 들을 전부 정리하고 수정된 asciiexp를 컴파일 해서 이곳에 새로 생성이 되는지 확인합니다.

<프로젝트에 포함된 솔루션 라이브러리 삭제>

 

mxp01_modified_ase.zip asciiexp의 프로젝트 이름과 파일 이름 등을 수정한 예제입니다.

간단한 플러그인이라면 ase 프로젝트를 정리해서 사용하는 것이 가장 무난합니다. ase는 강체 애니메이션(Rigid Body Animation)으로 구성되어 있습니다. 만약 스키닝을 추출하려면 ase와 다르게 구성해야 합니다. 먼저 ase에서 사용된 데이터 추출과 애니메이션을 구현해 보고 이후에 Skinng을 구현해 봅시다.

 

 

5.1.4 Wrapper 플러그인

맥스는 프로그램을 시작할 때 플러그인을 전부 로드 하고 있습니다. 따라서 플러그인을 지우거나 교체하려면 맥스를 중지 시켜야 합니다. 이것은 자주 디버딩 해야 하는 플러그인 제작에서는 상당히 불편합니다. 이 문제로 고생하다가 Tom Hudson 라는 분이 플러그인을 Wrapping 하는 방법을 인터넷에 아이디어와 코드를 올려놓았습니다. 다음의 내용은 Tom Hudson이 만든 것을 정리한 것입니다.

 

mxp01_lcmax.zip 에서 생성자 LcMax 클래스의 생성자 함수, 소멸자 함수, Ext() 함수, DoExport()함수에 다음과 같은 메시지 창을 넣고 디버그로 실행해 봅시다.

 

LcMax::LcMax()

{

      MessageBox(NULL, "Constructor LcMax", "Message", 0);

}

 

LcMax::~LcMax()

{

      MessageBox(NULL, "Destroyer LcMax", "Message", 0);

}

 

const TCHAR *LcMax::Ext(int n)

{

      MessageBox(NULL, "Constructor LcMax", "Message", 0);

        return _T("acm");

}

 

int     LcMax::DoExport()

{

      MessageBox(NULL, "Do Export LcMax", "Message", 0);

 

}

 

맥스에서 박스를 하나 그리고 파일 à Export를 선택합니다. 그러면 LcMax 클래스의 생성자의 브레이크 포인터에 프로세스가 걸립니다. 또 진행 하면 Ext() 함수에서, 그리고 다시 진행하면 소멸자에서 프로세스가 진행됨을 알 수 있습니다. 이 동작은 Export를 선택할 때 맥스가 플러그인들의 이름을 얻는 과정인 것을 알 수 있습니다. 저장할 이름을 선택하고 계속하면 생성자 à DoExport() à 소멸자로 프로세스가 진행됨을 볼 수 있습니다.

이 두 동작을 정리하면 맥스는 플러그인을 프로그램에서 로드하고 있고 만약 Export가 요청이 되면 SceneExport 클래스를 상속받은 객체의 생성자 à 작업에 필요한 함수 호출 à 소멸자를 반복한다고 할 수 있습니다.

 

앞서 맥스의 플러그인은 DLL이라고 했습니다. Wrapper 플러그인의 작동 순서는 작업 요청이 오면 그 작업에 해당하는 Child 플러그인을 로드 하고 Child 플러그인의 작업을 실행한 다음, 소멸자에서 Child DLL을 해제합니다. 이것을 그림으로 표현하면 다음과 같습니다.

 

<맥스에 사용되는 플러그인: 일반적인 플러그인과 Wrapper 플러그인>

 

Wrapper 플러그인을 만들기 위해서 LcMax 플러그인의 이름을 LcWrapper로 바꿉니다. 다음으로 테스트를 위해 "plugins" 폴더에 있던 "asdexp.dle" C 드라이브 루트로 옮겨 놓았습니다. 이 플러그인을 LcWrapper 플러그인이 래핑(Wrapping)할 것입니다.

 

LcWrapper 클래스는 다음과 같이 3 개의 멤버 변수를 추가합니다.

 

class LcWrapper : public SceneExport

        HINSTANCE      m_hInst;       // DLL Instance

        ClassDesc*     m_pChildDesc;  // DLL 에서 얻은 Child 객체 주소

        SceneExport*   m_pChild;      // Export에 요청되는 작업을 담당할 객체

 

m_pChildDesc 변수는 DLL에서 얻은 ClassDesc2 클래스를 상속받은 객체의 주소입니다. 이 객체의  Create()함수를 이용해서 m_pChild 객체를 생성합니다.

 

이것을 구체적으로 구현하면 LcWrapper 클래스의 생성자는 래핑 대상 플러그인(DLL)을 로드하기 위해 LoadLibray()함수를 사용합니다. 다음으로 GetProcAddress() 함수를 이용해서 ClassDesc 객체의 주소를 얻어옵니다. 마지막으로 ClassDesc 클래스의 Create()함수를 호출해서 SceneExport 객체를 생성합니다.

 

LcWrapper::LcWrapper()

{

m_hInst        = NULL;

        m_pChildDesc   = NULL;

        m_pChild       = NULL;

 

        // Load Plugin(DLL)

        m_hInst = LoadLibrary( "c:/asdexp.dle");

 

        if (!m_hInst)

        {

               MessageBox(NULL, "Load c:/asdexp.dle Failed", "Err", MB_ICONWARNING);

               m_hInst = NULL;

               return;

        }

 

        // DLL에서 제공하는 함수 중 Export를 생성할 수 있는 객체 주소 반환 함수 얻기

        typedef ClassDesc* (__stdcall *_LibClassDesc)(int i);

 

        _LibClassDesc pLibClassDesc

 = (_LibClassDesc) GetProcAddress(m_hInst, "LibClassDesc");

 

        if (pLibClassDesc)

        {

               m_pChildDesc = pLibClassDesc(0);

 

               // Export를 담당할 객체 생성

               m_pChild = (SceneExport*)m_pChildDesc->Create();

        }

}

 

DLL .def 파일에 있는 EXPORTS 섹션에 "LibClassDesc @3"로 정의되어 있어 함수의 이름대신 숫자로 대신해도 됩니다. 숫자를 사용하면 처리가 좀 더 빨라진다고 합니다.

 

GetProcAddress(m_hInst, (LPCSTR)3);

 

소멸자는 생성자에서 만들어진 SceneExport 객체 m_pChild를 먼저 해제 하고 플러그인을 해제합니다.

 

LcWrapper::~LcWrapper()

{

        if(m_pChild)

        {

               delete m_pChild;

               m_pChild = NULL;

        }

 

        // Plugin 해제

        if(m_hInst)

        {

               FreeLibrary(m_hInst);

               m_hInst = NULL;

        }

}

 

가장 중요한 함수 DoExport() 함수를 다음과 같이 수정합니다.

 

int     LcWrapper::DoExport(const TCHAR *name,ExpInterface *ei

,Interface *i, BOOL suppressPrompts, DWORD options)

{

        if(m_pChild)

               return m_pChild->DoExport(name, ei, i, suppressPrompts, options);

        return FALSE;

}

 

이런 방식으로 나머지 함수들도 전부 바꾸어 줍니다.

 

const TCHAR *LcWrapper::Ext(int n)

{

        if(m_pChild)

               return m_pChild->Ext(n);

        return _T("Failed Ext");

}

 

<Wrapper Plugin: mxp01_wrapper.zip>

 

전체 코드는 mxp01_wrapper.zip을 참고 하기 바랍니다. 래핑이 제대로 되고 있는지 확인하기 위해서 다음과 같이 만들어 보았습니다.

 

const TCHAR *LcWrapper::ShortDesc()

static TCHAR s[1024];

        if(m_pChild)   _stprintf(s, "Wrap(%s)", m_pChild->ShortDesc());

        else           _stprintf(s, _T("Failed to load Wrap"));

        return s;

 

 

5.2 Data Export

5.2.1 Node Export

맥스는 모든 객체를 노드(Node)로 관리합니다. 이 노드는 다음 그림과 같이 나무 자료 구조(Tree Data Structure)로 구성되어 있습니다.

 

<나무 자료 구조(Tree Data Structure)>

 

맥스는 INode 라는 인터페이스를 제공하며 INode 인터페이스 의 여러 함수 중에서 NumberOfChildren(), GetChildNode(index), GetParentNode(), GetParentNode() 함수 등으로 하위노드 숫자, 하위 노드, 부모 노드, Root 노드 등을 찾을 수 있습니다.

 

맥스는 Exporter 명령이 주어지면 SceneExport 클래스를 상속받은 클래스 멤버 함수 중에서 DoExport() 함수의 세 번째 인수인 Interface* 객체의 GetRootNode() 함수를 이용해서 Root Node를 얻을 수 있습니다. 간단하게 Root Node와 이름, 하위노드들을 찾는다면 다음과 같이 코딩 하면 됩니다.

 

int LcMax::DoExport(…,Interface *pi,…)

 

INode*  pRoot = m_pI->GetRootNode();

TCHAR*  sName  = pRoot->GetName();

INT     nChild = pRoot->NumberOfChildren();

 

이것을 실행하면 맥스의 Root Node 이름은 "Scene Root"로 출력이 됩니다.

만약 맥스에 작업한 모든 Node Export할 때는 Scene Root부터 하위 노드(Child Node)들을 재귀(Recursive) 순환을 적용해서 찾아야 합니다. 이를 위해서 먼저 다음과 같이 INode 객체 주소를 저장 하기 위해 STL의 벡터 컨테이너를 선언합니다.

 

std::vector<INode*>    m_vMaxNode;            // 맥스의 노드 자료구조

 

그리고 다음과 같은 재귀 함수를 이용해서 이 벡터 컨테이너에 노드의 주소를 저장합니다.

 

void LcMax::GatherNode(INode* pNode)

{

        if(!pNode)

               return;

 

        INT iChild = pNode->NumberOfChildren();

 

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

        {

               INode* pChild = pNode->GetChildNode(i);

 

               if(pChild)

                       GatherNode(pChild);

        }

}

 

이 방식은 깊이 우선 탐색 방식(DFS)입니다. 이렇게 노드를 모으고 이것을 파일로 저장합니다.

 

FILE*   fp = fopen(m_sN, "wt");

 

INode*  pRoot = m_pI->GetRootNode();

 

GatherNode(pRoot);

 

for(n =0; n<m_vMaxNode.size(); ++n)

{

        INode*  pNode = m_vMaxNode[n];

        TCHAR*  sName  = pNode->GetName();

        INT     nChild = pNode->NumberOfChildren();

 

if(pRoot == pNode->GetParentNode())

                       fprintf(fp, "\n");

 

        fprintf(fp, "Node Name: %s, Child Number: %d\n", sName, nChild);

}

 

fclose(fp);

 

프로그램을 완전하기 위해 리소스 뷰에서 다이얼로그에 다음과 같은 버튼을 만듭니다.

 

 

또한 DoExport버튼을 누르면 Export가 실행되도록 다음과 같이 콜백 함수를 수정합니다.

 

INT_PTR CALLBACK LcMaxOptionsDlgProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)

        WPARAM wLoParam = LOWORD(wParam);

        WPARAM wHiParam = HIWORD(wParam);

 

        if( WM_INITDIALOG == uMsg)

        else if( WM_COMMAND == uMsg)

        {

               if(imp && IDC_EXPORT == wLoParam)

               {

                       imp->m_bDoExport = TRUE;

                       SendMessage(hWnd, WM_CLOSE, 0, 0);

               }

 

               return TRUE;

        }

 

다이얼로그에 버튼 추가와 메시지 처리에 대한 자세한 내용은 WinAPI를 참고 하기 바랍니다.

이렇게 만든 코드를 테스트하기 위해서 다음과 같이 맥스를 실행하고 메뉴 à Create à System à Biped를 선택해서 Biped 인형 2개를 화면에 그립니다.

 

<Biped 추가>

 

또는 우측의 Create Tab에서 톱니바퀴 두 개가 물려 있는 버튼을 누르면 Systems 버튼이 나오고 이중에서 Biped 버튼을 선택한 다음 Biped를 추가합니다.

Biped는 맥스가 애니메이션을 편리하게 작성할 수 있도록 제공되는 유틸리티이며 나무 구조(Tree Structure)로 구성되어 있습니다. Biped의 노드를 파일로 저장할 수 있으면 나머지 맥스에서 구성된 오브젝트의 노드들도 같은 방법으로 파일에 저장할 수 있습니다.

이렇게 Biped 2개를 만들고 Export한 다음 파일을 열어보면 "Scene Root" 부터 "Bip01", " Bip02"로 시작하는 노드들이 모두 기록되어 있음을 볼 수 있습니다.

 

참고로 맥스의 Schematic View를 선택하면 현재 맥스에 있는 객체가 나타납니다.

 

<Schematic View>

 

전체 코드는 mxp11_nodes.zip를 참고 하기 바랍니다.

 

 

5.2.2 Object Export

Root 노드를 제외한 모든 노드는 Object를 가지고 있습니다. Object Node의 역할이라 생각하면 됩니다. 맥스의 Object는 시스템에서 파생된 Derived ObjectProcedural Object 두 종류가 있습니다. Derived Object는 시스템의 한 부분으로서 변경이 불가능하며 Procedural Object는 카메라, 조명, Helper, Geometry 등 수정이 가능한 Object입니다.

 

<맥스의 여러 종류 Object>

 

노드의 역할을 알아보기 위해 다음과 같이 Object Type을 확인합니다.

 

Object* pObject = pNode->GetObjectRef();

 

if(pObject)

{

        SClass_ID lSuperID = pObject->SuperClassID();

        Class_ID lClassID = pObject->ClassID();

 

        if(GEOMOBJECT_CLASS_ID == lSuperID)

        {

               if( BONE_OBJ_CLASSID == lClassID || Class_ID(BONE_CLASS_ID, 0) == lClassID)

               {

               }

               else

               {

               }

        }

        else if(CAMERA_CLASS_ID == lSuperID)

        else if(LIGHT_CLASS_ID == lSuperID)

}

 

카메라, 조명, Helper, Shape 등은 게임 내부에서 정한 데이터를 사용합니다. 따라서 이들은 특별한 경우가 아니라면 보통은 제외대상이며 우리의 주 관심은 Geometry Object 입니다.

Geometry Object ClassID() 함수로 한 번 조사해야 할 내용이 있습니다. 애니메이션을 좀 더 편리하게 하기 위해서 Bone, 또는 Bipe를 이용합니다. 이들도 폴리곤이 있으며 Geometry class 입니다. Export된 데이터를 확인하는 Viewer 프로그램에서 애니메이션을 눈으로 살피기 위해 Bone을 화면에 표시하므로 적정한 정점 구조체와 포맷으로 이들도 추출해야 합니다. Bone 처리에 대해서는 이후 애니메이션에서 다시 하겠습니다.

 

전체 코드는 mxp12_object.zipLcMax::DoExport() 함수를 참고하기 바랍니다.

 

 

5.2.3 Mesh

맥스는 Mesh를 통해서 화면에 오브젝트를 표현합니다. 메쉬는 정점 데이터, 인덱스 데이터 등을 포함하고 있어서 우리는 이것을 Export해서 게임에서 정점 버퍼와 인덱스 버퍼로 사용해야 합니다.

 

우리는 게임에서 주로 삼각형을 기본으로 렌더링 오브젝트를 구성합니다. 그런데 맥스의 Primitive D3D 보다 다양해서 맥스의 메쉬를 D3D에서 사용할 수 있도록 바꾸어야 합니다.

먼저 맥스의 Object에게 삼각형으로 메쉬의 구조를 바꿀 수 있는지 CanConvertToType() 함수로 확인하고, 전환이 가능하면 ConvertToType() 함수로 폴리곤을 구성하는 TriObject를 다음과 같이 얻어 냅니다.

 

if(!pObj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID, 0)))

        continue;

 

TriObject* pTri = (TriObject *)pObj->ConvertToType(0, Class_ID(TRIOBJ_CLASS_ID, 0));

 

다음으로 TriObject에서 Mesh를 다음과 같이 얻습니다.

 

Mesh* pMesh    = &pTri->GetMesh();

 

Mesh에 폴리곤을 구성하는 각각의 삼각형에 대한 인덱스 데이터와 정점 데이터가 포함되어 다음과 같이 인덱스, 정점의 숫자를 얻고, 이 숫자만큼 회전하면서 인덱스, 정점 데이터를 얻어 냅니다.

 

INT     iNvtx   = pMesh->getNumVerts();

INT     iNfce   = pMesh->getNumFaces();

 

// 인덱스 데이터 출력

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

{

        INT a, b, c;

        a = pMesh->faces[i].v[0];

        b = pMesh->faces[i].v[1];

        c = pMesh->faces[i].v[2];

 

        fprintf(fp, "  %4d %4d %4d\n", a, b, c);

}

 

// 정점 데이터 출력

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

{

        Point3 v = pMesh->verts[i];

        fprintf(fp, "  %12.5f %12.5f %12.5f\n", v.x, v.y, v.z);

}

 

전체 코드는 mxp13_mesh01.zip을 참고하기 바랍니다.

 

 

5.2.4 Viewer

우리가 Exporter를 만드는 것은 게임에서 필요한 데이터를 만들기 위함입니다. 따라서 지금부터 추출한 데이터가 원하는 형태로 구성되어 있는지 뷰어(Viewer) 프로그램으로 확인 해야 하고 이를 위해서 자료구조가 필요합니다.

이전에 작성한 Mesh를 수정해서 View에 올릴 수 있는 형태로 자료구조를 구성해 봅시다.

먼저 정점의 인덱스와 위치를 저장할 다음과 같은 구조체를 선언합니다.

 

struct VtxIdx

{

        WORD a, b, c;

};

 

struct VtxPos

{

        FLOAT x, y, z;

};

 

struct LcGeo

{

        INT            nFce;   // Number of Face

        INT            nPos;   // Number of Position

        VtxIdx*        pFce;   // Face List

        VtxPos*         pPos;   // Position List

};

 

LcMax 클래스에 다음과 같은 멤버 변수를 추가합니다.

 

class LcMax : public SceneExport

INT     m_nGeo;        // Number of Geometry

LcGeo*  m_pGeo;        // Geometry Data

 

이전처럼 파일에 그냥 출력을 하면 뷰어에서 데이터를 읽어오는데 일관성이 떨어지므로 Node를 모으고 나서 Geometry Data를 만들고 데이터를 채운 후에 파일로 출력을 해야 파일 구조가 완성 됩니다. 또한 Geometry에 대한 자료 구조가 완성되어 있으면 뷰어에서 텍스트 파일을 읽는 것보다 이진(Binary) 파일을 읽는 것이 편리해서 게임 또는 뷰어에서는 이진 파일을 사용하고 눈으로 확인 하기 위한 용도로는 텍스트 파일을 사용합니다. 이를 위해 마지막에 이 두 종류의 파일에 데이터를 저장합니다.

 

int     LcMax::DoExport()

// 1. Gather Node

INode*  pRoot = m_pI->GetRootNode();

GatherNode(pRoot);

 

// 2. Create and Setup Geometry

if(FAILED(SetupGeometry()))

        return FALSE;

 

// 3. Geometry를 이용해 Binary, Text 파일로 출력

WriteBinary();

WriteText();

 

void LcMax::WriteBinary()

        fwrite(&m_nGeo, 1, sizeof(INT), fp);

 

        for(n =0; n<m_nGeo; ++n)

        {

               LcGeo* pGeo = &m_pGeo[n];

               fwrite(pGeo->sName,  1, sizeof(char)*32, fp); // Node Name

               fwrite(&pGeo->nType, 1, sizeof(INT )  , fp);  // Node Type

               fwrite(&pGeo->nFce,  1, sizeof(INT )  , fp);  // Index Number

               fwrite(&pGeo->nPos,  1, sizeof(INT )  , fp);  // Vertex Number

               fwrite(pGeo->pFce, pGeo->nFce, sizeof(VtxIdx), fp);

               fwrite(pGeo->pPos, pGeo->nPos, sizeof(VtxPos), fp);

        }

 

전체 코드는 mxp13_mesh02.zip를 참고 하기 바랍니다.

 

다음으로 뷰어를 만들어야 합니다. 뷰어에서 이진 파일을 읽는 방법은 void LcMax::WriteBinary()

함수의 fwrite() 함수 대신 fread() 함수로 바꾸고 new 연산자로 동적 할당을 하면 됩니다.

mxp13_viewer1.zipmxp13_mesh02.zip에서 추출한 데이터를 확인하는 뷰어이며 CLcmAcm 클래스는 이진 파일을 읽어서 화면에 출력하는 클래스 입니다. 이 클래스의 LoadMdl() 함수는 모델을 읽어 오는 함수로 LcMax::WriteBinary()와 대응하는 함수입니다. 파일에서 데이터를 읽어올 때 Geometry와 인덱스, 버텍스 데이터는 new 연산자로 데이터를 동적 할당하고 있음을 주의해야 합니다.

 

INT CLcmAcm::LoadMdl(char* sFile)

        fread(&m_nGeo, 1, sizeof(INT), fp);

        m_pGeo = new LcGeo[m_nGeo];

 

        for(n =0; n<m_nGeo; ++n)

        {

               LcGeo* pGeo = &m_pGeo[n];

               fread(&pGeo->nFce, 1, sizeof(INT) , fp);

               fread(&pGeo->nPos, 1, sizeof(INT) , fp);

               pGeo->pFce = new VtxIdx[pGeo->nFce];

               pGeo->pPos = new VtxPos[pGeo->nPos];

 

               fread(pGeo->pFce, pGeo->nFce, sizeof(VtxIdx), fp);

               fread(pGeo->pPos, pGeo->nPos, sizeof(VtxPos), fp);

        }

 

렌더링은 User Memory Pointer DrawIndexedPrimitiveUP() 함수를 이용하고 있습니다.

 

void CLcmAcm::Render()

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

        {

               LcGeo*  pGeo    = &m_pGeo[i];

               m_pDev->SetFVF(VtxPos::FVF);

               m_pDev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, pGeo->nPos

                              , pGeo->nFce, pGeo->pFce, (D3DFORMAT)VtxIdx::FVF

                              , pGeo->pPos, sizeof(VtxPos)

                       );

        }

 

mxp13_viewer1.zip를 실행하고 "_Exec/model/object.acm" 을 불러오면 다음의 오른쪽 그림과 같이 출력됩니다.

 

 

<맥스 데이터: max_object1.zip. Viewer에서 확인한 데이터: mxp13_viewer1.zip>

 

그림을 비교해 보면 맥스 화면과 Viewer의 화면이 차이가 있습니다. 이것은 ASE Parsing에서도 이야기 했듯이 다음 그림과 같이 맥스는 오른손 좌표계를 사용하고 D3D는 왼손 좌표계를 사용하기 때문입니다.

 

<맥스와 D3D 좌표계>

 

ASE에서는 파일을 읽어올 때 위치에 대해서는 y z를 교환하고 인덱스는 b c를 교환한다고 했습니다. 이것을 파일을 읽어 올 때 처리하지 않고 플러그인 내부에서 인덱스와 정점 데이터를 저장할 때 다음과 같이 처리합니다.

 

INT LcMax::SetupGeometry()

// 인덱스 데이터 저장

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

{

        pGeo->pFce[i].a = pMesh->faces[i].v[0];

        pGeo->pFce[i].b = pMesh->faces[i].v[2];      // b <--> c 교환

        pGeo->pFce[i].c = pMesh->faces[i].v[1];

}

 

// 정점 데이터 저장

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

{

        Point3 v = pMesh->verts[i];

        pGeo->pPos[i].x = v.x;

        pGeo->pPos[i].y = v.z; //y <--> z 교환

        pGeo->pPos[i].z = v.y;

}

 

두 번째로 뷰어에서 확인한 모델은 전부 (0, 0, 0)을 중심으로 그리고 있음을 볼 수 있습니다. 이것은 각각의 Geometry가 지역좌표계로 그리기 때문입니다. 노드에 적용된 월드 행렬을 얻기 위해서 INode 인터페이스에서 GetObjTMAfterWSM() 함수를 이용합니다. 이 함수는 WSM(World Space Modifier)가 적용된 후의 Transform Matrix를 반환하는 함수입니다. 특히, 애니메이션이 있는 경우에 이 함수를 사용해서 월드 행렬을 얻어 옵니다. 노드에 적용되는 월드 행렬은 다음 공식처럼 만들 수 있습니다.

 

노드의 월드 행렬 = 노드의 지역 행렬 (Local Matrix) * 부모 노드의 월드 행렬

 

애니메이션이 없는 데이터라면 월드 행렬만 가지고 있거나 아니면 정점의 위치에 월드 행렬을 곱한 값을 적용하는 것이 더 나을 수 있습니다. 하지만 애니메이션이 있다면 크기 변환, 회전 변환, 이동 변환에 대한 행렬을 가지고 지역 행렬을 만들어야 하기 때문에 Node에 지역행렬을 저장하고 렌더링 할 때마다 월드 행렬을 계산하도록 구성하는 것이 애니메이션을 적용할 때 편리합니다. 노드의 지역 행렬은 이전의 식 양변에 부모 노드의 월드 행렬의 역행렬을 곱하면 다음과 같이 구해집니다.

 

노드의 지역 행렬 = 노드의 월드 행렬 * 부모 노드의 월드 행렬의 역행렬

 

부모 노드에 대한 인덱스를 포함 시켜 데이터가 나무 구조(Tree Struct)의 계층적(Hierarchy)으로 구성 되도록 다음과 같이 LcGeo 구조체에 부모 노드의 인덱스를 추가하고 또한 자신의 지역 행렬도 추가합니다.

 

struct LcGeo

        INT            nPrn;          // Parent Index

        D3DXMATRIX     mtLcl;         // Local Matrix

 

        LcGeo()

               nPrn    = -1;                  // 부모가 없음

               D3DXMatrixIdentity(&mtLcl);   // 지역행렬을 단위 행렬로 만든다.

        }

 

LcMax::SetupGeometry() 함수를 수정해서 다음과 같이 부모 노드에 대한 인덱스를 설정합니다.

 

// Setup Parent Index

for(n =0; n<m_nGeo; ++n)

{

        LcGeo*  pGeo = &m_pGeo[n];

        INode*  pNode = m_vMaxNode[n];

        INode*  pPrn = pNode->GetParentNode();

 

        // 부모의 노드를 찾아 인덱스를 설정

        if(pPrn)

        {

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

               {

                       INode*  pCur = m_vMaxNode[i];

                       if(pCur == pPrn && i != n )

                       {

                              pGeo->nPrn = i;

                              break;

                       }

               }

        }

}

 

또한 지역행렬도 다음과 같이 계산해서 저장합니다.

 

// Setup Local Matrix

for(n =0; n<m_nGeo; ++n)

{

        LcGeo*  pGeo = &m_pGeo[n];

        INode*  pNode = m_vMaxNode[n];

        INode*  pPrnt = pNode->GetParentNode();

 

        Matrix3 tmLocal;

        Matrix3 tmWorld = pNode->GetObjTMAfterWSM(0);

 

        if(!pPrnt)

               tmLocal = tmWorld;

        else

        {

               Matrix3 tmParent= pPrnt->GetObjTMAfterWSM(0);

               tmLocal = tmWorld * Inverse(tmParent);

        }

 

        MaxMatrixToD3D(&pGeo->mtLcl, &tmLocal);

}

 

Inverse() 함수는 맥스 SDK에서 제공하는 역행렬을 구하는 함수입니다. MaxMatrixToD3D()는 맥스의 행렬을 D3D에 맞게 자리 교환을 하는 함수입니다. 이것은 ASE Parsing에서도 했듯이 맥스의 행렬의 2,3 행을 교환하고 2, 3열 교환하면 D3D에서 사용하는 행렬로 만들어 집니다.

 

void LcMax::MaxMatrixToD3D(D3DXMATRIX* pDst, Matrix3* pSrc, BOOL bIdentity)

{

        Point3  v3;

        v3 = pSrc->GetRow(0); pDst->_11 = v3.x;      pDst->_12 = v3.z; pDst->_13 = v3.y;

        v3 = pSrc->GetRow(2); pDst->_21 = v3.x;      pDst->_22 = v3.z; pDst->_23 = v3.y;

        v3 = pSrc->GetRow(1); pDst->_31 = v3.x;      pDst->_32 = v3.z; pDst->_33 = v3.y;

        v3 = pSrc->GetRow(3); pDst->_41 = v3.x;      pDst->_42 = v3.z; pDst->_43 = v3.y;

}

 

부모 노드의 인덱스와 지역행렬을 저장할 수 있도록 LcMax:: WriteBinary() 함수와 LcMax:: WriteText() 함수를 수정합니다.

 

void LcMax::WriteBinary()

        for(n =0; n<m_nGeo; ++n)

        {

               LcGeo* pGeo = &m_pGeo[n];

               fwrite(&pGeo->nPrn,  1, sizeof(INT ), fp);   // Parent Index

               fwrite(&pGeo->mtLcl, 1, sizeof(D3DXMATRIX),fp);      // Local Matrix

        }

 

전체 코드는 mxp14_transform.zip를 참고 하기 바랍니다.

 

플러그인에서는 지역 행렬만 가지고 있지만 Viewer에서는 월드 행렬도 가지고 있어야 하위 노드에서 계산이 됩니다. 따라서 부모 인덱스, 지역 행렬과 월드 행렬이 있어야 합니다. 또한 매 번 부모 노드를 찾게 되면 불편하므로 파일에서 읽어올 때 부모 노드를 지정할 수 있도록 부모의 포인터를 추가합니다.

 

struct LcGeo

        INT                    nPrn;          // Parent Index

LcGeo*                 pPrn;          // Parent Node

        D3DXMATRIX             mtLcl;         // Local Matrix

        D3DXMATRIX             mtWld;         // World Matrix

 

파일을 읽는 함수도 수정합니다.

 

INT CLcmAcm::LoadMdl(char* sFile)

        for(n =0; n<m_nGeo; ++n)

        {

               LcGeo* pGeo = &m_pGeo[n];

               fread(&pGeo->nPrn,  1, sizeof(INT ), fp);    // Parent Index

               fread(&pGeo->mtLcl, 1, sizeof(D3DXMATRIX),fp);       // Local Matrix

 

               // 부모 노드 지정

               if(-1 != pGeo->nPrn)

                       pGeo->pPrn = &m_pGeo[pGeo->nPrn];

 

애니메이션이 있다는 전제하에 매 프레임 마다 행렬을 갱신할 수 있도록 다음과 같이 CLcmAcm::FrameMove() 함수를 수정합니다. 각각의 Geometry의 월드 행렬은 다음과 같은 공식을 이용해서 구현합니다.

 

노드의 월드 행렬 = 노드의 지역 행렬 (Local Matrix) * 부모 노드의 월드 행렬

 

만약 부모 노드가 없으면 자신의 지역 행렬을 월드 행렬로 설정합니다.

 

INT CLcmAcm::FrameMove()

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

        {

               LcGeo*  pCur = &m_pGeo[i];

               LcGeo*  pPrn = pCur->pPrn;

               if(pPrn)

                       pCur->mtWld = pCur->mtLcl * pPrn->mtWld;

               else

                       pCur->mtWld = pCur->mtLcl;

        }

 

행렬이 포함 되어 있으므로 각각의 Geometry에 대해서 월드 행렬을 설정해야 합니다.

 

void CLcmAcm::Render()

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

        {

               LcGeo*  pGeo    = &m_pGeo[i];

               m_pDev->SetTransform(D3DTS_WORLD, &pGeo->mtWld);

               m_pDev->DrawIndexedPrimitiveUP(…);

        m_pDev->SetTransform(D3DTS_WORLD, &mtI); // 월드 행렬 복귀

 

전체 코드는 mxp14_viewer2.zip를 실행하면 다음과 같은 화면을 볼 수 있습니다.

 

 

<월드 행렬이 적용된 Export 데이터: max_object2_rigid.zip, mxp14_viewer2.zip>

 

 

5.3 Animation

정점의 위치에 대한 변화를 시간의 순서대로 만든 것이 애니메이션입니다. 애니메이션은 정점의 위치 자체를 변환하는 방법이 있을 수 있고, 정점에 영향을 주는 월드 행렬만 시간에 따라 변환시키고 난 다음에 이것을 파이프라인에 적용하는 방법이 있을 수 있습니다. 좀 더 나은 방법은 미리 계층 구조를 만들어 놓고 이 계층 구조의 행렬을 변환한 다음에 정점에 적용하는 방법이 있습니다.

3D 맥스는 오브젝트에 애니메이션을 편리하게 적용할 수 있게 Bone Biped를 지원하며 대부분의 그래픽 작업은 이 둘을 이용해서 애니메이션을 구현합니다.

Bone은 그 언어 자체로 뼈대이며 여러 뼈대를 계층 구조(Hierarchy) 결합하고 사용자는 결합된 Bone을 회전, 이동 등을 적용해서 애니메이션을 구현합니다. 맥스는 Bone을 좀 더 편리하게 사용하고자 Bone을 특화한 Biped를 지원합니다.

 

 

<맥스의 Bone Biped>

 

Bone은 사용자가 자유롭게 계층 구조를 만들 수 있어서 숙련된 애니메이터(Animator)는 최소한의 Bone으로 다양한 동작을 만들어 낼 수 있습니다. Biped는 규격화 되어 애니메이션 제작이 편리하고 일관된 작업을 유도해서 공동 작업에 유리합니다. 애니메이션이 많고 복잡한 캐릭터 모델의 경우 Biped를 기본으로 하고 필요하다면 Bone을 추가 시켜 애니메이션을 작업을 합니다. 나무나 풀과 같은 관절이 간단한 경우라면 Bone으로 작업을 많이 합니다.

 

그래픽 작업자는 맥스로 Biped 또는 Bone으로 애니메이션을 만듭니다. 그리고 미리 만들어 놓은 3D 모델 데이터를 이들 Biped/Bone에 연결하는 리깅(Rigging) 작업을 합니다. 이 리깅 작업은 PHYSIQUESKIN 두 종류가 있습니다. 작업의 방법은 차이가 있지만 PHYSIQUE/SKIN 는 정점에 영향을 주는 Bone을 설정하는 일이며 이것은 D3D의 스키닝 애니메이션 구현이라 할 수 있습니다.

 

맥스에서의 작업이 정점에 대해서 하나의 Bone에만 영향 받도록 되어 있다면 강체(Rigid) 애니메이션을 바탕으로 플러그인을 구성해야 하고, 여러 Bone에 영향 받으면 스키닝 애니메이션으로 플러그인을 만들어야 합니다.

애니메이션에 대한 플러그인은 구현하기 쉬운 강체 애니메이션을 먼저 만들고 다음으로 스키닝에 대해서 만들어 보겠습니다.

 

 

5.3.1 Rigid Body Animation

강체 애니메이션(Rigid Body Animation)에 대한 플러그인 제작은 의외로 간단합니다. 플러그인 제작 순서는 먼저 시간 정보를 얻습니다. 다음으로 Bone의 계층 구조와 메쉬를 저장하고 마지막에 시간에 대한 월드 행렬 또는 지역 행렬을 추출하면 됩니다.

 

먼저 시간의 정보를 저장하기 위해 다음과 같은 구조체를 만듭니다.

 

struct LcHeader

{

        INT            nFrmB;         // Begin Frame

        INT            nFrmE;         // End Frame

        INT            nFrmP;         // Frame Rate(FPS)

        INT            nFrmT;         // Tick Frame

        INT            nGeo;          // Number of Geometry

};

 

이전에는 Geometry 개수를 멤버로 가져갔는데 프로그램의 편리성을 위해 LcHeader에 포함시켰습니다.

 

시간 정보는 다음과 같이 구현 합니다. 이 방법은 ase 플러그인 예제와도 비슷합니다.

 

int            iTick = GetTicksPerFrame();

Interval range = m_pI->GetAnimRange();

 

m_Header.nFrmB = range.Start() / iTick;

m_Header.nFrmE = range.End() / iTick;

m_Header.nFrmP = GetFrameRate();

m_Header.nFrmT = iTick;

 

프로그램을 편리하게 작성하기 위해 LcGeo를 다음과 같이 수정합니다.

 

struct LcGeo

{

        char           sName[32];     // Node Name

        INT            nType;         // 1:Geometry, 2: Bone, 0: Etc

        INode*         pNode;         // Node

        INT            nPrn;          // Parent Index

        D3DXMATRIX     mtLcl;         // Local Matrix

        INT            nFce;          // Number of Face

        INT            nPos;          // Number of Position

        VtxIdx*        pFce;          // Face List

        VtxPos*        pPos;          // Position List

        INT            nAni;          // Number of Animation

        D3DXMATRIX*    pAni;          // Animation Matrix

};

 

이 구조체를 이용해서 노드를 모으고 계층 구조의 NodeSTL을 이용해서 배열로 구성합니다. 이 부분은 이전 장에서 설명했으므로 생략하겠습니다.

맥스는 사용자가 바꾸지 않는 한 Default Bone에 대해서 "Bone"이라는 키워드가 Biped "Bip" 키워드가 오브젝트의 이름에 적용되어 다음과 같이 Bone인지 판단하는 함수 안에 구현 합니다.

 

void LcMax::SetupIsBone(LcGeo* pGeo)

        if( 0 == _strnicmp(pNode->GetName(), "Bone", 4) ||

               0 == _strnicmp(pNode->GetName(), "Bip", 3))

               pGeo->nType = LCX_BONE;

 

다음으로 지역 행렬, 메쉬 정보를 추출하고 마지막 단계에서 다음과 같이 애니메이션에 대한 행렬을 추출합니다.

 

typedef D3DXMATRIX     MATA;

void LcMax::SetupAnimation(LcGeo* pGeo)

        nAni = m_Header.nFrmE - m_Header.nFrmB +1;

 

        INode*  pNode = pGeo->pNode;

        INode*  pPrnt = pNode->GetParentNode();

 

        pGeo->nAni     = nAni;

        pGeo->pAni     = new MATA[nAni];

 

        dTime = dTimeB;

        i       = 0;

        for(; dTime<=dTimeE ; dTime += dTick, ++i)

        {

               MATA* pDest = &pGeo->pAni[i];

               Matrix3 tmLocal;

               Matrix3 tmWorld = pNode->GetObjTMAfterWSM(dTime);

 

               if(!pPrnt)

                       tmLocal = tmWorld;

               else

               {

                       Matrix3 tmParent= pPrnt->GetObjTMAfterWSM(dTime);

                       tmLocal = tmWorld * Inverse(tmParent);

               }

 

               MaxMatrixToD3D(pDest, &tmLocal);

        }

 

애니메이션에 대한 지역 행렬은 이전의 transform 예제처럼 다음 공식을 이용합니다.

애니메이션 지역 행렬 = 노드의 애니메이션 월드 행렬 * 부모 애니메이션 월드 행렬의 역행렬

 

GetObjTMAfterWSM() 함수는 주어진 시간에서 Modifier에 의한 월드 행렬을 반환 하므로 이를 이용해서 각 노드의 월드 행렬을 구한 후에 공식에 적용해서 시간에 대한 지역 행렬을 만들어 냅니다.

파일에 저장할 때는 애니메이션 정보는 메쉬 정보를 저장한 다음에 저장합니다.

 

void LcMax::WriteBinary()

        // Write Geometry

        // Write Animation

        for(n =0; n<m_Header.nGeo; ++n)

        {

               LcGeo* pGeo = &m_pGeo[n];

               if(1>pGeo->nAni)

                       continue;

 

               fwrite(pGeo->pAni, pGeo->nAni, sizeof(MATA), fp);    // Animation Matrix

        }

 

전체 코드는 mxp21_ani_rigid1.zip을 참고 하기 바랍니다.

 

이렇게 만든 데이터를 게임에 적용하려면 먼저 애니메이션 행렬의 인덱스를 구해야 합니다. 이 인덱스는 시간을 Frame으로 나누고 다시 전체 프레임으로 Modular 연산자(%)를 이용하면 최대 프레임을 넘지 않고 반복된 애니메이션을 구현할 수 있습니다.

 

m_nAni = INT( dTime/m_Header.nFrmP);

m_nAni %= m_Header.nFrmE;

 

플러그인으로 추출한 애니메이션 행렬은 애니메이션 지역 행렬이므로 다음 공식과 같이 애니메이션에 대한 월드 행렬을 구합니다.

 

노드의 애니메이션 월드 행렬 = 노드의 애니메이션 지역 행렬 * 부모의 애니메이션 월드 행렬

 

이것은 이전의 transform 예제와 거의 같으며 mxp21_viewer_rigid1.zip은 다음과 같이 이 공식을 구현하고 있습니다.

 

INT CLcmAcm::FrameMove()

        m_TimeC = GetTickCount();

        DWORD dTime = m_TimeC - m_TimeB;

 

        m_nAni = INT( dTime/m_Header.nFrmP);

        m_nAni %= m_Header.nFrmE;

 

        for(i=0; i<m_Header.nGeo; ++i)

        {

               LcGeo*  pCur = &m_pGeo[i];

               LcGeo*  pPrn = pCur->pPrn;

               D3DXMatrixIdentity(&pCur->mtWld);

 

               if(pPrn)

               {

                       if(pCur->pAni)

                              pCur->mtWld = pCur->pAni[m_nAni]* pPrn->mtWld;

                       else

                              pCur->mtWld = pCur->mtLcl * pPrn->mtWld;

               }

               else

                       pCur->mtWld = m_mtWld;

        }

 

mxp21_viewer_rigid1.zip를 실행하면 다음과 같은 화면을 얻을 수 있습니다.

 

 

<강체 애니메이션: mxp21_viewer_rigid1.zip>

 

 

5.3.2 Skinning Animation 플러그인

스키닝 애니메이션에 대한 플러그인 제작은 글을 쓰고 있는 저도 많은 좌절을 겪었던 부분입니다. 다행히도 Game Programming Gems 2 1.21 "스킨 익스포터" 부분에서 Physique에서 정점에 대한 Bone의 비중(Weight)과 인덱스를 구하는 방법이 소개되어 있고, 이것을 연구해서 PhysiqueSKIN 두 부분 모두에 대한 플러그인을 작성할 수 있었습니다.

 

만약 PHYSIQUE/SKIN 이 적용된 플러그인을 제작한다면 제일 먼저 정점의 위치를 추출하는 부분을 다음과 같이 수정해야 합니다.

 

Matrix3 tmWSM = pNode->GetObjTMAfterWSM(0); // 또는 tmWSM = pNode->GetObjectTM(0);

for (n=0; n<iNvtx; ++n)

{

        Point3 v = tmWSM * pMesh->verts[n];   // 변환된 정점을 사용

        pGeo->pPos[n].x = v.x;

        pGeo->pPos[n].y = v.z;

        pGeo->pPos[n].z = v.y;

}

 

이전의 강체 애니메이션에서는 변환하지 않은 정점을 사용했으나 스키닝이 적용되는 정점은 시작 시간 (Zero Time)의 행렬을 적용해서 사용합니다. 이것은 리깅 작업에 의해 변환된 모델링 데이터의 정점을 사용하는 것을 의미합니다.

PHYSIQUE/SKIN 에서 Bone의 인덱스와 비중을 구하는 방법을 간단히 설명하면 첫 번째로 PHYSIQUE/SKIN에 대한 수정자(Modifier) 찾고, 그 다음으로 수정자에서 PHYSIQUE/SKIN 객체를 얻는 것입니다. 세 번째로 PHYSIQUE/SKIN 객체에서 문맥(Context)를 얻고 이 문맥에서 영향 받는 정점의 수를 구하고 이 수만큼 Loop를 돌면서 이 문맥에 연결된 정점을 얻고 이 정점에 연결된 노드와 비중을 구합니다.

 

수정자의 역할은 객체의 변환이며 여러 수정자들이 객체에 적용이 되어 장면에 대해서 객체의 최종적인 결과물(외형)을 만들어냅니다. 이러한 이유로 수정자들에 의한 객체의 변환 과정을 객체의 파이프라인이라 부르기도 합니다. 각각의 노드는 수정자들에 의해 변환된 최종 객체(Derived Object)가 연결되어 있고 이 최종 객체는 맥스의 수정자 스택 시스템이 적용된 결과 입니다.

우리는 이 수정자 스택을 탐색하면서 다음과 같이 수정자(Modifier)를 찾으면 됩니다.

 

void* LcMax::FindModifier(INode *pNode, Class_ID nType)

{

        // 노드에 연결된 오브젝트를 구한다.

        Object* ObjectPtr = pNode->GetObjectRef();

 

        if(!ObjectPtr)

               return NULL;

 

        // Derived 오브젝트라면 이 오브젝트에서 수정자를 찾는다.

        while(ObjectPtr && ObjectPtr->SuperClassID() == GEN_DERIVOB_CLASS_ID)

        {

               IDerivedObject *pDerivedObj = (IDerivedObject *)ObjectPtr;

 

               // 수정자 스택을 전부 탐색한다.

               int ModStackIndex = 0;

               while(ModStackIndex < pDerivedObj->NumModifiers())

               {

                       // 스택 인덱스에 대한 수정자를 얻는다.

                       Modifier* ModifierPtr = pDerivedObj->GetModifier(ModStackIndex);

 

                       //이 수정자의 아이디가 PHYSIQUE 또는 SKIN인지 비교한다.

                       if(nType == ModifierPtr->ClassID())

                              return ModifierPtr;

 

                       // 수정자가 PHYSIQUE 또는 SKIN이 아니면 인덱스를 올린다.

                       ModStackIndex++;

               }

 

               // Derived 오브젝트에 연결된 다른 오브젝트를 얻는다.

               ObjectPtr = pDerivedObj->GetObjRef();

        }

 

        // 발견 못함.

        return NULL;

}

 

Derived 객체를 통해서 구한 수정자(Modifier) 안의 문맥(Context)를 구하면 Bone의 영향도를 구할 수 있습니다. 문맥을 구하는 방법은 수정자에서 PHYSIQUE/SKIN 객체를 얻고 이 객체에서 문맥을 다음과 같이 얻습니다.

 

IPhysiqueExport(또는 ISkin)*                  pExport = NULL;

IPhyContextExport(또는 ISkinContextData)*    pContext = NULL;

 

pExport = pMod->GetInterface(I_PHYINTERFACE/I_SKIN); // PHYSIQUE/SKIN 객체

pContext = pExport->GetContextInterface(pNode);              // Context

 

문맥을 얻고 나서 PHYSIQUE에만 문맥에 대해서 Rigid변경과 블렌딩 활성화를 지시합니다.

 

pContext->ConvertToRigid(TRUE);              // Context에 대해서 Rigid로 변경

pContext->AllowBlending(TRUE);        // 정점 Blending 활성화

 

다음으로 Bone에 영향 받는 정점의 수를 구합니다.

 

int nVtx = pContext->GetNumberVertices();    // Bone에 영향 받는 정점의 개수

 

이 정점의 수는 실제로 Geometry의 정점 개수와 일치합니다. 이 개수만큼 다음과 같이 for 문 안에서 문맥에서 Bone의 인덱스와 비중을 구합니다. 주의할 것은 RIGID_TYPE 이면 비중을 1로 하며 너무 작은 값이면 무시를 합니다.

 

// PHYSIQUE

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

{

        IPhyVertexExport* pPhyVtxExpt = pContext->GetVertexInterface(j);

        IPhyBlendedRigidVertex* pPhyBlend = pPhyVtxExpt;

        if(RIGID_TYPE == pPhyVtxExpt->GetVertexType())

        {

               INodepBone   = ((IPhyRigidVertex*)pPhyVtxExpt)->GetNode();

               INT     nBone   = FindBoneId(pBone); //본 인덱스

               FLOAT   fWgt    = 1.f;         //RIGID_TYPE Weight=1

               pGeo->pBlnd[j].vB.insert(std::pair<INT, FLOAT>(nBone, fWgt));

               continue;

        }

 

        int numBones = pPhyBlend->GetNumberNodes();

        for(int k = 0; k< numBones; ++k)

        {

               // k번째의 본을 찾는다.

               INodepBone   = pPhyBlend->GetNode(k);

               INT     nBone   = FindBoneId(pBone);

               FLOAT   fWgt    = pPhyBlend->GetWeight(k);

               // 값이 작으면 무시

               if(fWgt<0.00005f)

                       continue;

               pGeo->pBlnd[j].vB.insert(std::pair<INT, FLOAT>(nBone, fWgt));

        }

}

// SKIN

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

{

        int numBones = pContext->GetNumAssignedBones(j);

        for(int k=0; k<numBones; ++k)

        {

               int assignedBone = pContext->GetAssignedBone(j, k);

               if(assignedBone < 0)

                       continue;

 

               INodepBone   = pExport->GetBone(assignedBone);

               INT     nBone   = FindBoneId(pBone);

               FLOAT   fWgt    = pContext->GetBoneWeight(j, k);

               // 값이 작으면 무시

               if(fWgt<0.00005f)

                       continue;

               pGeo->pBlnd[j].vB.insert(std::pair<INT, FLOAT>(nBone, fWgt));

        }

}

 

PHYSIQUE/SKIN 에 대한 애니메이션 행렬을 구하는 방법은 강체 애니메이션과 차이가 있습니다. D3D를 기준으로 애니메이션 행렬은 다음과 같이 계산 됩니다.

 

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

 

이것을 구현하려면 먼저 노드에 대한 Pivot 행렬의 역행렬을 구합니다.

 

MATA    mtPivot;

Matrix3 tmPivot = pNode->GetNodeTM(0);

tmPivot.Invert();

MaxMatrixToD3D(&mtPivot, &tmPivot);

 

시간에 대해서 노드의 GetObjTMAfterWSM() 함수로 시간에 대한 노드의 월드 행렬을 구하고 앞의 공식을 다음과 같이 적용합니다.

 

for(n=0, dTime = dTimeB; dTime<=dTimeE ; dTime += dTick, ++n)

{

        MATA* pDest = &pGeo->pAni[n];

 

        Matrix3 tmWorld = pNode->GetObjTMAfterWSM(dTime);

        tmWorld.NoScale();

 

        MATA    mtAni;

        MaxMatrixToD3D(&mtAni, &tmWorld);

        mtAni  = mtPivot * mtAni;

        *pDest = mtAni;

}

 

이 때 크기 변환이 적용되지 않도록 "tmWorld.NoScale();" 구문을 꼭 넣는 것이 가장 중요합니다. 또한 애니메이션은 계층 구조를 적용하지 않고 곧 바로 월드 변환을 이용하고 있는 것 또한 주의해야 합니다.

계층 구조가 아닌 월드 변환을 이용하게 되면 플러그인에서 만든 데이터를 게임에서 애니메이션을 구현할 때 다음과 같이 적용해야 합니다.

 

DWORD   m_TimeB;       // Start Time

DWORD   m_TimeC;       // Current Time

INT     m_nAni;        // Animation Index

INT CLcmAcm::FrameMove()

m_TimeC = GetTickCount();

 

DWORD dTime = m_TimeC - m_TimeB;

 

m_nAni = INT( dTime/m_Header.nFrmP);

m_nAni %= m_Header.nFrmE;

 

// World 행렬 업데이트

for(n=0; n<m_Header.nGeo; ++n)

{

        LcGeo*  pGeo    = &m_pGeo[n];

 

        MATA    mtAni;

        D3DXMatrixIdentity(&mtAni);

 

        if(pGeo->pAni)

               mtAni = pGeo->pAni[m_nAni];

 

// World 행렬 설정

        …

}

 

이것으로 PHYSIQUE/SKIN에 대한 정점의 위치, Bone 인덱스와 비중, 애니메이션에 대한 플러그인에서 중용한 부분을 살펴보았습니다. 전체 코드는 mxp23_skinning.zip를 참고하기 바랍니다. 다음은 이 플러그인으로 추출한 데이터를 D3D 에서 스키닝 애니메이션으로 구현해볼 차례입니다.

 

 

5.3.3 Skinning Animation

스키닝 애니메이션 구현은 이미 고정 기능 파이프라인에서 DirectX X-File을 가지고 해 본적이 있습니다. 하나의 정점 에 영향을 주는 행렬을 , 비중을 라 하면 변환 후의 정점 위치 는 다음과 같이 계산이 되고,

 

 

괄호 안의 점 은 Σ밖으로 빼내올 수 있고, 수식을 정리하면

 

 

 

이 되며  를 각각 수행하고 이들을 더한 최종 행렬을 으로 계산한다고 했습니다. 이 내용을 CPU에 의한 소프트웨어(Software) 방법, GPU에 의한 하드웨어 지원 방법, 그리고 쉐이더를 이용한 방법, 3가지로 나누어서 적용해 봅시다.

 

먼저 소프트웨어적인 방법입니다. 우리의 목표는 을 구하는 것입니다. 다음의 코드에서 은 이중 for문 안의 mtW 변수 입니다.  (mtW)는 누적되어야 하므로 변수를 선언하고 나서 memset() 함수로 전부 0으로 초기화 합니다.

다음으로  를 적용하는 것인데 이것에 해당하는 부분은 코드의 "mtT * fW" 입니다.

이렇게 을 구했으면 마지막으로  를 적용해야 합니다. 이것은 원본 정점은 그대로 두고 원본을 복사한 정점의 위치를 변경하는 D3DXVec3TransformCoord(…); 부분입니다.

 

INT CLcmAcm::FrameMove()

LcGeo*  pGeo    = &m_pGeo[n];

for(j=0; j<pGeo->nBlnd; ++j)

{

        LcmBone*       pBlnd = &pGeo->pBlnd[j];

        INT            iBone = pBlnd->vB.size();

 

        MATA    mtW;

        memset(&mtW, 0, sizeof(mtW));

 

        for(k=0; k<iBone; ++k)

        {

               FLOAT   fW =pBlnd->vB[k].fW;

               INT     nM =pBlnd->vB[k].nB;

 

               MATA    mtT = m_pGeo[nM].mtWld;

               mtW += (mtT * fW);

        }

 

        D3DXVec3TransformCoord(&pGeo->pVxD[j].p, &pGeo->pVtx[j].p, &mtW);

}

 

렌더링에서는 오브젝트 자체의 월드 행렬도 다음과 같이 적용되어야 합니다.

 

void CLcmAcm::Render()

VtxPos* pVtx    = NULL;

pVtx= pGeo->pVxD;

m_pDev->SetTransform(D3DTS_WORLD, &m_mtWld);

m_pDev->DrawIndexedPrimitiveUP(…, pVtx, );

 

전체 코드는 mxp23_skinning_view1_soft.zip을 참고 하기 바랍니다.

 

소프트웨어 처리 방법은 하드웨어의 성능에 상관없이 동작한다는 장점이 있지만 정점의 복사본을 가지고 있어야 하기 때문에 객체가 많아지면 사용 메모리가 객체에 비례해서 늘어나고, 정점의 개수가 많아질수록 연산 량도 증가한다는 단점이 있습니다.

 

새로운 객체가 이전 객체와 같은 형태의 오브젝트의 경우 정점을 복사하지 않고 스키닝 애니메이션 행렬만 늘리는 하드웨어 처리 방법은 애니메이션 갱신에서는 스키닝 애니메이션 행렬 버퍼에 갱신된 애니메이션 행렬을 복사하고, 렌더링에서는 이를 그래픽 파이프라인에 연결하기만 하면 됩니다.

먼저 스키닝이 GPU에서 처리될 수 있도록 다음과 같은 구조체를 준비합니다. 스키닝에 대한 구조체 설정은 이전의 고정 기능 파이프라인에서의 스키닝 부분을 참고 하기 바랍니다.

 

struct VtxBlend

{

        VEC3           p;

        FLOAT          g[3];          // BLEND WEIGHT

        BYTE           m[4];          // MATRIX INDEX

        enumFVF = (D3DFVF_XYZB4 | D3DFVF_LASTBETA_UBYTE4),       };

};

 

다음으로 그래픽 파이프라인에 설정할 Blending 행렬 배열을 선언합니다.

 

D3DXMATRIX     m_mtBlnd[LCM_MAX_BLEND];      // Blending Matrix Buffer

 

애니메이션 갱신에서 앞의 행렬 배열에 최종 월드 행렬인 "애니메이션 갱신 행렬 * 자신의 월드 행렬" 값을 복사합니다.

 

INT CLcmAcm::FrameMove()

// World 행렬 업데이트

        for(n=0; n<m_Header.nGeo; ++n)

        {

               LcGeo*  pGeo    = &m_pGeo[n];

               MATA    mtAni;

               D3DXMatrixIdentity(&mtAni);

 

               if(pGeo->pAni)

                       mtAni = pGeo->pAni[m_nAni];

 

               m_mtBlnd[n] =  mtAni * m_mtWld;

        }

 

렌더링에서는 디바이스의 Software Vertex Processing을 활성화 합니다. 이것은 디바이스는 Mixed 또는 Software로 생성되어야 가능합니다.

다음으로 디바이스의 렌더링 상태에 대한 INDEXEDVERTEXBLENDENABLE을 활성화 하고, VERTEXBLEND에 적절한 값(D3DVBF_3WEIGHTS)을 설정합니다. 마지막으로 SetTransform() 함수를 이용해서 스키닝 행렬 배열을 전달합니다.

 

void CLcmAcm::Render()

for(n=0; n<m_Header.nGeo; ++n)

        {

               LcGeo*  pGeo = &m_pGeo[n];

               m_pDev->SetSoftwareVertexProcessing(TRUE);

               m_pDev->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE );

               m_pDev->SetRenderState(D3DRS_VERTEXBLEND, D3DVBF_3WEIGHTS );

 

               for(k=0; k<LCM_MAX_BLEND; ++k)

                       m_pDev->SetTransform( D3DTS_WORLDMATRIX(k), &m_mtBlnd[k] );

 

               m_pDev->DrawIndexedPrimitiveUP(…);

        }

 

전체 코드는 mxp23_skinning_view2_hard.zip을 참고 하기 바랍니다.

 

쉐이더를 이용한 방법은 를 저수준 또는 고수준 언어(High Level Shading Language)로 구현하는 것입니다. 고수준 언어 HLSL로 작성할 때 가장 중요한 부분은 정점 처리 함수의 입력 인수와 시멘틱 정의는 신중해야 합니다. 다음 함수는 스키닝에 대한 HLSL 코드입니다. 인덱스에 대한 인수 정의를 int4로 설정하고 있음을 기억하기 바랍니다.

 

입력 레지스터 값은 Read Only이기 때문에 값을 변경할 수 없습니다. 이런 이유로 Wgx 변수에 입력 레지스터 값(Wgt)을 복사하고 인덱스 3에 대한 값을 설정하고 있습니다. 간단히 for문에서 비중과 행렬을 곱셈한 후 이 결과를 누적시켜 를 구하고 있습니다.

또한 의 마지막 _44 값이 1 보다 멀리 떨어져 있으면 의 행렬을 단위행렬로 만들어야 합니다.

을 구하고 나서 이 행렬에 객체의 월드 행렬을 곱해서 최종 월드 행렬을 만든 다음 정점을 월드 변환, 뷰 변환, 투영 변환에 대한 연산을 진행합니다.

 

float4x4 m_mtBlnd[128]: WORLDMATRIXARRAY;    // Blending Matrix Buffer

 

float4 VtxBlendfloat4 Pos: POSITION

               , float4 Wgt: BLENDWEIGHT

               , int4   Idx: BLENDINDICES ) : POSITION

{

        float4         Pout =0.0f;

        float4x4       mtW = 0.0f;

        float4         Wgx = Wgt;

 

        Wgx[3] = 1.0f - (Wgx[0] + Wgx[1] + Wgx[2]);

 

        // ΣMi * Wi 연산

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

               mtW += Wgx[i] * m_mtBlnd[Idx[i]];

 

        // _44 값이 1에서 멀리 떨어져 있으면 단위행렬로 만든다.

        if(mtW._44 <0.001f)

               mtW = float4x4(1,0,0,00,1,0,00,0,1,00,0,0,1);

 

        mtW = mul(mtW, m_mtWld);      // 객체의 월드 행렬과 곱셈

        Pout = mul(Pos, mtW);         // 월드 변환

        Pout = mul(Pout, m_mtViw);    // 뷰 변환

        Pout = mul(Pout, m_mtPrj);    // 투영 변한

 

        return P;

}

 

쉐이더 코드는 mxp23_skinning_view3_shader1.zip "data/shader.fx"를 참고 하기 바랍니다.

 

렌더링에서는 뷰 행렬, 투영 행렬, 월드 행렬, Blending 행렬을 설정하고 장면을 연출합니다. Blending 행렬은 이전의 하드웨어 처리에서 사용한 행렬 배열을 그대로 사용 합니다.

 

m_pEft->SetMatrix("m_mtViw", &mtViw); // 뷰 행렬 설정

m_pEft->SetMatrix("m_mtPrj", &mtPrj); // 투영 행렬 설정

m_pEft->SetMatrix("m_mtWld", &m_mtWld);      // 월드 행렬 설정

m_pEft->SetMatrixArray("m_mtBlnd", m_mtBlnd, LCM_MAX_BLEND); // Blending 행렬 설정

hr = m_pDev->DrawIndexedPrimitiveUP(…);

 

전체 코드는 mxp23_skinning_view3_shader1.zip을 참고하기 바랍니다.

 

만약 쉐이더 코드 작성에서 입력 레지스터(입력 인수)의 설정에 어려움이 있으면 비중과 행렬의 인덱스를 텍스처 좌표에 적용해서 구현해도 됩니다. 다음은 렌더링 오브젝트가 두 개의 텍스처 좌표를 사용하고 있을 때 스키닝 구현 예입니다.

 

float4 VtxBlendfloat4 Pos: POSITION

               , float2 Tx0: TEXCOORD0       // 텍스처 좌표 0

               , float2 Tx1: TEXCOORD1       // 텍스처 좌표 1

               , float4 Wgt: TEXCOORD2       // Blending Weight

               , float4 Idx: TEXCOORD3       // Blending Index

 ) : POSITION

{

        float4  Pout =0.0f;

        float4x4 mtW = 0.0f;

 

        // ΣMi * Wi 연산

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

               mtW += Wgt[i] * m_mtBlnd[Idx[i]];

 

텍스처 좌표를 사용하게 되면 다음과 같이 정점 구조체와 FVF를 수정해야 합니다.

 

struct VtxBlend

{

        VEC3    p;

        VEC2    t0;     // Texture Coordinate 0

        VEC2    t1;     // Texture Coordinate 1

 

        VEC4    g;      // BLEND WEIGHT

        VEC4    m;      // MATRIX INDEX

        enumFVF = (D3DFVF_XYZ | D3DFVF_TEX4 | \

D3DFVF_TEXCOORDSIZE4(2) | D3DFVF_TEXCOORDSIZE4(3)), };

};

 

Blend Weight Matrix Index 4개를 사용하므로 D3DXVECTOR4를 사용하며 이 경우 텍스처 좌표 시스템을 D3DFVF_TEXCOORDSIZE4로 변경해야 합니다.

텍스처 좌표를 사용하게 되어 정점의 크기가 16Byte 더 사용하지만 가장 무난하고 안전하게 사용할 수 있는 방법입니다. mxp23_skinning_view3_shader2.zip을 참고하기 바랍니다.

 

  

<스키닝 애니메이션: Software, Hardware, Shader>

 

 

5.3.4 Interpolation

게임을 실행하는 개인용 컴퓨터의 성능은 차이가 있으므로 프레임과 프레임 사이를 보정해서 장면을 연출해야 합니다. 가장 간단한 보정 방법은 선형 보간(Linear Interpolation)으로 우리는 ASEParsing할 때 해봤습니다.

ASE에서는 위치, 회전에 사원수를 가지고 있는 상황에서 선형 보간을 구성했으나 지금은 행렬로 애니메이션 정보를 가지고 있어서 이 행렬에서 위치와 사원수를 얻어서 선형보간 한 다음 다시 행렬로 만들어야 합니다.

 

행렬에서 _41, _42, _43의 값은 이동에 대한 변환으로 위치를 나타냅니다. 회전의 경우에는 D3DXQuaternionRotationMatrix()함수를 이용해서 사원수를 구합니다.

위치는 다음의 선형 보간 공식으로 구합니다.

 

v' = (1 - t) * v1 + t * v2

 

사원수는 D3DXQuaternionSlerp() 함수를 이용합니다.

 

다음은 두 행렬과 비중 t가 주어질 때 위치와 회전에 대한 선형 보간입니다.

 

void CLcmAcm::MatrixLerp(D3DXMATRIX* pOut

, const D3DXMATRIX* m1, const D3DXMATRIX* m2, DOUBLE t)

{

        D3DXQUATERNION q, q1, q2;

        D3DXVECTOR3    v, v1, v2;

 

        D3DXQuaternionRotationMatrix(&q1, m1);               // 회전 추출

        D3DXQuaternionRotationMatrix(&q2, m2);

 

        v1 = D3DXVECTOR3(m1->_41, m1->_42, m1->_43); // 위치 추출

        v2 = D3DXVECTOR3(m2->_41, m2->_42, m2->_43);

 

        v = (1 - t) * v1 + t * v2;                   // 위치에 대한 선형 보간

        D3DXQuaternionSlerp(&q, &q1, &q2, t);                // 회전의 선형 보간

 

        D3DXMatrixRotationQuaternion(pOut, &q);              // 회전을 행렬로 전환

        pOut->_41 = v.x; pOut->_42 = v.y; pOut->_43 = v.z; // 위치 적용

}

 

그런데 시간에 대한 애니메이션 간격을 좁게 설정하고 행렬을 추출했으면 앞의 코드처럼 계산하지 않고 행렬 자체를 선형 보간해도 됩니다.

 

*pOut = (1 - t) * (*m1) + t * (*m2);

 

이것이 가능한 이유는 회전의 경우 보간하는 두 사원수의 각도가 작으면 가 되어 다음과 같이 회전의 사원수 보간 공식이 선형 보간으로 바뀌게 되기 때문에 행렬을 선형 보간 해도 되는 것입니다.

 

 

mxp24_interpolation.zip은 행렬을 선형 보간할 때와 사원수를 분리해서 보간하는 예입니다. 애니메이션의 간격이 좁기 때문에 눈에 띄게 보이는 차이는 없습니다.

 

<애니메이션 보간: mxp24_interpolation.zip>

 

 

5.4 기타

5.4.1 Normal

맥스의 모든 메쉬는 법선 벡터(Normal Vector)를 가지고 있습니다. 또한 법선 벡터의 개수는 정점의 수와 일치 합니다. 법선 벡터를 추출하기 위해서 다음과 같이 메쉬에게 정점의 법선과 Face(삼각형 평면)에 대한 법선 벡터 생성을 요청합니다.

 

Mesh* pMesh    = &pTri->GetMesh();

pMesh->buildNormals();

 

다음으로 법선 벡터를 저장할 공간을 생성하고 메쉬의 getNormal() 함수를 이용해서 법선 벡터를 얻어와 저장합니다.

 

INT iNvtx = pMesh->getNumVerts();

pGeo->pNor = new D3DXVECTOR3[iNvtx];

for (n=0; n<iNvtx; ++n)

{

        Point3 v = Normalize(pMesh->getNormal(n));   // 법선벡터 추출

        pGeo->pNor[n].x = v.x;

        pGeo->pNor[n].y = v.z; //y <--> z 교환

        pGeo->pNor[n].z = v.y;

}

 

 

5.4.2 Material

Geometry에 적용된 텍스처 파일 이름은 맥스의 재질(Material)에서 찾아야 합니다. 그런데 D3D의 재질은 하나의 메쉬 덩어리(Geometry Object)에 하나의 재질이 적용되는 데 반해서 맥스의 재질은 하나의 메쉬 안에서도 여러 재질을 사용하기도 합니다. 이러한 경우에는 삼각형들 각 재질에 따라 분리하고 다시 구축해야 하지만 이것은 프로그래머에게 너무나 많은 노력과 시간을 요구하게 되며 잘 해결되지도 않습니다. 여기서 원칙을 세우는 것이 중요한데 나의 메쉬에는 하나의 텍스처만 적용하고 여러 메쉬가 결합하는 경우라면 각각의 메쉬를 따로 추출한 다음 프로그램에서 링크(link)하는 것입니다. 이렇게 하면 게임에 올렸을 때 엉뚱한 텍스처가 붙거나 안 붙는 문제들을 어느 정도 해결할 수 있습니다.

 

본격적으로 재질에서 텍스처 파일 이름을 구해 봅시다.

INode 객체에서 우리는 재질을 다음과 같이 얻어올 수 있습니다.

 

INode*  pNode = m_vMaxNode[i];

Mtl*    pMtrl = pNode->GetMtl();

 

맥스의 재질은 INode처럼 하위 재질을 나무 구조(Tree Structure)로 가지고 있습니다. 따라서 재질을 얻은 다음에 하위 재질을 다음과 같이 얻어야 합니다.

 

int iSub = pMtl->NumSubMtls();

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

        Mtl* pSub = pMtl->GetSubMtl(i);

 

이것은 노드를 모을 때처럼 재귀 호출(Recursive Call) 함수를 만들어서 전체 재질을 모아야 합니다. 재질을 모을 때 어느 노드에서 얻어진 재질인지 다음과 같은 구조체를 작성합니다.

 

struct LcMtl

{

        INode*  pNode;  // Node

        Mtl*    pMtrl;  // Material

};

 

노드와 재질을 저장할 수 있도록 이 구조체를 이용해서 다음과 같은 STL의 벡터 컨테이너를 만듭니다.

 

std::vector<LcMtl>     m_vMaxMtrl;

 

재귀 호출 함수 안에서 먼저 입력 받은 노드와 재질을 묶어서 저장합니다. 다음으로 하위 재질을 얻고 이 함수를 다시 호출합니다.

 

void LcMax::GatherMaterial(INode* pNode, Mtl* pMtl)

        m_vMaxMtrl.push_back(LcMtl(pNode, pMtl));

        int iSub = pMtl->NumSubMtls();

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

        {

               Mtl* pSub = pMtl->GetSubMtl(i);

               GatherMaterial(pNode, pSub);

        }

 

이렇게 노드에 대한 모든 재질을 저장했습니다. 다음으로 재질 안에서 텍스처 파일 이름을 가져와야 합니다. 맥스의 재질은 여러 텍스처를 가지고 있습니다. 이들 텍스처는 조명 효과와 반사 효과 등에 대한 역할을 수행합니다. 우리가 가져오려는 텍스처는 조명 효과 중에서 Diffuse Map에 대한 텍스처이며 이 아이디는 " ID_DI" 입니다.

 

재질에서 이름을 가져오는 방법은 먼저 재질에서 하위 텍스처의 숫자를 알아내고 이 숫자만큼 순회하면서 Texmap 객체를 얻습니다. Texmap 객체를 BitmapTex 으로 캐스팅해서 GetMapName() 함수를 호출하면 이름을 가져오게 됩니다.

 

INT iSub = pMtrl->NumSubTexmaps();

for( j=0; j<iSub; ++j)

{

        Texmap* subTex = pMtrl->GetSubTexmap(j);

        DumpTexture(pLcMtl, subTex, j);

}

void LcMax::DumpTexture(LcMtl* pLcMtrl, Texmap* txm, INT iSubIdx)

        // 경로를 포함한 텍스처 파일 이름을 가져온다.

        TSTR bitmapFile = ((BitmapTex *)txm)->GetMapName();

        // 하위 텍스처 검사

        INT iSub = txm->NumSubTexmaps();

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

        {

               Texmap* pSub = txm->GetSubTexmap(i);

               DumpTexture(pLcMtrl, pSub, iSubIdx);

        }

 

각 노드는 같은 재질을 사용할 수 있습니다. 따라서 텍스처 파일이름이 중복 될 수 있는데 STL Set 컨테이너를 이용하면 중복된 이름을 제거할 수 있습니다. 노드와 텍스처 파일 이름 연결은 기술적인 부분이므로 mxp25_lcm_skinning_plugin.zip 예제의 GatherTexture(), SetupTextureIndex(), FindTextureIndex() 함수 등을 참고 하기 바랍니다.

 

 

5.4.3 텍스처 좌표

플러그인의 마지막인 텍스처 좌표 추출입니다. ASE에서 "맥스의 정점은 텍스처 좌표에서 공유 될 수 있다"라고 언급한 적이 있습니다. 따라서 정점의 개수를 텍스처 좌표만큼 늘려주어야 합니다. 그리고 맥스는 텍스처 좌표의 인덱스가 꼭 0에서부터 시작하지 않으며 또한 중간에 사용하지 않는 인덱스도 있습니다. 완벽에 가까운 플러그인은 이런 것도 처리해야 하지만 역시 간단한 방법은 그래픽 담당자와 협의를 해서 UV 정리를 부탁하는 것이 가장 현명한 방법입니다.

 

UV가 적용된 메쉬에서 Geometry를 구성하는 방법을 정리하면 다음과 같습니다.

 

1. T-face에서 UV 삼각형에 대한 인덱스 리스트를 추출한다.

2. T-vertex에서 UV를 추출한다.

3. T-face에 대응하는 인덱스는 Face인덱스와 같으므로 이들을 하나로 묶고, U, V 값도 설정한다.

4. 새로운 정점의 개수를 T-vertex 개수만큼 설정한다.

5. 정점의 위치, 법선 벡터, Bone의 인덱스와 비중 값 등을 새로 만들고 3번에서 묶은 인덱스 등을 참고해서 값을 새로 설정한다.

6. 이전의 위치, 법선, Bone에 대한 값들을 지우고 새로운 값들로 설정한다.

 

이 과정은 mxp25_lcm_skinning_plugin.zipLcMax::SetupUV() 함수에 구현되어 있습니다. 주요 코드를 설명하면 먼저 메쉬에서 T-face의 숫자와 T-vertex(UV) 숫자를 얻는 방법은 다음과 같습니다.

 

nTfce   = pMesh->getNumFaces();               // T-face Face숫자는 같음

nTvtt   = pMesh->getNumTVerts();      // T-vertex(UV) Number

 

다음으로 임시 버퍼에 T-face T-vertex를 저장합니다.

 

// T-face List 저장

for(n=0; n<nTfce; ++n)

{

        pNewFce[n]=VtxIdx( pMesh->tvFace[n].t[0]

                       , pMesh->tvFace[n].t[2]

                       , pMesh->tvFace[n].t[1]);

}

 

// T-vertex(UV) 저장

for(n=0; n<nTvtt; ++n)

{

        UVVert t = pMesh->tVerts[n];

        pTvtt[n].x = t.x;

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

}

 

T-face의 경우 Face처럼 인덱스가 0, 2, 1로 되어야 합니다. 그리고 UV에서 V값도 ASE처럼 "1.0- V" 로 설정해야 합니다.

다음으로 T-face, Face, U, V를 작업을 편리하게 하기 위해 하나의 묶음을 만듭니다.

 

std::vector<_Tpck >    lsFceVtxUV;

for(; itF != itL; ++itF)

{

        int nT = (*itF).first.n;

        int nV = (*itF).second;

        FLOAT U = pTvtt[nT].x;

        FLOAT V = pTvtt[nT].y;

        lsFceVtxUV.push_back( _Tpck(nT, nV, U, V));

}

 

Face대신 T-face를 인덱스 리스트로 정하고 정점의 개수, 법선의 개수, 정점에 대한 본의 비중을 변경하고 이전 값에서 찾아와 복사합니다.

 

INT     nNewVtxSize =  lsFceVtxUV.size();

D3DXVECTOR3* pNewPos = new D3DXVECTOR3[nNewVtxSize]; // new Position List

D3DXVECTOR3* pNewNor = new D3DXVECTOR3[nNewVtxSize]; // new Normal Vector

D3DXVECTOR2* pNewUVW = new D3DXVECTOR2[nNewVtxSize]; // new UVW Vector

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

{

        INT     G = lsFceVtxUV[n].G;          // 정점의 인덱스

        FLOAT   U = lsFceVtxUV[n].U;

        FLOAT   V = lsFceVtxUV[n].V;

        pNewPos[n] = pOldPos[G];

        pNewNor[n] = pOldNor[G];

        pNewUVW[n].x = U;

        pNewUVW[n].y = V;

}

 

만약 정점에 Bon의 인덱스와 비중이 있으면 이들 또한 조정합니다.

 

LcmBoneS*      pNewBon = new LcmBoneS[nNewVtxSize];  // new Bones

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

{

        int G = lsFceVtxUV[n].G;              // 정점의 인덱스

        LcmBoneS* pBlndS= &pOldBon[G]; // Source

        std::map<INT, FLOAT >::iterator       _F = pBlndS->vB.begin();

        std::map<INT, FLOAT >::iterator       _L = pBlndS->vB.end();

        for(; _F != _L; ++_F)

               pNewBon[n].vB.insert( *_F );

}

 

마지막으로 이전 값들을 지우고 새로운 값들로 변경합니다.

 

SAFE_DELETE_ARRAY(     pOldFce );

SAFE_DELETE_ARRAY(     pOldPos );

SAFE_DELETE_ARRAY(     pOldNor );

pGeo->pFce     = pNewFce;

pGeo->pPos     = pNewPos;

pGeo->pNor     = pNewNor;

pGeo->pUVW     = pNewUVW;

pGeo->nVtx     = nNewVtxSize;

 

파일에 기록할 때는 이전의 구조를 거의 그대로 유지하고 법선 벡터, 텍스처 파일 리스트, 정점 UV 리스트를 마지막에 기록 합니다. 전체 코드는 mxp25_lcm_skinning_plugin.zip을 참고 하기 바랍니다.

 

플러그인이 수정되어 뷰어도 수정해야 합니다. 뷰어에서 메쉬에 대한 정점 구조체를 다음과 같이 법선 벡터, 텍스처 좌표를 포함시키고 FVF도 변경합니다.

 

struct VtxBlend

{

        VEC3           p;

        FLOAT          g[3];          // BLEND WEIGHT

        BYTE           m[4];          // MATRIX Index

 

        VEC3           n;             // Normal Vector

        VEC2           t;             // Texture Coordinate

 

        enumFVF = (D3DFVF_XYZB4 | D3DFVF_LASTBETA_UBYTE4 |\

                                    D3DFVF_NORMAL | D3DFVF_TEX1),   };

};

 

파일을 읽는 부분과 렌더링에서 텍스처 연결은 어렵지 않으므로 넘어가겠습니다.

텍스처와 조명을 픽셀 쉐이더에서 처리할 수 있도록 다음과 같은 구조체를 선언합니다.

 

struct SVsOut

{

        float4  Pos: POSITION;

        float2  Tex: TEXCOORD0;        // 정점의 텍스처 좌표

        float3  Nor: TEXCOORD7;        // 정점의 법선 벡터

};

 

정점 데이터가 법선 벡터, 텍스처 좌표를 포함하고 있으므로 정점 처리 함수의 입력 인수에 법선과 텍스처 좌표를 추가합니다.

 

SVsOut VtxBlendfloat4 Pos: POSITION               // Position

                , float4 Wgt: BLENDWEIGHT     // Blending Weight

                , int4   Idx: BLENDINDICES    // Blending Index

                , float3 Nor: NORMAL          // Normal Vector

                , float2 Tex: TEXCOORD0               // Texture Coordinate

               )

{

        SVsOut         Out = (SVsOut)0;

        Out.Nor = Nor;

        Out.Tex = Tex;

 

        return Out;

}

 

법선 벡터와 텍스처는 특별히 처리해야 할 일이 없으므로 구조체에 복사해서 그대로 출력합니다. 픽셀 쉐이더 처리 함수에서는 UV 좌표를 이용해서 텍스처에서 색상을 샘플링(Sampling) 합니다.

 

float4 PxlBlend(SVsOut In) : COLOR0

{

        float4 Out = tex2D(smp0, In.Tex);     // Sampling from Texture by Sampler

        return Out;

}

 

조명은 처리하지 않았는데 이 부분은 여러분들께서 채워보시기 바랍니다. 다음으로 테크닉에 픽셀 처리 함수를 지정합니다.

 

technique Tech

        pass P1

        {

               VertexShader = compile vs_2_0 VtxBlend();

               PixelShader  = compile ps_2_0 PxlBlend();

        }

 

mxp25_lcm_skinning_viewer.zip을 실행하면 다음 그림처럼 UV가 적용된 화면을 볼 수 있습니다.

 

 

<텍스처가 적용된 스키닝 예제: mxp25_lcm_skinning_viewer.zip>



Copyright (c) 2004 3dapi.com All rights reserved.

Creative Commons License