본문 바로가기
그래픽스 API/DirectX 11

[DirectX 11 in Windows 10, 11] 3. DirectX 11의 초기화

by 니키티스 2023. 8. 9.

해당 글도 래스터텍의 글과 빠재님의 번역본을 참고하여 작성되었습니다.

빠재의 노트 :: DirectX11 Tutorial 3 - DirectX 11의 초기화 (tistory.com)

Tutorial 3: Initializing DirectX 11 (rastertek.com)

개념 정리

DX11 자체가 너무 알아야 할 개념이 많아서 그것부터 정리해 놓음.

DX11 학습에 필요한 개념 정리

새로운 프레임워크

D3DClass가 추가되었다.

이제 모든 Direct3D 시스템 함수를 관리(handle)할 새로운 클래스를 추가해 보자. 이를 위해 D3DClass를 추가했다. ApplicationClass 하위에 위치시켰는데, ApplicationClass가 이전 튜토리얼에서 말했듯이 모든 그래픽과 관련된 클래스를 해당 클래스에 캡슐화시키는 클래스이기 때문이다.

달라진 ApplicationClass를 한 번 보자.

ApplicationClass.h

////////////////////////////////////////////////////////////////////////////////
// Filename:  applicationclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _APPLICATIONCLASS_H_
#define _APPLICATIONCLASS_H_

여기 MY CLASS INCLUDES를 보면 d3dclass.h를 포함하도록 바꾸었다.

///////////////////////
// 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.3f;

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

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

private:
    bool Render();

private:
    D3DClass* m_Direct3D;
};

#endif

두 번째 변화는 private pointer로 D3DClass* m_Direct3D;를 추가한 점이다. 멤버 변수는 m_을 접두어로 붙이도록 코딩 표준을 정했으니, 클래스 멤버 변수인지 아닌지 이걸로 확인하도록 하자.

ApplicationClass.cpp

이전 튜토리얼에서는 애플리케이션 클래스 코드가 거의 비어 있었다. 이제 ApplicationClass의 멤버로 D3DClass가 추가되었으니, D3DClass 객체를 초기화하고 종료하는 코드를 추가해 보자. 그리고 Render 함수에 BeginScene, EndScene을 호출하는 부분을 추가하여 Direct3D를 통해 윈도우를 그려보도록 하자.

생성자 부분

안전을 위해 애플리케이션 클래스의 생성자에서 m_Direct3D 포인터를 null로 초기화해 준다.

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

ApplicationClass::ApplicationClass()
{
    m_Direct3D = 0;
}

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

ApplicationClass::~ApplicationClass()
{
}

Initialize 함수

Initialize 함수에서 D3DClass 객체를 만들고 D3DClass에게 Intialize 함수를 호출하도록 한다. 이때, 화면의 너비, 높이, 윈도우 핸들, applicationclass.h의 네 가지 전역 변수를 전달해 준다. 이를 통해 D3DClass가 Direct3D 시스템을 초기화시키게 할 것이다.

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

    // Direct3D 객체를 생성하고 초기화한다.
    m_Direct3D = new D3DClass;

    result = m_Direct3D->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;
}

Shutdown 함수

Shutdown 함수도 변경해 보자. 모든 그래픽스 객체를 종료시키는 함수이므로, D3Dclass도 해당 함수에서 종료되게 한다.

여기서 포인터가 초기화되었는지 아닌지를 확인한다는 점에 주목하자.

만약 포인터가 초기화되지 않았다면 아예 생성된 것이 아니니 굳이 종료시키지 않는다. 생성자에서 모든 포인터를 null로 초기화시켜야 하는 이유가 바로 이곳에 있다.

만약 포인터가 존재한다면 당연히 초기화된 것이기 때문에 D3DClass 객체를 닫고 제거해야 한다.

void ApplicationClass::Shutdown()
{
    // Direct3D 객체를 해제한다.
    if(m_Direct3D)
    {
        m_Direct3D->Shutdown();
        delete m_Direct3D;
        m_Direct3D = 0;
    }

    return;
}

Frame 함수

프레임 함수는 Render 함수를 매 프레임 호출될 수 있게 변경했다.

bool ApplicationClass::Frame()
{
    bool result;

    // 그래픽 씬을 렌더링한다.
    result = Render();
    if(!result)
    {
        return false;
    }

    return true;
}

Render 함수

마지막 변화는 Render 함수이다. m_Direct3D 객체를 호출하여 화면을 회색(grey) 컬러로 설정하도록 한다. 이후 EndScene 함수를 호출하여 윈도우에 회색 색상이 전달되도록 한다.

bool ApplicationClass::Render()
{
    // 버퍼를 비워서 씬을 시작한다.
    m_Direct3D->BeginScene(0.5f, 0.5f, 0.5f, 1.0f);

    // 렌더링된 씬을 화면으로 전달한다.
    m_Direct3D->EndScene();

    return true;
}

D3dclass.h

링크

헤더 파일에 처음 들어갈 내용은, 해당 객체 모듈을 사용할 때 링크할 라이브러리이다.

  • d3d11.lib: 첫 라이브러리는 설치와 DirectX 11 내에서 3D 그래픽스를 그리기 위한 모든 Direct3D 기능을 포함한다.
  • dxgi.lib: 두 번째는 컴퓨터의 하드웨어에 대한 인터페이스용 도구를 포함하는데, 이는 모니터 주사율(refresh rate), 현재 사용 중인 그래픽 카드 등에 관해 정보를 얻기 위해서 필요하다. Direct3D 라이브러리가 내부적으로 DXGI를 거쳐가기도 하고(아래 그림에서는 D3D10) 직접 DXGI에 접근할 수도 있다.

directX 10에서 DXGI가 쓰이는 도식표

  • d3dcompiler.lib: 세 번째 라이브러리는 다음 튜토리얼에서 다룰 쉐이더를 컴파일하기 위해 필요하다.
////////////////////////////////////////////////////////////////////////////////
// Filename: d3dclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _D3DCLASS_H_
#define _D3DCLASS_H_

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

다음은 위에서 링크한 라이브러리를 묶는 헤더파일과 DirectX 타입 정의와 수학 기능을 위한 헤더 파일을 include 한다.

//////////////
// INCLUDES //
//////////////
#include <d3d11.h>
#include <directxmath.h>
using namespace DirectX;

D3DClass의 정의는 가능한 단순하게 정의한다. 기본 생성자, 복사 생성자, 소멸자에다가 꼭 필요한 Initialize, Shutdown 함수가 들어간다(이번 튜토리얼에서 볼 내용임!).

이외에는 이번 튜토리얼에서는 크게 중요하지 않은 여러 도우미 함수가 있고 수많은 private 멤버 변수가 있다. 이 변수들은 d3dclass.cpp에서 볼 것이다.

현재로서는 Initialize와 Shutdown 함수만 주의하자.

////////////////////////////////////////////////////////////////////////////////
// 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(XMMATRIX&);
    void GetWorldMatrix(XMMATRIX&);
    void GetOrthoMatrix(XMMATRIX&);

    void GetVideoCardInfo(char*, int&);

    void SetBackBufferRenderTarget();
    void ResetViewport();

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;
    XMMATRIX m_projectionMatrix;
    XMMATRIX m_worldMatrix;
    XMMATRIX m_orthoMatrix;
    D3D11_VIEWPORT m_viewport;
};

#endif

Direct3D에 친숙한 분들에게는 여기에 view matrix가 없다는 걸 알 수 있다. 그 이유는 추후 튜토리얼에서 카메라 클래스에 view matrix를 넣을 예정이기 때문이다.

D3dclass.cpp

전처리 및 생성자/소멸자

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

일단 처음 시작은 생성자에서 모든 멤버 포인터를 null로 초기화하는 것이다.

(역자주: std::shared_ptr이나 std::unique_ptr을 이용하면 최초 초기화를 하지 않아도 된다!)

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()
{
}

D3DClass::Intialize 1: 변수 선언

Initialize 함수는 DirectX 11용 Direct3D를 전체적으로 설정한다(setup). 여기에는 필요한 모든 코드들도 있고, 추후의 튜토리얼에 있으면 좋을 여러 가지 요소까지 포함하고 있다.

여기 주어진 screenWidth, screenHeight는 SystemClass에서 생성한 윈도우의 너비와 높이다. Direct3D에서 이를 사용하여 동일한 크기의 윈도우 영역을 초기화하고 이용한다. hwnd 변수는 윈도우에 대한 핸들이다. Direct3D는 이전에 만든 윈도우에 접근하기 위해 해당 핸들이 필요하다. fullscreen 변수는 윈도우 모드에서 동작할지 전체화면에서 동작할지를 가리킨다. Direct3D 뿐만 아니라 윈도우를 올바른 설정으로 생성할 때에도 필요하다. screenDepth와 screenNear 변수는 윈도우에 렌더링될 3D 환경을 위한 깊이 설정에 해당한다. 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;
    // numerator는 분자, denominator는 분모를 가리킨다.
    unsigned int numModes, i, numerator, denominator;
    unsigned long long 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;
    float fieldOfView, screenAspect;

    // vsync 설정을 저장한다.
    m_vsync_enabled = vsync;

D3DClass::Intialize 2: 주사율, 그래픽 카드 메모리, 이름 가져오기

Direct3D를 초기화하기 전에 그래픽 카드/모니터로부터 주사율을 얻어야 한다. 컴퓨터마다 그 값이 조금씩 다르기 때문에 정보를 조회할 필요가 있다. 분자(nominator)와 분모(denominator) 값을 조회하여 DirectX에 전달하면 적절한 주사율을 계산해 준다. 이 과정을 거치지 않고 그냥 모든 컴퓨터에 맞지 않을 수도 있는 기본값으로 설정하면, DirectX는 버퍼 플립을 사용하지 않고 blit을 하게 되어, 성능을 떨어트리고 디버그 출력에 거슬리는 에러가 나오게 된다.

💡 버퍼 플립과 blit

실제 그래픽스에서는 파이프라인을 거쳐서 화면상에 그릴 이미지를 만들어내게 된다. 이때 만들어진 이미지는 바로 모니터 화면으로 출력되지 않고 백버퍼에 그려지게 된다. 이유는 화면에 바로 이미지를 그리게 되면, 이전 프레임의 이미지와 현재 프레임의 이미지가 섞여 마치 이미지가 찢어지는 현상(tearing)이 발생하기 때문이다.

따라서 백버퍼에 먼저 이미지를 그리고 나서, 프레임을 갱신할 때 실제 화면을 그리는 프론트 버퍼와 백버퍼를 바꾸는 식으로 그래픽을 그리게 된다. 이렇게 버퍼를 바꾸는 과정이 버퍼 플립(Buffer flip)이다.

그런데 모니터 주사율과 DirectX의 주사율이 달라지면 bit blit이라는 현상이 발생한다. bit blit이란 bit block transfer의 약어로 흔히 bit blt, blit으로 줄여 부른다. blit은 기본적으로 블록 전송을 가리키는데, 데이터 배열을 비트맵 배열 목적지에 복사하는 것이다. 실제로는 단순하게 복사하는 것보다 좀 더 복잡하게, 마스킹 연산 등의 Bool 연산을 포함한다.

여기서 말하는 blit은 이전 프레임의 이미지를 현재 프레임으로 단순히 복사한다고 봐야 할 듯하다.

blit 이란? : 네이버 블로그 (naver.com)

Bit blit - Wikipedia

    // 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;
    }

    // 그래픽 카드 전용 메모리를 MB 단위로 저장한다.
    m_videoCardMemory = (int)(adapterDesc.DedicatedVideoMemory / 1024 / 1024);

    // 그래픽 카드 이름을 문자 배열로 변환하여 저장한다.
    error = wcstombs_s(&stringLength, m_videoCardDescription, 128, adapterDesc.Description, 128);
    if(error != 0)
    {
        return false;
    }

💡 멀티 바이트 스트링과 와이드 문자 스트링(유니코드)

wcstombs_s라는 생소한 함수가 나오는데, 해당 함수를 알고 계신 분은 이 박스 부분은 넘어가도 괜찮을 듯하다.

위 함수는 와이드 문자 스트링(wchar_t)을 멀티바이트 스트링(char)으로 변경한다.

여기서 멀티바이트 스트링과 와이드 문자 스트링이 뭘까?

제일 초기에 C에서 사용했던 문자는 아스키코드(ASCII)이다. 이는 모든 문자가 1바이트로 구성되는데, 영어와 특수문자 정도만 표현할 수 있다.

더 많은 문자를 표현하기 위해, 멀티바이트 코드(MBCS; multi byte character set)가 등장했다. Microsoft에서 윈도우에서 사용하기 위해 만든 표준(세계 표준은 아니다)이다. 멀티바이트 코드는 가변형 길이의 문자열이다. 1바이트로 표현할 수 있는 문자는 1바이트 문자로, 그렇지 못한 경우 2바이트로 표현한다. 한글이나 일어와 같은 언어를 표현하려면 못해도 2바이트가 필요하므로, 이 경우에는 2바이트로 표현한다. 한글을 표현하는 MBCS 인코딩으로는 EUC-KR, CP949가 있다.

멀티바이트 코드 이외에도, 전 세계의 모든 문자를 표현하기 위해 유니코드(Unicode)가 만들어졌다. 유니코드는 WBCS(wide byte character set)로 구성되어, 모든 문자가 2바이트로 구성되어 있다. 유니코드를 표현하는 대표적인 예시로는 UTF-8이 있다. 여기서 WBCS가 와이드 문자 스트링에 해당하며, C++에서는 wchar_t로 표현한다.

유니코드, 멀티바이트 차이점 리서치 (tistory.com)

SBCS, MBCS와 WBCS / 유니코드 프로그래밍이 중요한 이유 (neonkid.xyz)

이제 주사율의 분자/분모도 저장했으니, 해당 정보를 얻기 위해 획득했던 구조체와 인터페이스도 해제하자.

    // 디스플레이 모드 리스트를 해제한다.
    delete [] displayModeList;
    displayModeList = 0;

    // 어댑터 출력을 해제한다.
    adapterOutput->Release();
    adapterOutput = 0;

    // 어댑터를 해제한다.
    adapter->Release();
    adapter = 0;

    // 팩토리를 해제한다.
    factory->Release();
    factory = 0;

D3DClass::Intialize 3: 스왑 체인부터 장치/장치 텍스트를 거쳐 렌더 타겟 뷰 생성하기

시스템으로부터 주사율을 얻었으므로 DirectX의 초기화를 시작할 수 있다.

가장 처음 해야 할 것은 스왑 체인(swap chain)의 description을 채워 넣는 것이다. 스왑 체인이란 그래픽이 실제로 그려질 프론트 버퍼와 백 버퍼를 말한다. 일반적으로는 하나의 백 버퍼를 사용한다. 여기에다가 모든 걸 그리고 나면, 프론트 버퍼와 교체(swap)하여 사용자의 화면에 그려진 것을 보여준다.

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

    // 백 버퍼를 하나로 설정한다.
    swapChainDesc.BufferCount = 1;

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

    // 백 버퍼를 일반 32비트 surface로 설정한다.
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

스왑 체인 description의 다음 부분은 주사율이다. 주사율은 1초에 얼마나 자주 백 버퍼에 그려서 프론트 버퍼로 내보낼 것인지를 가리킨다. applicationclass.h에서 vsync가 활성화되어 있다면 주사율이 시스템 설정 값(ex: 60hz)으로 고정된다. 이는 1초에 화면을 60번만 그린다는 뜻이다(혹은, 시스템 설정이 60보다 높으면 더 많이 그릴 수 있다). 하지만 vsync를 비활성화하면 컴퓨터는 1초에 최대한 자주 화면을 그리게 되는데, 이는 화면상에 특징적인 결점(visual artifacts)을 남길 수 있다.

    // 백 버퍼의 주사율을 설정한다.
    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;
    }

    // 백 버퍼의 용도(usage)를 결정한다.
    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을 설정한 후엔 feature level이라고 하는 변수를 하나 더 설정해주어야 한다. 해당 변수는 DirectX에게 어떤 버전을 사용할 것인지를 알려준다. 여기서는 DirectX 11을 가리키는 11.0으로 설정한다. 만약 여러 버전을 지원하거나 저성능 하드웨어에서도 작동하도록 할 것이라면 DirectX를 낮은 버전으로 사용하기 위해 10이나 9로 설정할 수 있다.

    // feature level을 DirectX 11로 설정한다.
    featureLevel = D3D_FEATURE_LEVEL_11_0;

이제 스왑 체인 description과 feature level을 채웠으니, 스왑 체인, Direct3D 장치, Direct3D 장치 컨텍스트를 생성할 수 있다. Direct3D 장치와 Direct3D 장치 컨텍스트는 매우 중요한데, 이들은 모든 Direct3D 함수로의 인터페이스가 된다. 이제부터는 대부분의 일을 위해 장치와 장치 컨텍스트를 이용할 것이다.

DirectX의 이전 버전에 익숙한 분들은 Direct3D 장치가 무엇인지 알겠지만 Direct3D 장치 컨텍스트에는 익숙지 않을 것이다. 기본적으로 이 둘은 Direct3D 장치의 기능을 가져와 둘로 쪼갠 것이므로 둘 다 사용해야 한다.

사용자가 DirectX 11을 지원하는 그래픽 카드를 갖고 있지 않다면 해당 함수는 장치와 장치 컨텍스트를 생성하는 데에 실패한다는 점에 유의하자. 또한 DirectX 11 기능을 테스트하는데 DirectX 11 지원 그래픽 카드가 없을 경우, D3D_DRIVER_TYPE_HARDWARED3D_DRIVER_TYPE_REFERENCE로 대체할 수 있으며 이 경우 DirectX는 그래픽 카드 하드웨어 대신 CPU를 통해 그리기 작업을 수행한다. 이는 1/1000의 속도로 작동하지만 컴퓨터에 DirectX 11을 지원하는 그래픽 카드가 없는 사람에게는 좋은 선택이다.

(실제로는 D3D_DRIVER_TYPE_REFERENCE 모드는 속도가 너무 느려 개발용이나 디버그용 정도로밖에 사용하지 않는다고 한다. 그래서 D3D_DRIVER_TYPE_HARDWARE를 사용할 수 없는 경우 D3D_DRIVER_TYPE_WARP를 먼저 사용하는데, 이 또한 사용자에게 Direct3D 지원 하드웨어가 없을 경우에 동작하며 D3D_DRIVER_TYPE_REFERENCE보다 훨씬 동작이 빠르다. 보통 HARDWARE→WARP→REFERENCE 순으로 앞에 있는 게 안 될 경우 뒤에 있는 걸 사용하는 식으로 쓴다고 한다.)

    // 스왑 체인과 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을 지원하는 기기가 있을 수도 있다. 또, 하이브리드 그래픽 카드는 기본 그래픽 카드가 저전력의 인텔 카드이고 두 번째가 고전력의 엔비디아 카드로 작동하기도 한다. 이러한 상황에서는 기본 디바이스를 사용하는 게 아니라, 기기의 모든 그래픽 카드를 나열하여 사용자가 장치 생성을 위해 어떤 그래픽 카드를 사용할 것인지를 선택할 수 있게 해야 한다.

이제 스왑 체인을 생성했으니, 백 버퍼에 대한 포인터를 받아와서 스왑 체인에 연결시켜줘야 한다. 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;

D3DClass::Intialize 4: 깊이-스텐실 버퍼 생성하기

또 깊이 버퍼 description도 필요할 것이다. 깊이 버퍼를 생성하여 3D 공간에 폴리곤이 적절하게 렌더링될 수 있게 할 것이다. 그와 동시에, 스텐실 버퍼를 깊이 버퍼에 연결할 것이다. 스텐실 버퍼는 모션 블러, 그림자 볼륨(volumetric shadows) 등의 효과를 구현할 때 쓰인다.

    // 깊이 버퍼의 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;

이제 해당 description을 통해 깊이/스텐실 버퍼를 생성한다. 버퍼를 생성할 때 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;

    // 픽셀이 앞면일 때 스텐실 연산
    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;

    // 픽셀이 뒷면일 때 스텐실 연산
    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가 깊이 버퍼를 깊이 스텐실 텍스처로 쓴다는 걸 인식할 수 있다. description을 채우고 나서 CreateDepthStencilView를 함수를 호출하여 깊이 스텐실 버퍼 뷰를 생성할 수 있다.

    // 깊이 스텐실 뷰를 초기화해준다.
    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;
    }

(깊이 스텐실 뷰 desc 객체의 설정에서, format에서는 깊이 비트를 24개, 스텐실 비트를 8개 쓰는 32비트 정수 z-버퍼를 쓴다는 것을 의미함. ViewDimemsion은 깊이 스텐실 리소스에 어떻게 접근할지를 말하는데, 1차원 배열로 접근할지 2차원 배열로 접근할지 따위를 정할 수 있다. 여기선 2D 텍스처로 접근함)

이제 생성한 것을 바탕으로 OMSetRenderTargets를 호출할 수 있다. 이는 렌더 타겟 뷰와 깊이 스텐실 버퍼를 출력 병합기(Output Merger) 단계에 바인딩한다. 이러한 방법으로 파이프라인을 통해 렌더링이 수행되어 이전에 만들었던 백 버퍼에 그래픽이 그려지게 될 것이다. 백 버퍼에 그려진 것을 프론트 버퍼와 교체하고 나면 우리가 그린 그래픽을 사용자의 화면에 보여줄 수 있게 된다.

    // 렌더 타겟 뷰와 깊이 스텐실 버퍼를 출력 병합기(Output Merger) 단계에 바인딩한다.
    m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

D3DClass::Intialize 5: 마무리 작업(래스터화, 뷰 포트, 변환 행렬들)

렌더 타겟이 설정되었으니, 추후에 사용할 몇 가지 추가적인 함수를 만들어 볼 것이다. 첫 번째로 만들 것은 래스터화 상태이다. 이는 얼마나 많은 폴리곤이 렌더링되는지 제어할 수 있게 해 줄 것이다. 장면을 와이어 프레임 모드로 보이게 만들거나 DirectX가 폴리곤의 앞, 뒷면을 모두 그리도록 하는 등의 일도 할 수 있다. 기본적으로, 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);

Direct3D에서 클립 공간 좌표를 렌더 타겟 뷰에 매핑시키려면 뷰 포트도 필요하다. 뷰 포트는 애플리케이션 창의 전체 크기로 설정하자.

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

    // 뷰포트를 생성한다.
    m_deviceContext->RSSetViewports(1, &m_viewport);

이제 투영 행렬을 생성할 것이다. 투영 행렬은 3D 장면을 방금 만든 2D 뷰포트로 변환할 때 사용된다. 셰이더에 장면을 그릴 수 있게 이 행렬을 전달해 주려면, 해당 행렬의 복사본을 계속 가지고 있어야 한다.

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

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

월드 행렬이라는 또 다른 행렬도 만들어줘야 한다. 이 행렬은 오브젝트의 정점의 좌표를 3D 세계 상의 정점 좌표로 변환해 준다(오브젝트 공간→월드 공간). 해당 행렬은 3D 공간에서 물체를 회전(rotae), 평행이동(translate), 확대 및 축소(scale)시킨다. 처음에는 행렬을 단위행렬로 초기화해서 복사본을 만든다. 복사본은 쉐이더가 렌더링할 때 전달된다.

    // 월드 행렬을 단위 행렬로 초기화한다.
    m_worldMatrix = XMMatrixIdentity();

이제 여기서는 보통 뷰 행렬을 생성하는 부분이다. 뷰 행렬은 현재 장면을 어느 위치에서 어느 방향으로 보고 있는지를 계산할 때 쓰인다. 이 행렬을 카메라라 본다면, 이 카메라를 통해서만 3D 세상을 본다고 생각할 수 있겠다. 이러한 취지에서, 뷰 행렬은 추후에 카메라 클래스를 만들어 그곳에서 다룰 것이며 우선 지금은 넘어가고자 한다.

Initialize 함수 설정에서 마지막으로 할 것은 직교 투영 행렬(orthographic projection matrix)이다. 이 행렬은 화면상의 UI처럼 2D의 요소를 렌더링하는 데에 쓰인다. 나중에 2D 그래픽 요소와 폰트를 렌더링할 때에 살펴볼 예정이다.

    // 2D 렌더링을 위해 직교 투영 행렬을 생성한다.
    m_orthoMatrix = XMMatrixOrthographicLH((float)screenWidth, (float)screenHeight, screenNear, screenDepth);

    return true;
}

D3DClass::Shutdown()

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;
}

BeginScene/EndScene() 함수

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;
}

GetDevice()/GetDeviceContext()

다음 함수는 단순히 Direct3D 장치와 장치 컨텍스트에 관한 포인터를 가져온다. 프레임 워크에서 종종 이 도우미 함수들이 호출할 것이다.

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

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

Matrix 관련 함수

다음 세 도우미 함수는 함수 호출을 위해 투영, 월드, 직교 행렬의 복사본을 제공한다. 대부분의 셰이더는 렌더링할 때 이 행렬들이 필요하기 때문에 이를 가져올 손쉬운 방법이 필요하다. 이번 예제에서는 안 쓸 예정이다.

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

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

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

GetVideoCardInfo 함수

이 도우미 함수는 그래픽 카드 이름과 비디오 메모리 크기에 대한 참조를 반환한다. 그래픽 카드 이름을 아는 것은 다른 설정에서 디버깅할 때 도움이 될 수 있다.

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

SetBackBufferRender/ResetViewport 함수

다음 두 도우미 함수는 텍스처 렌더링 튜토리얼에서 사용될 것이다.

void D3DClass::SetBackBufferRenderTarget()
{
    // 렌더 타겟 뷰와 깊이 스텐실 버퍼를 출력 병합기(Output Merger) 단계에 바인딩한다.
    m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

    return;
}

void D3DClass::ResetViewport()
{
    // 뷰포트를 설정한다.
    m_deviceContext->RSSetViewports(1, &m_viewport);

    return;
}

정리

이제 Direct3D를 초기화하고 종료할 수 있게 되었다! 코드를 컴파일하고 실행하게 되면 지난 튜토리얼과 똑같은 윈도우가 나오지만, 이제 Direct3D가 초기화돼서 윈도우를 회색으로 색칠하고 있다.

코드를 컴파일하고 실행해 보는 과정을 통해 컴파일러가 제대로 설정되어 있는지, Windows SDK로부터 헤더 파일과 라이브러리를 잘 읽어내고 있는지를 확인할 수 있다.

과제

  1. 첫 튜토리얼에서 밟았던 단계를 안 봤으면, 다시 코드를 컴파일하고 실행해서 DirectX가 잘 작동하는지 확인하자. 윈도우가 화면에 보이면 ESC 키를 눌러 닫을 수 있다.
  2. applicationclass.h의 전역 변수를 수정해서 전체화면으로 바꾸고 다시 컴파일해서 실행해 보자.
  3. ApplicationClass::Render에서 초기화 색상을 노란색으로 바꿔보자.
  4. 텍스트 파일에 그래픽 카드의 이름과 메모리를 출력해 보자.

 

해답

과제 2번: applicationclass.h로 들어가 FULL_SCREEN=true; 로 설정해 주면 된다. 이때, Debug 모드로 설정되어 있을 경우 오류가 발생하기도 한다. 따라서 디버거 왼쪽의 선택 창에서 Debug 모드를 Release 모드로 변경해 주자.

과제 3번: m_Direct3D->BeginScene(1.0f, 1.0f, 0.0f, 1.0f);로 색상을 수정해 주면 된다.

과제 4번: 우선 그래픽 카드의 이름과 메모리를 출력하는 함수를 만들었다.

bool ApplicationClass::WriteVideoCardToFile(const char* fileName)
{
    FILE* fp;
    errno_t result = fopen_s(&fp, fileName, "w");
    if (result != 0)
    {
        return false;
    }
    char videoCardName[128];
    int memory;
    m_Direct3D->GetVideoCardInfo(videoCardName, memory);

    fprintf(fp, "Video card information\nvideo card name: %s\ndedicated card memory: %d MB",
        videoCardName, memory);

    return true;
}

이제 이를 적당한 곳에서 호출하면 된다. 어디에 넣어도 상관이 없지만, 여기서는 applicationclass::Initialize 함수에서 호출해 주도록 했다.

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

    // Direct3D 객체를 생성하고 초기화한다.
    m_Direct3D = new D3DClass;

    result = m_Direct3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
    if (!result)
    {
        MessageBox(hwnd, L"Could not intialize Direct3D", L"Error", MB_OK);
        return false;
    }

    // (새로 추가됨) 과제: 텍스트 파일에 그래픽 카드 쓰기
    WriteVideoCardToFile("Video Card Info.txt");

    return true;
}

추가

  • 실제 rastertek 튜토리얼 본문에서는 출력 렌더 파이프라인(the output render pipeline)이라고 표현하고 있는 것을 ‘출력 병합기(Output merger) 단계’로 번역했습니다.
  • 실제 디스플레이 출력을 만들어내는 과정으로 알려져 있습니다. OMSetRenderTargets라고 되어 있는 것도 출력 병합기(OM)에 대해 설정하는 것이기 때문에 그렇고, 해당 함수를 통해서 렌더 타겟은 최대 8개까지 설정할 수 있습니다(다만, 깊이/스텐실 버퍼는 하나밖에 설정되지 않습니다).

참고 자료

빠재의 노트 :: DirectX11 Tutorial 3 - DirectX 11의 초기화 (tistory.com)

Tutorial 3: Initializing DirectX 11 (rastertek.com)

DirectX 11 기초 (Tutorial) .. : 네이버블로그 (naver.com)

blit 이란? : 네이버 블로그 (naver.com)

Bit blit - Wikipedia

유니코드, 멀티바이트 차이점 리서치 (tistory.com)

SBCS, MBCS와 WBCS / 유니코드 프로그래밍이 중요한 이유 (neonkid.xyz)

댓글