Graphics Pipeline

Shader Stage 들이 직접 코딩할 수 있는 부분이고, 나머지는 API 내부에서 처리해준다.

우리가 다룰 부분은 Vertex Shader와 Pixel Shader이다.

  • Vertex shader: 각 vertex에 적용할 수 있는 transformation이나 lighting 등을 수행
  • Pixel(fragment) shader : 각 pixel마다 색상을 계산하여 처리

 

 

 

 

Input-Assembler(IA) Stage

◇ CPU가 제공한 정보들을 primitive 형태로 모아준다.

  • 일반적으로 primitive는 삼각형을 의미한다. 
  • 관련해서 우리가 해줘야 할 일들은 다음과 같다.
    • 1) Create input buffers
    • 2) Create the input-layer object
    • 3) Bind objects to the input-assembler stage
    • 4) Specify the primitive type
    • 5) Call draw methods

 

 

 

IA Stage  - 1) Create input buffers

IA Stage에 전달해야 하는 버퍼 : Vertex buffer, Index buffer

 

Create a Vertex Buffer

 

1. Vertex 구조체를 만든다.

2. Vertex에 기하 정보(position)를 담은 메모리를 할당한다.

// header
struct SimpleVertex
{
	XMFLOAT3 Pos;
};

// global variables
ID3D11Buffer* g_pVertexBuffer = nullptr;

///////////////////////////////

SimpleVertex sVertices[] =
    {
        {XMFLOAT3 (0.0f, 0.5f, 0.5f)},
        {XMFLOAT3 (0.5f, -0.5f, 0.5f)},
        {XMFLOAT3 (-0.5f, -0.5f, 0.5f)},
    };

 

3. Buffer Description(버퍼 정보들)을 만들어준다. 

  • ByteWidth는 buffer의 크기, BindFlags는 어떤 buffer인지를 나타냄.

4. SUBRESOURCE_DATA를 만든다

  • 실제 데이터 정보에 대한 포인터를 전달해줌.
D3D11_BUFFER_DESC bd = {};
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof(SimpleVertex) * 3;
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = 0;


D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = sVertices;

 

5. CreateBuffer() 함수를 호출한다.

  • D3DDevice에 buffer를 만드는 것을 요청한다.
  • 함수 인자 값으로 앞서 만든 buffer description, subresource data, 그리고 buffer의 주소를 넣어준다.
hr = g_pd3dDevice->CreateBuffer(
    &bd,
    &initData,
    &g_pVertexBuffer
    );

if (FAILED(hr))
    return hr;

 

 

 Create a Index Buffer

같은 방식으로 Index Buffer도 만들어준다. 

 

1. Buffer를 만들고 index 정보를 저장한다.

2. Buffer description을 생성한다. 이때 flag를 Index buffer로 해야한다.

ID3D11Buffer* g_pIndexBuffer = nullptr;

///////////////////////////////

WORD sIndices[] = { 0, 1, 2 };

bd = {};
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof(WORD) * 3;
bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = 0;

 

3. SUBRESOURCE_DATA를 만든다.

4. CreateBuffer() 함수를 호출한다.

initData = {};
initData.pSysMem = sIndices;

hr = g_pd3dDevice->CreateBuffer(
    &bd,
    &initData,
    &g_pIndexBuffer
);

if (FAILED(hr))
    return hr;

 

 

 

 

IA Stage  - 2)  Create the Input-Layout Object

  Input-Layout ObjectIA Stageinput에 대한 정보(앞에서 만든 buffer )를 전달해주는 역할을 한다.

  • Input data에 대한 설명을 해주는 input-element description을 만들어 준다.
  • 하나의 vertex는 position, normal, uv 등의 element 정보를 가질 수 있음. 하지만 지금은 position만 사용할 예정이기 때문에 이를 첫번째 파라미터로 명시해줬다.
  • 2번째 파라미터 : semantic index
  • 3번째 파라미터 : element 데이터의 format = 입력 데이터 타입에 따라 명시해주면 된다.
ID3D11InputLayout* g_pVertexLayout = nullptr;

///////////////////////////////

ID3DBlob* pVertexShaderBlob = nullptr;
D3D11_INPUT_ELEMENT_DESC layouts[] =
{
    {"POSITION",
    0,
    DXGI_FORMAT_R32G32B32_FLOAT,
    0,
    0,
    D3D11_INPUT_PER_VERTEX_DATA,
    0
    },
};

UINT uNumElements = ARRAYSIZE(layouts);

 

  • 4번째 파라미터 : input slot

IA Stage에는 여러 개의 vertex buffer가 들어갈 수 있다.(= 오브젝트가 여러 개인 경우)

각 n개의 vertex buffer를 하나의 슬롯으로 할당하여 IA의 input으로 들어간다. 따라서 Input-layout object에서는 몇 번째 slot을 다룰지에 대한 정보가 필요하다. 이게 4번째 파라미터 값이다.

  • 5번째 파라미터 : offset = 데이터를 어디서부터 읽을지 

이후 D3DDevice에세 InputLayout을 만들어달라고 요청한다.

  • CreateInputLayout() 파라미터 : layout / element 수 / vertex shader의 포인터 / vertex shader의 크기 / out 파라미터
  • Blob : shader 코드 데이터를 가리킨다고 생각하면 된다.
hr = g_pd3dDevice->CreateInputLayout(
    layouts,
    uNumElements,
    pVertexShaderBlob->GetBufferPointer(),
    pVertexShaderBlob->GetBufferSize(),
    &g_pVertexLayout
);

pVertexShaderBlob->Release();

if (FAILED(hr))
    return hr;

 

 

 

 

 

 

IA Stage  - 3)  Bind Objects to the IA Stage

  지금까지 만들어준 input buffer들과 input layout objectIA stagebinding 해준다.

  • Stride : vertex buffer에서 하나의 vertex 정보 크기 = SimpleVertex 구조체의 크기
  • Offset : vertex buffer에서 시작 지점
UINT uStride = sizeof(SimpleVertex);
UINT uOffset = 0;

g_pImmediateContext->IASetVertexBuffers(
    0, 		// slot
    1, 		// buffer의 개수
    &g_pVertexBuffer, 	// vertex buffer
    &uStride, 	    	// stride
    &uOffset	 	// offset
);

g_pImmediateContext->IASetIndexBuffer(
    g_pIndexBuffer,	 // index buffer
    DXGI_FORMAT_R16_UINT,	 // format of the index data
    0		 	 // offset
);

///////////////////////////

g_pImmediateContext->IASetInputLayout(g_pVertexLayout);

 

 

 

 

 

IA Stage  - 4)  Specify the Primitive Type

  IA stage에서 어떤 primitive type으로 assemble할 지에 대해 설정
앞서 언급한 것 처럼 삼각형 primitive로 설정해줬다.
g_pImmediateContext->IASetPrimitiveTopology(
    D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
);

 

 

 

 

IA Stage  - 5)  Call Draw Method

  • Binding이 된 후 draw 함수를 호출해서 그려 달라고 요청한다. 
  • Draw 요청을 하게 되면, DirectX가 그래픽스 파이프라인을 쭉 실행하여 화면에 출력해 준다.
  • Draw 함수 종류
    • Draw(): Index buffer 없이 vertex에 따라 draw
    • DrawIndexed(): index buffer를 이용하여 index 순서대로 vertex그려준다.

 

 

 

 

Vertex Shader(VS) Stage

vertextransformation, skinning, per-vertex lighting 과 같은 처리들을 해준다.

  • vertex 개수만큼 반복 실행된다. = VS의 input도 vertex 하나이고, output도 vertex 하나이다.
  • VS는 하나의 vertex를 입력으로 하고, 하나의 vertex를 출력하는 함수라고 생각하면 된다.
  • 그리고 앞으로 이 함수를 우리가 정의해 줄 예정이다.

 

 

 

Vertex Shader

  • Shader는 High-Level Shading Language(HLSL)이라는 언어로 작성된다. C++이 아닌 HLSL로 따로 작성하는 이유는 GPU에서 Matrix와 Vector 연산을 훨씬 빠르게 처리할 수 있기 때문이다.
  • VS에서 가장 중요한 일은 좌표계 사이를 transform 해주는 것이다.

 

  • input과 output은 float4 형태의 벡터(x,y,z,w)
  • POSITION : input parameter의 sementic
  • SV_POSITION : 함수의 반환값. 이때 SV는 System Value를 의미한다. = Clip-space의 position

 

 

 

 

Rasterizer Stage

3차원 좌표계에 있는 vertex들을 화면에 그리기 위한 2D pixel(raster image)로 변환해준다.

3D graphic을 2D pixel에 mapping 시키는 단계

 

Rasterization

  • 화면은 2차원 grid로 pixel로 되어있고 각 pixel들은 색깔을 가지고 있다. 이 pixel들을 칠해서 모니터에 화면이 만들어진다.
  • 아래 그림에서 왼쪽은 3개의 vertex로 구성된 삼각형이고 이를 rasterization 하면 오른쪽 그림처럼 된다.
  • GPU는 어떤 pixel이 삼각형이 포함되고, 칠해져야 하는지 여부를 계산하게 된다. 이후 칠해져야 하는 pixel들만 pixel shader 단계를 진행하게 된다.

 

 

 

 

Pixel Shader(PS) Stage

  최종 pixel의 색깔을 결정하는 단계 

  • Per-pixel 단위로 Lighting이나 post-processing 같은 처리를 해준다.
  • Input : pixel의 색상을 결정할 정보들
  • Output : 계산 후 결정된 최종 pixel 색상 정보 
float4 PS(float4 Pos : SV_POSITION) : SV_Target
{
    return float4(1.0f, 1.0f, 0.0f, 1.0f);
}
  • 마찬가지로 input과 output은 float 4 타입이다.
  • Input의 semantic : SV_POSITION = vertex shader의 output
  • Output으로 color(RGBA)를 내보낸다. = SV_Target.

 

 

 

 

Creating the Shaders

D3DCompileFromFile 함수의 인자로 파일 이름이 들어가고, shader가 실행될 entry pointVS로 설정해준다. 이후 컴파일된 결과를 ppBlobOut으로 받아온다.
    DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

#if defined (DEBUG) || defined(_DEBUG)
    dwShaderFlags |= D3DCOMPILE_DEBUG;
    dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
    
    ID3DBlob* pErrorBlob = nullptr;
    hr = D3DCompileFromFile(
    “VS.hlsl”, 	// FileName
    nullptr, 		// shader macros
    nullptr, 		// include files
    “VS”,		// Entry point
    “vs_5_0”,		// shader target
    dwShaderFlags, 	// flag1 
    0, 		// flag2
    ppBlobOut, 		// ID3DBlob out
    &pErrorBlob		// error blob out
    );

 

컴파일 한 뒤에는 Vertex Shader Object를 만들어준다. 이 과정 역시 Device가 요청하게 된다.

  • 파라미터 : 컴파일된 shader의 포인터 / shader의 size / Class link(nullptr) / shader를 가리키는 포인터( ID3D11VertexShaer*)
ID3D11VertexShader*  g_pVertexShader = nullptr;

///////////////////////////////

    hr = g_pd3dDevice->CreateVertexShader(
        pVertexShaderBlob->GetBufferPointer(), 
        pVertexShaderBlob->GetBufferSize(), 
        nullptr, 
        &g_pVertexShader);

    if (FAILED(hr))
    {
        pVertexShaderBlob->Release();
        return hr;
    }

 

Pixel Shader도 마찬가지로 만들어준다.

ID3D11PixelShader*  g_pPixelShader = nullptr;

///////////////////////////////

    hr = g_pd3dDevice->CreatePixelShader(
    	pPixelShaderBlob->GetBufferPointer(), 
    	pPixelShaderBlob->GetBufferSize(), 
    	nullptr, 
    	&g_pPixelShader);

    if (FAILED(hr))
    {
        pPixelShaderBlob->Release();
        return hr;
    }

 

 

 

 

 

Setting the Shaders

이제 만들어둔 shader를 pipeline에 binding 해준다.

g_pImmediateContext->VSSetShader(g_pVertexShader, nullptr, 0);
g_pImmediateContext->PSSetShader(g_pPixelShader, nullptr, 0);

 

 

 

 

 

다음 포스팅에서는 3D Space와 Transformation 과정에 대해 자세히 다룰 예정이다.

 

 

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

 

 

이번 포스팅에서는 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 강형엽 교수님 강의의 실습 수업을 수강하며 정리한 내용입니다.

 

목표 : 3D 렌더링 엔진 만들기

  • Windlow Initialization / D3D Initialization
  • Support System
  • Cameras / Renderer
  • Models
  • Materials / Lights
  • Etc…

 

 

 

폴더 구조

  • Library Project : 일반적인 렌더링 엔진 구조, 게임에서 공통되게 사용하는 기능들 구현
  • Game Project : 어플리케이션에 따라 구체적인 기능들 구현

 

 

 

 

Window란? 

  • Application window 또는 Main window라고 불란다.
  • Frame 구성은 다음과 같다.
    • Title bar / Minimize, Maximize button / Other standard UI
  • Frame 안에 영역을 Client Area 라고 한다. 
  • OK 버튼이나 EditBox들도 Window의 한 종류이다. 

 

 

 

 

 

Parent Windows & Owner Windows

  • UI control window는 application window의 자식 관계이다.
  • Parent window는 Child window에게 좌표계를 제공하기 때문에 child는 parent의 좌표계 영역 밖에서 나타나지 않는다.
  • 새로운 window 알림 창이 나타나는 경우를 dialog라고 한다. 

 

 

 

Window Handles

  • Windows들은 object지만 C++의 class는 아니다.
  • 특정 Window를 나타내기 위해서 handle이라는 변수를 사용한다. 이때 Handle이란 OS가 object를 구분하기 위한 ID 숫자값이다. (포인터가 아니라 단순한 숫자임)
  • Window handle의 자료형은 HWND를 사용한다.

 

 

 

Screen & Window Coordinates

  • 컴퓨터 상에서 좌표계는 pixel 값에 따른다.
  • 기준에 따라 다음과 같이 3개의 좌표계가 존재한다.
    • Screen/Window/Client 좌표계
  • 모든 좌표계는 좌상단 점을 원점으로 한다.

 

 

 

 

WinMain

◇  모든 window 프로그램은 WinMain이라는 entry-point가 있다. 

  • C/C++에서 in main 함수와 같은 역할이다.

  • hInstance : handle to an instance라고 불리며 OS가 이 값을 이용해서 해당 프로그램이 메모리에 로드될 때 EXE파일의 ID를 구분하게 된다. Handle이기 때문에 포인터가 아닌 정수값이다. 
  • hPrevInstance : 지금은 중요하지 않은 값. 항상 0으로 사용할 것이다.
  • pCmdLind : command-lind argument를 Unicode로 인코딩된 string으로 가진다. 
  • pCmdShow : min applicaiton이 minimize/maximize/일반 중 어떤식으로 보여줄 지 나타내는 flag값
  • Return 값 : int (C/C++과 동일하게 프로그램의 상태를 나타내는 값이다.)

 

 

 

Game::Main.cpp - 전처리

UNICODE를 사용해 빌드하기 위해 전처리로 매크로 정의를 해준다.

그리고 Windows desktop 프로그램은 <windows.h>를 include 해야한다. 

#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>

 

 

 

 

Game::Main.cpp - wWinMain

기본적으로 사용되는 엔진구조는 아래와 같다. 

  • Initialization : 어플리케이션이 시작하면서 초기화한다. 필요한 에셋과 데이터를 로드하고 메모리 할당과 같은 초기화 작업을 진행한다.
  • Game Loop : 사용자의 입력의 받아 게임 로직을 수행하고, 이를 렌더링한다.
  • Destruction : 게임이 종료되면 메모리 해제와 같은 Destroy 과정을 거치며 종료한다. 
INT WINAPI wWinMain(
    _In_		HINSTANCE hInstance,
    _Int_opt 	HINSTANCE hPrevInstance,
    _In_		LPWSTR IpCmdLine,
    _In_		INT nCmdShow)
{
	// Initialization
    
    while(! /* is game terminated condition set? */)
    {
    	// Handle Input
        // Update (Game Logic)
        // Draw
    }
    
    // Destroy
    
    return 0;
}

 

wWinMain의 매개변수도 간단히 설명하자면,,

  • _In_ : 호출된 함수에 전달되고, 읽기 전용으로 취급
  • _Out_ : 호출된 함수가 쓸 공간만 제공. 호출된 함수는 해당 공간에 데이터를 쓴다..
  • _In_opt_ : _In_과 같으며, 매개변수가 선택 사항임을 나타낸다. 

 

 

 

 

다음에는 이어서 Window 초기화와 D3D 초기화에 대해 알아보겠다.

 

 

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

+ Recent posts