이번 포스팅에서는 Window 초기화와 D3D 11 초기화에 대해 설명할 예정이다.

먼저 Window 초기화부터 코드와 함께 알아보겠다. 

 

 

 

 

InitWindow

Game.cpp에서 해당 함수에 대한 정의를 해줄 것이고, 아래 3단계로 진행된다.  1. Register the window class  2. Create window  3. Show window

 

 

 

 

InitWindow - Register window

// Library::Game.cpp
#include "Game.h"

LPCWSTR g_pszWindowClassName = L"GGPWindowClass";

HRESULT InitWindow(_In_ HINSTANCE hInstance, _In_ INT nCmdShow)
{
    WNDCLASSEX wcex =
    {
        .cbSize = sizeof(WNDCLASSEX),
        .style = CS_HREDRAW | CS_VREDRAW,
        .lpfnWndProc = WindowProc,
        .cbClsExtra = 0,
        .cbWndExtra = 0,
        .hInstance = hInstance,
        .hIcon = LoadIcon(hInstance,IDI_APPLICATION),
        .hCursor = LoadCursor(nullptr, IDC_ARROW),
        .hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1),
        .lpszMenuName = nullptr,
        .lpszClassName = g_pszWindowClassName,
        .hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION),
    };
}

 

좀 중요하게 볼 것들만 적자면

  • IpfnWndProc : 현재 window에서 발생한 이벤트를 처리할 window procedure (WindowProc 함수를 만들어줄 예정)
  • hInstance : 현재 실행하려는 application. Handle instance로, 식별 ID를 의미한다.
  • IpszClassName : 현재 window class의 이름이다. Window끼리 구분을 위해 사용하고 접두사 L은 const wchar_t*를 의미한다.

 

if (!RegisterClassEx(&wcex))
{
    DWORD dwError = GetLastError();

    MessageBox(
        nullptr,
        L"Call to RegisterClassEx failed!",
        L"Game Graphics Programming",
        NULL
    );

    if (dwError != ERROR_CLASS_ALREADY_EXISTS)
    {
        return HRESULT_FROM_WIN32(dwError);
    }

    return E_FAIL;
}

 

WNDCLASSEX를 RegisterClassEx를 통해 등록해준다. 추가로 문제가 있을 경우 오류 처리를 해주는 구문을 사용

 

 

 

 

InitWindow - Creating window

CreateWindow 함수를 통해서 등록한 window instance를 생성해준다. 

창에 대한 handle값을 g_hWnd로 저장한다. 

// Library::Game.cpp

// Global Variable
HWND g_hWnd = nullptr;
HINSTANCE g_hInstance = nullptr;
LPCWSTR g_pszWindowName = L"GGP02: Direct3D 11 Basics";


// InitWindow() 이어서
g_hInstance = hInstance;
RECT rc = { 0, 0, 800, 600 };
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);

g_hWnd = CreateWindow(
    g_pszWindowClassName,
    g_pszWindowName,
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
    CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,
    nullptr,
    nullptr,
    hInstance,
    nullptr
);

 

 

 

 

 

InitWindow - Show Window

마지막으로 윈도우 창을 보여주면 초기화 단계는 끝이 난다! winMain에서 InitWindow를 해주면 실행할 수 있다.

하지만 아직 WindowProc을 구현하지 않았기 때문에 아무것도 보여지지 않고 사용자 입력처리하는 부분도 없다. 

이제 사용자와 OS로부터 event를 어떤식으로 받고 응답하게 되는지 살펴보자. 

// Library::Game.cpp

ShowWindow(g_hWnd, nCmdShow);

return S_OK;

 

 

 

 

Window Messages

사용자와 OS로부터 받는 event는 message의 형태로 전달된다. 이때 Message는 단순한 숫자 형태로 정의된다.

  • 사용자 입력 : 마우스 클릭, 키보드 입력
  • OS 입력 : 새로운 하드웨어 연결, 저전력 모드 전환

이러한 event들은 프로그램이 돌아가는 중에 언제든 일어날 수 있고, 어떤 순서로 들어올 지 모른다. 이를 유연하게 해결하기 위해 window에서는 message-passing model을 사용한다.

 

 

 

 

 

Messages Loop

올바른 window에 message가 전달되도록 프로그램이 while문에서 message를 체크해주는 것

 

Application은 실행되는 동안 수천개의 메세지를 받는다. 하지만 여러 개의 window를 가진 프로그램일 수도 있기 때문에 올바른 window에 message가 전달되어야 한다. 이를 위해서는 프로그램이 실행되어 있는 동안, while문으로 무한 반복문을 돌리며 message를 체크해줘야 하고, 이를 message loop라고 부른다.

  • OS는 이 message들을 다루기 위한 message queue를 만들어 둔다. 이 queue 순서대로 while문에서 처리를 해준다.
  • 이때 while문에서  PeekMessage()(GetMessage()) 함수를 통해 queue에서 message를 받아온다.

GetMessage()

  • 일반적으로는 queue에서 message를 받아올 때 GetMessage()를 활용한다.
  • 이 함수는 queue가 비어 있을 경우, message가 올 때까지 무한히 기다리게 된다.
  • 특정 이벤트가 없으면 렌더링도 못하고 대기를 해버리기 때문에,, 게임에는 부적합하다. → 대신 PeekMessage() 사용

Message Loop 동작 예시

  • 사용자가 마우스 왼쪽 클릭
  • OS가 WM_LBUTTONDOWN message를 queue에 넣음
  • Message Loop에서 PeekMessage()함수가 호출됨
  • WM_LBUTTONDOWN message를 pull하고,  MSG structure에 message 정보를 넣음.
  • 이후, TranslationMessage()와 DispatchMessage() 함수가 실행됨
  • DispatchMessage() 함수에서 메시지에 해당하는 window의 procedure를 실행
  • Window procedure에서 해당 메시지를 처리
  • 다시 Message Loop로 돌아가 다음 메시지를 처리

 

 

 

 

Window Procedure

◇  OS에 의해 호출되는 콜백 함수로 message가 발생할 때 해당 이벤트를 처리해준다.

 

우리는 직접 WindowProc 함수를 만들어서 사용할 예정이다. 

LRESULT CALLBACK WindowProc(
  _In_ HWND hWnd, 
  _In_ UINT uMsg, 
  _In_ WPARAM wParam, 
  _In_ LPARAM lParam
);
  • 매개변수 : Message 대상이 되는 window의 handle / Message 내용 / Message 추가 정보(wParam, IParam)
  • return : LRESULT 값으로, procedure의 결과 값을 나타내는 정수 형태 값이다. 
LRESULT CALLBACK WindowProc(_In_ HWND hWnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_SIZE: // window resizing
        break;
    default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return 0;
}

 

보통 이런식으로 switch 문을 활용한다. Message 종류가 늘어나면 대응하는 함수를 추가해주면 된다.

OS가 보내는 모든 message에 대해 처리를 할 수 없기 때문에, (해당 window에 필요 없는 message도 포함된) 기본적으로 처리하지 않을 message들에 대해서는 DefaultWindowProc() 함수로 처리해준다.

 

 

 

 

Closing Window

◇  DestroyWindow() 함수를 호출해서 창을 닫아준다.

  • 사용자가 window를 끌 때 닫기 버튼을 누를 수도 있고 Alt+F4를 누를 수도 있다. 이 경우 모두 WM_CLOSE message로 전달된다.
  • 아래 처럼 종료 시에 MessageBox를 통해 확인하는 창을 띄워줄 수도 있다.
  • WM_DESTROY : Window가 destroy 되기 전에 맞는 Message. 여기서 PostQuitMessage()를 호출해 Message Loop를 멈추고, 할당한 메모리들을 해제해준다.
LRESULT CALLBACK WindowProc(_In_ HWND hWnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_CLOSE:
        if (MessageBox(hWnd,
            L"진짜 종료하시겠습니까?",
            L"Game Graphics Programming",
            MB_OKCANCEL) == IDOK)
        {
            DestroyWindow(hWnd);
        }
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
        
    default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return 0;
}
  • 아래와 같은 종료 루프를 가진다.

 

 


 

지금까지 Window 초기화에 대해 코드와 함께 알아보았고, 여기서부터는 D3D 초기화에 대해 다룰 예정이다.

 

 

 

 

DirectX APIs

◇  Direct2D(D2D) / Direct3D (D3D)는 각각 2D/3D Graphics를 나타내기 위한 API이다.

  • Direct Write는 text layout을 담당한다.
  • DirecX Graphics Infrastructure(DXGI)는 low-level에서 rendering 관련된 일을 수행한다. D3D와 그래픽 드라이버 사이를 연결해준다. 
  • D3D는 하드웨어를 가속시킨다. 즉, CPU가 아닌 GPU를 통해 그래픽 연산을 훨씬 빠르게 처리할 수 있다. 

 

 

 

 

 

D3D Initialization

이제 D3D를 통해 만들어진 window에 Scene을 만들어 볼 것이다. 

초기화를 위해서는 Swap Chain / Device / Immediate Context 이 세가지를 만들어줘야한다. 

 

 

 

 

DirectX Device Resource

  • 앞서 DXGI를 통해 low-level graphics driver와 D3D를 연결한다고 설명했다.
  • 버퍼(buffer) : DXGI를 통해 가져오는 그려야 할 공간 = 렌더링하는 메모리 공간. GPU 메모리 상에 버퍼가 있고, 이를 DXGI를 통해 화면에 보여주게 된다.
  • 더블버퍼링(Double Buffering) : 화면상에 보이고 있는 버퍼를 'Front Buffer'라고 하고(수정할 일이 없기 때문에 Read-only), 보이지 않는 버퍼(그리고 있는 버퍼)를 'Back Buffer'라고 한다. Front가 보여지는 동안 미리 Back Buffer에서 새로운 화면을 그린다. 그리고 화면이 바뀔 때 Front와 Back Buffer를 swap(present)한다. 복사하는 것이 아니라 두 버퍼를 가리키는 포인터를 서로 맞바꾸는 것임. 
  • 렌더 타겟(Render Target) : 실제로 렌더링이 일어나고 있는 Back Buffer를 의미한다.

 

  • Swap Chain : Flip을 하기 위해 순차적으로 쌓이는 DXGI에 있는 버퍼들의 집합을 의미한다. D3D에서는 IDXGISwapChain이라는 인터페이스를 사용한다.
  • 화면에 draw를 하기 위해 다음 과정을 수행하게 된다. 
    1. application을 위한 window만들기 
    2. D3D 사용하기 위한 interface 만들기.
    3. 생성된 windowrendering하기 위한 swap chain을 만들기.
    4. Render target을 만들고, pixel을 채운다.
    5. Swap chainpresent 한다. (2개의 buffer를 교환하는 것

 

 

 

 

D3D 11 Device & Context

D3D를 쓰기 위해서는 GPU를 위한 interface를 생성해야 한다. 이때  ID3D11DeviceID3D11DeviceContext가 생성된다.

 

  • ID3D11Device : 자원 관리 (할당/해제)
    • 픽셀에 draw하기 위한 자원을 할당하고 구성하는 역할
    • 필요할 때 호출되는 method들로 이루어져 있다.
  • ID3D11DeviceContext : 기능 관리 (렌더링 과정)
    • 버퍼, 뷰, 기타 자원 로드, drawing과 같은 매 프레임 호출되는 method 들이 포함되어 있다.

InitDevice에서 D3D Initialization을 해준다. 아래와 같은 다양한 초기화를 담당한다. (자세한 코드는 생략)

HRESULT InitDevice()
{
    // Create D3D 11 device & context
    // Obtain DXGI Factory from device
    // Create swap chain
    // Create render target view
    // Setup the viewport
}

 

초기화 과정에서 D3D11CreateDevice() 함수를 통해 devicedevice context을 모두 만들어준다.
HRESULT D3D11CreateDevice(
    [in, optional]  IDXGIAdapter*           pAdapter,
                    D3D_DRIVER_TYPE         DriverType,
                    HMODULE                 Software,
                    UINT                    Flags,
    [in, optional]  const D3D_FEATURE_LEVEL*pFeatureLevels,
                    UINT                    FeatureLevels,
                    UINT                    SDKVersion,
    [out, optional] ID3D11Device**          ppDevice,
    [out, optional] D3D_FEATURE_LEVEL*      pFeatureLevel,
    [out, optional] ID3D11DeviceContext**   ppImmediateContext
);

 

 

 

 

 

 

 

Reference KHU 강형엽 교수님 강의의 실습 수업을 수강하며 정리한 내용입니다.

+ Recent posts