home lecture link bbs blame

◈3D 게임 제작 Auxiliary◈

 

교육기관에서 3D 기초 과정 수업이 끝나고 2~3주 정도 시간을 두어 게임 제작 실습을 진행합니다. 3D 기초 과정만으로도 게임 제작이 가능하지만 의외로 작은 부분들에서 시간을 많이 빼앗기거나 또는 코드 몇 줄 더 넣으면 게임 분위기가 확 달라지는 일들이 종종 있습니다. 이 장은 3D 기초 과정을 배우고 게임 제작을 시작하는 분들에게 특별한 주제들을 모아서 도움이 될만한 내용을 모았습니다. 3D의 전반적인 내용이 아닌 지역적인 내용이 많아 이 장은 넘어가도 상관없습니다.

 

 

3.1 포그(Fog)

D3D 3D의 현실감(Reality)를 높이기 위해서 Anti-aliasing, 범프 매핑(Bump Mapping), 반사나 굴절에 대한 환경 매핑(Environment Mapping), 스텐실, 버텍스 블렌딩(Vertex Blending), 포그(Fog) 등을 지원합니다. 이 중에서 포그는 컴퓨터 그래픽스에서 현실의 안개 효과를 표현한 것입니다. 포그를 적용하면 3D 장면을 구성하는 가상 카메라에서 멀리 있는 오브젝트는 포그의 색상에 가깝게 만들고 가까울수록 물체를 선명하게 보이게 합니다. 또한 포그를 사용하면 멀리 있는 물체는 디바이스가 '적당히' 그리게 되어 렌더링 속도가 향상되며 볼륨 체적의 Far 평면과의 거리 값보다 작을 때 오브젝트가 갑자기 튀어나오는 현상도 막을 수 있습니다.

 

D3D는 장면에 포그를 적용하는 정점 포그, 픽셀 포그 두 가지 방법이 있습니다. 이들 포그는 다음과 같은 선형 계산 공식으로 폴리곤에 적용됩니다.

 

최종 폴리곤 색상 C = f * 폴리곤 색상 + (1 - f)* 포그 색상

 

f는 포그 계수(Fog Factor)로 거리의 정도에 따라 계산하는데 다음과 같이 선형 포그(Linear Fog)와 지수 포그(Exponential Fog)로 분류 됩니다.

 

 (선형포그: D3DFOG_LINEAR)

(지수 포그: D3DFOG_EXP)

(지수 포그: D3DFOG_EXP)

<포그 공식과 그래프>

 

공식의 d는 카메라에서 정점까지 거리이고 e는 자연 대수(exponential)입니다. start는 선형포그가 적용되는 시작 거리, end는 포그의 효과가 더 이상 증가하지 않는 최대 거리입니다.

그래프를 보면 선형 포그는 전체적으로 선형적으로 변화하는 반면에 exp 포그는 급격히 변화했다가 천천히 변화하고 exp2 포그는 천천히 변화하다가 급격히 변하는 것을 알 수 있습니다.

 

  

<Linear, Exp, Exp2>

 

선형 포그는 end 값 이후에는 포그색상이 100%적용이 되어 왼쪽처럼 짙은 포그 색상의 띠를 만들기도 합니다.

 

D3D의 포그는 다음 그림처럼 영역 기반(Range-based)를 설정하거나 또는 해제(Plane-based) 할 수 있습니다.

 

<영역 기반(Range-based) 포그 설정, 미 지정>

 

<영역 기반 설정이 안된 포그에서의 오브젝트>

 

영역 기반으로 하면 카메라의 회전에도 포그가 자연스럽게 적용이 됩니다. 그러나 영역 기반을 비활성화 하면 카메라와의 거리가 아닌 z 값을 가지고 포그계수를 계산하기 때문에 카메라의 회전에 대해서 포그 적용이 될 수도 있고 안될 수도 있습니다.

 

정점 포그는 정점 변환과 조명을 적용할 때 포그 또한 적용이 되고, 픽셀 포그는 하드웨어에 따라서 미리 계산 된 참조 테이블을 통해서 픽셀의 깊이 값에 따라 포그가 적용이 됩니다. 이런 이유로 픽셀 포그를 테이블 포그라 하기도 하며 픽셀 포그는 영역 기반 설정이 제대로 안됩니다.

차이가 클 것 같지만 정점 포그와 픽셀 포그는 잘 보지 않으면 장면의 결과에서는 거의 차이가 없습니다.

 

포그의 기본적인 내용을 다 살펴 보았고 다음으로 이를 구현 하는 방법입니다. 먼저 다음과 같은 함수를 준비하는 것이 좋습니다.

 

inline DWORD FtoDW(float& p) { return *((DWORD*)&p); }

 

이것은 D3D의 상태 값 설정은 거의 DWORD형으로 되어 있습니다. 그런데 포그는 FLOAT형을 사용하고 있어서 단순히 자료를 복사하게 되면 제대로 처리가 안되어 DWORD형으로 자료를 캐스팅 해야만 합니다. 위와 같은 함수를 사용하면 자료의 유실 없이 올바로 넘길 수 있습니다.

 

다음으로 다음과 같이 포그의 색상, 시작, , 밀도(Density)에 대한 변수를 준비합니다.

 

DWORD   dFog    = D3DXCOLOR(0.2f, 1.f, 0.7f, 1.0f);

float   fStart  = 300.0f;

float   fEnd    = 700.0f;

float   fDens   = 0.0015f;

 

게임에서는 주로 선형 포그(Linear Fog)를 사용하고 있어서 EXP에서 사용되는 밀도(Density) 변수는 거의 설정 안 합니다.

디바이스의 상태 설정 함수 RenderState()를 이용해서 포그를 활성화합니다.

 

m_pDev->SetRenderState(D3DRS_FOGENABLE, TRUE);

 

정점 포그이면 다음과 같이 픽셀 기반 포그를 막고 정점 포그에 Linear, Exp, Exp2중 하나를 설정합니다.

 

m_pDev->SetRenderState(D3DRS_FOGTABLEMODE, D3DFOG_NONE);

m_pDev->SetRenderState(D3DRS_FOGVERTEXMODE, D3DFOG_"LINEAR", "EXP", or"EXP2");

 

픽셀 포그면 정점 포그를 막고 픽셀 포그 설정을 합니다.

 

m_pDev->SetRenderState(D3DRS_FOGVERTEXMODE, D3DFOG_NONE);

m_pDev->SetRenderState(D3DRS_FOGTABLEMODE, D3DFOG_"LINEAR", "EXP", or"EXP2");

 

마지막으로 포그 변수들과 영역 기반 활성화를 설정합니다.

 

m_pDev->SetRenderState(D3DRS_RANGEFOGENABLE, "TRUE"? "FALSE");

m_pDev->SetRenderState(D3DRS_FOGCOLOR,            "포그 색상");

m_pDev->SetRenderState(D3DRS_FOGSTART,       *((DWORD*)(&fStart)));

m_pDev->SetRenderState(D3DRS_FOGEND,       FtoDW(fEnd));

m_pDev->SetRenderState(D3DRS_FOGDENSITY,     FtoDW(fDens));

 

전체 코드는 daux01_fog.zip CMain::Render() 함수를 참고 하기 바랍니다.

 

 

3.2 폰트(Font)

3.2.1 ID3DXFont

ID3DXFont는 아주 쉽게 사용할 수 있는 폰트 출력용 확장 유틸리티입니다. 일반적으로 윈도우의 폰트를 기본으로 많이 사용합니다. 그런데 때로는 게임에 맞는 폰트를 따로 만들어 사용할 때도 있습니다. 이런 경우 윈도우 폴더에 폰트를 등록해 사용해야 하지만 간단히 Platform SDK 함수를 사용해서 프로그램 내부에서만 사용할 수 있는 방법이 있습니다.

 

AddFontResourceEx()/RemoveFontResourceEx() 함수는 폰트를 프로그램 내부에서 사용하고 해제 할 때 사용하는 함수입니다. 이 함수는 Windows 2000 이상에서만 지원이 됩니다. 따라서 "windows.h" 포함할 때 매크로로 상위버전임을 표시해야 합니다.

이 함수들을 사용해서 ID3DXFont객체 생성과 해제는 다음과 같이 요약할 수 있습니다.

 

#define _WIN32_WINNT   0x0500

#include <windows.h>

 

// 폰트 등록

hr = AddFontResourceEx("font/08SeoulNamsanB.ttf", FR_NOT_ENUM, NULL);

hr = AddFontResourceEx("font/08SeoulHangangM.ttf", FR_NOT_ENUM, NULL);

// ID3DXFont 객체 생성

hr = D3DXCreateFont(…, "08서울남산체 B", &m_pDXFont1);

// 폰트 해제

RemoveFontResourceEx("font/08SeoulNamsanB.ttf", FR_NOT_ENUM, NULL);

RemoveFontResourceEx("font/08SeoulHangangM.ttf", FR_NOT_ENUM, NULL);

 

< AddFontResourceEx() 함수로 프로그램 내부에서만 사용: daux02_font1_dx2d.zip>

 

 

3.2.2 3D 문자열

D3D는 확장 유틸리티로 3D 폰트도 지원합니다. D3DXCreateText() 함수를 사용하면 3D로 문자열을 만들 수 있습니다. 그러나 이 함수는 한글 문자열을 출력할 때는 유니코드(Unicode)를 사용하거나 다음과 같이 MultiByteToWideChar() 함수를 사용해서 문자열을 유니코드로 만들어야 출력이 됩니다.

D3DXCreateText() 함수는 또한 윈도우 DC(Device Context)를 인수로 받고 있어 문자열 출력을 위해 API 방식으로 CreateFont() 함수 또는 CreateFontIndirect() 함수로 서체에 대한 FONT 객체를 만든 다음, DC에 연결한 후에 D3DXCreateText()를 호출해야 함을 의미합니다.

다음 코드는 3D 문자열을 출력하는 예입니다.

 

// 3D 문자열 출력 객체

LPD3DXMESH     m_pMshStr;

// DC 생성

HDC hdc = CreateCompatibleDC( NULL );

HFONT hFontNew = NULL;

HFONT hFontOld = NULL;

 

LOGFONT hfont={0};

hfont.lfHeight      = 32;

_tcscpy(hfont.lfFaceName, _T("08서울남산체 B"));

 

hFontNew = CreateFontIndirect(&hfont);

hFontOld = (HFONT)SelectObject(hdc, hFontNew);

 

 

char    sSrc[256] = "안녕하세요  Hello world !!! \0";

WCHAR sDst[256]={0};

 

// 멀티 바이트를 유니코드로 변경

LcStr_AnsiToUnicode(sDst, sSrc, strlen(sSrc)+1);

 

// 3D 문자열을 생성

D3DXCreateTextW(m_pDev, hdc, sDst, 200.f, 3.f, &m_pMshStr, 0, 0);

 

SelectObject(hdc, hFontOld);

DeleteObject( hFontNew);

 

// DC 해제

DeleteDC( hdc );

 

LcStr_AnsiToUnicode() 함수는 유니코드로 문자열을 바꾸어주는 사용자 정의 함수 입니다.

 

<3D 문자열 출력: daux02_font2_dx3d.zip>

 

 

3.2.3 후면 버퍼(Back Buffer) 출력

ID3DXFont는 사용이 편리합니다. 그러나 문자열이 많아지면 렌더링 속도가 상당히 느려집니다. 만약 문자열이 화면 가득해도 최대 속도를 원한다면 후면 버퍼에 직접 출력하는 방법이 있습니다.

D3D9은 알파가 없는 불투명 서피스(Surface), D3DFMT_R5G6B5, D3DFMT_X1R5G5B5, D3DFMT_R8G8B8, D3DFMT_X8R8G8B8 형식으로 만든 서피스에 대해서 DC를 가져올 수 있게 했습니다.  D3DPRESENT_ PARAMETERS 구조체 변수 BackBufferFormatX8R8G8B8 또는 D3DFMT_R5G6B5로 지정하고 Flags 변수 또한 D3DPRESENTFLAG_LOCKABLE_BACKBUFFER 으로 지정을 해야만 후면 버퍼에서 DC를 가져올 수 있습니다.

 

m_d3dpp. BackBufferFormat = D3DFMT_X8R8G8B8;

m_d3dpp.MultiSampleType         = D3DMULTISAMPLE_NONE;

m_d3dpp.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;

 

후면 버퍼의 DC는 아무 때나 사용할 수 있는 것이 아니라 다음과 같이 BeginScene()/EndScene() 밖에서만 사용해야 합니다.

 

m_pd3dDevice->BeginScene();

m_pd3dDevice->EndScene();

 

LPDIRECT3DSURFACE9     pBackBuffer = NULL;

m_pd3dDevice->GetBackBuffer( 0, 0, D3DBACKBUFFER_TYPE_MONO, &pBackBuffer );

if(pBackBuffer)

{

        HDC hDC = NULL;

        pBackBuffer->GetDC(&hDC);

        if(hDC)

        {

               HFONT   hFontOld = NULL;

               hFontOld = (HFONT)SelectObject(hDC, m_hFont);

               …

               TextOut(hDC, 10, 200, "출력 문자열", strlen("출력 문자열"));

               SelectObject(hDC, hFontOld);

               DeleteDC( hDC );

               pBackBuffer->ReleaseDC(hDC);

        }

        pBackBuffer->Release();

 

< 후면 버퍼 DC에 문자열 출력: daux02_font3_backbuffer.zip>

 

상당히 빠르게 동작하지만 문제는 Flags 변수를 D3DPRESENTFLAG_LOCKABLE_BACKBUFFER 으로 설정하면 Anti-Aliasing이 지원이 안됩니다. 또한 BeginScene()/EndScene() 밖에서 처리해야 하므로 문자열 출력을 위해서 전체 시스템 자체를 바꾸어야 하는 문제도 있습니다. 이 모든 것을 감수해도 가장 큰 문제는 후면버퍼에 직접 출력이 되어 게임 안 여러 팝업 창의 Overlap에서 모든 문자가 앞으로 보이는 문제를 제어하기가 어렵습니다.

 

 

3.2.4 텍스처를 이용한 출력

ID3DXFont는 문자열이 많아지면 내부 버퍼를 갱신하는 횟수도 증가해 렌더링이 느려지는 것입니다. 앞서 불투명한 서피스에 대해서 D3D9 DC를 가져와 사용할 수 있다고 했습니다. 이것을 잘 이용하면 자주 갱신이 안 되는 문자열을 텍스처에 출력해 가지고 있다면 렌더링의 속도는 분명히 증가할 것입니다.

텍스처에 문자열을 출력하기 위해 다음과 같이 FONT 객체, 텍스처, 서피스를 준비합니다.

 

HFONT                  m_hFont;

LPDIRECT3DTEXTURE9     m_pTex;

LPDIRECT3DSURFACE9     m_pSfc;

 

다음으로 폰트 객체를 만들고 텍스처는 D3DFMT_X8R8G8B8 형식으로 만듭니다. 텍스처 객체를 만든 후에 서피스를 텍스처에서 가져 옵니다. 이 부분은 daux02_font4_tex.zip CMain::Init()에 구현되어 있습니다.

 

// 1. 폰트 객체 생성

LOGFONT logf={0};

logf.lfHeight = 32;

_tcscpy(logf.lfFaceName, _T("궁서"));

m_hFont = CreateFontIndirect(&logf);

 

// 2. 텍스처 생성

hr = D3DXCreateTexture(…, 1, 0, D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &m_pTex);

 

// 3. 서피스 가져옴

m_pTex->GetSurfaceLevel(0, &m_pSfc);

 

텍스처를 생성할 때 밉(Mip) 레벨을 1로 하고, 윈도우 전환에도 문제 없도록 메모리 풀을 MANAGED로 설정하는 것이 좋습니다. 이제 서피스에서 DC를 얻고 문자열을 출력하면 됩니다. 다음 코드는 daux02_font4_tex.zip CMain::FrameMove() 함수에 구현되어 있습니다.

 

// 텍스처 서피스의 DC에 문자열 출력

if(m_pSfc)

{

        HDC hDC = NULL;

        m_pSfc->GetDC(&hDC);

        if(hDC)

        {

               TCHAR sMsg[256] = "";

               sprintf( sMsg, "텍스처 DC에 문자열 출력입니다");

               HFONT hFontOld = (HFONT)SelectObject(hDC, m_hFont);

               SetBkMode(hDC, TRANSPARENT);

               SetTextColor(hDC, RGB(255, 0, 255));

               TextOut(hDC, 50, 50, sMsg, strlen(sMsg));

               SelectObject(hDC, hFontOld);

               DeleteDC( hDC );

               m_pSfc->ReleaseDC(hDC);

        }

}

 

< 서피스의 DC에 문자 출력: daux02_font4_tex.zip>

 

 

3.2.5 텍스처 출력에 대한 폰트 클래스

이제 텍스처에 출력이 된다는 것을 알았습니다. 남은 것은 이 내용을 ID3DXFont처럼 사용하기 편리하게 클래스로 만드는 일입니다. daux02_lnfont_2d.zipCLnFont2D 클래스는 텍스처에 문자열을 출력하는 클래스 입니다.

CLnFont2D 클래스의 선언에 보면 문자열을 포함하는 텍스처는 다음과 같이 선언되어 있습니다.

 

LPDIRECT3DTEXTURE9     m_pTxD; // Output Texture

 

보통 문자열 이외의 공간은 전부 투명하게 해야 합니다. 그런데 서피스에서 DC를 가져올 수 있는 것은 불투명 텍스처 밖에 없습니다. 문자열의 배경을 투명하게 만들기 위해서 먼저 임시로 불투명 텍스처를 만들고 이 불투명 텍스처의 DC에 문자열을 출력합니다. 다음으로 알파가 있는 출력용 텍스처를 만들어서 불투명 텍스처 전부를 복사합니다.

CLnFont2D::SetTexture() 함수는 바로 이런 일을 처리합니다. 이 함수의 코드를 설명하면 먼저 다음과 같이 불투명 텍스처, 이 불투명 텍스처에 대한 서피스, 출력용 텍스처에 대한 서피스를 준비합니다.

 

LPDIRECT3DTEXTURE9     pTxS    = NULL;        // 불투명 텍스처

LPDIRECT3DSURFACE9     pSfTxtS = NULL;        // 불투명 텍스처의 서피스

LPDIRECT3DSURFACE9     pSfTxtD = NULL;        // 출력용 텍스처 m_pTxD의 서피스

 

다음으로 불투명 텍스처를 문자열의 폭과 높이만큼 텍스처를 만들고 서피스를 가져옵니다.

문자열 높이와 폭은 CLnFont2D::SetString(TCHAR* sStr) 함수에서 GetTextExtentPoint32() 함수와 GetTextMetrics() 함수를 통해서 계산합니다.

 

D3DXCreateTexture(…, D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &pTxS);     // 불투명 Texture 생성

pTxS->GetSurfaceLevel(0, &pSfTxtS);

 

이 불투명 텍스처의 DC 를 얻고 문자열을 씁니다.

 

pSfTxtS->GetDC(&hDC);

TextOut(hDC, …, m_sStr, m_iLen);

 

마지막 단계에서 알파가 있는 출력용 텍스처를 다시 생성하고 불투명 텍스처에서 알파가 있는 출력용 텍스처로 D3DXLoadSurfaceFromSurface() 함수를 이용해서 전부 복사를 합니다. 이 때 완전 불투명 검정색(0xFF000000)을 투명 값으로 설정합니다.

 

D3DXCreateTexture(…, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &m_pTxD); // 반투명 텍스처 생성

m_pTxD->GetSurfaceLevel(0, &pSfTxtD);

D3DXLoadSurfaceFromSurface( pSfTxtD, …, pSfTxtS, D3DX_DEFAULT, 0xFF000000);

 

0xFF000000 색상 값을 투명 키로 만들어서 하나의 색상(0xFF000000)을 사용할 수 없지만 전체적으로 텍스처를 이용하고 있어서 문자열이 자주 바뀌지 않는 한 다른 어떤 방법보다 렌더링 속도가 우수하다고 할 수 있습니다. 또한 자주 변하는 문자열을 따로 관리해서 이에 의한 렌더링 속도 저하를 막을 수 있습니다. 예를 들면 채팅과 같은 자주 문자열이 갱신되고 화면 스크롤이 필요한 문자열에 대해서 각 문자열 라인 별로 텍스처를 만들어서 문자열을 출력해 관리하면 문자열에 의한 속도 저하를 막을 수 있습니다.

외곽선 추출 알고리즘 없이 9번을 DC에 써서 간단하게 문자열에 테두리를 만들 수도 있습니다. 그리고 이런 방식을 Direct3DMobile에 적용을 하면 한글과 같은 2Byte 문자열을 쉽게 출력할 수 있습니다.

다음 그림은 텍스처에 문자열을 출력해서 만든 예제 입니다.

 

 

<텍스처에 문자열 출력 클래스: daux02_lnfont_2d.zip, daux02_lnfont_3d.zip>

 

DXSDK 2003 summer 버전까지 예제 중에 CD3DFont 클래스가 있었습니다. 이 클래스는 아스키 (ASCII) 코드에 대해서만 출력하고 있습니다. 그런데 재미있는 것은 아스키 문자열 중에서 특수 문자를 제외하고 아스키 번호 32(space) ~ 126까지 문자들을 텍스처에 미리 만들어 놓고, 문자열출력을 요청하면 화면 위치에 한 글자씩 출력을 합니다. 이 방법은 OpenGL에서도 비슷하게 이용하는 방법이니 나중에 시간이 있으면 들여다 보기 바랍니다.

 

 

3.3 FPS (Frames Per Second)

2D 게임 프로그래밍에서 Frame 계산을 배웠습니다. 프레임 계산은 평균을 이용 1초 동안 화면에 렌더링 하는 횟수를 계산 합니다. 따라서 가장 정확한 시간을 사용해야 프레임도 정확히 계산 될 수 있습니다.

윈도우 시스템은 시간 측정을 위해 GetTickCount() timeGetTime() 함수가 존재 합니다. 그런데 만약 윈도우 시스템을 사용하지 않는 모발일 시스템 같은 경우에는 C-Library를 사용할 수 밖에 없는 경우도 있습니다. 이 때 timeGetTime() 대신 clock() 함수를 사용합니다.

clock()함수는 대략 1/CLOCKS_PER_SEC 정도의 정확도를 가지며 윈도우 시스템 시간 보다 정확도는 많이 떨어지는 편이지만 C 언어가 지원되는 어떤 플랫폼에서도 사용할 수 있고 프레임이 높지 않은 경우에는 대안으로 사용할 수 있습니다.

 

다음은 clock()함수를 사용해서 프레임의 평균을 계산하는 코드 입니다.

 

FLOAT UseClock()

{

        static DOUBLE  fFps = 0.;

        static INT     iCnt = 0;

        static clock_t dBgn = clock();

        clock_t dCur = clock();

        ++iCnt;

 

        if(iCnt>=MaxCount)

        {

               fFps = DOUBLE(dCur - dBgn);

               fFps = iCnt*1000./fFps;

               iCnt = 0;

               dBgn = dCur;

        }

        return (FLOAT)fFps;

}

 

같은 코드에 시간 호출 함수를 윈도우의 timeGetTime(), 또는 GetTickCount() clock() 함수 자리에 바꾸어 놓으면 윈도우 시스템에서 잘 동작하는 프레임 계산 함수가 됩니다.

 

FLOAT UseTimeGetTime()

{

        static DOUBLE  fFps = 0.;

        static INT     iCnt = 0;

        static DWORD   dBgn = timeGetTime();

        DWORD   dCur = timeGetTime();  // GetTickCount()

        ++iCnt;

 

        if(iCnt>=MaxCount)

        {

               fFps = DOUBLE(dCur-dBgn);

               fFps = iCnt*1000./fFps;

               iCnt = 0;

               dBgn = dCur;

        }

        return (FLOAT)fFps;

}

 

만약 Intel CPU를 대상으로 한다면 다음의 GetIntelPrecisionTime() 함수와 같이 Inline Assembly를 이용해서 1/1,000,000 초 단위의 고해상도(High-Resolution) 시간을 측정할 수 있습니다. 이 함수는 clock() 또는 GetTickCount() 과 비슷한 동작을 위해 작성 되었습니다. 시간의 정밀도를 위해 double 형을 사용하고 있음을 주의하기 바랍니다.

 

DOUBLE GetIntelPrecisionTime()

{

        #define cpuid __asm __emit 0fh __asm __emit 0a2h

        #define rdtsc __asm __emit 0fh __asm __emit 031h

 

        static LARGE_INTEGER   Freq ={0};

        static BOOL            bEnable = 1;

        LARGE_INTEGER          dCur ={0};

        DOUBLE                 dTime =0;

 

        if(0 == bEnable)

               return -1;

 

        if(0 == Freq.QuadPart)

        {

               bEnable = QueryPerformanceFrequency(&Freq);

               if(0 == bEnable)

                       return -1;

        }

 

        __asm

        {

               cpuid

               rdtsc

               mov dCur.LowPart, eax

               mov dCur.HighPart, edx

        }

 

        dTime = DOUBLE(dCur.QuadPart)/DOUBLE(Freq.QuadPart);

        dTime *=1000.;

        return  dTime;

}

 

이전의 프레임 계산에서 clock(), 또는 timeGetTime()GetIntelPrecisionTime () 함수로 바꾸면 인텔 칩의 고해상도 타이머를 이용한 프레임 계산 함수가 됩니다.

 

FLOAT UseTimeGetPrecisionTime()

{

        static DOUBLE  fFps = 0.;

        static INT     iCnt = 0;

        static DOUBLE  dBgn = GetIntelPrecisionTime();

        DOUBLE  dCur = GetIntelPrecisionTime();

        ++iCnt;

 

        if(iCnt>=MaxCount)

        {

               fFps = dCur-dBgn;

               fFps = iCnt*1000./fFps;

               iCnt = 0;

               dBgn = dCur;

        }

        return (FLOAT)fFps;

}

 

윈도우 시스템에서는 고해상도 타이머를 위한 QueryPerformanceCounter()QueryPerformance-Frequency() 함수가 있습니다. 만약 CPU에서 고해상도 타이머를 지원이 되면 이 둘의 함수를 사용하는 것이 좋습니다.

먼저 QueryPerformanceFrequency() 함수를 가지고 진동수를 가져 옵니다. 만약 가져오기가 실패하면 0을 반환합니다. 다음으로 QueryPerformanceCounter() 함수를 사용해서 수행한 Counter를 얻어 옵니다. 이 값을 앞에서 구한 진동수로 나누면 프레임이 됩니다. 다음 함수는 이 두 함수를 사용해서 프레임을 계산하는 코드 입니다.

 

FLOAT UsePerformanceCounter()

{

        static DOUBLE          fFps = 0.;

        static INT             iCnt = 0;

        static LARGE_INTEGER   Freq = {0};

        static LARGE_INTEGER   dBgn = {0};

        static BOOL            bEnable=1;

        LARGE_INTEGER  dCur = {0};

 

        if(0 == bEnable)

               return -1.f;           // 하드웨어 지원이 안됨

 

        if(0 == Freq.QuadPart)        // 최초 함수 호출 시

        {

               bEnable = QueryPerformanceFrequency( &Freq);

               if(0 == bEnable)

                       return -1.f;

 

               QueryPerformanceCounter(&dBgn);

        }

 

        QueryPerformanceCounter(&dCur);

        ++iCnt;

 

        if(iCnt>=MaxCount)

        {

               DOUBLE fElapsedTime =

 DOUBLE(dCur.QuadPart - dBgn.QuadPart)/ DOUBLE(Freq.QuadPart);

 

               fFps = iCnt/fElapsedTime;

               iCnt = 0;

               dBgn = dCur;

        }

        return (FLOAT)fFps;

}

 

QueryPerformanceCounter() 에서 얻은 값을 QueryPerformanceFrequency() 함수에서 얻은 진동수로 나누면 시간이 되어 timeGetTime()을 대신할 수 있습니다. 이 때 주의할 것은 고해상도 타이머는 해상도가 1/1,000,000 초 입니다. timeGetTime() 1/1,000 초 단위이므로 이 둘을 맞추려면 고해상도 타이머부분에 1000.0을 곱해야 서로가 동일한 시간을 만들어 줍니다.

 

DOUBLE TimeGetPrecisionTime()

{

        static LARGE_INTEGER   Freq ={0};

        static BOOL            bEnable = 1;

        LARGE_INTEGER  dCur ={0};

        DOUBLE         dTime =0;

 

        if(0 == bEnable)

               return -1;

 

        if(0 == Freq.QuadPart)

        {

               bEnable = QueryPerformanceFrequency(&Freq);

 

               if(0 == bEnable)

                       return -1;

        }

 

        QueryPerformanceCounter(&dCur);

        dTime = DOUBLE(dCur.QuadPart)/DOUBLE(Freq.QuadPart);

        dTime *=1000.;

        return  dTime;

}

 

다음 그림은 이들 함수를 이용해서 프레임과 시간을 계산한 결과 입니다.

 

 

<30 프레임 평균, 4 프레임 평균: daux03_frame.zip>

 

프레임을 계산하는데 대략 30을 평균 횟수로 설정한 경우에는 왼쪽처럼 고해상도 또는 저해상도 타이머를 사용해도 거의 차이가 없습니다. 그런데 횟수를 줄이면 해상도가 낮은 clock(), timeGetTime() 함수는 계산이 엉망이 됩니다. 따라서 아주 빠른 시스템을 대상으로 게임을 만들 경우에는 고해상도 타이머를 이용하는 것이 유리할 것 같지만 꼭 그렇지도 않은 것이 실제로 게임의 렌더링 속도는 프로그래머들이 15~60 사이에 맞추는 경향이 있습니다. 게임이 1000 프레임 이상 안 되는 경우에는 여전히 timeGetTime()과 같은 함수로 프레임 계산을 해도 충분합니다.

 

 

3.4 한글 IME

한글 문자열은 ID3DXFont 등을 이용해 출력할 수 있었습니다. 그런데 한글 입력은 어떻게 처리할까요? 영어는 아스키 코드로 GetAsyncKeyState() 함수 등을 이용해 즉시 처리가 가능하지만 한글의 경우 글자를 조합해야 합니다.

윈도우 시스템은 입력기 또는 입력 방식 편집기라는 IME(Input Method Editor) 시스템을 이용해서 아스키 코드 이외의 문자들을 조합하며, IME를 사용하기 위해서는 윈도우의 메시지 시스템을 이용해야 합니다.

 

순수 한글만 출력하는 경우에 윈도우 메시지 WM_IME_STARTCOMPOSITION, WM_IME_COMPOSITION, WM_CHAR 세 가지만 사용합니다. WM_IME_STARTCOMPOSITION 메시지는 한글 입력을 막 시작할 때, WM_IME_COMPOSITION는 글자를 조합 중일 때, WM_CHAR는 한글 조합 이외에 아스키 코드가 입력될 때 처리를 합니다.

 

메시지 프로시저(Message Procedure) 함수에서 먼저 다음과 같이 WM_IME_STARTCOMPOSITION에서 조합이 시작 됐음을 인지 합니다. 코드에서는 아무 일도 안 하고 있는데 필요하다면 뭔가를 넣어주는 것도 좋습니다.

 

LRESULT CLcHangul::MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )

        INT     len = 0;

        HIMC    hIMC= 0;

        WPARAM  wLo = LOWORD(wParam);

 

if(WM_IME_STARTCOMPOSITION == uMsg)

        return TRUE;

 

다음으로 글자를 조합 중인 WM_IME_COMPOSITION 메시지 처리 입니다. 글자가 조합 중 또는 조합이 완료 되면 이 메시지로 옵니다. 먼저 IME 핸들을 ImmGetContext()함수를 통해서 가져옵니다. 물론 다 사용했으면 ImmReleaseContext() 함수로 자원을 반납 해야 합니다.

 

else if(WM_IME_COMPOSITION == uMsg)           // 글씨 조합 중

        hIMC = ImmGetContext(hWnd);           // Get IME Handle

 

순서상으로 글자의 조합 중인 것을 먼저 처리 하지 않고 조합이 완료된 것을 먼저 처리합니다. 완료에 대한 판단은 LPARAM 값을 GCS_RESULTSTR 값으로 비트 연산을 통해서 판별합니다. 완료된 문자열은 ImmGetCompositionString() 함수를 두 번 사용해서 얻는데 먼저 IME 핸들과 GCS_RESULTSTR 인수 만을 넣어서 문자열의 길이를 얻어 오고 다음으로 이 길이와 버퍼를 연결해서 버퍼를 통해서 문자열을 가져 옵니다.

 

if (lParam & GCS_RESULTSTR)

        len = ImmGetCompositionString(hIMC, GCS_RESULTSTR, NULL, 0); // 조합이 완료된 길이

 

        if( 0 < len)

        {

               ImmGetCompositionString(hIMC, GCS_RESULTSTR, "완성된 문자열 버퍼", len);

 

조합 중인 문자열도 화면에 출력을 해야 합니다. LPARAM 값에 GCS_COMPSTR 값으로 비트 연산을 하면 조합 중인지 알 수 있습니다. 앞의 완료된 문자열을 얻듯이 ImmGetCompositionString() 함수를 두 번 사용해서 조합 중인 문자열을 얻어 옵니다. 이 때는 GCS_RESULTSTR 대신 GCS_COMPSTR 값을 사용해야 합니다.

글자를 백 스페이스(Back Space) 키로 지워서 조합 중인 길이를 0으로 만들 때가 있습니다. 이 경우에는 조합중인 버퍼를 말끔히 비우는 것이 좋습니다.

 

else if (lParam & GCS_COMPSTR)

        len = ImmGetCompositionString(hIMC, GCS_COMPSTR, NULL, 0); // 조합중인 길이

              

               if(0==len)             // 조합 중에 Back space가 온 경우

                       EraseWord();

               else

                       ImmGetCompositionString(hIMC, GCS_COMPSTR, m_sWrd, len);

 

한글 입력을 다 처리 했으면 IME 핸들을 반환합니다.

 

ImmReleaseContext(hWnd, hIMC);

 

한글과 같이 영문도 출력해야 하므로 IME 작동이 아닐 때 아스키 문자들에 대해서 WM_CHAR 메시지에서 처리합니다.

 

else if(WM_CHAR == uMsg)

        if(wParam>=32 && wParam<127)

               m_sStr[strlen(m_sStr)] = wParam;

 

아스키 문자도 0~31 번까지는 통신, 제어에 관련된 문자입니다. 이들은 화면에 출력할 필요가 없으므로 32~126 사이의 문자만 출력하게 합니다.

 

<IME 한글: daux04_IME_hangul.zip>

 

예제 daux04_IME_hangul.zip IME를 사용해서 한글을 출력하는 예제 입니다. 한글을 입력하고 엔터 키를 누르면 윈도우 타이틀에 해당 문자열을 출력해 줍니다. 다시 한글 입력을 사용하려면 엔터 키를 한 번 더 누르면 됩니다.

 

 

3.5 Texture

3.5.1 밉맵 사용 비교

텍스처를 파일에서 읽어 오는 함수는 D3DXCreateTextureFromFileEx() 함수와 D3DXCreateTexture-FromFile() 함수를 가장 많이 사용하고 있습니다. 앞서 2D 게임 강의에서 2D 게임의 텍스처는 D3DXCreateTextureFromFileEx() 함수를 사용한다고 했습니다.

도움말을 보면 D3DXCreateTextureFromFile() 함수는 D3DXCreateTextureFromFileEx(pDevice, pSrcFile, D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_DEFAULT, D3DX_DEFAULT, 0, NULL, NULL, ppTexture)와 같다고 되어 있습니다.

 

텍스처의 필터링에서 우리는 밉맵을 배웠습니다. 밉맵은 Anti-Aliasing을 위해 원본 텍스처를 2의 승수로 하위 텍스처의 크기는 1/4씩 줄여 나가면서 만든다고 했습니다. 원본 텍스처와 하위 텍스처로 인해 늘어난 메모리는 등비 수열이 되어 대략 다음과 같이 계산이 됩니다.

 

  ~ 1.33

 

 

이 계산은 약 33 %의 메모리 증가를 보이고 있습니다. 실제로도 비슷한 결과가 되는지 살펴 봅시다.

daux05_texture1_mipmap.zip 는 밉맵과 밉맵 레벨을 1로 할 때에 사용하는 메모리를 비교 하고 있습니다. 메모리 비교를 위해서 MEMORYSTATUS 타입의 전역 변수를 설정하고 텍스처를 생성하기 전에 이 전역 변수에 시스템의 사용 메모리를 GlobalMemoryStatus() 함수를 이용해서 기록합니다.

 

MEMORYSTATUS   g_MmSt;        // 전역 변수

GlobalMemoryStatus(&g_MmSt);

 

대략 100 정도 D3DXCreateTextureFromFileEx(), D3DXCreateTextureFromFile() 함수로 같은 파일을 호출해서 이 둘의 메모리 사용을 비교해 보면 밉맵 레벨을 풀어 놓으면 약 30%정도 증가 하고 있음을 볼 수 있습니다.

3D는 밉맵이 필요하지만 2D는 밉맵이 전혀 필요가 없습니다. 따라서 밉맵 레벨을 조정할 수 있는 D3DXCreateTextureFromFileEx() 함수를 사용하면 D3DXCreateTextureFrom-File() 함수를 사용할 때보다 메모리를 30% 정도 절약할 수 있게 됩니다.

 

 

3.5.2 텍스처 파일 Merge

파일의 보호를 위해 머지(Merge: 합병)할 필요도 있습니다. 간단한 머지 방법은 전체 텍스처의 숫자를 파일에 기록하고 순차적으로 파일의 이름, 크기, 시작 포인터, 종료 포인터 텍스처 데이터를 반복해서 기록합니다.

 

<여러 가지 머지 방법>

 

좀 더 향상된 기술은 파일의 이름, 크기, 시작포인터, 종료 포인터 정보를 먼저 기록한 후에 텍스처의 파일을 헤더에 기록된 순서대로 머지 합니다. 아니면 텍스처 파일을 먼저 머지 한 후에 마지막에 파일 정보를 기록하는 방법도 있습니다.

어떤 방식으로 머지를 하든 다시 이 파일들을 풀어서 텍스처를 생성해야 합니다. 그런데 텍스처를 임시 폴더에 풀어서 D3DXCreateTextureFromFile(0 함수 등으로 텍스처를 생성하게 되면 하드 디스크를 사용하게 되어 파일이 많은 경우 로딩에 시간이 많이 듭니다.

 

D3D는 파일 이외에 메모리, 리소스에서 텍스처를 생성하는 함수가 있습니다.

D3DXCreateTextureFromFileInMemoryEx(),D3DXCreateTextureFromFileInMemory() 함수는 메모리에서 텍스처를 생성하며, D3DXCreateTextureFromResourceEx(),D3DXCreateTextureFromResource() 함수는 프로그램의 리소스에서 텍스처를 생성합니다.

daux05_texture2_merge_file.zip은 머지를 한 후에 3DXCreateTextureFromFileInMemoryEx() 함수로 메모리에서 텍스처를 생성하는 예제 입니다.

 

머지를 위해서 각 파일에 대한 정보를 기록할 수 있도록 다음과 같이 파일 이름, 크기, 시작 포인터, 끝 포인터를 저장할 수 있는 클래스가 필요합니다.

 

struct MrgTx

{

        char    sFile[256];            // File Name

        int     iSize;                 // File Size

        int     iBgn;                  // Starting point

        int     iEnd;                  // End point

};

 

typedef std::vector<MrgTx* >  lsTx;

 

머지를 실행하면 파일의 정보를 먼저 모으고 그 다음으로 하나의 파일에 기록을 합니다. 여기서는 앞의 머지 방법 그림 중에 가운데 부분 그림처럼 파일의 개수, 파일의 정보, 파일 내용을 기록하겠습니다. CMain::MergeFile() 함수가 다음과 같이 이를 구현하고 있습니다.

 

먼저 파일의 정보를 읽어와 머지 정보를 가지고 있는 벡터에 넣습니다.

 

void CMain::MergeFile()

lsTx    vTx;

 

// 1. Merge File Information

vTx.push_back(new MrgTx("Texture/seatosky1.jpg"));

vTx.push_back(new MrgTx("Texture/seatosky2.jpg",     vTx[vTx.size()-1]));

 

다음으로 머지 파일을 생성하고 파일의 개수를 기록한 다음벡터에 저장되어 있는 파일들의 정보를 기록합니다.

 

int     iN=vTx.size();

BYTE*   pSrc;

FILE* fpDst = fopen("Texture/Merge.mrg", "wb");

// 2. Write File Counter

fwrite(&iN, 1, sizeof(iN), fpDst);

 

// 3. Write File Information

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

{

        MrgTx* pTxF = vTx[i];

        fwrite(pTxF->sFile,    1, sizeof(pTxF->sFile), fpDst);

}

 

다음으로 파일을 통째로 읽어와서 머지 파일에 기록합니다.

 

// 5. Write File Data

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

{

        MrgTx* pTxF = vTx[i];

        fpSrc = fopen(pTxF->sFile, "rb");

        pSrc = new BYTE[pTxF->iSize];

        fread(pSrc, 1, pTxF->iSize, fpSrc);

        fclose(fpSrc);

 

        fwrite(pSrc, 1, pTxF->iSize, fpDst);

}

 

이렇게 머지 파일을 만들었으면 이것을 다시 풀어서 확인해 봐야 합니다. CMain::GetFileData() 함수는 new 연산자로 임시 버퍼를 만들고 머지 파일에서 파일 데이터를 읽어오는 역할을 합니다.

 

BYTE* CMain::GetFileData(int idx, int* iSize)

FILE* fp = fopen("Texture/Merge.mrg", "rb");

 

// 3. Read File Information

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

{

        MrgTx* pTxF = new MrgTx;

        vTx.push_back(pTxF);

        pTxF    = vTx[i];

        fread(pTxF->sFile,     1, sizeof(pTxF->sFile), fp);

        fread(&pTxF->iSize,    1, sizeof(pTxF->iSize), fp);

}

 

MrgTx* pTxF = vTx[idx];

 

// 4. Move File Point

fseek(fp, pTxF->iBgn, SEEK_SET);

// 6. Read File Data

pBuf = new BYTE[pTxF->iSize];

fread(pBuf, 1, pTxF->iSize, fp);

fclose(fp);

return pBuf;

 

 

CMain::GetFileData() 함수로 읽은 파일의 데이터에서 D3DXCreateTextureFromFileInMemoryEx()함수로 텍스처를 생성합니다. 이 예는 CMain::FrameMove()에 구현되어 있습니다.

 

HRESULT CMain::FrameMove()

int     iIdx = rand()%10;

INT     iSize=0;

BYTE*   pSrc = GetFileData(iIdx, &iSize);

…     

D3DXCreateTextureFromFileInMemoryEx(…);

 

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

 

< 합병한 파일에서 텍스처 생성: daux05_texture2_merge_file.zip>

 

 

3.5.3 Private Data

Private Data는 단어 뜻 그대로 내부적인 데이터로 D3D에서 IDirect3DResource9, IDirect3DBaseTexture9, IDirect3DCubeTexture9, IDirect3DTexture9, IDirect3DVolumeTexture9, IDirect3DIndexBuffer9, IDirect3DVertexBuffer9 등의 자원에 어떤 목적을 위해 보조적으로 할당한 자원을 저장하는 공간입니다.

예를 들어 D3D는 텍스처를 2의 승수로 만들어 버립니다. 2의 승수가 아닌 텍스처는 원본 정보를 어떤 식으로든지 저장을 해야만 렌더링에서 정확 이미지 영역을 표시할 수 있습니다. 그런데 만약 원본 정보를 넘길 수 없는 상황이 있다면 어떻게 할까요?

물론 원본 이미지 정보를 받을 수 있도록 시스템을 수정해야 하는 것이 가장 현명한 방법입니다. 이 마 저도 안될 때는 Private Data를 사용합니다.

 

다음과 같이 텍스처를 생성하고 원본 이미지의 정보를 new 연산자로 동적 할당한 다음에 해당 텍스처에 SetPrivateData() 함수로 연결합니다.

 

 

GUID IID_ILcTexInfo ={0x85c31227, 0x3de5, 0x4f00, 0x9b

, 0x3a, 0xf1, 0x1a, 0xc3, 0x8c, 0x18, 0xb5};

 

INT LcUtil_TextureLoad(…, LPDIRECT3DTEXTURE9* pTx, TCHAR* sFile,…)

D3DXIMAGE_INFO* pImg = new D3DXIMAGE_INFO;

D3DXCreateTextureFromFileEx(pDev, sFile, …, pImg, NULL, pTx);

(*pTx)->SetPrivateData( IID_ILcTexInfo, &pImg, sizeof(pImg), 0);

 

이렇게 연결하고 나서 다음과 같이 GetPrivateData() 함수를 통해서 이미지 정보를 가져와 사용합니다.

 

HRESULT CMain::Render()

DWORD          dSize= 0;

DWORD          dData= 0;

D3DXIMAGE_INFO* pData;

m_pTex1->GetPrivateData(IID_ILcTexInfo, &dData, &dSize);

 

pData = (D3DXIMAGE_INFO*)dData;

RECT    rc1={0,0, pData->Width, pData->Height};

 

m_pd3dSprite->Draw(m_pTex1, &rc1, …);

 

이렇게 동적 할당으로 만든 Private Data는 프로그램 해제할 때 같이 소멸시켜야 하는 것은 당연합니다. 또한 FreePrivateData() 함수를 호출해서 Private Data 사용을 해제합니다.

 

HRESULT CMain::Destroy()

DWORD          dData= 0;

D3DXIMAGE_INFO* pData;

 

m_pTex1->GetPrivateData( IID_ILcTexInfo, &dData, &dSize);

m_pTex1->FreePrivateData(IID_ILcTexInfo);

pData = (D3DXIMAGE_INFO*)dData;

delete pData;

 

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

 

<Private Data: daux05_texture3_privatedata.zip>

 

SetPrivateData() 함수를 호출할 때 첫 번째 인수는 구이드 번호(Guid Number) 라는 값으로 윈도우 시스템에서 만든 유일 값을 보장하는 수입니다. Visual Studio는 구이드 번호를 생성하는 프로그램이 GuidGen.exe 또는 uuidGen.exe 가 있습니다. 2005 버전의 GuidGen.exe를 찾아서 실행하면 다음 윈도우가 실행이 되며 Copy 버튼을 눌러 구이드 번호를 복사 합니다. New GUID 버튼을 누를 때마다 전세계에서 하나 밖에 없는 번호를 만들어 냅니다.

 

<GuidGen.exe>

 

이 번호를 복사 해서 다음과 같이 GUID 구조체에 채워서 SetPrivateData(), GetPrivateData() 함수 호출의 인수로 사용합니다.

 

GUID IID_ILcTexInfo ={0x85c31227, 0x3de5, 0x4f00

, 0x9b, 0x3a, 0xf1, 0x1a, 0xc3, 0x8c, 0x18, 0xb5};

 

 

3.5.4 Targa PNG

D3D DXSDK에서 이미지를 불러오는 함수가 지원이 있어 이미지를 읽고 쓰는 일들을 고민 안 해도 됩니다. 그런데 만약 D3D로 모바일 등의 기기에 이식하기 위한 Emulator를 만든다고 한다면 이 기기의 환경에 맞는 이미지 형식으로 코딩을 해야 합니다.

여러 이미지 포맷 중에서 가장 많이 사용하는 포맷은 Targa 형식입니다. Targa 파일은 읽고 쓰기가 편리해서 대부분의 게임 프로그래밍 책에 소개가 되어 있습니다. D3D를 공부하는 여러분도 Targa 파일 만큼은 DXSDK 함수를 사용하지 않고 읽고 쓰는 법을 나중에라도 연습하기 바랍니다.

 

daux05_texture4_ToTarga.zip 파일은 PNG 파일을 D3D 함수로 읽은 후에 서피스의 픽셀을 얻어와 Targa로 저장하는 예제 입니다. Targa의 읽고 쓰는 방법은 거의 일정합니다. 따라서 소스 코드만 보더라도 누구나 쉽게 이해 할 수 있습니다. 그래서 Targa에 대한 자세한 설명은 생략하고 주의점 정도만 보겠습니다.

 

Targa 파일을 읽는 방법은 고정이 되어 있어서 어떤 코드를 얻더라도 거의 같은 방식으로 구성되어 있습니다. 그런데 주의할 점은 픽셀을 읽어 오고 나서 이 픽셀이 24비트 이면 32 비트로 바꾸어 사용하는 것이 좋습니다.

다음 코드의 for 문에서 이러한 일을 처리하고 있습니다.

 

INT LoadTGAFile()

fp = fopen(filename, "rb");

// read bit depth

fread(&ImgBit, sizeof(BYTE), 1, fp);

// colormode -> 3 = BGR, 4 = BGRA

colorMode = ImgBit / 8;

 

// bgr ->bgra

if(3 == colorMode)

{

        BYTE* pPixel4  = new BYTE[ImgWidth * ImgHeight * 4];

 

        for (INT i = 0; i < imageSize/3; ++i)

        {

               INT idx1 =  i*3;

               INT idx2 =  i*4;

 

               pPixel4[idx2+0] = pPixel[idx1 + 0];

               pPixel4[idx2+1] = pPixel[idx1 + 1];

               pPixel4[idx2+2] = pPixel[idx1 + 2];

               pPixel4[idx2+3] = 0xFF;

        }

 

D3D는 색상 32 비트의 순서가 낮은 바이트부터 B->G->R->A 순으로 되어 있습니다. 만약 색상의 순서가 A->R->G->B로 구성되어 있는 시스템에서는 앞의 코드 for 문에서 이 순서를 바꾸어 주면 됩니다.

 

pPixel4[idx2+3] = pPixel[idx1 + 0];

pPixel4[idx2+2] = pPixel[idx1 + 1];

pPixel4[idx2+1] = pPixel[idx1 + 2];

pPixel4[idx2+0] = pPixel[idx1 + 2] 또는 0xFF;

 

이렇게 픽셀의 순서를 바꾸는 일은 다른 플랫폼에 이식할 때 또는 파일 형식이 다른 경우에 종종 발생합니다. 다음의 예는 DXSDK 함수로 생성한 텍스처를 Targa가 형식으로 저장하는 함수 입니다. 픽셀을 파일에 저장할 때 Targa 파일은 for 문 안에서 ARGB 형식을 BGRA로 바꾸고 있음을 볼 수 있습니다.

 

INT SaveTGAFile(…)

// ARGB -> BGRA

BYTE *b, *g, *r, *a;

 

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

{

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

        {

               BYTE* pData = pPixel + (  (imageHeight-1-j)*RealWidth + i)*4;

 

               a = pData + 3;

               r = pData + 2;

               g = pData + 1;

               b = pData + 0;

               fwrite(b, sizeof(BYTE), 1, fp);

               fwrite(g, sizeof(BYTE), 1, fp);

               fwrite(r, sizeof(BYTE), 1, fp);

               fwrite(a, sizeof(BYTE), 1, fp);

        }

}

 

daux05_texture4_ToTarga.zip 의 압축을 풀면 다음 그림의 왼쪽처럼 PNG 파일을 불러와서 Targa로 저장한 프로그램 Mck.exe가 있습니다. PNG 파일의 압축에 대한 알고리즘과 코드는 공개되어 있습니다. 오른쪽 그림은 http://zlib.net/ 사이트에서 PNG 파일의 입출력 코드를 얻어와 라이브러리를 만들어 DC에 적용시킨 화면입니다. wn_56_ReadPng 폴더에 헤더, 라이브러리, 실행 파일 McApi.exe가 있습니다.

 

 

< Targa 파일 저장과 PNG 로드: daux05_texture4_ToTarga.zip>

 

 

3.6 Anti-Aliasing

Aliasing은 고해상도 데이터가 저해상도 데이터로 샘플링 되는 것을 Aliasing이라 했습니다. 이 문제를 해결하는 Anti-Aliasing중에서 수퍼 샘플링 또는 멀티 샘플링 처리를 간단히 배웠습니다.

하드웨어가 얼마만큼의 멀티 샘플링 타입을 지원하는지 알아보려면 다음과 같이 최대 값부터 성공할 때까지 검사하면 됩니다.

 

DWORD dQualityLevels;

for(int nType=D3DMULTISAMPLE_16_SAMPLES; nType>=0; --nType)

{

        if(SUCCEEDED(m_pD3D->CheckDeviceMultiSampleType(D3DADAPTER_DEFAULT

                              , D3DDEVTYPE_HAL

                              , m_d3dpp.BackBufferFormat

                              , TRUE

                               , (D3DMULTISAMPLE_TYPE)nType

                              , &dQualityLevels)))

        {

               m_d3dpp.MultiSampleType = (D3DMULTISAMPLE_TYPE)nType;

               m_d3dpp.MultiSampleQuality = dQualityLevels-1;

               break;

        }

}

 

이렇게 찾은 후에 디바이스를 생성하면 되며 자동으로 Anti-aliasing이 적용이 됩니다. 화면의 전환에서 디바이스는 리셋이 되므로 다음과 같이 Anti-aliasing을 활성화 시켜야 합니다.

 

m_pd3dDevice->SetRenderState (D3DRS_MULTISAMPLEANTIALIAS, TRUE);

m_pd3dDevice->SetRenderState (D3DRS_ANTIALIASEDLINEENABLE, TRUE);

 

전체 코드는 daux06_antialias1.zip을 참고 하기 바랍니다. 다음 그림은 Anti-aliasing에 대한 예제 입니다. F2, F3 키를 누르면 Anti-aliasing의 비활성화/활성화를 설정 할 수 있습니다. 오른쪽 그림을 보면 Anti-aliasing을 활성화 하면 렌더링 속도가 감소된 것을 볼 수 있습니다.

 

 

<Anti-aliasing: daux06_antialias1.zip, daux06_antialias2.zip>

 

 

3.7 ID3DXMesh X-file

3.7.1 ID3DXMesh

Tutorial 예제와 주전자, 원통 등 확장 유틸리티를 통해서 ID3DXMesh의 사용을 배웠습니다. ID3DXMesh는 장면의 연출에 필요한 기본적인 정점 버퍼와 인덱스 버퍼를 가지고 있고, 렌더링 속도를 올리기 위해서 삼각형의 인덱스를 그룹(Group)으로 묶어 놓은 속성 버퍼를 가지고 있습니다.

 

보통 하나의 삼각형에 재질(Material) 또는 텍스처(같은 단계(stage)일 경우) 1개 밖에 설정이 불가능합니다. 만약 하나의 렌더링 오브젝트에 재질이 여러 개의 경우라면 각 정점을 재질에 맞게 분류해야 합니다. 이렇게 되면 재질만 다른 같은 삼각형이라도 복사를 해야 합니다.

속성 버퍼는 재질이 여러 개인 경우에 정점 버퍼를 각각 분리하지 않고, 각 재질에 따른 삼각형의 인덱스를 모아 놓은 자료 구조입니다. 속성 버퍼를 사용하면 정점 버퍼와 인덱스 버퍼를 그대로 유지하면서 서로 다른 재질 또는 텍스처를 적용할 수 있습니다.

 

<속성 테이블, 속성 버퍼, 인덱스 버퍼, 정점 버퍼>

 

ID3DXMesh 객체를 렌더링 할 때 DrawSubset() 함수를 호출한 적이 있습니다. DrawSubset() 함수의 인수에 전달하는 인덱스가 속성 테이블에 기록된 인덱스 입니다. 속성 테이블에는 아이디, Face 시작 인덱스, Face 개수, 정점의 시작 인덱스 정점의 개수가 기록되어 있습니다.

인덱스 버퍼를 만들 때 WORD 또는 DWORD형의 자료 구조를 그대로 사용하지 않고 다음과 같은 구조체를 사용한다면 Face 인덱스와 이 구조체의 인덱스를 동일하게 맞출 수 있습니다.

 

struct VtxIdx

{

        WORD a;    WORD b;    WORD c;

};

 

삼각형의 면(Face)를 구성하는 구조체가 없다면 여러분은 프로그램을 작성할 때 계산을 잘 해야 합니다.

 

연습을 위해 텍스처가 매핑 된 6 면체 ID3DXMesh 객체로 렌더링 해봅시다. 먼저 다음 같은 ID3DXMesh 객체를 선언합니다. 그리고 6면에 대해서 6장의 텍스처를 매핑 하는 것으로 하겠습니다.

 

ID3DXMesh*             m_pMsh;

INT                    m_nMtl;

LPDIRECT3DTEXTURE9     m_pTex[6];

 

다음 단계에서 Face의 개수, 정점 개수, FVF 등을 D3DXCreateMeshFVF() 함수의 인수로 전달하고 메쉬 객체를 생성합니다.

 

D3DXCreateMeshFVF("정점의 개수", "Face의 개수", …, FVF, …, &m_pMsh);

 

이 함수는 메쉬 객체의 정점과 인덱스의 공간을 만듭니다. 다음으로 Lock()/Unlock() 함수로 정점 또는 인덱스의 데이터를 변경합니다.

 

VtxUV1 pVtxS[24];

pVtxS[ 0] = VtxUV1(-6.F, -6.F, -6.F0.f, 0.f);

pVtxS[ 1] = VtxUV1(-6.F6.F, -6.F0.f, 1.f);

 

VtxIdx pIdxS[12];

pIdxS[ 0] = VtxIdx012);

pIdxS[ 1] = VtxIdx023);

 

// 정점, 인덱스 복사

VtxUV1* pVtx = NULL;

m_pMsh->LockVertexBuffer(0, (void**)&pVtx);

        memcpy(pVtx, pVtxS, sizeof(VtxUV1) * 24);

m_pMsh->UnlockVertexBuffer();

 

VtxIdx* pIdx = NULL;

m_pMsh->LockIndexBuffer(0, (void**)&pIdx);

        memcpy(pIdx, pIdxS, sizeof(VtxIdx) * 12);

m_pMsh->UnlockIndexBuffer();

 

정점과 인덱스 설정 만으로도 렌더링은 가능합니다. 매핑 여러 개라면 다음과 같이 속성 버퍼의 내용을 갱신합니다.

 

DWORD* attBuf = NULL;

m_pMsh->LockAttributeBuffer(0, &attBuf);

attBuf[ 0] = 0;

attBuf[ 1] = 1;

m_pMsh->UnlockAttributeBuffer();

 

앞서 인덱스 버퍼를 구성할 때 VtxIdx와 같은 구조체를 사용하는 것이 좋다고 했습니다. 삼각형에 대해서 속성 버퍼에 기록하는 인덱스는 VtxIdx 인덱스와 같습니다.

 

마지막 단계에서 속성 버퍼에 저장된 삼각형의 인덱스에 맞추어 렌더링 속도를 위해 최적화를 합니다. 이 단계는 선택 사항이며 만약 사용자가 속성 버퍼를 잘 구성 했다면 할 필요는 없습니다.

 

DWORD* pAdjacencyBuf = new DWORD[12 * 3];

 

m_pMsh->GenerateAdjacency(0.f, pAdjacencyBuf);

m_pMsh->OptimizeInplace(

        D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_COMPACT | D3DXMESHOPT_VERTEXCACHE

        , pAdjacencyBuf, 0, 0, 0);

 

delete [] pAdjacencyBuf;

 

OptimizeInplace() 함수의 최적화 옵션은 다음과 같습니다.

D3DXMESHOPT_COMPACT: 사용하지 않는 정점과 인덱스를 지우고 재 구성 합니다.

D3DXMESHOPT_ATTRSORT: 속성 버퍼의 내용을 참고해서 Face(삼각형) 정렬 합니다.

D3DXMESHOPT_VERTEXCACHE: Face를 재구성해서 버텍스 캐시에 대한 적중률을 높입니다.

D3DXMESHOPT_STRIPREORDER: 가능한 한 커다란 스트립으로 구성하도록 인덱스를 재 구성합니다.

D3DXMESHOPT_IGNOREVERTS: 버텍스 정보는 무시하고 인덱스 정보만 최적화 합니다

D3DXMESHOPT_DONOTSPLIT: 속성의 정렬 중에서 속성 그룹 사이에 공유되고 있는 정점들은 분할 안 합니다.

D3DXMESHOPT_DEVICEINDEPENDENT는 정점 캐시의 사이즈에 영향을 주며 하드웨어에서 문제 발생을 없애기 위해 디폴트 정점 캐시 사이즈가 지정됩니다.

 

속성 테이블의 내용을 알기 위해서 GetAttributeTable() 함수를 사용합니다. 이 함수를 사용하려면 먼저 다음과 같이 먼저 테이블 수를 가져오고 테이블 내용을 기록할 버퍼를 동적으로 만든 다음 이 버퍼와 테이블 수를 인수로 GetAttributeTable() 함수를 호출하면 내용을 가져올 수 있습니다.

 

DWORD   nNumSubset=0;

hr= m_pMsh->GetAttributeTable(0, &nNumSubset);

D3DXATTRIBUTERANGE* attTable= new D3DXATTRIBUTERANGE[nNumSubset];

hr= m_pMsh->GetAttributeTable(attTable, &nNumSubset);

delete [] attTable;

 

m_nMtl = nNumSubset;

 

전체 내용은 daux07_mesh01.zip을 참고 하기 바랍니다.

 

 

<사용자 정의에 의한 ID3DXMesh: daux07_mesh01.zip>

 

ID3DXMesh를 이용해서 장면을 연출하지 않고 메쉬 데이터를 복사해서 사용할 경우도 있습니다. 이 때 메쉬 객체 내부의 데이터를 가져오기 위해서 최소한 다음과 같은 지오메트리 구조체와 멤버 변수를 가지고 있어야 합니다.

지오메트리(Geometry) 구조체는 ID3DXMesh 객체의 Subset 1:1 관계입니다. 렌더링에 필요한 Face 숫자와, Face 리스트가 필요합니다.

 

 

struct McGeo                  // Geometry

{

        UINT    nFce;          // Face 숫자

        UINT    nVtx;          // 정점 숫자

        VtxIdx* pIdx;          // Face 리스트

};

 

지오메트리를 가지고 있을 클래스의 멤버 변수는 지오메트리의 숫자와 데이터를 가져야 하고, ID3DXMesh 객체에서 정점 버퍼를 복사해와야 하므로 정점의 수, 정점 하나의 크기, FVF, 정점 버퍼를 있어야 합니다.

 

INT            m_nGeo;        // 지오메트리 수

McGeo*         m_pGeo;        // 지오메트리

UINT           m_nVtx;        // 정점의 개수

UINT           m_dVtx;        // 정점 하나의 크기

DWORD          m_dFVF;        // FVF

void*          m_pVtx;        // 정점 버퍼

 

ID3DXMesh 객체에서 데이터를 꺼내오는 방법은 다음과 같이 먼저 정점의 수, 인덱스(Face)의 수를 얻어 옵니다.

 

ID3DXMesh* pMeshSrc = "ID3DXMesh 객체 생성 함수";

INT     nVtxS= pMeshSrc->GetNumVertices();

INT     nFceS= pMeshSrc->GetNumFaces();

 

렌더링에 필요한 정점 하나의 크기, FVF, 정점 버퍼를 생성합니다. 정점의 포맷은 알지 못하기 때문에 malloc() 함수 등으로 메모리만 확보합니다.

 

m_nVtx  = nVtxS;

m_dVtx  = (UINT)pMeshSrc->GetNumBytesPerVertex();

m_dFVF  = pMeshSrc->GetFVF();

m_pVtx  = malloc(m_nVtx * m_dVtx);

 

ID3DXMesh 객체에서 정점을 복사합니다.

 

VtxUV1* pVtxS=NULL;

pMeshSrc->LockVertexBuffer(0, (void**)&pVtxS);

        memcpy(m_pVtx, pVtxS, sizeof(VtxUV1) * nVtxS);

pMeshSrc->UnlockVertexBuffer();

 

인덱스는 지오메트리에 기록될 것입니다. 따라서 ID3DXMesh에 있는 인덱스는 임시 버퍼를 만들고 여기에 복사합니다.

 

VtxIdx* pFce = new VtxIdx[nFceS];

 

VtxIdx* pIdxS=NULL;

pMeshSrc->LockIndexBuffer(0, (void**)&pIdxS);

        memcpy(pFce, pIdxS, sizeof(VtxIdx) * nFceS);

pMeshSrc->UnlockIndexBuffer();

 

지오메트리 객체의 내용을 설정하기 위해서 속성 테이블의 개수와 내용을 가져옵니다. 속성 테이블의 숫자는 지오메트리의 숫자와 동일하게 하고 이 개수만큼 지오메트리를 만듭니다.

 

DWORD   nNumSubset=0;

hr= pMeshSrc->GetAttributeTable(0, &nNumSubset);

 

// Geometry 숫자를 설정한다.

m_nGeo = nNumSubset;

m_pGeo = new McGeo[m_nGeo];

 

D3DXATTRIBUTERANGE* attTable = new D3DXATTRIBUTERANGE[nNumSubset];

hr= pMeshSrc->GetAttributeTable(attTable, &nNumSubset);

 

속성 테이블의 내용을 가지고 지오메트리의 인덱스(Face)를 구성합니다.

 

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

{

        McGeo* pGeo = &m_pGeo[i];

        pGeo->nFce     = attTable[i].FaceCount;

        pGeo->nVtx     = attTable[i].VertexCount;

        pGeo->nFceB    = attTable[i].FaceStart;

 

        // Geometry의 인덱스 버퍼 생성

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

 

        // Index Buffer 복사

        VtxIdx* pIdxDst = pFce + pGeo->nFceB;

        memcpy(pGeo->pIdx, pIdxDst, sizeof(VtxIdx) * pGeo->nFce);

}

 

장면을 구성할 정점 버퍼, 지오메트리의 내용을 다 채웠습니다. ID3DXMesh객체와 임시로 사용한 인덱스 버퍼, 속성테이블은 해제합니다. 렌더링은 다음과 같이 정점 버퍼와 지오메트리의 정보를 이용합니다.

 

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

{

        McGeo* pGeo = &m_pGeo[i];

        m_pDev->SetTexture( 0, m_pTex[i] );

        m_pDev->SetFVF(m_dFVF);

        m_pDev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, m_nVtx

, pGeo->nFce, pGeo->pIdx, D3DFMT_INDEX16, m_pVtx, m_dVtx);

}

 

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

 

 

<ID3DXMesh 객체의 내용을 복사한 지오메트리로 렌더링: daux07_mesh02.zip>

 

DXSDK는 연속된 정점 데이터에 대해서 충돌에 대한 경계상자(Bounding Box)와 경계 구(Bounding Sphere)를 구해주는 함수가 있습니다. 만약 ID3DXMesh 객체에 대한 Bounding Box, Sphere는 다음과 같이 합니다.

 

D3DXVECTOR3* pVtx=NULL;

D3DXVECTOR3 vcMax; D3DXVECTOR3 vcMin; D3DXVECTOR3 vcCenter; FLOAT fRadius;

 

DWORD nVtx     = m_pMsh->GetNumVertices();

DWORD dFVF     = m_pMsh->GetFVF();

DWORD dStride  = D3DXGetFVFVertexSize(dFVF);

 

pMesh->LockVertexBuffer(D3DLOCK_READONLY, (void**)&pVtx);

D3DXComputeBoundingBox((D3DXVECTOR3*)pVtx, nVtx, dStride, &vcMin, &vcMax);

D3DXComputeBoundingSphere((D3DXVECTOR3*)pVtx, nVtx, dStride, &vcCenter, &fRadius);

pMesh->UnlockVertexBuffer();

 

 

3.7.2 X-file

3D 기초 Tutorial에서 호랑이 X-file을 우리는 화면에 올려 보았습니다. X-file 장면에 대한 렌더링 오브젝트의 정보를 가지고 있는 X-file에 대한 해석은 DXSDK에서 지원 되어 별도의 노력이 필요 없습니다. 다루기가 쉬워서 게임 개발 기간 중에 테스트 파일로 가장 많이 사용되는 포맷입니다. 게임 회사는 오브젝트에 대한 자체 포맷이 있습니다. 그런데 이 포맷을 개발하는 단계에서 제대로 작동하는지 비교를 하기 위해서 X-file을 가장 많이 사용합니다. 게임 프로그래밍을 처음 배우는 사람들도 게임 오브젝트를 X-file로 많이 가져갑니다. 따라서 X-file에 대한 클래스를 만들어 놓는 것은 필 수 입니다.

 

클래스를 만들기 위해 필요한 자료구조를 모으니 다음과 같이 메쉬, 재질, 텍스처 객체로 모아집니다.

 

class CMcXFile

        LPDIRECT3DDEVICE9      m_pDev;

        char                   m_sFile[MAX_PATH];

        ID3DXMesh*             m_pMsh;

        D3DMATERIAL9*          m_pMtl;

        LPDIRECT3DTEXTURE9*    m_pTex;

        INT                    m_nMtl;

 

파일에서 D3DXLoadMeshFromX() 함수로 메쉬를 읽어오는 것은 호랑이를 올려 보았으면 이미 알고 있는 내용입니다.

 

ID3DXBuffer* AdjBuf = NULL;

ID3DXBuffer* MtlBuf = NULL;

hr = D3DXLoadMeshFromX(sFile, D3DXMESH_SYSTEMMEM, m_pDev

                       , &AdjBuf, &MtlBuf, 0, &nMtl, &m_pMsh);

 

다음으로 ID3DXMesh에서 배운 삼각형의 인접 정보를 이용한 최적화를 진행합니다.

 

DWORD dOpt  = D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_COMPACT | D3DXMESHOPT_VERTEXCACHE;

DWORD* pAdj = (DWORD*)AdjBuf->GetBufferPointer();

hr = m_pMsh->OptimizeInplace(dOpt, pAdj, 0, 0, 0);

 

DXSDKID3DXBuffer 인터페이스는 자료를 연속적인 메모리 공간에 저장하기 위해서 범용적으로 만든 자료 구조입니다. ID3DXBuffer 인터페이스는 자료 자체를 가져오는 GetBufferPointer()와 저장된 자료의 크기(Byte)를 가져오는 GetBufferSize() 함수가 있습니다.

GetBufferPointer() 함수로 자료의 시작 주소를 가져와서 필요한 경우에 캐스팅해서 사용합니다. ID3DXBuffer 객체의 Release() 함수를 호출하면 ID3DXBuffer 객체 내부에 저장된 자료들도 같이 소멸합니다. 이 객체는 이후 쉐이더에서 에러 등의 메시지를 반환 받을 때도 사용됩니다.

D3DXCreateBuffer() 라는 함수를 사용해서 직접 연속된 메모리를 확보할 수 있습니다.

 

삼각형 최적화가 끝나면 재질과 텍스처를 생성하고 재질 먼저 복사 합니다.

 

// 재질과 텍스처 생성

m_pMtl = new D3DMATERIAL9[m_nMtl];

m_pTex = new LPDIRECT3DTEXTURE9[m_nMtl];

 

// 텍스처 포인터 초기화

memset(m_pTex, 0, sizeof(LPDIRECT3DTEXTURE9) *m_nMtl);

 

// 재질 데이터 가져옴

D3DXMATERIAL* pMtl = (D3DXMATERIAL*)MtlBuf->GetBufferPointer();

 

// 재질 복사.

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

        memcpy(&(m_pMtl[i]), &(pMtl[i].MatD3D), sizeof(D3DMATERIAL9));

 

텍스처는 조금 주의해야 합니다. X-file 내부에 저장된 이미지 경로와 실제 이미지의 위치가 다를 수 있습니다. 가장 좋은 방법은 해당 X-file에 텍스처를 같이 놓거나 아니면 X-file이 있는 폴더의 특정이름이 있는 하위 폴더에 텍스처를 가지고 있으면 코딩의 일관성이 생깁니다.

여기서는 같은 경로에 있다고 생각하고 다음과 같이 _splitpath() 함수와 _makepath() 함수로 이미지 경로를 조정해서 텍스처가 생성될 수게 있게 합니다.

 

char    sParentPath[MAX_PATH]={0}; char       dir[_MAX_DIR]={0};

_splitpath( sFile, NULL, dir, NULL, NULL);

_makepath(sParentPath, NULL, dir, NULL, NULL);

 

// 텍스처 생성        

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

{

        char sTexFile[MAX_PATH]={0};

char fname[_MAX_FNAME]={0};

char ext[_MAX_EXT]={0};

        char* sTexSrc = pMtl[i].pTextureFilename;

 

        if(NULL == sTexSrc || strlen(sTexSrc)<3)

               continue;

 

        _splitpath( sTexSrc, NULL, NULL, fname, ext);

        _makepath(sTexFile, NULL, sParentPath, fname, ext);

 

        IDirect3DTexture9* pTx = NULL;

        D3DXCreateTextureFromFile(m_pDev, sTexFile, &pTx);

        m_pTex[i] = pTx;

 

전체 코드는 daux07_xfile01.zip을 참고하고 CMcScene::Create() 함수의 주석을 바꾸어보면 다음과 같은 Dwarf와 건물을 볼 수 있습니다.

 

 

<x-file: daux07_xfile01.zip>

 

daux07_xfile01.zip Dwarf를 실행하면 건물보다 상당히 작게 나옵니다. 월드 행렬을 바꾸어서 바꾸면 크게 출력할 수 있을 것입니다.

이렇게 X-file의 클래스 작업은 끝이 나 보입니다. 그런데 여기서 잠깐 생각해봐야 할 것이 있습니다. 만약 같은 모델의 Dwarf를 여러 개 출력하게 된다면 매 번 파일에서 읽어와야 할까요?

매번 파일에서 불러와 생성하면 보조 기억 장치를 접근하는 것이므로 렌더링 속도의 저하는 불을 보듯 뻔합니다. 그렇다면 하나의 모델을 가지고 복제해서 사용하는 방법을 생각할 수 있습니다.

 

메쉬의 기하 정보를 복사하는 CloneMeshFVF() 함수를 통해서 정점과 인덱스 등을 복사 하고 텍스처는 변하는 일이 없으므로 원본에서 참조하도록 하면 될 것 같습니다. 그런데 이 방법도 더 생각해 봐야 합니다. 파티클과 같이 정점의 위치를 파이프라인에 올리기 전에 바꾸어야 하는 것은 기하 정보를 반드시 복사를 해야 하지만 렌더링 파이프라인의 월드 행렬만 바꾸어도 되는 오브젝트는 복사가 아닌 원본을 참조하는 방법으로 오브젝트를 구성해야 합니다.

 

 이런 좋은 아이디어가 있어도 막상 코드로 표현 하는 것이 어려울 수도 있습니다. 이럴 때 무조건 이전의 코드를 다 바꾸지 말고 잘 구성되어 있다면 새로운 클래스를 추가해서 관계를 만들어 가는 것이 더 좋습니다.

 

그림처럼 IMcXFile를 순수 가상함수들로 구성된 인터페이스를 하나를 만들고 CMcXFile 클래스는 이를 상속합니다. 그리고 렌더링 오브젝트인 CMcXFileIns 클래스는 IMcXFile 클래스를 상속 받고 CMcXFile 클래스를 포함합니다.

여기서 원칙을 하나 만들면 CMcXFile은 렌더링이 가능해도 그 역할을 하지 않고 오직 CMcXFileIns 클래스만 장면을 렌더링 하게 하면 CMcXFile의 인스턴스는 메모리 복제용으로만 사용하게 됩니다.

 

<X-file 객체를 표현하기 위한 클래스 구조>

 

이것을 코드로 구현해 보면 먼저 IMcXFile의 인터페이스를 다음과 같이 만듭니다.

 

#define interface struct

 

interface IMcXFile

{

        virtual INT    Create(…, char* sFileName=NULL, void* pOriginal=NULL)=0;

        virtual void   Destroy()=0;

        virtual INT    FrameMove()=0;

        virtual void   Render()=0;

 

        virtual IMcXFile* GetOrigin()=0;

        virtual void   SetWorldMatrix(const D3DXMATRIX*)=0;

        virtual D3DXMATRIX* GetWorldMatrix()=0;

};

 

Create(), Destroy(), FrameMove(), Render()함수는 객체의 생성, 소멸, 데이터 갱신, 렌더링에 대한 함수 입니다. GetOrigin() 함수는 X-file의 내용을 갖고 있는 CMcXFile의 객체를 반환하는 함수 입니다. 각 렌더링 오브젝트는 월드 행렬이 있어야 하므로 월드 행렬을 설정하거나 가져올 수 있는 함수를 준비합니다.

 

객체의 생성은 다음과 같은 함수로 클래스의 인스턴스를 만듭니다.

 

INT McXCreate_Xfile(IMcXFile** pOut, LPDIRECT3DDEVICE9 pDev, char* sFile, void* pOriginal)

{

        IMcXFile*      pObj= NULL;

 

        if(sFile)

               pObj = new CMcXFile;

        else if(pOriginal)

               pObj = new CMcXFileIns;

        else{   *pOut = NULLreturn -1;     }

 

        if(FAILED(pObj->Create(pDev, sFile, pOriginal)))

        {

               delete pObj;   return -1;

        }

 

        *pOut = pObj;

        return 0;

}

 

이 함수는 해당 인스턴스를 어느 클래스에서 만들지 결정합니다. 만약 파일 이름이 주어지면 CMcXFile 클래스의 객체를 만들고 파일 이름 대신 CMcXFile 객체에 대한 Original 포인터가 주어지면 복제에 대한 요구로 받아 들여 CMcXFileIns 클래스의 객체를 만듭니다.

CMcXFile 클래스는 다음과 같이 IMcXFile 인터페이스를 상속 받고 복제에 필요한 정보를 전달 할 수 있도록 몇 개의 메소드를 추가했습니다.

 

class CMcXFile : public IMcXFile

public:

        ID3DXMesh*             GetMesh()      {       return m_pMsh; }

        D3DMATERIAL9*          GetMaterial()  {       return m_pMtl; }

        LPDIRECT3DTEXTURE9*    GetTexture()   {       return m_pTex; }

        INT                    GetNumMaterial(){      return m_nMtl; }

 

렌더링 역할을 하게 될 CMcXFileIns는 원본 CMcXFile 클래스의 객체와 장면 구성에 필요한 월드 행렬을 멤버로 구성합니다. 또한 외부에서 월들 행렬을 설정하거나 가져올 수 있는 멤버 함수를 구현합니다.

 

class CMcXFileIns : public IMcXFile

        CMcXFile*      m_pOrg;        // Original X-file Instance

        D3DXMATRIX     m_mtWld;

 

public:

        virtual IMcXFile* GetOrigin()         {       return m_pOrg;         }

        virtual void   SetWorldMatrix(const D3DXMATRIX* v){  m_mtWld = *v;  }

        virtual D3DXMATRIX* GetWorldMatrix()  {       return &m_mtWld;       }

 

남은 것은 CMcXFileIns 클래스에서 렌더링에 필요한 데이터 초기화 입니다. CMcXFileIns:: Create() 함수를 보면 다음과 같이 간단하게 처리하고 있음을 볼 수 있습니다.

 

INT CMcXFileIns::Create(, void* pOriginal)

        m_pOrg = (CMcXFile*)pOriginal;

        m_pMsh = m_pOrg->GetMesh();

        m_pMtl  = m_pOrg->GetMaterial();

        m_pTex  = m_pOrg->GetTexture();

        m_nMtl  = m_pOrg->GetNumMaterial();

 

메쉬, 재질, 텍스처에 대한 주소를 가질 필요는 없지만 렌더링 함수에서 매번 원본에서 호출하는 부담을 덜기 위해서입니다.

 

이 클래스의 Render()함수는 다음과 같이 월드 행렬을 설정하고 원본의 메쉬, 재질 등의 주소를 저장한 포인터로 렌더링 합니다.

 

void CMcXFileIns::Render()

        m_pDev->SetTransform(D3DTS_WORLD, &m_mtWld);

 

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

        {

               m_pDev->SetMaterial( &m_pMtl[i] );

               m_pDev->SetTexture( 0, m_pTex[i] );

               m_pMsh->DrawSubset( i );

        }

 

        static D3DXMATRIX mtI(1,0,0,00,1,0,00,0,1,00,0,0,1);

        m_pDev->SetTransform(D3DTS_WORLD, &mtI);

 

마지막으로 테스트가 남았습니다. daux07_xfile02.zipCMcScene 클래스 선언을 보면 다음과 같이 원본과 원본의 데이터를 참조하는 3개의 렌더링 오브젝트 인스턴스를 볼 수 있습니다.

 

class CMcScene

        IMcXFile*      m_pXOrg;

        IMcXFile*      m_pXClone1;

        IMcXFile*      m_pXClone2;

        IMcXFile*      m_pXClone3;

 

3개의 렌더링 오브젝트들은 CMcScene::Create() 함수에서 다음과 같이 원본을 먼저 생성한 다음 이 인스턴스를 이용해서 생성합니다.

 

INT CMcScene::Create(LPDIRECT3DDEVICE9 pDev)

        hr = McXCreate_Xfile(&m_pXOrg, m_pDev, "xfile/dwarf/dwarf.x");

 

        hr = McXCreate_Xfile(&m_pXClone1, m_pDev, NULL, m_pXOrg);

        hr = McXCreate_Xfile(&m_pXClone2, m_pDev, NULL, m_pXOrg);

        hr = McXCreate_Xfile(&m_pXClone3, m_pDev, NULL, m_pXOrg);

 

월드 행렬 갱신과 렌더링은 CMcScene::FrameMove() 함수와 CMcScene::Render() 함수에서 구현되어 있으니 참고 하기 바랍니다.

daux07_xfile02.zip를 실행하면 다음과 같이 Dwarf 3개가 있는 화면을 볼 수 있습니다.

 

<X-file 클래스에 대한 추상화: daux07_xfile02.zip>

 

 

3.8 X-file Animation

3.8.1 Skinning Animation 기초

정적인 모델의 DXSDK Sample 폴더에 SkinnedMesh 예제가 있습니다. 이 예제는 애니메이션 정보가 포함되어 있는 X-file을 화면에 출력해 줍니다.

 

<DXSDK Skinning 예제>

 

3D 게임에서 애니메이션이라는 것은 간단하게 시간에 대한 정점의 이동이라 할 수 있겠습니다. 이 정점을 이동 시키는 방법은 시간에 대해서 오브젝트의 모든 위치를 저장한 데이터에서 가져오는 방법이 있습니다. 이 방법은 가장 빠르고 정확한 애니메이션을 만들어 내지만 문제는 정점의 숫자와 애니메이션의 시간에 곱에 메모리를 엄청나게 필요로 합니다.

이의 개선 책으로 많이 선택하는 것이 자료 구조 중 나무 구조(tree structure)처럼 오브젝트를 구성하고 있는 개별적인 Geometry를 부모와 자식간의 관계로 설정하고, 시간에 대한 변화를 각 Geometry의 행렬로 저장해서 해당 시간에 부모의 행렬을 자신의 지역 행렬에 곱해 애니메이션을 하는 방법이 있습니다. 이 방법을 사용하면 시간이 늘어나도 애니메이션 정보의 행렬 만 커질 뿐 정점의 개수와는 무관하게 되어 효율적인 프로그램이 되고 현재 대부분의 게임은 이 방법을 채용하고 있습니다

3D에서 하나의 행렬을 하나의 오브젝트에 가하면 오브젝트를 구성하는 정점 사이의 관계 특히, 거리의 비율은 이전과 변함이 없습니다. 이렇게 이전의 특성이 유지되는 애니메이션을 딱딱한 애니메이션(Rigid body: 강체)라 부르는 강체 애니메이션입니다. 강체라는 것은 순전히 물리학에서 온 용어로 어떤 외부의 힘을 가해도 그 성질이 변하지 않는 실 세계는 존재하지 않는 가상의 물체입니다. 강체 애니메이션을 화면에 표현하면 동작이 로봇처럼 딱딱합니다.

 

Mechatronics를 배경으로 게임을 만든다면 강체 애니메이션 만으로도 충분합니다. 그런데 이 강체 애니메이션을 사람이나 동물을 주요 소재로 하는 게임에 적용하면 팔과 같은 관절 부위에서 피부가 다른 피부를 파고 들거나 벌어지는 현상이 만들어져 자연스러움이 많이 감소됩니다.

특히 격투 게임처럼 캐릭터가 화면 가득 채우는 상황이라면 강체 애니메이션은 조금 부족합니다. 과거에는 이런 문제를 팔이나 다리에 보호대 또는 갑옷을 덧씌워서 해결을 했으며 지금도 많이 사용하는 멋진 방법입니다. 최근에는 Skinning 이라는 방식이 도입되어 관절 부위의 부 자연스러운 모습을 없앴습니다. 이로 인해 게임 캐릭터의 육감적이고 야성적인 모습을 나타내기 위해 걸치는 옷감의 양이 많이 줄었습니다.

 

<강체 애니메이션과 Skinning 애니메이션>

 

Skinning 애니메이션 구현은 의외로 간단합니다. 정점을 구성하는 Geometry 중에서 관절 부위에 해당하는 점들은 해당 Geometry의 행렬뿐만 아니라 인접한 Geometry의 행렬도 같이 적용하는 것입니다.

그림에서 강체 애니메이션의 정점 p Geometry A의 행렬에만 영향을 받지만 Skinning 애니메이션의 정점 p Geometry B의 행렬에도 영향을 받습니다. 정점에 영향을 주는 정도를 비중(Weight)로 설정해서 수식으로 표현 하면 다음과 같습니다.

 

정점 p의 변환 = (p * Geometry A 행렬) * Geometry A의 비중

 + (p * Geometry B 행렬) * Geometry B의 비중

 

단순하게 2개의 행렬 만 관절부위의 정점에 적용하면 벌어지거나 겹치는 문제는 해결이 되지만 부드러운 곡면을 만들기가 어렵습니다. 이를 위해 여러 개의 행렬을 적정에 적용을 합니다.

 

정점 p의 변환 = (p * Geometry A 행렬) * Geometry A의 비중

 + (p * Geometry B 행렬) * Geometry B의 비중 + …

+ (p * Geometry M 행렬) * Geometry M의 비중

 

수학 기호를 사용해서 표현하면 영향을 주는 행렬을 , 비중을 라 하면 변환 후의 정점 위치 는 다음과 같이 표현 됩니다.

 

 

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

 

 , 

 

이 됩니다. 우측의 식은 이전의 식처럼 각 행렬마다 정점을 변환 시키는 것이 아니라 각 행렬에 대해서  계산을 먼저하고 이들을 더한 전부 더한 다음에 정점의 변환을 수행하기 때문에 처리 속도에 이득입니다. 그리고 모든 비중의 합을 1로 유지합니다. 만약 1보다 크거나 작으면 정점의 위치에 크기 변환이 적용된 것과 같은 상황을 만듭니다. 정점에 하나의 비중만 있고 값이 1이라면 이 애니메이션은 강체 애니메이션과 같습니다. 따라서 강체 애니메이션은 Skinning 애니메이션의 부분 집합이라 할 수 있습니다.

이렇게 간단한 수식 조차도 하드웨어에서 지원되기 시작한 것은 최근의 일입니다.

 

고정 기능 파이프라인에서 D3D는 하나의 정점에 최대 4개까지 영향을 주는 행렬을 설정 할 수 있고 최대 255개까지 영향을 주는 행렬들을 만들 수 있습니다. (쉐이더를 사용하면 이 제한은 거의 없습니다.) 하지만 하드웨어는 D3D 뜻대로 정점에 4개의 행렬을 연결할 수 있지만 영향을 줄 수 있는 행렬들의 총 수가 255개가 안될 수 있습니다.

 

그래서 먼저 프로그램을 적용하기 전에 하드웨어에서 지원되는 행렬의 최대 숫자를 다음과 같이 확인 합니다.

 

D3DCAPS9 d3dCaps;

m_pDev->GetDeviceCaps( &d3dCaps );

m_nMaxMatrixSize = d3dCaps.MaxVertexBlendMatrixIndex;

 

간단한 Skinning 예제로 "태극기 휘날리며" 를 만들어 봅시다. 먼저 다음과 같이 화면에 태극기를 하나 출력합니다.

 

 

<태극기 정점>

 

태극기가 바람에 제대로 펄럭이도록 정점 버퍼를 그림의 왼쪽처럼 삼각형을 40개 정도 생성합니다.

우리의 목표는 왼쪽에 표시된 삼각형을 움직이는 것으로 정점 마다 행렬을 각각 따로 설정할 것입니다. 오른쪽 화면과 같은 정점을 출력하기 위해서 가지는 구조체는 다음과 같을 것입니다.

 

struct VtxUV1

{

        D3DXVECTOR3    p;

        FLOAT          u, v;

        enumFVF = (D3DFVF_XYZ |D3DFVF_TEX1),      };

};

이 구조체를 개량해서 행렬의 인덱스와 비중을 설정합니다. 예를 들어 정점 하나에 영향을 주는 행렬을 두 개로 설정한다면 정점 구조체는 다음과 같이 만들어야 합니다.

 

struct VtxUV1

{

        D3DXVECTOR3    p;

 

        FLOAT           g;      // 비중(Weight)

        BYTE            m[4];   // 행렬의 인덱스

        enumFVF = (D3DFVF_XYZB2 | D3DFVF_LASTBETA_UBYTE4 | …),  };

};

 

구조체를 보면 비중은 float 1개로 설정되어 있습니다. 이것은 하나를 설정하면 나머지 하나는 파이프라인에서 1.0f - g 값으로 자동 계산 됩니다. 행렬의 인덱스는 m 변수에 저장합니다.

FVF에서 XYZ 뒤에 B2가 붙었습니다. 이것은 Blending 인덱스의 개수가 2를 의미하며 행렬의 인덱스 4바이트 m를 하위 바이트부터 index0, index 1 두 개의 인덱스 사용을 뜻합니다.

D3DFVF_LASTBETA_UBYTE4는 앞의 구조체처럼 비중과 인덱스를 동시에 가지고 있을 때 마지막 값을 unsigned byte 4개를 인덱스로 사용함을 의미합니다. 거의 이 포맷을 사용하기 때문에 버텍스에 행렬을 여러 개 설정할 때는 이 플래그는 고정이라 생각하는 것이 좋습니다.

이렇게 정점에 행렬의 인덱스를 적용해서 Skinning을 구현하기 때문에 (Matrix) Indexed 방식 또는 Matrix Palette 방식의 정점 블렌딩(Blending) 이라 부릅니다.

 

아직 익숙하지 않으니 비슷한 예를 하나 더 만들어봅시다. 정점에 영향을 주는 행렬이 3개인 정점 구조체와 FVF는 구성할까요?

 

struct VtxUV1

{

        D3DXVECTOR3    p;

 

        D3DXVECTOR2    g;      // 비중(Weight)

        DWORD           m;      // 행렬의 인덱스

        …

        enumFVF = (D3DFVF_XYZB3 | D3DFVF_LASTBETA_UBYTE4 | …),  };

};

 

다시 태극기 문제로 돌아가서 행렬을 4개 설정을 할 수 있고, 텍스처의 좌표가 적용되는 구조체는 다음과 같을 것입니다.

 

struct VtxBlend

{

        D3DXVECTOR3    p;             // 정점의 변환된 좌표

 

        FLOAT          g[3];          // blend weight

        BYTE           m[4];          // Index

 

        FLOAT          u, v;          // UV

        enumFVF = (D3DFVF_XYZB4 | D3DFVF_LASTBETA_UBYTE4 | D3DFVF_TEX1), };

};

 

이 구조체를 받아들이는 파이프라인은 행렬을 다음과 같이 계산을 한 다음 정점의 위치를 변환합니다.

 

 

다음으로 태극기의 정점에 영향을 주는 행렬 배열의 크기를 D3D가 지원하는 최대 값으로 설정합니다.

 

D3DXMATRIX     m_mtWld[256];

 

다음으로 정점 데이터를 만들 때 비중과 행렬 인덱스를 연결합니다. 지금은 정점에 순차적으로 행렬을 적용하고 있지만 이런 방법은 특별한 경우입니다.

 

for(…)

        m_pVtx[2*i + 0].g[0] = 1.0F;          // 비중

        m_pVtx[2*i + 0].m[0] = i;             // 행렬 인덱스

 

        m_pVtx[2*i + 1].g[0] = 1.0F;          // 비중

        m_pVtx[2*i + 1].m[0] = i;             // 행렬 인데스

 

매 프레임 마다 행렬을 갱신합니다.

 

FLOAT   fAngle = GetTickCount() * 0.4f;

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

{

        float fTheta = fAngle + i * (360.f/15);

        m_mtWld[i]._41 = cosf( D3DXToRadian(fTheta) ) * 1.5f;

        m_mtWld[i]._42 = sinf( D3DXToRadian(fTheta) ) * 4.0f;

}

 

렌더링만 남았습니다. 먼저 프로그램 시작할 때 하드웨어에서 지원되는 행렬의 최대 숫자를 가지고 지금의 행렬의 개수가 이보다 클 경우에는 다음과 같이 소프트웨어 버텍스 프로세싱을 호출합니다.

 

if(m_nMaxMatrixSize<128)

        m_pDev->SetSoftwareVertexProcessing(TRUE);

 

SetSoftwareVertexProcessing() 함수는 MIXED_VERTEX_PROCESSING에만 적용이 됩니다. 따라서 MIXED로 디바이스를 생성하거나 아니면 SOFTWARE_VERTEX_PROCESSING으로 디바이스를 만들어야 합니다. 다음에 보여줄 예제는 MIXED로 디바이스를 만들었습니다.

다음으로 인덱스에 의한 버텍스 블렌딩을 활성화합니다.

 

m_pDev->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE );

 

또한 버텍스 블렌딩의 비중에 대한 최대 인덱스를 지정합니다. 구조체에서 행렬에 대해서 4개의 인덱스를 지정하고 있으므로 0,1,2,3에서 3의 값을 선택해 다음과 같이 디바이스에 적용합니다.

 

m_pDev->SetRenderState(D3DRS_VERTEXBLEND, D3DVBF_3WEIGHTS );

 

행렬 배열을 디바이스에 설정하고 오브젝트를 렌더링 합니다.

 

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

        m_pDev->SetTransform( D3DTS_WORLDMATRIX(i), &m_mtWld[i] );

m_pDev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 40, m_pVtx, sizeof(VtxBlend));

 

디바이스를 원상태로 되돌려 놓습니다.

 

m_pDev->SetRenderState( D3DRS_INDEXEDVERTEXBLENDENABLE, FALSE);

m_pDev->SetRenderState( D3DRS_VERTEXBLEND, D3DVBF_DISABLE  );

m_pDev->SetSoftwareVertexProcessing(FALSE);

 

// 디바이스의 행렬 초기화

static D3DXMATRIX mtIdentity( 1,0,0,00,1,0,00,0,1,00,0,0,1);

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

        m_pDev->SetTransform( D3DTS_WORLDMATRIX(i), &mtIdentity);

 

전체 코드는 daux08_skinning01.zip을 참고하십시오. 이를 실행하면 다음과 같은 화면을 얻을 수 있습니다.

 

<Skinning 태극기 예제: daux08_skinning01.zip>

 

 

3.8.2 Modified SkinnedMesh

DXSDK Sample SkinnedMesh X-file의 정보를 받아서 Skinning으로 애니메이션을 구현한 예제입니다.

예제는 파일에서 애니메이션 정보를 만드는 CAllocateHierarchy 클래스, 장면의 렌더링에 필요한 메쉬, 텍스처, 재질, 행렬 등을 가진 D3DXMESHCONTAINER 클래스, 메쉬 컨테이너들을 모아서 하나의 나무 구조(Tree Structure)를 만드는 D3DXFRAME 세 부분으로 구성되어 있고, Skinning에 대한 애니메이션을 Non-Index, Indexed, 정점 쉐이더(Vertex Shader), 고 수준 쉐이더(HLSL) 4개의 방식 중에서 하나를 선택해서 적용해 볼 수 있습니다.

 

 

<SkinnedMesh 예제의 수정된 자료 구조>

 

Non-Index는 기본적으로 Indexed 방식으로 애니메이션을 구현하지만 정점에 설정된 행렬의 인덱스가 하드웨어에서 지원되는 최대 크기를 넘어서는 경우에 소프트웨어(CPU)로 정점의 처리하도록 코드가 구성되어 있습니다. 앞서 태극기 예제에서 이런 방법을 위해서 디바이스의 SetSoftware-VertexProcessing() 함수를 호출한 적이 있습니다. 이것이 지원되려면 MIXED로 디바이스를 생성해야 한다고 했고, 이 때문에 SkinnedMesh 예제도 MIXED로 디바이스를 만들었음을 볼 수 있습니다.

 

이 예제를 그대로 사용하기는 무척 어렵습니다. 그래서 코드를 게임에 적용할 수 있도록 재배치 해야 합니다. daux08_skinning02.zip는 이 예제를 게임 프로그램에 적용할 수 있도록 수정한 예입니다.

코드를 재배치 하면서 Shader 부분은 과감히 생략하고, Indexed 방식도 Non-Index에 포함되어 있으므로 이 부분도 제거했습니다. 이전의 X-file을 클래스로 만들 때처럼 구조는 거의 그대로 두고(변수 이름과 함수 이름은 일부 바꾸었습니다.) 몇 개의 인터페이스와 클래스를 추가하여 앞의 그림과 같은 구조로 변경해서 만든 예가 daux08_skinning02.zip입니다.

 

ILcMdl X-file Skinning에 대한 최상위 클래스로 인터페이스 역할을 합니다. 이를 상속받은 CLcXSkin 클래스는 애니메이션에 필요한 시간과 객체에 대한 월드 행렬의 멤버 변수를 가지고 있고 이들을 설정하거나 참조할 수 있는 멤버 함수를 다음과 같이 가지고 있습니다.

 

class CLcXSkin : public ILcMdl

        D3DXMATRIX     m_mtWorld      ;       // 객체의 월드 행렬

        DOUBLE         m_dTimeCur     ;       // 객체의 시간

        virtual INT    SetAttrib(char* sCmd, void* pVal);

        virtual INT    GetAttrib(char* sCmd, void* pVal);

        virtual INT    SetPosition(FLOAT* float3);

        virtual INT    GetPosition(FLOAT* float3);

 

속성이 많아 지면 이에 대한 함수도 증가할 수 있어 코드가 엉망이 되기 쉽습니다. 간단한 데이터에 대한 설정과 참조는 하나의 함수에서 다음과 같이 처리하는 것이 전체 구조를 잘 가지고 갈 수 있는 비결입니다.

 

INT CLcXSkin::SetAttrib(char* sCmd, void* pVal)

{

        if(0 ==_stricmp(sCmd, "World Matrix"))

        {

               m_mtWorld =*((D3DXMATRIX*)pVal);

               return 0;

        }

        else if(0 ==_stricmp(sCmd, "Elapsed Time"))

        {

               FLOAT fTime =*((FLOAT*)pVal);

               m_dTimeCur += fTime;

               return 0;

        }

}

 

INT CLcXSkin::GetAttrib(char* sCmd, void* pVal)

{

        if(0 ==_stricmp(sCmd, "World Matrix"))

        {

               *((D3DXMATRIX*)pVal) = m_mtWorld;

               return 0;

        }

        else if(0 ==_stricmp(sCmd, "Current Time"))

        {

               *((DOUBLE*)pVal) = m_dTimeCur;

               return 0;

        }

}

 

내부에서 if ~ else의 증가는 어쩔 수 없지만 밖에서 보는 클래스의 외형적 구조는 간결합니다.

 

메쉬, 애니메이션 정보, 나무 구조(Tree Structure)를 구성하는 CLcXSkinSrc 클래스는 다음과 같이 애니메이션 제어에 대한 객체와 행렬의 배열을 가지고 있습니다.

 

class CLcXSkinSrc : public CLcXSkin

        SFrame*                       m_pFrameRoot   ;       // Root

        LPD3DXANIMATIONCONTROLLER     m_pAC          ;       // Animation Controller

        UINT                          m_NumBoneMatricesMax;  // 행렬 인덱스의 수

        D3DXMATRIX*                   m_pBoneMatrices        ;       // 행렬의 배열

        virtual INT    Create();

        void            UpdateFrameMatrices();

        void            DrawFrame();

        void            RenderNonIndexed();

 

CLcXSkinSrc::Create() 함수는 D3DXLoadMeshHierarchyFromX() 함수를 이용해서 메쉬와 계층 구조(hierarchy)를 가져옵니다. 이 함수는 사용자가 ID3DXAllocateHierarchy 상속 받는 클래스를 정의한 자료를 인수로 받습니다. 사용자는 다음의 세 함수에 대해서 반드시 구현해야 합니다.

 

class CLcXSkinAlloc: public ID3DXAllocateHierarchy

public:

        virtual HRESULT CreateFrame();

        virtual HRESULT CreateMeshContainer();      

        virtual HRESULT DestroyFrame();

        virtual HRESULT DestroyMeshContainer();

 

이들 함수는 D3DXLoadMeshHierarchyFromX() 함수에서 호출되며 다음과 같은 역할을 합니다.

 

CreateFrame(): Frame을 메모리에 생성할 때 호출합니다.

CreateMeshContainer(): 메쉬 컨테이너를 메모리에 생성할 때 호출합니다.

DestroyFrame(): 생성한 Frame을 해제할 때 호출합니다.

DestoryMeshContainer(): 생성한 메쉬 컨테이너를 해제할 때 호출합니다.

 

코드를 옮기면서 Mesh Container 구조를 상속에서 독립된 클래스로 바꾸었습니다. Mesh Container  장면을 만드는 객체로 메쉬, 변환에 필요한 행렬의 포인터, 재질(Material), Texture 등을 포함하고 있습니다.

 

struct SMeshContainer //: D3DXMESHCONTAINER

        D3DXMESHDATA            MeshData;

        LPD3DXMATERIAL          pMaterials;

        DWORD                   NumMaterials;

        LPDIRECT3DTEXTURE9*    ppTextures;       // array of textures

        // Skin Mesh info            

        DWORD                NumAttributeGroups;

        DWORD                NumInfl;         // 컨테이너에 영향을 주는 행렬의 수

        LPD3DXBUFFER         pBoneCombinationBuf;

        D3DXMATRIX**         m_pBoneMatrix;

 

Frame 객체는 계층적인 나무(Tree) 구조를 형성하는 객체로 노드(Node)의 포인터들과 함께 장면과 애니메이션을 구성하기 위한 변환 행렬, 메쉬 포인터 등을 포함 한 객체입니다. Root 노드부터 너비 우선 탐색(breadth-first search: BFS)으로 행렬을 갱신하고, Mesh Container 포인터를 이용해서 렌더링을 수행 합니다.

 

struct SFrame  //: public D3DXFRAME

        D3DXMATRIX             tmMatrix;      // 지역 좌표계의 행렬

        SMeshContainer*                pmcMesh;       // 렌더링에 대한 메쉬 컨테이너

 

        SFrame*                pFrameSibling; // 이웃 형제 노드

        SFrame*                pFrameFirstChild;// 첫 번째 자식 노드

        D3DXMATRIX             tmWorld;       // 월드 행렬 = tmMatrix * 부모 행렬

        SFrame *FindFrame(char *szFrame);

 

CLcXSkinSrc::UpdateFrameMatrices() 함수를 보면 Frame 객체의 월드 행렬을 자신의 지역 행렬과 부모의 월드 행렬 곱으로 만들고 있고 형제 노드와 자식 노드에 대해서 BFS로 노드를 찾아가 데이터를 갱신하고 있음을 볼 수 있습니다.

 

if (pParentMatrix != NULL)

                D3DXMatrixMultiply(&pFrame->tmWorld, &pFrame->tmMatrix, pParentMatrix);

else

                pFrame->tmWorld = pFrame->tmMatrix;

 

if (pFrame->pFrameSibling != NULL)

                UpdateFrameMatrices(pFrame->pFrameSibling, pParentMatrix);

 

if (pFrame->pFrameFirstChild != NULL)

                UpdateFrameMatrices(pFrame->pFrameFirstChild, &pFrame->tmWorld);

 

렌더링도 CLcXSkinSrc::DrawFrame()함수를 보면 BFS로 진행하고 있습니다.

 

pMeshContainer = pFrame->pmcMesh;

while (pMeshContainer != NULL)

{

                DrawMeshContainer(pMeshContainer, pFrame);

                pMeshContainer = pMeshContainer->pNextMeshContainer;

}

 

if (pFrame->pFrameSibling != NULL)

                DrawFrame(pFrame->pFrameSibling);

 

if (pFrame->pFrameFirstChild != NULL)

                DrawFrame(pFrame->pFrameFirstChild);

 

프로그램에서 렌더링과 애니메이션 정보를 가지고 있는 CDXSkinSrc 클래스의 인스턴스를 참조해서 장면을 구성하는 CDXSkinInst 클래스는 다음과 같은 간단한 구조로 구성이 됩니다.

 

class CLcXSkinIns : public CLcXSkin

        CLcXSkinSrc*                  m_pOrg;        // Original Pointer

        LPD3DXANIMATIONCONTROLLER     m_pAC;         // Animation Controller

        SFrame*                       m_pFrameRoot;  // Root Frame

        virtual INT    Create(void* =0, void* =0, void* =0, void* =0);

        virtual void   Destroy();

        virtual INT    FrameMove();

        virtual void   Render();

 

Root Frame은 매번 Original에서 가져오는 번거로움을 없애기 위해 만든 변수 입니다. INT CLcXSkinIns::Create() 함수를 보면 Animation Controller를 복제하고 사용의 편리를 위해 Original로부터 필요한 정보를 얻습니다.

 

INT CLcXSkinIns::Create(…)

        m_pOrg = (CLcXSkinSrc*)p3;

        m_pFrameRoot = (SFrame*)m_pOrg->GetRootFrame();

 

        LPD3DXANIMATIONCONTROLLER     pAC =

(LPD3DXANIMATIONCONTROLLER)m_pOrg->GetAnimationController();

 

        UINT MaxNumAnimationOutputs   = pAC->GetMaxNumAnimationOutputs();

        UINT MaxNumAnimationSets      = pAC->GetMaxNumAnimationSets();

        UINT MaxNumTracks             = pAC->GetMaxNumTracks();

        UINT MaxNumEvents             = pAC->GetMaxNumEvents();

 

        pAC->CloneAnimationController( MaxNumAnimationOutputs

                                      , MaxNumAnimationSets

                                      , MaxNumTracks

                                      , MaxNumEvents

                                      , &m_pAC);

 

렌더링은 Animation ColtrollerAdvanceTime () 함수에 전체 장면을 1 프레임 렌더링 하는데 소모된 시간을 인수로 전달하면서 시작합니다. 만약 한 장면에 들었던 시간 대신 총 시간을 사용한다면 다음과 같이 Track의 위치를 0으로 설정해야 합니다. 다음으로 Original UpdateFrame-Matrices() 함수와 DrawFrame() 함수를 순차적으로 호출해서 장면을 그립니다.

 

void CLcXSkinIns::Render()

m_pAC->SetTrackPosition(0, 0);

m_pAC->AdvanceTime(m_dTimeCur, NULL);

m_pOrg->UpdateFrameMatrices(m_pFrameRoot, &m_mtWorld);

m_pOrg->DrawFrame(m_pFrameRoot);

 

CMain 클래스에는 ILcMdl에 대한 Original 객체 1개와 이를 참조해서 만들 3개의 ILcMdl 객체를 선언하고 CMain::Init() 함수에서 있습니다. LcMdl_Create()함수에 파일 이름으로 Original을 만들고 Original을 인수로 전달해서 나머지 3개의 객체를 만들고 있음을 볼 수 있습니다.

 

LcMdl_Create(NULL, &m_pDXMeshOrg, m_pd3dDevice, m_strMeshFilename);

LcMdl_Create(NULL, &m_pDxMeshIns1, m_pd3dDevice, NULL, m_pDXMeshOrg);

LcMdl_Create(NULL, &m_pDxMeshIns2, m_pd3dDevice, NULL, m_pDXMeshOrg);

LcMdl_Create(NULL, &m_pDxMeshIns3, m_pd3dDevice, NULL, m_pDXMeshOrg);

 

LcMdl_Create() 함수는 애니메이션이 없는 X-file와 비슷하게 파일이름이 있으면 원본 객체를 만들고 원본 객체의 주소가 오면 이를 참조하는 객체를 만듭니다.

 

INT LcMdl_Create(char* sCmd , ILcMdl** pData // Output Data

                , void* p1            // Device

                , void* p2            // File Name

                , void* p3            // Source Model

                , void* p4 )

{

        ILcMdl* pObj = NULL;

        *pData = NULL;

        if( NULL != p2)               pObj = new CLcXSkinSrc;

        else if(NULL != p3)    pObj = new CLcXSkinIns;

        else                   return -1;

 

        if(FAILED(pObj->Create(p1, p2, p3, p4)))

        {

               delete pObj;   return -1;

        }

 

        *pData = pObj;

        return 0;

}

 

daux08_skinning02.zip를 실행하면 다음과 같이 서로 다른 시간에 애니메이션을 하는 3개의 객체를 볼 수 있습니다.

 

 

<Modified SkinnedMesh: daux08_skinning02.zip>

 

코드를 전환하면서 가장 어려웠던 것은 데이터의 갱신과 렌더링이 분리가 안되어 있는 점과 애니메이션 설정이 쉽지가 않다는 것입니다. Animation Set이 여러 개 있을 경우 0 Set으로 두면 다른 객체에 영향을 주어 할 수 없이 마지막 Set으로 애니메이션을 결정해야 했습니다.

if(m_nAniMax)

        m_pAC->SetTrackAnimationSet(0, m_vAS[m_nAniMax - 1]);

 

완벽하지는 않지만 제대로 된 라이브러리를 얻기 전까지 쓸만하다고 생각되며, 특히 애니메이션을 처음 접하는 분들에게 도움이 되리라 생각됩니다.

 

 

3.9 2D Sprite

3.9.1 RHW Sprite

3D 기초와 게임 프로그램에 필요한 애니메이션까지의 지식만으로도 3D 게임을 얼마든지 만들 수 있습니다. 그런데 대부분 3D를 배워놓고도 무엇을 해야 할지 막막할 때가 많이 있습니다. 이럴 때 권하고 싶은 것이 ID3DXSprite 3D로 만들어 보는 것입니다. 3D 게임에서 화면의 UI(User Interface)를 구성할 때 2D Sprite를 사용하고 ID3DXSprite를 대부분 사용합니다. 그런데 ID3DXSprite라는 것이 D3D 또는 D3DX 버전 마다 약간의 차이가 있습니다. 만약 3D 2D Sprite를 구현해 놓으면 D3D 버전에도 영향을 받지 않고 완벽하게 3D 2D를 구현 하는데 의미가 있습니다.

 

3D는 정점이 있어야 장면을 연출할 수 있으며 따라서 2D도 정점이 있어야 합니다. 파이프라인의 변환 과정을 거치지 않는 가장 적합한 형태는 RHW(Reciprocal of Homogeneous W) 형태로 정점을 구성하는 것입니다. 프로그램의 인터페이스는 D3D와 유사하게 구성하는 것이 지금까지 만든 프로그램과 호환성을 유지에 도움이 됩니다. 이런 아이디어를 종합해서 프로그램을 작성해 봅시다.

 

먼저 2D Sprite의 인터페이스와 Sprite 객체를 생성하는 함수 입니다.

 

interface ILcSprite           // 2D Sprite Interface

{

        virtual ~ILcSprite(){};

        virtual INT    Draw(void* pTx0, const RECT* pRcDrw=NULL

, const FLOAT* pvcPos=NULL, DWORD Color=0xFFFFFFFF)=0;

};

 

INT LcxCreateSprite(ILcSprite** pOut, void* pd3dDevice);

 

Sprite 객체를 생성하는 LcxCreateSprite() 함수에서 객체에 대한 반환을 인수의 앞쪽에 넣었습니다.

CLcSprite 클래스는 ILcSprite를 상속받아서 실제로 Draw() 함수를 처리하는 클래스입니다.

 

class CLcSprite : public ILcSprite

        LPDIRECT3DDEVICE9      m_pDev;

        LPDIRECT3DSTATEBLOCK9  m_pStCur;      // Current State Block

        LPDIRECT3DSTATEBLOCK9  m_pStSpt;      // Sprite State Block

        INT            Create(LPDIRECT3DDEVICE9 pDev);

        virtual INT    Draw(…);

 

Sprite 객체에 의해 디바이스의 상태 값이 변하지 않도록 상태를 저장하기 위해서 D3D State Block 객체를 가지고 있습니다.

State Block 객체 생성은 D3D 디바이스의 BeginStateBlock()함수 호출을 하면서 저장할 상태를 설정하고 마지막에 EndStateBlock() 함수에서 객체를 생성합니다.

 

m_pDev->BeginStateBlock();

m_pDev->SetRenderState( D3DRS_FOGENABLE,     FALSE );

m_pDev->SetRenderState( D3DRS_LIGHTING,              FALSE );

m_pDev->EndStateBlock(&m_pStSpt);

 

상태 설정이 길어질 때 유용합니다. State Block도 해제할 때는 Release() 함수를 이용합니다.

 

Draw() 함수는 원본 이미지의 크기와 화면에 그리게 될 이미지 영역, 화면 좌표, 색상을 이용해서 4개의 정점을 완성하고 출력하는 것입니다. 텍스처 서피스에서 너비와 높이를 가져올 수 있고 이와 함께 화면에 출력할 이미지의 영역을 이용해서 텍스처의 UV좌표를 계산합니다.

 

INT CLcSprite::Draw(…, const RECT* pRcImg, const RECT* pRcDrw, const FLOAT* pvcPos, …)

// 서피스의 Width, Height를 가져온다.

        D3DSURFACE_DESC dsc;

        pTex0->GetLevelDesc(0, &dsc);

        FLOAT fImgW = FLOAT(dsc.Width);              // Width of Surface

        FLOAT fImgH = FLOAT(dsc.Height);      // Height of Surface

 

        // Draw Region Width, Height

        if(pRcDrw)

        {

               fDrwL = FLOAT(pRcDrw->left);

               fDrwT = FLOAT(pRcDrw->top);

               fDrwW = FLOAT(pRcDrw->right - pRcDrw->left);

               fDrwH = FLOAT(pRcDrw->bottom- pRcDrw->top );

        }

        // Setup UV

        uv0.x = (fDrwL +     0)/fImgW;

        uv1.x = (fDrwL + fDrwW)/fImgW;

        uv0.y = (fDrwT +     0)/fImgH;

        uv1.y = (fDrwT + fDrwH)/fImgH;

 

// Setup Vertices

        struct TVtxRHWUV1

        {

               FLOAT   p[4];

                DWORD   d;

                FLOAT   u, v;

        } pVtx[4] =

        {

               {pos.x +     0, pos.y +     001,  dColor,   uv0.x,  uv0.y},

               {pos.x + fDrwW, pos.y +     001,  dColor,   uv1.x,  uv0.y},

               {pos.x + fDrwW, pos.y + fDrwH,  01,  dColor,   uv1.x,  uv1.y},

               {pos.x +     0, pos.y + fDrwH,  01,  dColor,   uv0.x,  uv1.y},

        };

 

        // Rendering 2D

        m_pStCur->Capture();

        m_pStSpt->Apply();

        m_pDev->SetTexture(0, pTex0);

        m_pDev->SetFVF(D3DFVF_XYZRHW|D3DFVF_DIFFUSE|D3DFVF_TEX1);

        m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(TVtxRHWUV1) );

        m_pDev->SetTexture(0, NULL);

        m_pStCur->Apply();

 

스프라이트 객체는 매번 정점을 설정해야 하므로 User Memory Pointer를 사용하는 것이 편리합니다.

전체 코드는 daux09_2d_sprite01.zip 참고 하고 이를 실행하면 다음과 같이 ID3DXSprite 객체와 비교한 화면이 출력됩니다.

 

<RHW Sprite: daux09_2d_sprite01.zip>

 

그림의 작은 입자의 붉은 색 동그라미를 보면 UV가 안 맞는 것이 보입니다. 이것은 텍스처를 만들 때 필터링이 설정되었기 때문입니다. 또한 불투명 텍스처에 대한 칼라 키(Color Key)가 설정이 안 되어 있어서 검정색 배경 색상이 그대로 출력되고 있음을 볼 수 있습니다. 이런 문제들을 생각했을 때 지금까지 잘 사용하고 있었던 IDirect3DTexture9 객체 주소와 D3DXIMAGE_INFO 구조체 변수를 Wrapping 해서 사용할 수 있는 인터페이스가 필요하게 됩니다.

 

다음과 같이 간단한 구조체로 만들어서 사용할 수 있습니다.

 

struct LcTexture

{

        D3DXIMAGE_INFO         pInf;

        LPDIRECT3DTEXTURE9     pTex;

};

 

한 번 해 놓으면 계속 쓸 수 있으니 이왕이면 다음과 같이 작성하는 것이 좋습니다. 먼저 인터페이스와 생성 함수를 이전의 Sprite와 비슷하게 만듭니다. ILcTexture 생성 함수에는 필터링, 칼라 키를 전달할 수 있는 인수를 추가합니다.

 

interface ILcTexture

{

        virtual ~ILcTexture(){};

 

        virtual void*   GetTexture()=0;        // LPDIRECT3DTEXTURE9

        virtual INT     GetImgW()=0;   // Image Width

        virtual INT     GetImgH()=0;   // Image Height

        virtual INT     GetImgD()=0;   // Image Depth

};

 

INT LcxCreateTextureFromFile(ILcTexture** pOut, void* pd3dDevice

, char* sFile, DWORD dFilter=-1, DWORD dColor=0x00FFFFFF);

 

인터페이스 ILcTexture를 구현할 클래스 CLcTexture를 다음과 같이 작성합니다.

 

class CLcTexture : public ILcTexture

        LPDIRECT3DTEXTURE9     m_pTex;

        D3DXIMAGE_INFO         m_pInf;

        INT     Create(LPDIRECT3DDEVICE9 pDev, char* sFile

, DWORD dFilter=-1, DWORD dColorKey=0x00FFFFFF);

 

 

CLcTexture::Create() 함수는 D3DXCreateTextureFromFileEx() 함수를 이용해서 텍스처와 원본 이미지 정보를 저장합니다. 필터가 -1이면 MipLevels를 최대로 설정하고 그렇지 않은 경우에는 1로 설정해서 2D환경에서 사용되지 않는 Sub Texture를 안 만듭니다.

 

INT CLcTexture::Create()

        UINT MipLevels = (dFilter==-1? -1 : 1);

        D3DXCreateTextureFromFileEx(pDev, sFile

                              , D3DX_DEFAULT, D3DX_DEFAULT

, MipLevels, 0

                              , D3DFMT_UNKNOWN, D3DPOOL_MANAGED

, dFilter, dFilter

, dColorKey, &m_pInf, NULL, &m_pTex);

 

 

daux09_2d_sprite02.zip를 실행 하면 다음과 같이 Lc, DX 모두 원하는 대로 출력하고 있습니다.

 

<RHW Sprite: daux09_2d_sprite02.zip>

 

 

3.9.2 회전 변환

2D 회전을 결정하는 것이 쉬우면서도 어렵습니다. 그것은 Sprite의 활용에 따라 달라집니다. 만약 빌보드 효과(Billboard Effect)과 같은 3D에서 2차원 평면을 위한 것이라면 Sprite의 회전은 3D 변환 과정과 동일해야 합니다. 그렇지 않고 화면에만 맞춘다면 2차원 회전만 고려하면 됩니다. 여기서는 화면을 기준으로 회전을 적용하겠습니다.

 

회전 각도(Radian)를 θ로 하면 회전 전 좌표를 (x, y)라 하고 회전 후 좌표를 (x', y') 위치 변환은 다음과 같이 됩니다.

 

x' = x * cosθ - y * sinθ

y' = x * sinθ + y * cosθ

<오른손 좌표계>

x' =  x * cosθ + y * sinθ

y' = -x * sinθ + y * cosθ

<왼손 좌표계>

 

만약 회전 중심 축이 있다면 위치를 이 중심축만큼 이동하고 나서 회전을 적용한 다음 다시 원래의 위치로 와야 합니다.

 

Δx = x- rot x, Δy = x- rot y

 

Δx' = Δx * cosθ - Δy * sinθ

Δy' = Δx * sinθ + Δy * cosθ

 

x' = Δx' + rot x

y' = Δy' + rot y

또는 (x' y') = rot(x, y) + (Δx', Δy')

 

daux09_2d_sprite03.zip CLcSprite::Draw()는 이러한 회전을 적용한 함수로 코드의 중간에 회전 중심 축이 주어지면 이를 구현하고 있습니다. 회전은 반 시계방향으로 회전하도록 하기 위해서 왼손 좌표계 공식을 적용했습니다.

 

if(pvcRot)

{

        D3DXVECTOR2 vcR = D3DXVECTOR2(pvcRot[0], pvcRot[1]);

        D3DXVECTOR2 d(0,0);

        FLOAT   fCos = cosf(fTheta);

        FLOAT   fSin = sinf(fTheta);

 

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

        {

               D3DXVECTOR2 vcT = pos[i] - vcR;

               d.x = vcT.x * fCos + vcT.y * fSin;

               d.y =-vcT.x * fSin + vcT.y * fCos;

 

               pos[i] = vcR + d;

        }

}

 

각도는 D3D Radian [0, 2π]을 사용하나 Degree [0, 360] 가 많이 사용되므로 입력 값은 Degree로 하고 내부에서 Radian으로 처리하도록 합니다.

다음 그림은 daux09_2d_sprite03.zip 실행 결과입니다.

 

<회전이 적용된 2D  Sprite: daux09_2d_sprite03.zip>

 

 

3.9.3 크기 변환

크기 변환도 회전처럼 화면 기준으로 작성합니다. 만약 크기에 음수가 오면 좌우, 또는 상하를 반대로 뒤 짚어 줍니다. 반전이 있으면 삼각형을 그리는 정점의 순서가 CW에서 CCW로 바뀝니다. 따라서 Culling Mode None으로 하는 것이 좋습니다.

 

m_pDev->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE);

 

ID3DXSprite는 크기 변환 값을 음수로 설정하면 위치를 보정해야 합니다. 이것은 불편하므로 반전이 있어도 시작 위치를 변동시키지 않도록 코드에서 정점의 위치에 대한 계산을 다음과 같이 작성합니다.

 

FLOAT   fSclX = pvcScl[0];     FLOAT   fSclY = pvcScl[1];

 

if(fSclX<0)

        pos[1].x = pos[0].x;

        pos[2].x = pos[1].x;

        pos[0].x = pos[1].x - fDrwW * fSclX;

        pos[3].x = pos[0].x;

if(fSclY<0)

        pos[3].y = pos[0].y;

        pos[2].y = pos[3].y;

        pos[0].y = pos[3].y - fDrwH * fSclY;

        pos[1].y = pos[0].y;

 

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

 

<크기 변환: daux09_2d_sprite04.zip>

 

 

3.9.4 알파 맵(Alpha Map)

Sprite에 기능을 넣기 시작하면 한도 끝도 없을 정도로 무궁무진 합니다. 그래도 한가지 넣고 싶은 것이 있다면 알파 맵을 넣는 것입니다. 이 기능은 다음과 같이 원본 이미지에서 특정한 부분을 잘라내거나 아니면 반투명을 처리하는 것입니다.

 

<2D Sprite 알파 맵에 의한 특정 영역의 출력>

 

이것은 3D의 다중 텍스처(Multi-Texturing)을 적용하면 아주 쉽게 해결이 됩니다. 다음과 같이 다중 텍스처 처리 단계 1에서는 단계 0의 결과 값을 Current로 저장하고 색상은 그대로 출력합니다.

 

m_pDev->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT );

m_pDev->SetTextureStageState( 1, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);

 

알파만 Current값과 곱셈을 하고 출력합니다.

 

m_pDev->SetTextureStageState( 1, D3DTSS_ALPHAARG1, D3DTA_CURRENT );

m_pDev->SetTextureStageState( 1, D3DTSS_ALPHAARG2, D3DTA_TEXTURE );

m_pDev->SetTextureStageState( 1, D3DTSS_ALPHAOP,   D3DTOP_SELECTARG2);

 

텍스처 좌표는 단계 0에서 사용한 좌표를 그대로 사용합니다. 이 부분은 이 후 Sprite의 기능 보강에서 필요하다면 수정하면 됩니다.

 

m_pDev->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0);

 

물론 Draw() 함수도 수정해야 합니다. 이전의 Draw() 함수와 구분하기 위해서 새로운 DrawEx() 함수를 만드는 것이 좋습니다. 그리고 이전 Draw()함수는 DrawEx()를 호출해서 사용하도록 합니다.

 

INT CLcSprite::Draw(…)

{

        return DrawEx(pTx0, NULL, …);

}

 

daux09_2d_sprite05.zip는 알파 맵을 적용한 Sprite 예제입니다. 실행하면 다음과 같은 화면을 볼 수 있습니다.

 

<알파 맵이 있는 2D Sprite: daux09_2d_sprite05.zip>



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

Creative Commons License