DirectX11 Tutorial 3 - DirectX 11의 초기화

강좌번역/DirectX 11 2013. 1. 5. 19:55 by 빠재

원문: http://www.rastertek.com/dx11tut03.html

이 튜토리얼이 DirectX 11로 작업하는 첫번째 예제가 될 것입니다. 여기서는 Direct3D를 초기화하고 정리하는 것, 그리고 윈도우에 그리는 방법에 대한 내용도 다룹니다.

프레임워크

이번에 기존 프레임워크에 Direct3D 기능들을 다루는 클래스를 추가합니다. 이 클래스를 D3DClass라고 부르도록 하겠습니다. 바뀐 프레임워크의 클래스 다이어그램은 다음과 같습니다.

여러분도 볼 수 있듯이 D3DClassGraphicsClass 내부에 위치해 있습니다. 앞선 튜토리얼에서 말했듯이 모든 그래픽과 관련된 객체들은 GraphicsClass에 캡슐화되어 있고 그것이 왜 새로운 D3DClass의 가장 좋은 위치가 이 그래픽 클래스인지 이해하실 수 있을 겁니다. 그렇다면 GraphicsClass의 바뀐 점을 살펴보도록 하겠습니다.

Graphicsclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_

여기가 첫번째 변경점입니다. windows.h의 include문을 없애고 d3dclass.h을 include하였습니다.

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "d3dclass.h"


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;


////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
    GraphicsClass();
    GraphicsClass(const GraphicsClass&);
    ~GraphicsClass();

    bool Initialize(int, int, HWND);
    void Shutdown();
    bool Frame();

private:
    bool Render();

private:

그리고 두번째 바뀐 것은 D3DClass를 참조하는 새로운 private 포인터인 m_D3D입니다. 제가 왜 모든 멤버 변수에 m_이라는 접두어를 붙이는 걸 궁금해하실 분이 있을지도 모르겠네요. 이렇게 하면 코드를 짤 때 어떤 변수가 클래스의 멤버 변수인지 아닌지 쉽게 구분할 수 있습니다.

    D3DClass* m_D3D;
};

#endif

Graphicsclass.cpp

이전 튜토리얼을 기억해 보면 원래 이 클래스 함수는 아무 코드도 없었습니다. 하지만 지금은 D3DClass를 멤버로 가지고 있기 때문에 GraphicsClass에 이 멤버를 초기화하고 정리하는 코드를 넣을 것입니다. 그리고 이제부터는 Direct3D를 이용하여 윈도우를 그리기 때문에 Render 함수에 BeginSceneEndScene를 호출하는 부분을 넣을 것입니다.

가장 먼저 바뀐 것은 클래스 생성자입니다. 모든 작업이 포인터를 통해 이루어지기 때문에 먼저 안전을 위해 클래스 포인터를 null로 초기화합니다. (역자주: std::shared_ptr 또는 std::unique_ptr과 같은 스마트 포인터를 멤버 변수로 사용하면 초기화를 생략할 수 있습니다.)

GraphicsClass::GraphicsClass()
{
    m_D3D = 0;
}

두 번째 변경사항은 GraphicsClassInitialize 함수입니다. 여기서 D3DClass 객체를 생성하고 D3DClass::Initialize 함수를 호출합니다. 이 함수에 화면의 너비, 높이, 윈도우의 핸들, 그리고 Graphicsclass.h에 정의된 네 개의 전역 변수들을 전달합니다. D3DClass에서는 이 변수들을 사용하여 Direct3D 시스템을 설정합니다. 이후 d3dclass.cpp파일을 볼 때 좀 더 자세한 내용을 다루겠습니다.

bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
    bool result;

        
    // Direct3D 객체를 생성합니다.
    m_D3D = new D3DClass;
    if(!m_D3D)
    {
        return false;
    }

    // Direct3D 객체를 초기화합니다.
    result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize Direct3D", L"Error", MB_OK);
        return false;
    }

    return true;
}

그 다음 변경은 GraphicsClassShutdown 함수입니다. 모든 그래픽 객체의 해제가 이 함수에서 일어나기 때문에 D3DClass의 정리도 이 함수에서 하도록 합니다. 포인터가 초기화되었는지 확인하는 부분을 주의하시기 바랍니다. 만약 제대로 초기화되지 않았다면 설정되지 않은 것으로 간주하고 정리하지 않을 것입니다. 이것을 보면 생성자에서 모든 포인터를 null로 초기화하는 것이 왜 중요한지 알 수 있습니다. 만약 이 포인터가 초기화되어 null이 아닌 다른 값이 되었다면 프로그램은 D3DClass 객체를 정리하고 포인터를 초기화할 것입니다.

void GraphicsClass::Shutdown()
{
    // D3D 객체를 반환합니다.
    if(m_D3D)
    {
        m_D3D->Shutdown();
        delete m_D3D;
        m_D3D = 0;
    }

    return;
}

Frame 함수도 매 프레임마다 Render함수를 부르도록 바뀌었습니다.

bool GraphicsClass::Frame()
{
    bool result;


    // 그래픽 렌더링을 수행합니다.
    result = Render();
    if(!result)
    {
        return false;
    }

    return true;
}

마지막 변경은 Render 함수입니다. D3D 객체에게 화면을 회색으로 초기화하도록 합니다. 그 뒤에 EndScene을 호출하여 회색이 윈도우에 나타나게 합니다.

bool GraphicsClass::Render()
{
    // 씬 그리기를 시작하기 위해 버퍼의 내용을 지웁니다.
    m_D3D->BeginScene(0.5f, 0.5f, 0.5f, 1.0f);


    // 버퍼에 그려진 씬을 화면에 표시합니다.
    m_D3D->EndScene();

    return true;
}

이제 새로운 클래스인 D3DClass의 헤더파일을 보도록 하겠습니다.

D3dclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: d3dclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _D3DCLASS_H_
#define _D3DCLASS_H_

헤더에서 먼저 나타나는 것은 객체 모듈을 사용하기 위해 링크하는 라이브러리들을 명시하는 것입니다. 이 라이브러리들은 DirectX의 초기화, 3D그래픽의 렌더링, 또한 새로고침 비율(역자주: FPS)을 얻어내거나 사용하는 그래픽카드의 정보 등과 같은 하드웨어로의 통신을 위해 필요한 모든 Direct3D의 기능들을 담고 있습니다. 일부 DirectX 10의 라이브러리가 계속 이용되는걸 볼 수 있는데, 해당 라이브러리는 DirectX 11에서도 바뀌지 않아 업그레이드되지 않았기 때문입니다.

/////////////
// LINKING //
/////////////
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d3dx11.lib")
#pragma comment(lib, "d3dx10.lib")

그 다음으로 링크한 라이브러리들, DirectX 타입 정의와 같은 것들에 대한 헤더를 include합니다.

//////////////
// INCLUDES //
//////////////
#include <dxgi.h>
#include <d3dcommon.h>
#include <d3d11.h>
#include <d3dx10math.h>

D3DClass의 정의는 가능한 한 간단하게 하였습니다. 기본 클래스 생성자가 있고, 복사 생성자와 파괴자가 있습니다. 더 중요한 함수로는 Initialize 함수와 Shutdown 함수가 있습니다. 이 예제에서는 이 두 함수에 초점을 맞출 것입니다. 그 외에 지금 당장은 중요하게 살펴보지 않을 몇몇 도우미 함수와 멤버 변수들이 더 있습니다. 멤버 변수들은 d3dclass.cpp를 볼때 자세히 살펴보겠습니다. 우선 지금 우리에게 중요한 것은 InitializeShutdown 함수입니다.

////////////////////////////////////////////////////////////////////////////////
// Class name: D3DClass
////////////////////////////////////////////////////////////////////////////////
class D3DClass
{
public:
    D3DClass();
    D3DClass(const D3DClass&);
    ~D3DClass();

    bool Initialize(int, int, bool, HWND, bool, float, float);
    void Shutdown();
    
    void BeginScene(float, float, float, float);
    void EndScene();

    ID3D11Device* GetDevice();
    ID3D11DeviceContext* GetDeviceContext();

    void GetProjectionMatrix(D3DXMATRIX&);
    void GetWorldMatrix(D3DXMATRIX&);
    void GetOrthoMatrix(D3DXMATRIX&);

    void GetVideoCardInfo(char*, int&);

private:
    bool m_vsync_enabled;
    int m_videoCardMemory;
    char m_videoCardDescription[128];
    IDXGISwapChain* m_swapChain;
    ID3D11Device* m_device;
    ID3D11DeviceContext* m_deviceContext;
    ID3D11RenderTargetView* m_renderTargetView;
    ID3D11Texture2D* m_depthStencilBuffer;
    ID3D11DepthStencilState* m_depthStencilState;
    ID3D11DepthStencilView* m_depthStencilView;
    ID3D11RasterizerState* m_rasterState;
    D3DXMATRIX m_projectionMatrix;
    D3DXMATRIX m_worldMatrix;
    D3DXMATRIX m_orthoMatrix;
};

#endif

이미 Direct3D에 친숙한 분들은 아마 여기에 뷰 행렬 변수가 없다는 걸 눈치채셨을 것입니다. 뷰 행렬은 나중 튜토리얼에 나올 카메라 클래스에 들어갈 것이기 때문입니다.

D3dclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: d3dclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "d3dclass.h"

다른 대부분의 클래스들처럼 이 클래스도 모든 멤버 포인터들을 생성자에서 null로 초기화합니다. 헤더 파일에 정의된 모든 포인터들을 초기화합니다.

D3DClass::D3DClass()
{
    m_swapChain = 0;
    m_device = 0;
    m_deviceContext = 0;
    m_renderTargetView = 0;
    m_depthStencilBuffer = 0;
    m_depthStencilState = 0;
    m_depthStencilView = 0;
    m_rasterState = 0;
}


D3DClass::D3DClass(const D3DClass& other)
{
}


D3DClass::~D3DClass()
{
}

Initialize 함수에서는 DirectX 11의 전체 Direct3D 설정이 일어나는 곳입니다. 여기에는 중요한 코드뿐만 아니라 나중 튜토리얼을 위해 넣어둔 추가적인 코드들도 있습니다. 따라서 좀 더 간략하게 쓸 수 있는 부분도 있고 일부 코드를 지울 수도 있지만 이번 기회에 미리 다루는 것이 더 좋을 것 같습니다.

인자로 넘어온 screenWidthscreenHeight 변수는 SystemClass에서 만들었던 윈도우의 너비와 높이입니다. Direct3D에서는 이를 이용하여 동일한 크기의 영역을 초기화하고 이용합니다. hWnd 변수는 만들어진 윈도우에 대한 핸들입니다. Direct3D에서는 이 변수를 이용하여 만들어진 윈도우에 접근합니다. fullscreen 변수는 이 어플리케이션이 윈도우 모드에서 동작할지 전체 화면 모드에서 동작할지 알려줍니다. Direct3D뿐만 아니라 윈도우를 올바로 생성할 때에도 이 값이 필요합니다. screenDepth 변수와 screenNear 변수는 윈도우에 그려질 3D 환경에서의 깊이(depth)값입니다. vsync 변수는 Direct3D 렌더링을 모니터의 주사율에 맞출 것인지, 아니면 가능한 한 빠르게 다시 그릴 것인지 지정합니다(역자주: 일반적으로 게임의 수직동기화 기능입니다.).

bool D3DClass::Initialize(int screenWidth, int screenHeight, bool vsync, HWND hwnd, bool fullscreen, 
              float screenDepth, float screenNear)
{
    HRESULT result;
    IDXGIFactory* factory;
    IDXGIAdapter* adapter;
    IDXGIOutput* adapterOutput;
    unsigned int numModes, i, numerator, denominator, stringLength;
    DXGI_MODE_DESC* displayModeList;
    DXGI_ADAPTER_DESC adapterDesc;
    int error;
    DXGI_SWAP_CHAIN_DESC swapChainDesc;
    D3D_FEATURE_LEVEL featureLevel;
    ID3D11Texture2D* backBufferPtr;
    D3D11_TEXTURE2D_DESC depthBufferDesc;
    D3D11_DEPTH_STENCIL_DESC depthStencilDesc;
    D3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc;
    D3D11_RASTERIZER_DESC rasterDesc;
    D3D11_VIEWPORT viewport;
    float fieldOfView, screenAspect;


    // vsync(수직동기화) 설정을 저장합니다.
    m_vsync_enabled = vsync;

Direct3D를 초기화하기 전에 우리는 그래픽카드/모니터의 주사율(새로고침 비율)을 알아야 합니다. 컴퓨터마다 그 값이 조금씩 다르기 때문에 컴퓨터에 해당 정보를 조회해야 합니다. 이 비율의 분자/분모 값을 조회한 뒤 설정 중에 DirectX에 그 값을 알려주면 적절한 주사율을 계산합니다. 만일 이 작업을 하지 않고 모든 컴퓨터를 지원하지는 앖겠지만 대충 기본값으로 맞춰놓는다면 DirectX는 화면을 표시할 때 버퍼 플립을 사용하지 않고 blit를 사용하게 되어 성능이 저하되고 디버그 출력에 거슬리는 에러 메세지를 남기게 됩니다.

    // DirectX 그래픽 인터페이스 팩토리를 만듭니다.
    result = CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)&factory);
    if(FAILED(result))
    {
        return false;
    }

    // 팩토리 객체를 사용하여 첫번째 그래픽 카드 인터페이스에 대한 아답터를 만듭니다.
    result = factory->EnumAdapters(0, &adapter);
    if(FAILED(result))
    {
        return false;
    }

    // 출력(모니터)에 대한 첫번째 아답터를 나열합니다.
    result = adapter->EnumOutputs(0, &adapterOutput);
    if(FAILED(result))
    {
        return false;
    }

    // DXGI_FORMAT_R8G8B8A8_UNORM 모니터 출력 디스플레이 포맷에 맞는 모드의 개수를 구합니다.
    result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, NULL);
    if(FAILED(result))
    {
        return false;
    }

    // 가능한 모든 모니터와 그래픽카드 조합을 저장할 리스트를 생성합니다.
    displayModeList = new DXGI_MODE_DESC[numModes];
    if(!displayModeList)
    {
        return false;
    }

    // 디스플레이 모드에 대한 리스트 구조를 채워넣습니다.
    result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, displayModeList);
    if(FAILED(result))
    {
        return false;
    }

    // 이제 모든 디스플레이 모드에 대해 화면 너비/높이에 맞는 디스플레이 모드를 찾습니다.
    // 적합한 것을 찾으면 모니터의 새로고침 비율의 분모와 분자 값을 저장합니다.
    for(i=0; i<numModes; i++)
    {
        if(displayModeList[i].Width == (unsigned int)screenWidth)
        {
            if(displayModeList[i].Height == (unsigned int)screenHeight)
            {
                numerator = displayModeList[i].RefreshRate.Numerator;
                denominator = displayModeList[i].RefreshRate.Denominator;
            }
        }
    }

이렇게 주사율의 분자와 분모를 갖게 되었습니다. 마지막으로 그래픽 카드의 이름과 사용 가능한 그래픽 카드 메모리의 크기를 어댑터를 이용하여 받아옵니다.

    // 어댑터(그래픽카드)의 description을 가져옵니다.
    result = adapter->GetDesc(&adapterDesc);
    if(FAILED(result))
    {
        return false;
    }

    // 현재 그래픽카드의 메모리 용량을 메가바이트 단위로 저장합니다.
    m_videoCardMemory = (int)(adapterDesc.DedicatedVideoMemory / 1024 / 1024);

    // 그래픽카드의 이름을 char형 문자열 배열로 바꾼 뒤 저장합니다.
    error = wcstombs_s(&stringLength, m_videoCardDescription, 128, adapterDesc.Description, 128);
    if(error != 0)
    {
        return false;
    }

이제 저장된 새로고침 비율의 분자/분모값과 그래픽카드의 정보를 알고 있기 때문에 정보를 얻기 위해 사용했던 구조체들과 인터페이스들을 해제해야 합니다.

    // 디스플레이 모드 리스트의 할당을 해제합니다.
    delete [] displayModeList;
    displayModeList = 0;

    // 출력 아답터를 할당 해제합니다.
    adapterOutput->Release();
    adapterOutput = 0;

    // 아답터를 할당 해제합니다.
    adapter->Release();
    adapter = 0;

    // 팩토리 객체를 할당 해제합니다.
    factory->Release();
    factory = 0;

지금 우리는 시스템에서 얻어온 주사율을 알고 있으므로 DirectX를 초기화할 수 있습니다. 가장 먼저 해야 할 일은 스왑 체인의 description 구조체를 채워 넣는 일입니다. 스왑 체인은 실제로 렌더링일 한 곳이 기록되는 프론트버퍼와 백버퍼입니다. 보통 렌더링을 할 때 하나의 백버퍼만을 사용하며, 그 위에 장면을 그린 뒤, 프론트 버퍼와 바꿔치기(swap)하여 유저의 화면에 보이게 됩니다. 스왑 체인이라고 하는 이름은 여기서 나온 것입니다.

    // 스왑 체인 description을 초기화합니다.
    ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));

    // 하나의 백버퍼만을 사용하도록 합니다.
    swapChainDesc.BufferCount = 1;

    // 백버퍼의 너비와 높이를 설정합니다.
    swapChainDesc.BufferDesc.Width = screenWidth;
    swapChainDesc.BufferDesc.Height = screenHeight;

    // 백버퍼로 일반적인 32bit의 서페이스를 지정합니다.
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

스왑 체인 description 구조체의 다음 부분은 주사율입니다. 주사율은 1초당 몇 장의 백버퍼를 프론트 버퍼와 바꿔치기하는지를 나타내는 숫자입니다. 만약 vsync 변수가 true로 설정되어 있다면 시스템에서 정한 새로고침 비율로 고정됩니다(ex: 60Hz). 이것은 1초에 60번 화면을 그릴 것이라는 의미입니다(시스템 설정이 60보다 크다면 더 빠르게 그립니다). 반면에 vsync 변수가 false라면 프로그램은 1초에 최대한 빠르게 화면을 그려내려고 할 것입니다. 하지만 이것은 화면 일부에 결점들을 남깁니다.

    // 백버퍼의 새로고침 비율을 설정합니다.
    if(m_vsync_enabled)
    {
        swapChainDesc.BufferDesc.RefreshRate.Numerator = numerator;
        swapChainDesc.BufferDesc.RefreshRate.Denominator = denominator;
    }
    else
    {
        swapChainDesc.BufferDesc.RefreshRate.Numerator = 0;
        swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
    }

    // 백버퍼의 용도를 설정합니다.
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

    // 렌더링이 이루어질 윈도우의 핸들을 설정합니다.
    swapChainDesc.OutputWindow = hwnd;

    // 멀티샘플링을 끕니다.
    swapChainDesc.SampleDesc.Count = 1;
    swapChainDesc.SampleDesc.Quality = 0;

    // 윈도우 모드 또는 풀스크린 모드를 설정합니다.
    if(fullscreen)
    {
        swapChainDesc.Windowed = false;
    }
    else
    {
        swapChainDesc.Windowed = true;
    }

    // 스캔라인의 정렬과 스캔라이닝을 지정되지 않음으로(unspecified) 설정합니다.
    swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;

    // 출력된 이후의 백버퍼의 내용을 버리도록 합니다.
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

    // 추가 옵션 플래그를 사용하지 않습니다.
    swapChainDesc.Flags = 0;

스왑 체인 description 구조체를 설정하고 나서 피쳐 레벨이라는 변수도 설정해야 합니다. 이 변수는 우리가 어느 버전의 DirectX를 사용할 것인지 알려줍니다. 우리는 여기서 DirectX 11을 의미하는 11.0으로 설정합니다. 물론 사양이 좋지 않은 컴퓨터들을 지원하려고 낮은 버전의 DirectX를 사용할 것이라면 이 값을 10 또는 9로 설정할 수 있습니다.

    // 피쳐 레벨을 DirectX 11로 설정합니다.
    featureLevel = D3D_FEATURE_LEVEL_11_0;

이렇게 스왑 체인 description과 피쳐 레벨이 채워지면 이제 스왑 체인, Direct3D 장치 그리고 Direct3D 장치 컨텍스트를 만들 수 있습니다. Direct3D 장치와 Direct3D 장치 컨텍스트는 모든 Direct3D 함수들의 인터페이스가 되기 때문에 매우 중요합니다. 이제부터는 장치와 장치 컨텍스트를 사용하여 대부분의 작업을 수행할 것입니다.

이전 버전의 DirectX에 익숙한 분이라면 Direct3D 장치는 친숙하나 Direct3D 장치 컨텍스트는 조금 생소할 수 있습니다. 기본적으로 이 둘은 기존 Direct3D 장치의 기능을 두 개의 장치로 쪼갠 셈이므로 두 객체가 모두 필요합니다.

만약 유저가 DirectX 11을 지원하는 그래픽카드를 가지고 있지 않다면 장치와 장치 컨텍스트를 생성하는 함수가 실패할 것입니다. 또한 DirectX 11의 기능을 테스트해보고는 싶지만 DirectX 11을 지원하는 그래픽 카드가 없는 경우 이 부분에서 D3D_DRIVER_TYPE_HARDWARED3D_DRIVER_TYPE_REFERENCE로 바꾸어서 그래픽 카드가 아닌 CPU에서 렌더링을 처리하게 할 수 있습니다. 속도는 11000 정도로 느리지만 DirectX 11을 지원하는 그래픽카드가 없는 사람들에게는 좋은 기능입니다.

    // 스왑 체인, Direct3D 디바이스, Direct3D 디바이스 컨텍스트를 생성합니다.
    result = D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, &featureLevel, 1, 
                           D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, NULL, &m_deviceContext);
    if(FAILED(result))
    {
        return false;
    }

가끔씩 첫번째 그래픽카드가 DirectX 11과 호환되지 않는 경우 실패하는 경우가 있습니다. 첫번째 그래픽카드가 DirectX 10을 지원하고 두번째 그래픽카드가 DirectX 11을 지원하는 기기가 있을 수도 있습니다. 또한 일부 하이브리드 그래픽카드의 경우 첫번째 그래픽 카드는 저전력의 인텔 그래픽카드이고 두번째 카드가 Nvidia의 그래픽카드로 동작할수 있습니다. 이런 경우들에 모두 대응하기 위해서는 절대로 기본 그래픽 카드만을 사용하지 말아야 하고 기기의 모든 비디오 카드들을 나열하여 유저가 직접 가장 잘 맞는 그래픽 카드로 장치를 생성할 수 있도록 해야 합니다.

이제 스왑 체인이 있으므로 다음으로 백버퍼의 포인터를 받아와 스왑 체인에 연결시켜 주어야 합니다. CreateRenderTargetView 함수를 사용하여 백버퍼를 스왑 체인에 연결합니다.

    // 백버퍼의 포인터를 가져옵니다.
    result = m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBufferPtr);
    if(FAILED(result))
    {
        return false;
    }

    // 백버퍼의 포인터로 렌더타겟 뷰를 생성합니다.
    result = m_device->CreateRenderTargetView(backBufferPtr, NULL, &m_renderTargetView);
    if(FAILED(result))
    {
        return false;
    }

    // 백버퍼 포인터를 더이상 사용하지 않으므로 할당 해제합니다.
    backBufferPtr->Release();
    backBufferPtr = 0;

이에 더해 깊이 버퍼의 description 구조체도 작성해야 합니다. 이를 이용하여 깊이 버퍼를 만들어야 3D공간에서 우리의 폴리곤들이 올바르게 그려집니다. 또한 동시에 스텐실 버퍼도 이 깊이 버퍼에 연결할 것입니다. 스텐실 버퍼는 모션 블러라던가, 볼류메트릭 그림자 등의 효과를 낼 때 사용됩니다.

    // 깊이 버퍼의 description을 초기화합니다.
    ZeroMemory(&depthBufferDesc, sizeof(depthBufferDesc));

    // 깊이 버퍼의 description을 작성합니다.
    depthBufferDesc.Width = screenWidth;
    depthBufferDesc.Height = screenHeight;
    depthBufferDesc.MipLevels = 1;
    depthBufferDesc.ArraySize = 1;
    depthBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    depthBufferDesc.SampleDesc.Count = 1;
    depthBufferDesc.SampleDesc.Quality = 0;
    depthBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    depthBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    depthBufferDesc.CPUAccessFlags = 0;
    depthBufferDesc.MiscFlags = 0;

이 정보를 이용하여 깊이/스텐실 버퍼를 생성합니다. CreateTexture2D 함수로 버퍼를 생성하는 부분에 주의하시기 바랍니다. 결국 버퍼 역시 2D 텍스쳐인 것입니다. 그 이유는 폴리곤들이 정렬되고 래스터화된 이후에는 어쨌든 2D좌표의 픽셀들이 되기 때문입니다. 그리고 이 2D 버퍼가 화면에 그려집니다.

    // description을 사용하여 깊이 버퍼의 텍스쳐를 생성합니다.
    result = m_device->CreateTexture2D(&depthBufferDesc, NULL, &m_depthStencilBuffer);
    if(FAILED(result))
    {
        return false;
    }

이제 필요한 것은 깊이-스텐실 description을 작성하는 것입니다. 이것은 우리가 Direct3D에서 각 픽셀에 어떤 깊이 테스트를 할 것인지 정할 수 있게 해 줍니다.

    // 스텐실 상태의 description을 초기화합니다.
    ZeroMemory(&depthStencilDesc, sizeof(depthStencilDesc));

    // 스텐실 상태의 description을 작성합니다.
    depthStencilDesc.DepthEnable = true;
    depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
    depthStencilDesc.DepthFunc = D3D11_COMPARISON_LESS;

    depthStencilDesc.StencilEnable = true;
    depthStencilDesc.StencilReadMask = 0xFF;
    depthStencilDesc.StencilWriteMask = 0xFF;

    // Stencil operations if pixel is front-facing.
    depthStencilDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_INCR;
    depthStencilDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

    // Stencil operations if pixel is back-facing.
    depthStencilDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_DECR;
    depthStencilDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
    depthStencilDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

위에서 작성한 description을 가지고 깊이-스텐실 상태 변수를 만듭니다.

    // 깊이-스텐실 상태를 생성합니다.
    result = m_device->CreateDepthStencilState(&depthStencilDesc, &m_depthStencilState);
    if(FAILED(result))
    {
        return false;
    }

상태 변수를 만들었다면 이것을 적용하도록 합니다. 장치 컨텍스트를 사용하는 점에 주의하시기 바랍니다.

    // 깊이-스텐실 상태를 설정합니다.
    m_deviceContext->OMSetDepthStencilState(m_depthStencilState, 1);

다음으로 만들어야 할 것은 깊이-스텐실 버퍼의 뷰에 대한 description입니다. 이 작업을 해야 Direct3D가 깊이 버퍼를 깊이-스텐실 텍스쳐로 인식합니다. 이 구조체를 채우고 난 후에 CreateDepthStencilView 함수를 호출하여 깊이-스텐실 버퍼 뷰를 생성합니다.

    // 깊이-스텐실 뷰의 description을 초기화합니다.
    ZeroMemory(&depthStencilViewDesc, sizeof(depthStencilViewDesc));

    // 깊이-스텐실 뷰의 description을 작성합니다.
    depthStencilViewDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    depthStencilViewDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
    depthStencilViewDesc.Texture2D.MipSlice = 0;

    // 깊이-스텐실 뷰를 생성합니다.
    result = m_device->CreateDepthStencilView(m_depthStencilBuffer, &depthStencilViewDesc, &m_depthStencilView);
    if(FAILED(result))
    {
        return false;
    }

여기까지 왔다면 OMSetRenderTarget 함수를 호출할 수 있습니다. 이 함수는 렌더타겟뷰와 깊이-스텐실 뷰를 출력 렌더링 파이프라인에 바인딩시킵니다. 이렇게 하여 파이프라인을 이용한 렌더링이 수행될 때 우리가 만들었던 백버퍼에 장면이 그려지게 됩니다. 그리고 백버퍼에 그려진 것을 프론트 버퍼와 바꿔치기하여 유저의 모니터에 출력하는 것이죠.

    // 렌더타겟 뷰와 깊이-스텐실 버퍼를 각각 출력 파이프라인에 바인딩합니다.
    m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

렌더 타겟이 설정된 뒤 우리는 앞으로의 예제들을 위한 추가적인 기능들을 설정할 수 있습니다. 첫번째로 만들게 되는 것은 래스터화기 상태(rasterizer state)입니다. 이것은 도형이 어떻게 픽셀로 그려지는지에 대한 제어를 할 수 있게 해 줍니다. 우리의 화면을 와이어프레임 모드로 그리거나 도형의 앞뒷면을 모두 그리도록 할 수도 있습니다. 아래와 똑같은 설정으로 만들어져 돌아가는 DirectX 기본 래스터화기 상태가 있지만 여러분이 직접 만들지 않는 이상 이에 대한 제어권이 없습니다.

    // 어떤 도형을 어떻게 그릴 것인지 결정하는 래스터화기 description을 작성합니다.
    rasterDesc.AntialiasedLineEnable = false;
    rasterDesc.CullMode = D3D11_CULL_BACK;
    rasterDesc.DepthBias = 0;
    rasterDesc.DepthBiasClamp = 0.0f;
    rasterDesc.DepthClipEnable = true;
    rasterDesc.FillMode = D3D11_FILL_SOLID;
    rasterDesc.FrontCounterClockwise = false;
    rasterDesc.MultisampleEnable = false;
    rasterDesc.ScissorEnable = false;
    rasterDesc.SlopeScaledDepthBias = 0.0f;

    // 작성한 description으로부터 래스터화기 상태를 생성합니다.
    result = m_device->CreateRasterizerState(&rasterDesc, &m_rasterState);
    if(FAILED(result))
    {
        return false;
    }

    // 래스터화기 상태를 설정합니다.
    m_deviceContext->RSSetState(m_rasterState);

물론 뷰포트도 있어야 렌더타겟 공간에서 클리핑을 수행할 수 있습니다. 우선 이것을 윈도우 전체 크기와 동일하게 설정합니다.

    // 렌더링을 위한 뷰포트를 설정합니다.
    viewport.Width = (float)screenWidth;
    viewport.Height = (float)screenHeight;
    viewport.MinDepth = 0.0f;
    viewport.MaxDepth = 1.0f;
    viewport.TopLeftX = 0.0f;
    viewport.TopLeftY = 0.0f;

    // 뷰포트를 생성합니다.
    m_deviceContext->RSSetViewports(1, &viewport);

이제 투영 행렬(projcection matrix)을 생성할 차례입니다. 투영 행렬은 3D의 화면을 앞서 만들었던 2D 뷰포트 공간으로 변환하도록 해 주는 것입니다. 이 행렬의 복사본을 만들어 셰이더에서 장면을 그릴 때 사용할 수 있도록 해야 합니다.

    // 투영 행렬을 설정합니다.
    fieldOfView = (float)D3DX_PI / 4.0f;
    screenAspect = (float)screenWidth / (float)screenHeight;

    // 3D 렌더링을 위한 투영 행렬을 생성합니다.
    D3DXMatrixPerspectiveFovLH(&m_projectionMatrix, fieldOfView, screenAspect, screenNear, screenDepth);

또한 월드 행렬이라는 또다른 행렬을 만들어야 합니다. 이 행렬은 오브젝트들의 좌표를 3D 세계의 좌표로 변환하는 데 사용됩니다. 또한 3차원 공간에서의 회전/이동/크기 변환에도 사용됩니다. 처음에는 이 행렬을 단위 행렬로 만들고 복사본을 만듭니다. 이 복사본 역시 셰이더에 전달되어 사용할 수 있게 합니다.

    // 월드 행렬을 단위 행렬로 초기화합니다.
    D3DXMatrixIdentity(&m_worldMatrix);

여기가 보통 뷰 행렬을 생성하는 곳입니다. 뷰 행렬은 현재 장면에서 우리가 어느 위치에서 어느 방향을 보고 있는가를 계산하는 데 이용됩니다. 3D세계를 카메라라고 한다면 카메라에 대한 행렬이라고 생각할 수 있습니다. 그 목적이 명확하기 때문에 지금은 뷰 행렬을 건너뛰고 나중 예제에 나오는 카메라 클래스에서 다루도록 하겠습니다.

그리고 Initialize함수에서 마지막으로 해야 할 것은 직교 투영 행렬을 만드는 것입니다. 이 행렬은 3D객체가 아니라 UI와 같은 2D의 요소들을 그리기 위해 사용됩니다. 나중에 2D 그래픽과 폰트를 다루는 예제에서 보게 될 것입니다.

    // 2D 렌더링에 사용될 직교 투영 행렬을 생성합니다.
    D3DXMatrixOrthoLH(&m_orthoMatrix, (float)screenWidth, (float)screenHeight, screenNear, screenDepth);

    return true;
}

Shutdown 함수는 직관적으로 Initialize 함수에서 사용했던 포인터들을 해제하고 정리하는 일을 할 것입니다. 하지만 이 작업을 하기 전에 저는 스왑 체인을 윈도우 모드로 바꾸는 함수를 호출합니다. 만일 이 작업을 하지 않고 풀스크린 상태에서 스왑 체인을 해제하면 몇몇 예외가 발생하게 됩니다. 따라서 그런 예외를 피하기 위해 Direct3D를 종료하기 전에는 언제나 스왑 체인을 윈도우 모드로 바꿔주어야 합니다.

void D3DClass::Shutdown()
{
    // 종료하기 전에 이렇게 윈도우 모드로 바꾸지 않으면 스왑체인을 할당 해제할 때 예외가 발생합니다.
    if(m_swapChain)
    {
        m_swapChain->SetFullscreenState(false, NULL);
    }

    if(m_rasterState)
    {
        m_rasterState->Release();
        m_rasterState = 0;
    }

    if(m_depthStencilView)
    {
        m_depthStencilView->Release();
        m_depthStencilView = 0;
    }

    if(m_depthStencilState)
    {
        m_depthStencilState->Release();
        m_depthStencilState = 0;
    }

    if(m_depthStencilBuffer)
    {
        m_depthStencilBuffer->Release();
        m_depthStencilBuffer = 0;
    }

    if(m_renderTargetView)
    {
        m_renderTargetView->Release();
        m_renderTargetView = 0;
    }

    if(m_deviceContext)
    {
        m_deviceContext->Release();
        m_deviceContext = 0;
    }

    if(m_device)
    {
        m_device->Release();
        m_device = 0;
    }

    if(m_swapChain)
    {
        m_swapChain->Release();
        m_swapChain = 0;
    }

    return;
}

D3DClass에는 몇 가지 도우미 함수가 있습니다. 처음 두 개는 BeginSceneEndScene입니다. BeginScene는 매 프레임의 시작마다 3D 화면을 그리기 시작할 때 호출됩니다. 이 함수는 버퍼를 빈 값으로 초기화하고 렌더링이 이루어지도록 준비합니다. 다른 함수는 EndScene입니다. 이것은 매 프레임의 마지막에 스왑 체인에게 백버퍼에 그린 3D화면을 표시하도록 하게 합니다.

void D3DClass::BeginScene(float red, float green, float blue, float alpha)
{
    float color[4];


    // 버퍼를 어떤 색상으로 지울 것인지 설정합니다.
    color[0] = red;
    color[1] = green;
    color[2] = blue;
    color[3] = alpha;

    // 백버퍼의 내용을 지웁니다.
    m_deviceContext->ClearRenderTargetView(m_renderTargetView, color);
    
    // 깊이 버퍼를 지웁니다.
    m_deviceContext->ClearDepthStencilView(m_depthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0);

    return;
}


void D3DClass::EndScene()
{
    // 렌더링이 완료되었으므로 백버퍼의 내용을 화면에 표시합니다.
    if(m_vsync_enabled)
    {
        // 새로고침 비율을 고정합니다.
        m_swapChain->Present(1, 0);
    }
    else
    {
        // 가능한 한 빠르게 표시합니다.
        m_swapChain->Present(0, 0);
    }

    return;
}

다음 함수들은 단순히 Direct3D 디바이스와 디바이스 컨텍스트의 포인터를 가져오는 것입니다. 이 도우미 함수들은 프레임워크에서 종종 호출될 것입니다.

ID3D11Device* D3DClass::GetDevice()
{
    return m_device;
}


ID3D11DeviceContext* D3DClass::GetDeviceContext()
{
    return m_deviceContext;
}

다음 세 도우미 함수는 투영, 월드, 그리고 직교 투영 행렬을 반환합니다. 대부분의 셰이더에서는 이 행렬들이 필요하기 때문에 이를 가져올 손쉬운 방법이 필요합니다. 일단 이번 예제에서는 사용하지 않습니다.

void D3DClass::GetProjectionMatrix(D3DXMATRIX& projectionMatrix)
{
    projectionMatrix = m_projectionMatrix;
    return;
}


void D3DClass::GetWorldMatrix(D3DXMATRIX& worldMatrix)
{
    worldMatrix = m_worldMatrix;
    return;
}


void D3DClass::GetOrthoMatrix(D3DXMATRIX& orthoMatrix)
{
    orthoMatrix = m_orthoMatrix;
    return;
}

마지막 도우미 함수는 그래픽카드의 이름과 사용가능한 메모리의 양을 반환합니다. 그래픽 카드의 이름과 메모리의 크기를 아는 것은 서로 다른 설정에서 디버깅하는 데 도움이 됩니다.

void D3DClass::GetVideoCardInfo(char* cardName, int& memory)
{
    strcpy_s(cardName, 128, m_videoCardDescription);
    memory = m_videoCardMemory;
    return;
}

마치면서

드디어 Direct3D를 초기화하고 끌 수 있을 뿐만 아니라 윈도우에 하나의 색상을 출력할 수도 있게 되었습니다. 코드를 컴파일하고 돌려보면 이전 튜토리얼과 같은 윈도우를 만들게 되겠지만 Direct3D가 구동되고 윈도우가 회색으로 깨끗해진 모습을 볼 수 있습니다. 또한 직접 빌드하고 실행시켜 봄으로 여러분의 컴파일러와 라이브러리 등이 제대로 설정되어 있는지 확인할 수 있습니다.

연습문제

  1. 이 코드를 다시 컴파일하고 DirectX가 동작하는지 확인해보십시오. 이전 튜토리얼을 보지 않은 분들을 위해, Esc키를 눌러 프로그램을 종료할 수 있습니다.

  2. graphicsclass.h에 정의된 전역변수를 풀스크린 상태로 바꾸고 컴파일하고 실행해보십시오.

  3. GraphicsClass::Render의 초기화 색상을 노란색으로 바꾸어 보십시오.

  4. 텍스트(txt)파일에 그래픽 카드의 이름과 메모리 용량을 출력해 보십시오.

소스 코드

Visual Studio 2010 프로젝트: dx11tut03.zip

소스코드: dx11src03.zip

실행파일: dx11exe03.zip

Nav