본문 바로가기

DirectX

[DirectX 12] Direct3D 초기화

Direct3D를 초기화 하는 과정은 다음과 같다.

1. D3D12CreateDevice를 이용해서 ID3D12Device를 생성한다.
2. ID3D12Fence 객체를 생성하고, 서술자들의 크기를 얻는다.
3. 4X MSAA 품질 수준 지원 여부를 검사한다.
4. 커맨드 큐과 커맨드 리스트 할당자, 그리고 주 커맨드 리스트을 생성한다.
5. Swap chain을 서술한 뒤 이를 통해 생성한다.
6. 응용 프로그램에 필요한 서술자 힙들을 생성한다. 
7. 후면 버퍼의 크기를 설정하고, 후면 버퍼에  대한 렌더 대상 뷰(Render target view)를 생성한다.
8. 깊이 * 스텐실 버퍼를 생성하고 이와 연관된 깊이 * 스텐실 뷰를 생성한다.
9. 뷰포트와 가위 판정용 사각형을 설정한다.

1. D3D12CreateDevice를 이용해서 ID3D12Device를 생성한다.

이 때 중요한 것은 바로 D3DDevice 즉 Direct3D 디바이스에 대해 알아야한다.
Direct3D 디바이스는 CPU 버스 상에서 디스플레이 어뎁터를 나타내는 객체이다. 
일반적으로 그래픽 하드웨어 장치(예로 들면 GPU)를 말한지만 하드웨어 그래픽 기능을 흉내내서 표현하는 소프트웨어 어댑터(예로들어 WARP 어댑터) 또한 존재한다.
Direct3D 12의 경우에는 기능 지원 점검에 쓰이며, 자원이나 뷰, 명령 목록 등등의 다른 모든 Direct3D 인터페이스 객체들의 생성에도 ㅆ인다. 장치를 생성할 때는 다음과 같이 사용한다.

HRESULT WINAPI D3D12CreateDevice(
	IUnknown* pAdapter,
	D3D_FEATURE_LEVEL MinimumFeatureLevel, 
	REFIID riid,
	void** ppDevice);



pAdapter : 장치가 나타내는 디스플레이 어뎁터를 지정한다.
MinimumFeatureLevel : 응용 프로그램이 요구하는 최소 기능 수준. 만약 어댑터가 이 수준을 지원하지 않으면 장치 생성이 실패한다. 
riid : 생성하고자 하는 ID3D12Device 인터페잇의 COM ID
ppDevice : 생성된 상치가 이 매개변수에 설정된다.(출력 매개 변수)

생성 예시는 다음과 같다
   

    // Direct3D 12 하드웨어 디바이스를 생성한다.
    // 하위 최소 지원 레벨은 Direct3D 11로 한다.
    HRESULT hardwareResult = D3D12CreateDevice(
        nullptr,             // default adapter
        D3D_FEATURE_LEVEL_11_0,
        IID_PPV_ARGS(&m_d3dDevice));

    // 만약 하드웨어 디바이스가 없다면 WARP(윈도우 고급 래스터화 플랫폼) 디바이스 즉 소프트웨어로 교체한다.
    if (FAILED(hardwareResult))
    {
        ComPtr<IDXGIAdapter> pWarpAdapter;
        ThrowIfFailed(m_dxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

        ThrowIfFailed(D3D12CreateDevice(
            pWarpAdapter.Get(),
            D3D_FEATURE_LEVEL_11_0,
            IID_PPV_ARGS(&m_d3dDevice)));
    }


2. ID3D12Fence 객체를 생성하고, 서술자들의 크기를 얻는다.

그 이후 펜스를 생성해야 한다. 
펜스는 CPU와 GPU의 동기화를 위한 객체라고 보면 된다. 그리고 이후에 필요할 서술자들의 크기들을 미리 조회해서 설정해둔다.

m_RtvDescriptorSize = m_d3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
m_DsvDescriptorSize = m_d3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
m_CbvSrvUavDescriptorSize = m_d3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);


    
서술자 크기는 GPU마다 다를 수 있기 때문에 이처럼 적절한 메서드를 호출해서 알아내야 한다.
나중에 서술자 크기가 필요할 때 바로 사용할 수 있도록, 크기들을 적절한 멤버 변수에 저장해둔다.

3. 4X MSAA 품질 수준 지원 여부를 검사한다.

그 이후 4X MSAA 지원 여부를 점검한다. 4X MSAA의 경우 사용 비용이 크지 않으면서 동시에 많은 효과를 볼 수 있다는 장점이 있기 때문에, 많이 사용되는 편이다.
Direct3D 11 이상인 경우 4X MSAA의 가능 여부를 점검할 필요가 없다.

 

    // 백버퍼의 4X MSAA 퀄리티 지원을 체크합니다.
    // Direct3D 11이 가능한 장비들은 4X MSAA를 모든 렌터 타켓 포맷에 지원합니다.
    // 그렇기 때문에 퀄리티 지원 부분만 체크를 합니다.
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
    msQualityLevels.Format = m_BackBufferFormat;
    msQualityLevels.SampleCount = 4;
    msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
    msQualityLevels.NumQualityLevels = 0;
    ThrowIfFailed(m_d3dDevice->CheckFeatureSupport(
        D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
        &msQualityLevels,
        sizeof(msQualityLevels)));



4. 커맨드 큐과 커맨드 리스트 할당자, 그리고 주 커맨드 리스트을 생성한다.

저번 포스트에서 설명한 커맨드 큐, 커맨드 할당자 그리고 커맨드 리스트을 코드로 설명한다

    // 커맨드 큐에 대한 서술자를 채우고 생성한다.
    D3D12_COMMAND_QUEUE_DESC queueDesc = {};
    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
    ThrowIfFailed(m_d3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_CommandQueue)));

    // 커맨드 할당자를 생성한다.
    // 이 할당자는 커맨드 리스트의 메모리를 할당하기 위해 존재한다.
    // 명령 목록에 추가된 명령들은 이 할당자의 메모리에 저장된다.
    ThrowIfFailed(m_d3dDevice->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT, // GPU가 직접 실행하는 명령 목록
        IID_PPV_ARGS(m_DirectCmdListAlloc.GetAddressOf())));    // RIID와 void** 타입의 커맨드 할당자 COM 객체를 넣어준다.

    // 커맨드 큐에 넣을 커맨드 리스트를 생성한다.
    ThrowIfFailed(m_d3dDevice->CreateCommandList(
        0,
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        m_DirectCmdListAlloc.Get(),     // 관련된 커맨드 할당자를 넣는다.
        nullptr,                        // PipelineStateObject를 초기화한다.
        IID_PPV_ARGS(m_CommandList.GetAddressOf())));

    // 닫힌 상태로 시작을 한다.
    // 처음 커맨드 리스트를 참조할 때 이를 리셋할 것이다.
    // 그리고 커맨드 리스트는 리셋하기 전에는 닫혀있어야 한다.
    m_CommandList->Close();



5. Swap chain을 서술한 뒤 이를 통해 생성한다.

초기화의 다음 단계는 Swap chain을 생성하는 것이다. 이를 위해서는 DXGI_SWAP_CHAIN_DESC 서술자를 멤버들을 채운 뒤 이를 이용해서, Swap chain에 맞게 설정한다.

   

    // 재생성 하기 전에 리셋을 진행한다.
    m_SwapChain.Reset();

    // 스왑 체인에 대한 설명을 적는다.
    // 스왑 체인은 DXGI의 인터페이스이다.
    DXGI_SWAP_CHAIN_DESC sd;
    sd.BufferDesc.Width = m_tRS.nWidth;
    sd.BufferDesc.Height = m_tRS.nHeight;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferDesc.Format = m_BackBufferFormat;
    sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    sd.SampleDesc.Count = m_4xMsaaState ? 4 : 1;
    sd.SampleDesc.Quality = m_4xMsaaState ? (m_4xMsaaQuality - 1) : 0;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.BufferCount = SwapChainBufferCount;
    sd.OutputWindow = m_hWnd;
    sd.Windowed = true;
    sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

    // 주의 : 스왑 체인은 플러싱 하기 위해서 커맨드 큐를 사용합니다.
    ThrowIfFailed(m_dxgiFactory->CreateSwapChain(
        m_CommandQueue.Get(),
        &sd,
        m_SwapChain.GetAddressOf()));



내가 만드는 엔진에서는 만약 이전과 다른 설정으로 Swap chain을 설정하고 싶다면 이를 해제한 뒤 새 Swap Chain을 생성하는 방식으로 코드를 작성했다.

6. 응용 프로그램에 필요한 서술자 힙들을 생성한다. 

다음으로 응용 프로그램에 필요한 서술자와 뷰를 담을 서술자 힙을 만든다. 이 때 서술하기 위한 자료형 타입으로 D3D12_DESCRIPTOR_HEAP_DESC가 있다. 
그리고 힙을 사용하기 위해서는 ID3D12Device::CreateDescriptorHeap() 메서드를 이용해서 생성한다.

위 코드에서 SwapChainBufferCount라는 변수가 있었다. 이는 2가 저장이 되어있는 int 변수이다. 
이 SwapChainBufferCount의 숫자 2는 버퍼가 2개라는 뜻이고, 이에 맞춰서 우리는 Render target view 그리고 Depth/Stencil view를 생성한다.
렌더 타깃 뷰는 Swap chain에서 렌더링의 대상이 되는 버퍼 자원을 서술하고, Depth/Stencil view는 깊이 판정을 위한 버퍼 자원을 서술한다.
예시 코드는 다음과 같다.

    // Render Targer View에 대한 서술자를 생성한다.
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
    rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    rtvHeapDesc.NodeMask = 0;
    ThrowIfFailed(m_d3dDevice->CreateDescriptorHeap(
        &rtvHeapDesc, IID_PPV_ARGS(m_RtvHeap.GetAddressOf())));

    // Depth/Stencil View에 대한 서술자를 생성하고 작성한다.
    D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
    dsvHeapDesc.NumDescriptors = 1;
    dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
    dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    dsvHeapDesc.NodeMask = 0;
    ThrowIfFailed(m_d3dDevice->CreateDescriptorHeap(
        &dsvHeapDesc, IID_PPV_ARGS(m_DsvHeap.GetAddressOf())));




7. 후면 버퍼의 크기를 설정하고, 후면 버퍼에  대한 렌더 대상 뷰(Render target view)를 생성한다.

DirectX의 경우는 자원 자체를 파이프라인 단계에 직접 묶지 않는다. 대신 반드시 자원에 대한 뷰(서술자)를 생성해서 이 뷰를 파이프라인 단계에 묶어야 한다.
특히 후면 버퍼를 파이프라인의 출력 병합기(Output Merger) 단계에 묶으려면(이게 되야 Direct3D가 장면을 후면 버퍼에 렌더링 할 수 있다.) 후면 버퍼에 대한 렌더 타킷 뷰를 생성해야 한다.

먼저 Swap chain에 저장된 버퍼 자원을 얻어야 한다. 이를 위해서는 IDXGISwapChain::GetBuffer() 메서드를 사용한다.
이 메서드를 호출해서 해단 후면 버퍼의 COM을 참조하면 참조 횟수가 올라가기 때문에 반드시 사용 후 해제를 해야 한다.
렌더 타깃 뷰를 생성할 때는 ID3D12Device::CreateRenderTargetView() 메서드를 사용해서 생성을 한다. 

코드로 보면 다음과 같이 사용한다.

   

    // Render targer view 서술자 힙을 가져와서 핸들에 저장한다.
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(m_RtvHeap->GetCPUDescriptorHandleForHeapStart());
    for (UINT i = 0; i < SwapChainBufferCount; i++)
    {
        // Swap chain의 i번째 버퍼를 가져온다.
        ThrowIfFailed(m_SwapChain->GetBuffer(i, IID_PPV_ARGS(&m_SwapChainBuffer[i])));
        // 그 버퍼에 대한 Render Target View를 생성한다.
        m_d3dDevice->CreateRenderTargetView(m_SwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
        // 힙의 다음 항목으로 넘어간다.
        rtvHeapHandle.Offset(1, m_RtvDescriptorSize);
    }



8. 깊이/스텐실 버퍼를 생성하고 이와 연관된 깊이/스텐실 뷰를 생성한다.

이전에 이야기 했드시 깊이 버퍼는 가장 가까운 물체의 깊이 정보(스텐실을 사용하는 경우 이 정보도) 저장하는 2차원 텍스처이다.
텍스처는 GPU 자원이기 때문에 이에 대한 서술이 필요하다. 이 때 사용하는 자료형은 D3D12_RESOURCE_DESC이다.
그리고 이 서술자를 채운 뒤에는 ID3D12Device::CreateCommittedResource()를 이용해서 깊이/스텐실 버퍼를 생성한다.
생성 예제 코드는 다음과 같다.

    // depth/stencil 버퍼와 뷰를 생성한다.
    D3D12_RESOURCE_DESC depthStencilDesc;
    depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
    depthStencilDesc.Alignment = 0;
    depthStencilDesc.Width = m_tRS.nWidth;
    depthStencilDesc.Height = m_tRS.nHeight;
    depthStencilDesc.DepthOrArraySize = 1;
    depthStencilDesc.MipLevels = 1;
    depthStencilDesc.Format = m_DepthStencilFormat;
    depthStencilDesc.SampleDesc.Count = m_4xMsaaState ? 4 : 1;
    depthStencilDesc.SampleDesc.Quality = m_4xMsaaState ? (m_4xMsaaQuality - 1) : 0;
    depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
    depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

    // D3D12_CLEAR_VALUE는 특정 자원을 지우기 위해(즉 완전 초기화)서 
    // 이를 서술하기 위한 타입이다.
    D3D12_CLEAR_VALUE optClear;
    optClear.Format = m_DepthStencilFormat;
    optClear.DepthStencil.Depth = 1.0f;
    optClear.DepthStencil.Stencil = 0;
    ThrowIfFailed(m_d3dDevice->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
        D3D12_HEAP_FLAG_NONE,
        &depthStencilDesc,
        D3D12_RESOURCE_STATE_COMMON,
        &optClear,
        IID_PPV_ARGS(m_DepthStencilBuffer.GetAddressOf())));

    // 밉 레벨이 0인 전체 리소스 형식을 사용하여 서술자 생성
    m_d3dDevice->CreateDepthStencilView(m_DepthStencilBuffer.Get(), nullptr, DepthStencilView());

    // 밉 레벨이 0인 전체 리소스 형식을 사용하여 서술자 생성
    m_d3dDevice->CreateDepthStencilView(m_DepthStencilBuffer.Get(), nullptr, DepthStencilView());

    // 리소스가 초기화 된 상태에서 Depth 버퍼를 사용하도록 전환한다.
    // CD3DX12_RESOURCE_BARRIER는 GPU가 자원을 다 기록하지 않았거나
    // 기록조차 시작하지 않았을 때 이에 접근하는 것 즉 Resource Hazard를 방지한다.
    m_CommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_DepthStencilBuffer.Get(),
        D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_DEPTH_WRITE));



9. 뷰포트와 가위 판정용 사각형을 설정한다.

보통 3차원 장면을 화면 전체에 해당하는 후면 버퍼(전체 화면의 경우) 또는 창의 클라이언트 영역 전체에 해당하는 후면 버퍼 전체에 그리지만, 
필요하다면 3차원 장면을 후면 버퍼의 일부를 차지하는 직사각형 영역에만 그릴 수 있다.

장면을 그려 넣고자 하는 후면 버퍼의 부분 직사각형 영역을 뷰포트라 부른다.
그리고 이 뷰포트를 설명하기 위해 있는 구조체 자료형이 D3D12_VIEWPORT이다.

이 구조체는 다음과 같이 구성되어 있다.

typedef struct D3D12_VIEWPORT {
  FLOAT TopLeftX;
  FLOAT TopLeftY;
  FLOAT Width;
  FLOAT Height;
  FLOAT MinDepth;
  FLOAT MaxDepth;
} D3D12_VIEWPORT;



이 때 주의해야 할 점은 Depth는 0.0~1.0으로 한정되어 있다.
작성자가 작성한 엔진에는 다음과 같이 되어있다.

    m_ScreenViewport.TopLeftX = 0;
    m_ScreenViewport.TopLeftY = 0;
    m_ScreenViewport.Width = static_cast<float>(m_tRS.nWidth);
    m_ScreenViewport.Height = static_cast<float>(m_tRS.nHeight);
    m_ScreenViewport.MinDepth = 0.0f;
    m_ScreenViewport.MaxDepth = 1.0f;



그리고 이 때 이 뷰포트에 특정 픽셀들을 선별(Culling 컬링이라 한다.)하는 용도로 가위 직사각형(Scissor Rectangle)이라 한다.
이 가위 직사각형을 정의하면 렌더링 시 이 영역 밖에 있는 픽셀들은 후면 버퍼에 래스터화(실제 그려지도록 픽셀화하는 것)되지 않는다.

예를 들어 다른 모든 것을 가리는 직사각형 UI가 특정 부분에 있다고 가정하자. 그 부분에 있는 3차원 상의 픽셀들은 짜피 UI가 가리기 때문에 처리될 필요가 없는 것이다.
이러한 자료는 윈도우에서 제공하는 RECT 구조체를 이용하며 작성한다. 그리고 이는 커맨드 리스트에 넘겨주면 된다.

    m_ScissorRect = { 0, 0, static_cast<long>(m_tRS.nWidth), static_cast<long>(m_tRS.nHeight) };
    m_CommandList->RSSetScissorRects(1, &m_ScissorRect);



이 때 RSSetScissorRects는 첫번째 매개변수로 가위 직사각형의 갯수, 그리고 두번째 매개변수는 직사각형 구조체들의 배열을 가리키는 포인터를 준다.

 

'DirectX' 카테고리의 다른 글

[DirectX 12] 렌더링 파이프라인  (0) 2022.02.15
[DirectX 12] 명령 대기열  (0) 2022.01.06
[DirectX 12] DirectX 기초 지식  (0) 2022.01.04