Programming/DirectX12

D3D11on12, Direct2D, DirectWrite를 이용한 텍스트 출력

LaivY 2022. 4. 22. 01:22

Graduation Project

UI를 위해 화면에 텍스트를 출력하는 것은 필수이다.

텍스트를 출력하는 방법 중 미리 만들어둔 텍스쳐를 이용하는 방법도 있지만, 해당 방법은 다른 폰트 종류와 크기를 사용하기 위해서는 텍스쳐도 따로 만들어야하고 한글은 출력할 수 없다는 단점 때문에 D3D11on12, Direct2D, DirectWrite를 이용해서 텍스트를 출력하게 됬다.

 

 

 

 

 

void GameFramework::LoadPipeline()
{
    // 팩토리 생성
    CreateFactory();

    // 디바이스 생성(팩토리 필요)
    CreateDevice();

    // 명령큐 생성(디바이스 필요)
    CreateCommandQueue();

    // 11on12 디바이스 생성(명령큐 필요)
    CreateD3D11On12Device();

    // D2D, DWrite 생성(11on12 디바이스 필요)
    CreateD2DDevice();

    // 스왑체인 생성(팩토리, 명령큐 필요)
    CreateSwapChain();

    // 렌더타겟뷰, 깊이스텐실뷰의 서술자힙 생성(디바이스 필요)
    CreateRtvDsvDescriptorHeap();

    // 렌더타겟뷰 생성(디바이스 필요)
    CreateRenderTargetView();

    // 깊이스텐실뷰 생성(디바이스 필요)
    CreateDepthStencilView();

    // 루트시그니쳐 생성(디바이스 필요)
    CreateRootSignature();
    ...
}

게임프레임워크가 생성될 때 해야할 일이 기존보다 늘었다.

'CreateD3D11on12Device`, 'CreateD2DDevice' 함수가 추가되었고 'CreateRenderTargetView' 함수에 변경 사항이 있다.

 

 

 

 

 

void GameFramework::CreateD3D11On12Device()
{
    // Create an 11 device wrapped around the 12 device and share 12's command queue.
    ComPtr<ID3D11Device> d3d11Device;
    DX::ThrowIfFailed(D3D11On12CreateDevice(
        m_device.Get(),
        D3D11_CREATE_DEVICE_BGRA_SUPPORT,
        nullptr,
        0,
        reinterpret_cast<IUnknown**>(m_commandQueue.GetAddressOf()),
        1,
        0,
        &d3d11Device,
        &m_d3d11DeviceContext,
        nullptr
    ));

    // Query the 11On12 device from the 11 device.
    DX::ThrowIfFailed(d3d11Device.As(&m_d3d11On12Device));
}

D3D11on12디바이스는 Direct12 디바이스와 같은 명령큐를 사용한다.

따라서 이 함수 호출 전에 명령큐를 만들어놓아야한다.

 

 

 

 

 

void GameFramework::CreateD2DDevice()
{
    // Create D2D/DWrite components.
    D2D1_FACTORY_OPTIONS d2dFactoryOptions{};
    D2D1_DEVICE_CONTEXT_OPTIONS deviceOptions = D2D1_DEVICE_CONTEXT_OPTIONS_NONE;
    DX::ThrowIfFailed(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(ID2D1Factory3), &d2dFactoryOptions, &m_d2dFactory));
    ComPtr<IDXGIDevice> dxgiDevice;
    DX::ThrowIfFailed(m_d3d11On12Device.As(&dxgiDevice));
    DX::ThrowIfFailed(m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice));
    DX::ThrowIfFailed(m_d2dDevice->CreateDeviceContext(deviceOptions, &m_d2dDeviceContext));
    DX::ThrowIfFailed(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), &m_dWriteFactory));
}

D3D11on12 디바이스를 이용해서 D2D 팩토리, 디바이스와 DirectWrite 팩토리를 만든다.

 

 

 

 

 

void GameFramework::CreateRenderTargetView()
{
    // Query the desktop's dpi settings, which will be used to create D2D's render targets.
    float dpiX;
    float dpiY;
#pragma warning(push)
#pragma warning(disable : 4996) // GetDesktopDpi is deprecated.
    m_d2dFactory->GetDesktopDpi(&dpiX, &dpiY);
#pragma warning(pop)

    D2D1_BITMAP_PROPERTIES1 bitmapProperties = D2D1::BitmapProperties1(
        D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
        D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED),
        dpiX,
        dpiY
    );

    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle{ m_rtvHeap->GetCPUDescriptorHandleForHeapStart() };
    for (UINT i = 0; i < FrameCount; ++i)
    {
        m_swapChain->GetBuffer(i, IID_PPV_ARGS(&m_renderTargets[i]));
        m_device->CreateRenderTargetView(m_renderTargets[i].Get(), NULL, rtvHandle);

        // Create a wrapped 11On12 resource of this back buffer. Since we are 
        // rendering all D3D12 content first and then all D2D content, we specify 
        // the In resource state as RENDER_TARGET - because D3D12 will have last 
        // used it in this state - and the Out resource state as PRESENT. When 
        // ReleaseWrappedResources() is called on the 11On12 device, the resource 
        // will be transitioned to the PRESENT state.
        D3D11_RESOURCE_FLAGS d3d11Flags = { D3D11_BIND_RENDER_TARGET };
        DX::ThrowIfFailed(m_d3d11On12Device->CreateWrappedResource(
            m_renderTargets[i].Get(),
            &d3d11Flags,
            D3D12_RESOURCE_STATE_RENDER_TARGET,
            D3D12_RESOURCE_STATE_PRESENT,
            IID_PPV_ARGS(&m_wrappedBackBuffers[i])
        ));

        // Create a render target for D2D to draw directly to this back buffer.
        ComPtr<IDXGISurface> surface;
        DX::ThrowIfFailed(m_wrappedBackBuffers[i].As(&surface));
        DX::ThrowIfFailed(m_d2dDeviceContext->CreateBitmapFromDxgiSurface(
            surface.Get(),
            &bitmapProperties,
            &m_d2dRenderTargets[i]
        ));

        rtvHandle.Offset(m_rtvDescriptorSize);

        // 명령할당자 생성
        DX::ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocators[i])));
    }
}

렌더 타겟 뷰를 만드는 함수이다.

렌더링할 때 DX12를 이용한 객체들을 모두 렌더링하고, 그 뒤에 2D에 텍스트를 렌더링할 것이다.

'wrappedResource'는 DX11에서 만든 것이지만 DX12에서도 사용할 수 있는 리소스이다.

그 다음으로는 Direct2D가 렌더링할 비트맵을 만들어준다.

 

 

 

 

 

void GameFramework::OnRender()
{
    PopulateCommandList();
    ID3D12CommandList* ppCommandList[] = { m_commandList.Get() };
    m_commandQueue->ExecuteCommandLists(_countof(ppCommandList), ppCommandList);
    Render2D();
    DX::ThrowIfFailed(m_swapChain->Present(1, 0));
    WaitForPreviousFrame();
}

이제 렌더링할 때  'Render2D' 부분이 추가됬다.

'Render2D' 에서 텍스트들을 렌더링한다.

 

 

 

 

 

void GameFramework::Render2D() const
{
    // Acquire our wrapped render target resource for the current back buffer.
    m_d3d11On12Device->AcquireWrappedResources(m_wrappedBackBuffers[m_frameIndex].GetAddressOf(), 1);

    // Render text directly to the back buffer.
    m_d2dDeviceContext->SetTarget(m_d2dRenderTargets[m_frameIndex].Get());
    m_d2dDeviceContext->BeginDraw();

    if (m_scene) m_scene->Render2D(m_d2dDeviceContext);

    DX::ThrowIfFailed(m_d2dDeviceContext->EndDraw());

    // Release our wrapped render target resource. Releasing 
    // transitions the back buffer resource to the state specified
    // as the OutState when the wrapped resource was created.
    m_d3d11On12Device->ReleaseWrappedResources(m_wrappedBackBuffers[m_frameIndex].GetAddressOf(), 1);

    // Flush to submit the 11 command list to the shared command queue.
    m_d3d11DeviceContext->Flush();
}

위 함수에서 볼 수 있듯이 2D 렌더링을 위해 해줘야할 세팅들이 있다.

기존 렌더링도 'Scene::Render' 에서 하듯이 'Scene::Render2D' 함수에서 텍스트 객체들을 렌더링한다.

 

 

 

 

 

void TextObject::Render(const ComPtr<ID2D1DeviceContext2>& device)
{
    device->SetTransform(D2D1::Matrix3x2F::Translation(m_position.x, m_position.y));
    device->DrawText(
        m_text.c_str(),
        static_cast<UINT32>(m_text.size()),
        s_formats[m_format].Get(),
        &m_rect,
        s_brushes[m_brush].Get()
    );
}

텍스트를 렌더링하는 함수이다. 'SetTransform' 을 통해 크기, 회전, 이동시킬 수 있다.

'class TextObject' 를 상속받는 다른 클래스들의 렌더 함수는 다르게 생겼다.

 

 

 

 

 

void BulletTextObject::Render(const ComPtr<ID2D1DeviceContext2>& device)
{
    ...

    // 현재 총알 텍스트 애니메이션
    m_text = to_wstring(m_bulletCount);
    D2D1::Matrix3x2F matrix{};
    matrix.SetProduct(D2D1::Matrix3x2F::Scale(m_scale, m_scale, { m_rect.right, m_rect.bottom }), D2D1::Matrix3x2F::Translation(m_position.x - maxBulletTextWidth - slashTextWidth, m_position.y));
    device->SetTransform(matrix);
    device->DrawText(m_text.c_str(), static_cast<UINT32>(m_text.size()), s_formats["BULLETCOUNT"].Get(), &m_rect, m_bulletCount == 0 ? s_brushes["RED"].Get() : s_brushes["BLACK"].Get());
}

총알 개수를 출력하는 텍스트 객체이다. 총알 개수가 바뀔 때마다 잠깐 커졌다가 작아지는 효과를 주고싶었다.

그래서 'SetTransform' 함수의 파라미터로 크기 행렬과 이동 행렬을 곱한 행렬을 넘겨주고 있다.

 

 

 

 

 

결과

마음에 드는 결과가 나온 것 같다.

체력도 총알처럼 체력이 바뀔 때 위와 같은 애니메이션이 되도록 만들었다.