D3D11on12, Direct2D, DirectWrite를 이용한 텍스트 출력
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' 함수의 파라미터로 크기 행렬과 이동 행렬을 곱한 행렬을 넘겨주고 있다.
마음에 드는 결과가 나온 것 같다.
체력도 총알처럼 체력이 바뀔 때 위와 같은 애니메이션이 되도록 만들었다.