If all you care about is the code (i.e. you're a copy and paste coder), please feel free to skip to the end where the full source is posted.
Introduction
DirectWrite is a Microsoft technology for rendering text and glyphs, as a replacement for GDI. Direct2D is a hardware accelerated 2D rendering technology which can be used in conjunction with DirectWrite to render text to the screen. The combination of the two provides a very powerful mechanism for rendering properly formatted text with minimal effort. Plugging it into your 3D application (game, level editor, etc), gives you a very powerful set of tools for producing text (and other 2d graphics) that can be rendered to a texture and presented in your game on virtual computer screens, projective textures, etc.
For the purposes of this snippet we will be rendering a basic triangle to the screen along with some text that will be drawn over it in a nice lime green.
Initializing Direct3D11
When you're initialing Direct3D11 with the idea of supporting Direct2D in mind you need to be sure to indicate during the device creation that you desire to support the surface formats Direct2D uses. Specifically, Direct2D requires BGRA support as that is the same format GDI uses. We can indicate this to Direct3D11 during device creation by passing in the D3D11_CREATE_DEVICE_BGRA_SUPPORT flag.
Other than that the device creation is quite straightforward:
auto result = D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, // BGRA Support is necessary for D2D functionality. D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG, // D2D works with all of our feature levels (10.0 - 11.1), so we don't actually care which oen we get. featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, &featureLevel, &m_deviceContext);
.Getting Going With Direct2D
Direct2D is not capable of directly talking to a Direct3D11 texture, instead you need to use a DXGI Surface. Thankfully, all D3D textures (since 10) are DXGI surfaces, thus we can simply QueryInterface for the an IDXGISurface on the appropriate texture, or in the case of the back buffer (as in this sample), we simply query for the IDXGISurface from the swap chain.
CComPtr backBufferSurface;// Get a DXGI surface for D2D use.auto result = m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBufferSurface));if (FAILED(result)) { std::cout << "Failed to get DXGI surface for back buffer." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result;}// DXGI_FORMAT_UNKNOWN will cause it to use the same format as the back buffer (R8G8B8A8_UNORM)auto d2dRTProps = D2D1::RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED), dpiX, dpiY);// Wraps up our DXGI surface in a D2D render target.result = m_d2dFactory->CreateDxgiSurfaceRenderTarget(backBufferSurface, &d2dRTProps, &m_d2dRenderTarget);if (FAILED(result)) { std::cout << "Failed to create D2D DXGI Render Target." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result;}
At this point, with a Direct2D render target in our hands we're ready to do pretty much anything Direct2D can do, except render text. We can, however, create brushes, draw shapes, etc.DirectWrite
DirectWrite is not specifically a standalone API. It works in conjunction with other APIs such as Direct2D to properly format text and glyphs for display. It has a great many tools, including the ability to build text layout objects which describe text that has multiple formatting characteristics, and then properly render that layout to the screen with such niceties as word wrapping and breaking (hyphenation), proper character spacing, Unicode handling, etc.
For us, and with such a simple sample in mind, we're going to do the bare minimum necessary to get text onto the screen. That calls for us to simply create a text format, which includes information about the font to use, font size, the weight and style, any stretching information, and the locale.
auto result = m_dwFactory->CreateTextFormat(L"Consolas", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 14.0f, L"", &m_dwFormat);if (FAILED(result)) { std::cout << "Failed to create DirectWrite text format." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result;}
.Rendering Text
At this point we're ready to start rendering to our back buffer. The question is, do we want our text to render infront of whatever is on the screen, or behind it? This is actually something you would have to determine on a case by case basis depending on what the text actually is (for instance, if it's on the screen of a computer in the game, whatever you're holding might obscure it).
In our case we desire the text to be topmost, so we render our text as the last thing in the rendering chain before presenting.
{ m_deviceContext->ClearRenderTargetView(m_backBufferRTV, clearColor); // Draw our triangle first m_deviceContext->Draw(3, 0); // Then render our text over it. m_d2dRenderTarget->BeginDraw(); m_d2dRenderTarget->DrawText(m_text.c_str(), m_text.length(), m_dwFormat, D2D1::RectF(0, 0, 512, 512), m_d2dSolidBrush); m_d2dRenderTarget->EndDraw(); m_swapChain->Present(0, 0);}
.Full Sample
#include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "d3d11.lib")#pragma comment(lib, "d2d1.lib")#pragma comment(lib, "dwrite.lib")#pragma comment(lib, "d3dcompiler.lib")#ifdef UNICODEtypedef std::wstring tstring;typedef wchar_t tchar;#elsetypedef std::string tstring;typedef char tchar;#endifstruct Vertex { float position[4]; float color[4];};class MainWindow : public CWindowImpl {public: MainWindow() { RECT bounds = { 0, 0, 800, 600 }; AdjustWindowRect(&bounds, WS_OVERLAPPEDWINDOW, false); bounds = { 0, 0, bounds.right - bounds.left, bounds.bottom - bounds.top }; Create(nullptr, bounds, _T("D3DSample Window"), WS_OVERLAPPEDWINDOW); ShowWindow(SW_SHOW); // A traditional text. For a traditional time. m_text = _T("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); } bool ProcessMessages() { MSG msg; while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE) != 0) { if (msg.message == WM_QUIT) return false; TranslateMessage(&msg); DispatchMessage(&msg); } return true; } void Present() { static float clearColor[] = { 0, 0, 0, 1 }; { m_deviceContext->OMSetRenderTargets(1, &m_backBufferRTV.p, nullptr); m_deviceContext->IASetInputLayout(m_inputLayout); m_deviceContext->VSSetShader(m_vertexShader, nullptr, 0); m_deviceContext->PSSetShader(m_pixelShader, nullptr, 0); m_deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); size_t stride = sizeof(Vertex); size_t offsets = 0; m_deviceContext->IASetVertexBuffers(0, 1, &m_vertexBuffer.p, &stride, &offsets); } { m_deviceContext->ClearRenderTargetView(m_backBufferRTV, clearColor); // Draw our triangle first m_deviceContext->Draw(3, 0); // Then render our text over it. m_d2dRenderTarget->BeginDraw(); m_d2dRenderTarget->DrawText(m_text.c_str(), m_text.length(), m_dwFormat, D2D1::RectF(0, 0, 512, 512), m_d2dSolidBrush); m_d2dRenderTarget->EndDraw(); m_swapChain->Present(0, 0); } }public: BEGIN_MSG_MAP(MainWindow) MESSAGE_HANDLER(WM_DESTROY, [](unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { PostQuitMessage(0); return 0; }); MESSAGE_HANDLER(WM_SIZE, OnSize); MESSAGE_HANDLER(WM_CREATE, OnCreate); END_MSG_MAP()private: HRESULT CreateD3DVertexAndShaders() { // Hard coded shaders, not a great idea, but works for the sample. std::string vertexShader = "struct VS_IN { float4 pos : POSITION; float4 col : COLOR; }; struct PS_IN { float4 pos : SV_POSITION; float4 col : COLOR; }; PS_IN main( VS_IN input ) { PS_IN output = (PS_IN)0; output.pos = input.pos; output.col = input.col; return output; }"; std::string pixelShader = "struct VS_IN { float4 pos : POSITION; float4 col : COLOR; }; struct PS_IN { float4 pos : SV_POSITION; float4 col : COLOR; }; float4 main( PS_IN input ) : SV_Target { return input.col; }"; // If compilation fails, we don't report the errors, just that it failed. CComPtr vsBlob; CComPtr vsError; auto result = D3DCompile(vertexShader.c_str(), vertexShader.length() * sizeof(tchar), nullptr, nullptr, nullptr, "main", "vs_5_0", 0, 0, &vsBlob, &vsError); if (FAILED(result)) { std::cout << "Failed to compile vertex shader." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } // If compilation fails, we don't report the errors, just that it failed. CComPtr psBlob; CComPtr psError; result = D3DCompile(pixelShader.c_str(), pixelShader.length() * sizeof(tchar), nullptr, nullptr, nullptr, "main", "ps_5_0", 0, 0, &psBlob, &psError); if (FAILED(result)) { std::cout << "Failed to compile pixel shader." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } CComPtr inputLayoutBlob; result = D3DGetInputSignatureBlob(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), &inputLayoutBlob); if (FAILED(result)) { std::cout << "Failed to get input layout." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } // Hard coded triangle. Tis a silly idea, but works for the sample. Vertex vertices[] = { { 0.0, 0.5, 0.5, 1.0, 1.0, 0.0, 0.0, 1.0 }, { 0.5f, -0.5f, 0.5f, 1.0, 0.0, 1.0, 0.0, 1.0 }, { -0.5f, -0.5f, 0.5f, 1.0, 0.0, 0.0, 1.0, 1.0 } }; D3D11_BUFFER_DESC desc = { sizeof(vertices), D3D11_USAGE_DEFAULT, D3D11_BIND_VERTEX_BUFFER }; D3D11_SUBRESOURCE_DATA data = { vertices }; result = m_device->CreateBuffer(&desc, &data, &m_vertexBuffer); if (FAILED(result)) { std::cout << "Failed to create vertex buffer." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } D3D11_INPUT_ELEMENT_DESC inputElementDesc[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0 }, { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 16 } }; result = m_device->CreateInputLayout(inputElementDesc, sizeof(inputElementDesc) / sizeof(D3D11_INPUT_ELEMENT_DESC), inputLayoutBlob->GetBufferPointer(), inputLayoutBlob->GetBufferSize(), &m_inputLayout); if (FAILED(result)) { std::cout << "Failed to create input layout." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } result = m_device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &m_vertexShader); if (FAILED(result)) { std::cout << "Failed to create vertex shader." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } result = m_device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &m_pixelShader); if (FAILED(result)) { std::cout << "Failed to create pixel shader." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } return S_OK; } HRESULT CreateD3DResources() { D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, }; // We only want to draw to the portion of the window that is the client rect. // This will also work for dialog / borderless windows. RECT clientRect; GetClientRect(&clientRect); DXGI_SWAP_CHAIN_DESC swapChainDesc = { { clientRect.right, clientRect.bottom, { 60, 1 }, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED, DXGI_MODE_SCALING_UNSPECIFIED }, { 1, 0 }, DXGI_USAGE_BACK_BUFFER | DXGI_USAGE_RENDER_TARGET_OUTPUT, 1, m_hWnd, true, DXGI_SWAP_EFFECT_DISCARD, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH }; // At the moment we don't actually care about what feature level we got back, so we don't keep this around just yet. D3D_FEATURE_LEVEL featureLevel; auto result = D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, // BGRA Support is necessary for D2D functionality. D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG, // D2D works with all of our feature levels, so we don't actually care which oen we get. featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, &featureLevel, &m_deviceContext ); if (FAILED(result)) { std::cout << "Failed to create D3D device and DXGI swap chain." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } // And lets create our D2D factory and DWrite factory at this point as well, that way if any of them fail we'll fail out completely. auto options = D2D1_FACTORY_OPTIONS(); options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; result = D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, options, &m_d2dFactory); if (FAILED(result)) { std::cout << "Failed to create multithreaded D2D factory." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } result = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(&m_dwFactory)); if (FAILED(result)) { std::cout << "Failed to create DirectWrite Factory." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } return S_OK; } HRESULT CreateBackBufferTarget() { CComPtr backBuffer; // Get a pointer to our back buffer texture. auto result = m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)); if (FAILED(result)) { std::cout << "Failed to get back buffer." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } // We acquire a render target view to the entire surface (no parameters), with nothing special about it. result = m_device->CreateRenderTargetView(backBuffer, nullptr, &m_backBufferRTV); if (FAILED(result)) { std::cout << "Failed to create render target view for back buffer." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } return S_OK; } HRESULT CreateD2DResources() { CComPtr backBufferSurface; // Get a DXGI surface for D2D use. auto result = m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBufferSurface)); if (FAILED(result)) { std::cout << "Failed to get DXGI surface for back buffer." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } // Proper DPI support is very important. Most applications do stupid things like hard coding this, which is why you, // can't use proper DPI on most monitors in Windows yet. float dpiX; float dpiY; m_d2dFactory->GetDesktopDpi(&dpiX, &dpiY); // DXGI_FORMAT_UNKNOWN will cause it to use the same format as the back buffer (R8G8B8A8_UNORM) auto d2dRTProps = D2D1::RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED), dpiX, dpiY); // Wraps up our DXGI surface in a D2D render target. result = m_d2dFactory->CreateDxgiSurfaceRenderTarget(backBufferSurface, &d2dRTProps, &m_d2dRenderTarget); if (FAILED(result)) { std::cout << "Failed to create D2D DXGI Render Target." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } // This is the brush we will be using to render our text, it does not need to be a solid color, // we could use any brush we wanted. In this case we chose a nice solid red brush. result = m_d2dRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LimeGreen), &m_d2dSolidBrush); if (FAILED(result)) { std::cout << "Failed to create solid color brush." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } return S_OK; } HRESULT CreateDWriteResources() { auto result = m_dwFactory->CreateTextFormat(L"Consolas", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 14.0f, L"", &m_dwFormat); if (FAILED(result)) { std::cout << "Failed to create DirectWrite text format." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return result; } return S_OK; }private: LRESULT OnSize(unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { // We need to release everything that may be holding a reference to the back buffer. // This includes D2D interfaces as well, as they hold a reference to the DXGI surface. m_backBufferRTV.Release(); m_d2dRenderTarget.Release(); m_d2dSolidBrush.Release(); // And we make sure that we do not have any render tarvets bound either, which could // also be holding references to the back buffer. m_deviceContext->ClearState(); int width = LOWORD(lParam); int height = HIWORD(lParam); auto result = m_swapChain->ResizeBuffers(1, width, height, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH); if (FAILED(result)) { std::cout << "Failed to resize swap chain." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return -1; } // We need to recreate those resources we disposed of above, including our D2D interfaces if (FAILED(CreateBackBufferTarget())) return -1; if (FAILED(CreateD2DResources())) { return -1; } D3D11_VIEWPORT viewport = { 0.0f, 0.0f, static_cast(width), static_cast(height), 0.0f, 1.0f }; // We setup our viewport here as the size of the viewport is known at this point, WM_SIZE will be sent after a WM_CREATE. m_deviceContext->RSSetViewports(1, &viewport); return 0; } LRESULT OnCreate(unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { if (FAILED(CreateD3DResources())) return -1; if (FAILED(CreateBackBufferTarget())) return -1; if (FAILED(CreateD3DVertexAndShaders())) return -1; if (FAILED(CreateD2DResources())) return -1; if (FAILED(CreateDWriteResources())) return -1; return 0; }private: CComPtr m_swapChain; CComPtr m_device; CComPtr m_deviceContext; CComPtr m_backBufferRTV; CComPtr m_vertexBuffer; CComPtr m_inputLayout; CComPtr m_vertexShader; CComPtr m_pixelShader; CComPtr m_d2dFactory; CComPtr m_d2dRenderTarget; CComPtr m_d2dSolidBrush; CComPtr m_dwFactory; CComPtr m_dwLayout; CComPtr m_dwFormat; tstring m_text;};int main() { MainWindow window; float clearColor[] = { 0, 0, 0, 0 }; while (true) { if (!window.ProcessMessages()) break; window.Present(); } return 0;}
It would be great if there was something similar for opengl, the Kronos group should consider it