본문 바로가기

프로그래밍/DirectX

DXUT Framework 간단 가이드

DXUT Framework를 이용하면 별다른 설정 없이 손쉽게 그래픽 작업을 시작할 수 있습니다. DirectX Sample Browser에서 Empty Project를 원하는 이름으로 수정한 뒤에 Install을 하면 그 뼈대(Framework)를 제공해 줍니다.

하지만 안타깝게도 이 DXUT Framework에 대한 문서나 튜토리얼은 쉽게 찾기가 어렵습니다. 그래서 Framework의 구조와 여기서 쓰이는 Callback 함수에 대해 약간의 정보를 적어봅니다.

DXUT Callback 함수

프로젝트 안의 Winmain()의 내용을 보면

DXUTSetCallbackDeviceCreated( OnCreateDevice );
DXUTSetCallbackDeviceReset( OnResetDevice );
	
와 같은 내용들이 있습니다. 이 함수들은 메인루프를 돌다가 필요한 시점에 호출되는 함수들을 설정하는 부분입니다. DXUT에서 제공하는 Callback 함수 중 정말 기본적으로 필요한 몇몇 사항에 대해 설명하겠습니다. ( Empty Project에서 이미 만들어져 있는 Callback 함수의 이름으로 설명합니다. )

  • OnCreateDevice, OnDestroyDevice

    이것은 프로세스가 시작하고 끝날 때 단 한 번만 실행되는 Callback 함수입니다. 여기서는 IDirect3DDevice를 이용하지 않는 자원들이 초기화되고 파괴됩니다.

    HRESULT CALLBACK OnCreateDevice( IDirect3DDevice9* pd3dDevice,
    	const D3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext )
    {	
    	D3DXVECTOR3 vEye = D3DXVECTOR3( 0.0f, 0.0f, -5.0f );
    	D3DXVECTOR3 vLookAt = D3DXVECTOR3( 0.0f, 0.0f, -0.0f );
    	g_Camera.SetViewParams( &vEye, &vLookAt );
    	return S_OK;
    }
    	
  • OnResetDevice, OnLostDevice

    이것은 IDirect3DDevice를 갱신할 때마다 불리우는 Callback 함수입니다.

    IDirect3DDevice는 DirectX를 이용하는 데 있어 기본이 되는 객체입니다. 거의 모든 그래픽 자원은 이 장치(객체)를 통해 요청하고 반환합니다. 하지만 이 장치는 해상도 변경과 같은 설정 변경시마다 장치를 다시 얻어야만 합니다. 장치를 다시 얻어오면 이전 장치를 이용한 자원들은 모두 새로이 갱신을 해주어야 합니다. 이전 장치를 반환할 때 호출되는 함수가 OnLostDevice, 새 장치를 얻어올 때 호출되는 함수가 OnResetDevice입니다.

    처음 실행시에도 OnResetDevice는 호출됩니다. 문맥 그대로 재설정(Reset)이라고 받아들이지 말고, 새 장치를 얻어올 때 불리우는 함수라고 생각하시는 게 좋습니다.

    DXUT 객체들은 일반적으로 OnResetDevice()와 OnLostDevice() 함수를 제공합니다. IDirect3DDevice를 이용하는 DXUT 객체들은 위 함수들을 알맞게 호출해주면 됩니다. 그리고 다른 DirectX 객체들은 Release() 함수들을 제공합니다. 이 객체들이 IDirect3DDevice를 이용하여 생성되었다면, OnLostDevice에서 Release()가 호출되어야 합니다.

    HRESULT CALLBACK OnResetDevice( IDirect3DDevice9* pd3dDevice, 
    	const D3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext )
    {
    	HRESULT hr;
    
    	V_RETURN( D3DXCreateBox( pd3dDevice, 1.0f, 1.0f, 1.0f, &g_pMesh, 0 ) );
    	V_RETURN( pd3dDevice->CreateVertexBuffer( 
    		sizeof( VERTEX ) * verticesCube.size(), D3DUSAGE_WRITEONLY, 
    		VERTEX::FVF, D3DPOOL_DEFAULT, &g_pVB, 0 ) );
    	...
    }
    
    void CALLBACK OnLostDevice( void* pUserContext )
    {
    	SAFE_RELEASE( g_pVB );
    	SAFE_RELEASE( g_pMesh );
    	...
    }
    	
  • MsgProc

    WInAPI의 MsgProc와 동일합니다. DXUT는 라디오 버튼, 텍스트 박스와 같은 컨트롤을 제공하므로, 그런 처리를 할 때 MsgProc를 이용합니다.

  • FrameRender

    실제 그림을 그리는 함수입니다. 여기서 IDirect3DDevice::BeginScene(), IDirect3DDevice::EndScene() 내부에 IDirect3DDevice::DrawPrimitive()나 ID3DXMesh::DrawSubset()을 통해 실제 그림을 그립니다.

OnCreateDevice와 OnResetDevice의 차이점만 알면 큰 어려움은 없을겁니다.

DXUT Framework 매크로

매크로를 잘 이용하면 편한 디버깅이 가능합니다.

  1. V(), V_RETURN()

    함수 실패시 __FILE__, __LINE__의 정보를 뿌려줍니다. HRESULT를 반환하는 함수를 인자로 주면 실패 원인과 부가 정보를 출력해줍니다. 이 매크로를 사용하기 전에는 HRESULT hr;이 먼저 선언되어 있어야 합니다. V_RETURN()은 반환값이 FAILED일 때 그 값을 그대로 또 반환합니다.

    void CALLBACK OnFrameRender( IDirect3DDevice9* pd3dDevice, 
    	double fTime, float fElapsedTime, void* pUserContext )
    {
    	HRESULT hr;
    
    	// Clear the render target and the zbuffer 
    	V( pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
    		D3DCOLOR_ARGB(0, 45, 50, 170), 1.0f, 0) );
    	...
    }
    
    HRESULT CALLBACK OnResetDevice( IDirect3DDevice9* pd3dDevice, 
    	const D3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext )
    {
    	HRESULT hr;
    
    	V_RETURN( D3DXCreateBox( pd3dDevice, 1.0f, 1.0f, 1.0f, &g_pMesh, 0 ) );
    	V_RETURN( pd3dDevice->CreateVertexBuffer( 
    		sizeof( VERTEX ) * verticesCube.size(), D3DUSAGE_WRITEONLY, 
    		VERTEX::FVF, D3DPOOL_DEFAULT, &g_pVB, 0 ) );
    	...
    }
    	
  2. SAFE_RELEASE(p)

    if( p ) p->Release();를 실행해 줍니다. DirectX의 거의 모든 객체는 Release()함수를 지니고 있기에 유용합니다.

DXUT Framework에서 카메라 설치하기

3D 프로그래밍을 입문하면서 처음에 제일 골아픈 것이 바로 카메라입니다. DXUT는 기본으로 제공하는 카메라가 있으며, 작은 설정으로 그럴듯한 카메라를 이용할 수 있게 해 줍니다. CFirstPersonCamera와 CModelViewerCamera의 두 가지를 제공합니다만, 여기서는 먼저 CModelViewerCamera의 설치법만을 소개합니다. CModelViewerCamera는 마우스 드래그를 통해 해당 위치 주위를 선회하는 카메라를 구현합니다.

  1. CModelViewerCamera를 모든 Callback 함수가 접근할 수 있는 위치에 지정합니다.

    이 예제에서는 전역에 선언하도록 하겠습니다.

    CModelViewerCamera g_Camera;
    	
  2. OnCreateDevice에 카메라를 설치합니다.

    이 카메라 객체는 단순히 계산만 해주는 객체입니다. 따라서 IDirect3DDevice를 이용하지 않기에 OnCreateDevice에서 초기화합니다.

    HRESULT CALLBACK OnCreateDevice( IDirect3DDevice9* pd3dDevice, 
    	const D3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext )
    {	
    	...
    	D3DXVECTOR3 vEye = D3DXVECTOR3( 0.0f, 0.0f, -5.0f );
    	D3DXVECTOR3 vLookAt = D3DXVECTOR3( 0.0f, 0.0f, -0.0f );
    	g_Camera.SetViewParams( &vEye, &vLookAt );
    	...
    }	
    	

    vEye는 카메라의 위치이고 vLookAt은 물체의 위치입니다. 이 카메라는 마우스 드래그를 통해 카메라의 위치만을 변경하고 항상 현재 설정한 물체의 위치를 바라봅니다.

  3. MsgProc에서 입력을 받아 FrameMove에서 카메라의 위치를 계산합니다.

    MsgProc에서 입력을 알맞은 카메라 행동으로 바꾸어주고, 실제 계산은 FrameMove에서 하게 됩니다.

    LRESULT CALLBACK MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, 
    						 bool* pbNoFurtherProcessing, void* pUserContext )
    {
    	...
    	g_Camera.HandleMessages( hWnd, uMsg, wParam, lParam );
    	...
    }
    	
    void CALLBACK OnFrameMove( IDirect3DDevice9* pd3dDevice, 
    	double fTime, float fElapsedTime, void* pUserContext )
    {
    	...
    	g_Camera.FrameMove( fElapsedTime );
    	...
    }	
    	
  4. FrameRender에서 계산된 행렬을 적용합니다.

    카메라는 IDirect3DDevice를 직접 지닌 것이 아니라 사용자가 직접 계산결과를 적용해주는 것이 필요합니다.

    내용추가 :

    Projection은 OnResetDevice 때마다, World, View는 FrameMove떄마다 호출해도 됩니다. Projection은 Aspect Ratio, 즉 스크린의 가로세로비가 바뀔 때만 그 행렬이 달라지게 되고, World, View는 카메라의 위치 혹은 방향이 바뀌지 않는 한 변경되지 않기 떄문입니다.

    참고로 카메라의 정보를 설정하는 내용도 OnResetDevice에 추가하였습니다.

    HRESULT CALLBACK OnResetDevice( IDirect3DDevice9* pd3dDevice, 
    	const D3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext )
    {
    	...
    	// Setup the camera's projection parameters
    	float fAspectRatio = pBackBufferSurfaceDesc->Width 
    		/ (FLOAT)pBackBufferSurfaceDesc->Height;
    	g_Camera.SetProjParams( D3DX_PI/4, fAspectRatio, 0.1f, 1000.0f );
    	g_Camera.SetWindow( pBackBufferSurfaceDesc->Width, 
    		pBackBufferSurfaceDesc->Height );	
    	V( pd3dDevice->SetTransform( D3DTS_PROJECTION, g_Camera.GetProjMatrix() ) );
    	...
    }
    
    void CALLBACK OnFrameMove( IDirect3DDevice9* pd3dDevice, double fTime, float fElapsedTime, void* pUserContext )
    {
    	...
    	V( pd3dDevice->SetTransform( D3DTS_WORLD, g_Camera.GetWorldMatrix() ) );
    	V( pd3dDevice->SetTransform( D3DTS_VIEW, g_Camera.GetViewMatrix() ) );
    	...
    }
    	

    각각의 물체에 대해 세계 좌표를 부여하였다면, 카메라의 WorldMatrix에 각 물체의 WorldMatrix를 곱해주어야 합니다. 단 카메라의 WorldMatrix가 먼저 적용되어야 하므로 뒤에 곱해주어야 합니다. 즉, 카메라의 WorldMatrix를 Mw, 물체의 WorldMatrix를 Mo라고 할 때, WorldMatrix = Mo * Mw이 되어야 합니다. 물체를 원점에 배치했다면 위의 예제대로만 하여도 무리없이 나옵니다.

조금 많기는 합니다만 자원 획득 및 파괴를 제외하면 계산 및 적용만 호출해주면 되는 셈입니다. 3D Programming에 입문하면서 제일 어려운 내용이 카메라였는데, 그런 면에서 DXUT로 입문하면 이런 구현된 모듈이 있다는 것이 편한 듯 합니다. ( 문서가 적은게 흠입니다. )

이 글은 DXUT Framework의 이용에 초점을 맞추었으므로, 따로 VertexBuffer나 IndexBuffer의 이용법에 대해서는 적지 않았습니다. 위의 내용은 책에도 많고 문서에도 많으니 잘 찾아서 적용해보세요. 기왕 이렇게 시작한 스터디 잘 따라노셔서 끝까지 갈 수 있었으면 좋겠습니다.

'프로그래밍 > DirectX' 카테고리의 다른 글

Animation with DirectX - 1. Skeletal Animation  (2) 2007.05.10