2011년 12월 5일 월요일

[포프의 쉐이더 입문강좌] 01. 쉐이더란 무엇이죠? Part 2

이전편 보기

샘플파일 받기

쉐이더 프로그래밍
자, 그럼 쉐이더가 무엇인지는 대충 알아보았는데 쉐이더를 코드를 짠다는 것은 무슨 뜻일까요? 일단 그림 1.1을 다시 한번 살펴보죠. 그림 1.1을 보면 사각형으로 표현한 파이프라인 단계도 있고 둥글게 표현한 것도 있죠? 원형으로 표현한 단계들은 GPU(graphics processing unit, 그래픽 처리장치)가 알아서 처리해주는 -- 즉 프로그래머가 따로 제어할 수 없는 -- 단계들입니다. 그와 반대로 사각형으로 표현한 단계들은 프로그래머가 마음대로 제어할 수 있는 단계들이죠. 이 단계에서 사용할 함수를 작성하는 것이 바로 쉐이더 프로그래밍입니다. 그림 1.1에서 사각형으로 표현된 단계들은 정점쉐이더와 픽셀쉐이더 뿐인 거 보이시죠? 따라서 정점쉐이더와 픽셀쉐이더에 사용할 함수를 하나씩 만드는 것이 쉐이더 프로그래밍입니다. (DirectX 10과 11에서 새로운 쉐이더들이 추가되었습니다. 하지만 아직 실무에서 널리 사용되지 않아서 실용적인 접근이 어렵고, 입문자에게 적당하지 않은 내용이라 이 책에서 다루지 않습니다.)

시중에 나와 있는 여러 쉐이더 언어 중에 이 책에서 사용할 언어는 DirectX에서 지원하는 HLSL입니다. HLSL(High Level Shader Language, 고수준 쉐이더언어)은 C와 매우 비슷한 문법을 사용하는 언어로 GLSL(OpenGL Shader Language의 약자로 OpenGL에서 지원하는 쉐이더 언어입니다. HLSL과 문법 정도가 조금 다릅니다.)이나 CgFX(엔비디아에서 지원하는 쉐이더 언어입니다. HLSL과 한두 개 빼고는 완전히 똑같습니다.)등의 기타 쉐이더 언어와 매우 흡사합니다. 따라서 HLSL을 배우시면 다른 쉐이더 언어를 익히시는데도 큰 무리가 없을 것입니다.

한 언어를 배우는 최선의 방법은 직접 코딩을 하면서 배우는 것입니다. 이 언어의 철학은 이러네, 이 언어의 문법은 저러네 하면서 백날 떠들어봐야 입문자들은 하품만 하고 무슨 이야긴지 알아듣지도 못합니다. 일단 재미있게 코드를 짜봐야 프로그래밍에 애착도 생기고, 애착이 생기면 보다 나은 프로그래머가 되기 위해 노력을 하지요. 따라서 이 책에서는 쓸데없이 HLSL 문법을 나열하면서 독자분들의 짜증을 부추기는 대신 무조건 아주 쉬운 쉐이더부터 짜보는 방법으로 HLSL을 배우도록 하겠습니다. 정 문법이 궁금하신 분들은 부록을 참고하시길 바랍니다.

하지만 HLSL 코드를 곧바로 짜기 전에 준비해야 할 것들이 좀 있군요. 이건 좀 지루하시더라도 꾹 참고 따라 해주시기 바랍니다.

쉐이더 프로그래밍을 위한 기본준비
서문에서도 말씀드렸듯이 이 책의 초점은 쉐이더 프로그래밍입니다. 이 책에서 DirectX에 대한 내용을 자세히 다루지 않기로 결정한 이유는 이미 훌륭한 DirectX 입문 책들이 시중에 나와있는데 굳이 DirectX를 다시 처음부터 소개하면서 쓸데없이 지면을 낭비하고 싶지 않았기 때문입니다. (지면이 늘어나면 쓸데없이 값도 오릅니다.) 또한 프로그래머 분들 외에 테크니컬 아티스트 분들도 이 책을 읽으실 수 있도록 하기 위해서입니다.

마찬가지 이유로 이 책에서 쉐이더를 만드는 과정도 둘로 나눴습니다. 첫 번째 단계는 쉐이더 작성만을 하는 단계로 AMD(전 ATI) 사의 렌더몽키(render monkey)라는 프로그램을 사용합니다. 이 단계는 프로그래머와 아티스트 분들을 모두 대상으로 하므로 반드시 따라 해 주시기 바랍니다.

두 번째 단계는 렌더몽키에서 만든 쉐이더를 C++/DirectX 프레임워크에서 불러와 사용하는 것으로 프로그래머 분들을 위한 단계입니다. 프로그래머이시더라도 C++/DirectX 프레임워크에 관심이 없으신 분들은 이 단계를 건너 뛰셔도 됩니다. 실제로 쉐이더 코드를 작성하는 곳은 첫 번째 단계입니다.

자, 그러면 위 두 단계에서 쉐이더를 배우는 데 필요한 것들을 준비해보죠.

렌더몽키
렌더몽키는 AMD사에서 제공하는 쉐이더 작성도구로 프로토타이핑에 유용합니다. 부록 디스크에서 /RenderMonkey/ RenderMonkey.2008-12-17-v1.82.322.msi를 찾아 설치해 주세요. 그냥 기본(default) 옵션으로 설치하시면 되겠습니다.

선택사항: 간단한 DirectX 프레임워크
C++/DirectX 프레임워크에서 쉐이더를 실행해보고 싶으신 분들만 이 절을 따라 해주세요.

우선 비주얼 C++ 2008과 DirectX SDK를 설치하시기 바랍니다. 비주얼 C++을 소장하고 계시지 않으신 분들은 마이크로소프트사의 웹 페이지에서 공짜 버전인 익스프레스 버전을 다운받으실 수 있습니다. DirectX SDK는 부록 CD의 DXSDK 폴더에 포함되어 있습니다.

위 두 프로그램의 설치를 마치셨다면 비주얼 C++ 2008에서 부록 CD에 있는 samples/01_DxFramework/BasicFramework.sln 파일을 여시기 바랍니다. 별다른 수정 없이 이 프로그램을 실행하면 다음과 같은 파란 화면을 보실 수 있을 것입니다.

그림 1.2. 별볼일 없는 초 간단 프레임워크


이 프레임워크는 다음과 같은 기능들을 구현합니다.

  • 창의 생성 및 메시지 루프 등의 기본적인 윈도우 기능
  • Direct 3D 장치 생성
  • 텍스처, 모델, 쉐이더 등의 자원 로딩
  • 간단한 게임루프
  • 간단한 키보드 입력처리


참고로 말씀드리는데 이 프로그램은 쉐이더 코드를 재빨리 실행할 수 있도록 매우 간단하게 만든 프레임워크입니다. 그 결과, 모든 함수들이 .cpp 파일 하나 안에 들어있고, 클래스나 개체도 사용하지 않지요. 따라서 모든 함수들은 C스타일로 작성되어 있고, 모든 변수들도 전역적으로 선언되어 있습니다. 실제 게임을 만드실 때, 이렇게 프레임워크를 만드시면 절대 안됩니다. 다시 한 번 말씀드리는데 이 프레임워크는 쉐이더 데모를 실행할 수 있도록 만든 프로그램일 뿐입니다.

자, 그럼 적당히 주의도 드렸으니 이제 프레임워크를 살펴보도록 합시다. 우선 BasicFramework.h를 엽니다.

//**********************************************************************
//
// ShaderFramework.h
//
// 쉐이더 데모를 위한 C스타일의 초간단 프레임워크입니다.
// (실제 게임을 코딩하실 때는 절대 이렇게 프레임워크를
// 작성하시면 안됩니다. -_-)
//
// Author: Pope Kim
//
//**********************************************************************


#pragma once

#include <d3d9.h>
#include <d3dx9.h>

// ---------- 선언 ------------------------------------
#define WIN_WIDTH 800
#define WIN_HEIGHT 600

// ---------------- 함수 프로토타입 ------------------------

// 메시지 처리기 관련
LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam );
void ProcessInput(HWND hWnd, WPARAM keyPress);

// 초기화 과련
bool InitEverything(HWND hWnd);
bool InitD3D(HWND hWnd);
bool LoadAssets();
LPD3DXEFFECT LoadShader( const char * filename );
LPDIRECT3DTEXTURE9 LoadTexture(const char * filename);
LPD3DXMESH LoadModel(const char * filename);

// 게임루프 관련
void PlayDemo();
void Update();

// 렌더링 관련
void RenderFrame();
void RenderScene();
void RenderInfo();

// 뒷정리 관련
void Cleanup();


이 헤더파일에서 눈 여겨 볼만한 것은 WIN_WIDTH와 WIN_HEIGHT밖에 없습니다. 이 두 #define문은 데모 프로그램의 창 크기를 정의합니다. 나머지 코드들은 단순히 함수선언들일 뿐입니다. 실제 함수들의 구현은 ShaderFramework.cpp 파일에 들어 있으니 ShaderFramework.cpp 파일을 열어보도록 할까요?

이 파일의 제일 위에는 다음과 같은 전역변수들이 정의되어 있습니다.

//----------------------------------------------------------------------
// 전역변수
//----------------------------------------------------------------------

// D3D 관련
LPDIRECT3D9       gpD3D          = NULL;        // D3D
LPDIRECT3DDEVICE9 gpD3DDevice    = NULL;        // D3D 장치

// 폰트
ID3DXFont*        gpFont         = NULL;

// 모델

// 쉐이더

// 텍스처

// 프로그램 이름
const char*       gAppName       = "초 간단 쉐이더 데모 프레임워크";


이제 프로그램의 창을 생성할 차례입니다.


//-----------------------------------------------------------------------
// 프로그램 진입점/메시지 루프
//-----------------------------------------------------------------------

// 진입점
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )
{

프로그램의 창을 생성하려면 우선 윈도우 클래스를 등록해야 합니다.


    // 윈도우 클래스를 등록한다.
    WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
                      GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
                      gAppName, NULL };
    RegisterClassEx( &wc );


이제 CreateWindow 함수를 사용해서 위에서 등록한 윈도우 클래스의 인스턴스를 만듭니다. 이 때, 앞서 정의했던 WIN_WIDTH와 WIN_HEIGHT를 창의 크기로 지정합니다.


    // 프로그램 창을 생성한다.
    DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
    HWND hWnd = CreateWindow( gAppName, gAppName,
                              style, CW_USEDEFAULT, 0, WIN_WIDTH, WIN_HEIGHT,
                              GetDesktopWindow(), NULL, wc.hInstance, NULL );

창의 크기를 WIN_WIDTH와 WIN_HEIGHT로 만들면 실제 렌더링을 할 수 있는 공간이 이보다 작습니다. 창의 크기에 타이틀 바 및 경계선이 포함되기 때문이라죠. 따라서 실제 렌더링이 가능한 공간(client rect)이 WIN_WIDTH와 WIN_HEIGHT와 같도록 창의 크기를 재조정해야 겠네요.

    // Client Rect 크기가 WIN_WIDTH, WIN_HEIGHT와 같도록 크기를 조정한다.
    POINT ptDiff;
    RECT rcClient, rcWindow;

    GetClientRect(hWnd, &rcClient);
    GetWindowRect(hWnd, &rcWindow);
    ptDiff.x = (rcWindow.right - rcWindow.left) - rcClient.right;
    ptDiff.y = (rcWindow.bottom - rcWindow.top) - rcClient.bottom;
    MoveWindow(hWnd,rcWindow.left, rcWindow.top, WIN_WIDTH + ptDiff.x, WIN_HEIGHT + ptDiff.y, TRUE);

이제 창의 크기도 적절히 조정했으니 창을 보여줄 차례입니다.

    ShowWindow( hWnd, SW_SHOWDEFAULT );
    UpdateWindow( hWnd );


다음은 Direct3D를 초기화하고 모든 D3D 자원들(텍스처, 쉐이더, 메쉬 등)을 로딩합니다. 이 모든 기능들은 InitEverything() 함수 안에 포함되어 있습니다. 만약 Direct3D 및 기타 초기화에 실패하면 데모를 보여주는 게 불가능하므로 프로그램을 종료합니다.

    // D3D를 비롯한 모든 것을 초기화한다.
    if( !InitEverything(hWnd) )
    {
        PostQuitMessage(1);
    }

D3D 및 기타 초기화를 마쳤다면 남은 일은 WM_QUIT 메시지를 받을 때까지 데모를 실행하는 것이 전부입니다. WM_QUIT은 데모를 종료하라는 윈도우 메시지입니다.


    // 메시지 루프
    MSG msg;
    ZeroMemory(&msg, sizeof(msg));
    while(msg.message!=WM_QUIT)
    {
        if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
        {
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
        else // 메시지가 없으면 게임을 업데이트하고 장면을 그린다
        {
            PlayDemo();
        }
    }

데모를 종료할 때가 되면 윈도우 클래스의 등록을 해제하고 프로그램을 끝마칩니다.


    UnregisterClass( gAppName, wc.hInstance );
    return 0;
}

다음은 윈도우 메시지를 처리하는 함수입니다.


// 메시지 처리기
LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
    switch( msg )
    {

키보드 입력은 ProcessInput이라는 함수에서 처리할 것입니다.

    case WM_KEYDOWN:
        ProcessInput(hWnd, wParam);
        break;


창이 닫힐 때는 초기화 도중에 생성했던 D3D 자원들을 해제하고 프로그램을 종료하라는 메시지를 보냅니다.


    case WM_DESTROY:
        Cleanup();
        PostQuitMessage(0);
        return 0;
    }

이 데모에서 처리하지 않는 윈도우 메시지들은 기본(default) 메시지 처리기가 처리하도록 합니다.

    return DefWindowProc( hWnd, msg, wParam, lParam );
}

이 프레임워크가 현재 처리하는 키보드 입력은 ESC 키가 전부입니다. ESC키가 눌리면 프로그램의 실행을 마칩니다.

// 키보드 입력처리
void ProcessInput( HWND hWnd, WPARAM keyPress)
{
switch(keyPress)
{
// ESC 키가 눌리면 프로그램을 종료한다.
case VK_ESCAPE:
PostMessage(hWnd, WM_DESTROY, 0L, 0L);
break;
}
}


이제 초기화 코드를 살펴볼까요?


//------------------------------------------------------------
// 초기화 코드
//------------------------------------------------------------
bool InitEverything(HWND hWnd)
{

우선 InitD3D함수를 호출하여 D3D를 초기화합니다. D3D 초기화에 실패하지 않았다면 LoadAssets() 함수를 통해 모델, 쉐이더, 텍스처 등의 D3D 자원들을 로딩합니다.

    // D3D를 초기화
    if( !InitD3D(hWnd) )
    {
        return false;
    }

    // 모델, 쉐이더, 텍스처 등을 로딩
    if( !LoadAssets() )
    {
        return false;
    }

그 다음은 폰트를 로딩할 차례입니다. 이 폰트를 사용하여 화면에 디버그 정보 등을 보여줄 것입니다.


    // 폰트를 로딩
    if(FAILED(D3DXCreateFont( gpD3DDevice, 20, 10, FW_BOLD, 1, FALSE,
        DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY,
        (DEFAULT_PITCH | FF_DONTCARE), "Arial", &gpFont )))
    {
return false;
    }

    return true;
}


위의 D3DXCreateFont()에서 사용했던 각 매개변수의 의미는 순서대로 다음과 같습니다.

  • gpD3DDevice: D3D 장치
  • 20: 폰트의 높이
  • 10: 폰트의 너비
  • FW_BOLD: 볼드체(두꺼운 폰트)를 이용함
  • 1: 밉맵레벨
  • FALSE: 이탤릭체를 쓰지 않음
  • DEFAULT_CHARSET: 기본 문자셋을 사용
  • OUT_DEFAULT_PRECIS: 실제 화면에 출력되는 폰트가 여기서 지정한 속성과 어느 정도 비슷해야 하는지를 설정
  • DEFAULT_QUALITY: 여기서 지정하는 폰트와 실제 폰트의 품질이 얼마나 비슷해야 하는지 설정
  • DEFAULT_PITCH | FF_DONTCARE: 기본 피치를 사용하고 폰트군은 상관 없음
  • "Arial": 사용할 폰트이름
  • gpFont: 새로 만든 폰트를 저장할 포인터


이제 D3D 객체와 D3D 장치를 생성하는 InitD3D 함수를 살펴봅시다. D3D를 이용해서 자원을 로딩하거나 렌더링을 하려면 반드시 D3D장치를 생성해야 합니다.


// D3D 객체 및 장치 초기화
bool InitD3D(HWND hWnd)
{

우선 Direct3D 객체를 구합니다.

    // D3D 객체
    gpD3D = Direct3DCreate9( D3D_SDK_VERSION );
    if ( !gpD3D )
    {
        return false;
    }

그 다음에는 D3D 장치를 생성할 때 필요한 구조체에 정보를 채워 넣겠습니다.

    // D3D장치를 생성하는데 필요한 구조체를 채워 넣는다.
    D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory( &d3dpp, sizeof(d3dpp) );

    d3dpp.BackBufferWidth            = WIN_WIDTH;
    d3dpp.BackBufferHeight           = WIN_HEIGHT;
    d3dpp.BackBufferFormat           = D3DFMT_X8R8G8B8;
    d3dpp.BackBufferCount            = 1;
    d3dpp.MultiSampleType            = D3DMULTISAMPLE_NONE;
    d3dpp.MultiSampleQuality         = 0;
    d3dpp.SwapEffect                 = D3DSWAPEFFECT_DISCARD;
    d3dpp.hDeviceWindow              = hWnd;
    d3dpp.Windowed                   = TRUE;
    d3dpp.EnableAutoDepthStencil     = TRUE;
    d3dpp.AutoDepthStencilFormat     = D3DFMT_D24X8;
    d3dpp.Flags                      = D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL;
    d3dpp.FullScreen_RefreshRateInHz = 0;
    d3dpp.PresentationInterval       = D3DPRESENT_INTERVAL_ONE;


위에서 주목할만한 정보들은 다음과 같습니다.

  • BackBufferWidth: 백버퍼(렌더링 영역)의 너비
  • BackBuferHeight: 백버퍼의 높이
  • BackBufferFormat: 백버퍼의 포맷
  • AutoDepthStencilFormat: 깊이/스텐실 버퍼의 포맷
  • SwapEffect: 백버퍼를 스왑(swap)할 때의 효과. 성능 상 D3DSWAPEFFECT_DISCARD를 사용하는 것이 좋습니다.
  • PresentationInterval: 모니터 주사율과 백버퍼를 스왑하는 빈도간의 관계. D3DPRESENT_INTERVAL_ONE은 모니터가 수직동기될 때마다 백버퍼를 스왑해 줍니다. 게임에서는 성능상 모니터의 수직동기를 기다리지 않고 곧바로 스왑해 주는 경우가 많습니다.(D3DPRESENT IMMEDIATE). 단, 이럴 땐 전 프레임과 현재 프레임이 서로 찢겨 보이는 부작용이 있습니다.


이제 위에서 채워 넣은 정보들을 이용해서 D3D장치를 생성합니다.

    // D3D장치를 생성한다.
    if( FAILED( gpD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
        D3DCREATE_HARDWARE_VERTEXPROCESSING,
        &d3dpp, &gpD3DDevice ) ) )
    {
        return false;
    }

    return true;
}

D3D 자원을 읽어오는 LoadAssets() 함수 안에는 현재 아무 내용도 들어있지 않습니다. 이 책을 진행하면서 텍스처, 쉐이더, 모델 등을 읽어올 일이 있을 때마다 이 함수 안에서 LoadShader(), LoadTexture(), LoadModel() 함수들을 호출하겠습니다.

bool LoadAssets()
{
    // 텍스처 로딩

    // 쉐이더 로딩

    // 모델 로딩

    return true;
}

다음은 .fx 포맷으로 저장된 쉐이더 파일을 불러오는 LoadShader() 함수입니다. .fx파일은 정점쉐이더와 픽셀쉐이더를 모두 포함하고 있는 텍스트 파일로 D3DXCreateEffectFromFile() 함수를 통해 로딩 및 컴파일합니다. 따라서 HLSL코드에 문법적인 오류가 있다면 이 함수를 호출하는 도중에 쉐이더 컴파일 에러가 나겠죠? 그럴 때는 D3DXCreateEffectFromFile()의 마지막 매개변수를 통해 에러메시지를 구해올 수 있습니다. 이 에러메시지를 비주얼 C++의 출력 창에 보여주도록 하겠습니다.


// 쉐이더 로딩
LPD3DXEFFECT LoadShader(const char * filename )
{
    LPD3DXEFFECT ret = NULL;
    LPD3DXBUFFER pError = NULL;
    DWORD dwShaderFlags = 0;

#if _DEBUG
    dwShaderFlags |= D3DXSHADER_DEBUG;
#endif

    D3DXCreateEffectFromFile(gpD3DDevice, filename,
        NULL, NULL, dwShaderFlags, NULL, &ret, &pError);

    // 쉐이더 로딩에 실패한 경우 output창에 쉐이더
    // 컴파일 에러를 출력한다.
    if(!ret && pError)
    {
        int size  = pError->GetBufferSize();
        void *ack = pError->GetBufferPointer();

        if(ack)
        {
            char* str = new char[size];
            sprintf(str, (const char*)ack, size);
            OutputDebugString(str);
            delete [] str;
        }
    }

    return ret;
}

위에서 D3DXCreateEffectFromFile() 함수를 호출할 때 사용한 인자들의 의미는 순서대로 아래와 같습니다.

  • gpD3DDevice: D3D 장치
  • filename: 읽어올 쉐이더 파일의 이름
  • NULL: 쉐이더를 컴파일 할 때 추가로 사용할 #define 정의
  • NULL: #include 지시문을 처리할 때 사용할 인터페이스 포인터
  • dwShaderFlags: 쉐이더를 컴파일 할 때 사용할 플래그
  • NULL: 매개변수 공유에 사용할 이펙트 풀
  • ret: 로딩된 이펙트를 저장할 포인터
  • pError: 컴파일러 에러 메시지를 가리킬 포인터


다음은 .x 포맷으로 저장된 모델을 로딩해오는 코드입니다. .x 파일은 DirectX에서 자체적으로 지원하는 메쉬 포맷입니다.

// 모델 로딩
LPD3DXMESH LoadModel(const char * filename)
{
    LPD3DXMESH ret = NULL;
    if ( FAILED(D3DXLoadMeshFromX(filename,D3DXMESH_SYSTEMMEM, gpD3DDevice,
        NULL,NULL,NULL,NULL, &ret)) )
    {
        OutputDebugString("모델 로딩 실패: ");
        OutputDebugString(filename);
        OutputDebugString("\n");
    };

    return ret;
}

위에서 D3DXLoadMeshFromX() 호출에 사용했던 매개변수의 의미는 순서대로 다음과 같습니다.

  • filename: 로딩해 올 메쉬의 파일명
  • D3DXMESH_SYSTEMMEM: 시스템 메모리에 메쉬를 로딩할 것
  • gpD3DDevice: D3D 장치
  • NULL: 인접 삼각형 데이터를 따로 구해오지 않음.
  • NULL: 머테리얼(material) 정보를 따로 구해오지 않음
  • NULL: 이펙트 인스턴스를 따로 구해오지 않음
  • NULL: 머테리얼 수를 따로 구해오지 않음
  • ret: 로딩해온 메쉬를 저장할 포인터


아래는 다양한 포맷으로 저장된 이미지들을 텍스처로 로딩해 오는 LoadTexture() 함수입니다.

// 텍스처 로딩
LPDIRECT3DTEXTURE9 LoadTexture(const char * filename)
{
    LPDIRECT3DTEXTURE9 ret = NULL;
    if ( FAILED(D3DXCreateTextureFromFile(gpD3DDevice, filename, &ret)) )
    {
        OutputDebugString("텍스처 로딩 실패: ");
        OutputDebugString(filename);
        OutputDebugString("\n");
    }

    return ret;
}


다음은 게임루프 함수인 PlayDemo()입니다. 이 함수는 처리할 윈도우 메시지가 없을 때마다 호출됩니다. 실제 게임에서는 지난 프레임으로부터 경과한 시간을 구해서 업데이트 및 렌더링에 사용할 테지만 이 프레임워크에서는 간략함을 위해 그 부분을 생략했습니다.


//------------------------------------------------------------
// 게임루프
//------------------------------------------------------------
void PlayDemo()
{
    Update();
    RenderFrame();
}

현재 Update() 함수에는 아무 내용도 없습니다. 나중에 필요하다면 코드를 채워 넣도록 하지요.


// 게임로직 업데이트
void Update()
{
}

다음은 정작 무언가를 그리는 함수인 RenderFrame()입니다.

//------------------------------------------------------------
// 렌더링
//------------------------------------------------------------

void RenderFrame()
{

우선 화면을 파란색으로 지웁니다.

    D3DCOLOR bgColour = 0xFF0000FF; // 배경색상 - 파랑

    gpD3DDevice->Clear( 0, NULL, (D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER),
        bgColour, 1.0f, 0 );

그 후, 장면(scene)과 디버그 정보를 그립니다.

    gpD3DDevice->BeginScene();
    {
        RenderScene(); // 3D 물체등을 그린다.
        RenderInfo(); // 디버그 정보 등을 출력한다.
    }
    gpD3DDevice->EndScene();


모든 그리기를 마쳤다면 백 버퍼에 저장되어 있는 렌더링 결과를 화면에 뿌려줍니다.

    gpD3DDevice->Present( NULL, NULL, NULL, NULL );
}


현재 3D 물체 등을 그리는 RenderScene() 함수에는 아무 코드도 들어있지 않습니다. 다음 장에서 3D 물체를 그릴 때 여기에 코드를 채워 넣도록 하지요.

// 3D 물체 등을 그린다.
void RenderScene()
{
}

RenderInfo() 함수는 간단한 키 매핑 정보를 화면에 보여줍니다.

// 디버그 정보 등을 출력.
void RenderInfo()
{
    // 텍스트 색상
    D3DCOLOR fontColor = D3DCOLOR_ARGB(255,255,255,255);  

    // 텍스트를 출력할 위치
    RECT rct;
    rct.left=5;
    rct.right=WIN_WIDTH / 3;
    rct.top=5;
    rct.bottom = WIN_HEIGHT / 3;

    // 키 입력 정보를 출력
    gpFont->DrawText(NULL, "데모 프레임워크\n\nESC: 데모종료", -1, &rct,
        0, fontColor );
}

프로그램을 종료할 때, GPU상의 메모리 누수를 방지하려면 D3D를 통해 생성했던 자원들을 모두 해제(release)해줘야 합니다. 자원들을 모두 해제한 뒤에는 D3D 장치와 D3D 객체들도 해제해 줍니다.

//------------------------------------------------------------
// 뒷정리 코드.
//------------------------------------------------------------

void Cleanup()
{
    // 폰트를 release 한다.
    if(gpFont)
    {
        gpFont->Release();
        gpFont = NULL;
    }

    // 모델을 release 한다.

    // 쉐이더를 release 한다.

    // 텍스처를 release 한다.

    // D3D를 release 한다.
    if(gpD3DDevice)
    {
        gpD3DDevice->Release();
        gpD3DDevice = NULL;
    }

    if(gpD3D)
    {
        gpD3D->Release();
        gpD3D = NULL;
    }
}


자, 이것으로 아주 간단한 쉐이더 프레임워크의 작성을 마쳤습니다. 위의 코드가 잘 이해가 안 되시는 분들이 계실지도 모르겠는데, 이 책에서 HLSL 프로그래밍을 익히는 데는 크게 문제가 안됩니다. 단, 그래픽 프로그래머가 되는 것이 꿈이신 분들은 이 책을 마친 뒤에라도 반드시 DirectX를 제대로 배우시라고 권해드리고 싶습니다.

여기까지 다소 지루한 준비과정을 견뎌내시느라 수고하셨습니다. 후딱 정리를 마친 뒤에 다음 장부터 실제로 재미있게 쉐이더를 작성해 보기로 하죠!

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 쉐이더는 어느 픽셀을 어떤 색으로 칠할지를 계산하는 함수이다.
  • 쉐이더를 화가가 그림을 그리는 것에 비유하면 정점쉐이더는 투시를 픽셀쉐이더는 명암을 담당한다.
  • 쉐이더 프로그래밍이란 정점쉐이더와 픽셀쉐이더에서 실행시킬 함수를 작성하는 것이다.
  • AMD사의 렌더몽키를 사용하면 재빨리 쉐이더를 프로토타입할 수 있다.

댓글 8개:

  1. dx 코드 다 외워야 하나여?

    답글삭제
  2. 아뇨. 책 보면서 하셔도 되요.... (저보고 외우라고 해도 배쨈... -_-)

    답글삭제
  3. 프로젝트가 2010으로 짜여져 있나보네요 ㅜㅜ 흑.. direct sdk는 언제 버전을 써야 되나요?? 최신껄 까니깐 링크 정의가 안된것도 많고 IDX3Font 같은것들은 잡히지가 않네요... opengl만 사용하다 DX로 넘어오니 이런것들이 헷갈립니다 ㅜㅜ 도와주세요~

    답글삭제
    답글
    1. Visual C++ 2010 Express 버전은 공짜니까 그냥 다운 받아 쓰시지요 ^_^
      DX SDK버전은 2010년 6월을 씁니다(이게 젤 최신거일텐데요..)

      IDX3Font를 쓰시려면 d3d9.lib 외에도 d3dx9.lib(또는 d3dx9.lib)도 링크해주셔야 해요.

      삭제
  4. 정말 쉽게 잘 설명해주세요!! 너무 재밋다능 ㅜㅜ 감사합니다~

    답글삭제
  5. 렌더몽키 주소가 옮겨졌네요 http://gpuopen.com/archive/gamescgi/rendermonkey-toolsuite/

    답글삭제
  6. DirectX에 대한 책은 이미 잘 나와있다고 하는 데 어떤 책인지 알 수 있을까요?

    답글삭제