Words can be like X-rays if you use them properly — they’ll go through anything. You read and you’re pierced.
– Aldous Huxley
Most games need some way to render high-quality text to the screen. DirectWrite provides just that and when used in
combination with Direct2D, DirectWrite is hardware accelerated, thus fast and robust.
To use DirectWrite in our project, we first initialize Direct2D, tell Direct3D and Direct2D to play together nicely, and
we then set up DirectWrite to output text to our back buffer. Obviously, having access to Direct2D will be useful later
as well, as we will need high-performance 2D and text rendering for menus, the user-interface and Heads-up Displays.
Setting up Direct2D and DirectWrite to work together with Direct3D basically takes seven little steps, and most of these
tasks are similar to the initialization of Direct3D:
Create the Direct2D and the DirectWrite factories.
Create the Direct2D device and its context.
Set up Direct2D to render to the same buffer as Direct3D.
Resize the Direct2D render targets when the game window is resized.
Set up brushes and text formats.
Set up text layouts.
Print!
Creating the Factories
The first thing to do is to create factories for Direct2D and DirectWrite.
To creat the DirectWrite factory, we call upon the
DWriteCreateFactory function:
The first parameter specifies whether the factory object will be shared or isolated. We will use
DWRITE_FACTORY_TYPE_SHARED to indicate that we intend to use the DirectWrite factory as a shared factory, which allows
the reuse of cached font data and generally leads to better performance.
REFIID iid
Directly from the MSDN: A GUID value that identifies the DirectWrite factory interface, such as __uuidof(
IDWriteFactory).
IUnknwon **factory
After the function returns, this parameter contains the address to a pointer to the newly created factory.
And here is the actual (isolated) C++-code to create the DirectWrite factory:
Creating a Direct2D factory, is the job of
the D2D1CreateFactory function:
The first parameter is the threading model of the factory and the resources it creates. We will set this to
D2D1_FACTORY_TYPE_MULTI_THREADED, enabling safe access to the Direct2D factory from multiple threads, which will be very
useful later on.
REFIID riid
The second parameter is a reference to the IID of ID2D1Factory that is obtained by using __uuidof(ID2D1Factory).
The third parameter specifies
the level of detail provided to the
debugging layer. In release mode, we set this to D2D1_DEBUG_LEVEL_NONE, telling Direct2D to not produce any debugging
output. In debug mode, however, it is useful to set this to D2D1_DEBUG_LEVEL_INFORMATION, telling Direct2D to send
error messages, warnings, and additional diagnostic information.
void **ppIFactory
Once the function returns, the fourth parameter contains the address to a pointer to the newly creasted factory.
And here is the C++ code to create the Direct2D factory:
The device and its context
After having created the factory, we can use it to create a Direct2D device and then use the device to create a Direct2D
device context. To create these Direct2D objects, we must first obtain the DXGI device associated to the Direct3D device
of the application.
Retrieving the DXGI device is as easy as follows:
The QueryInterface function takes as
first parameter an identifier of the interface being requested and returns a pointer to that interface via its second
parameter.
To create the actual Direct2D device, a simple call to the
D2D1CreateDevice function is
sufficient:
The first parameter of the D2D1CreateDevice function is a pointer to the DXGI device the desired Direct2D should be
associated with. The function also has an optional second parameter, which we will not set, telling DirectX that we wish
for the Direct2D device to inherit its threading mode from the DXGI device. After the function returns, the actual
Direct2D device will be stored in the third parameter.
flag tells Direct2D to distribute all of its rendering work across multiple threads. Once the function returns, the
actual device context is stored in the second parameter.
Selecting a render target
Now that the Direct2D device and its context are created, it is time to tell Direct3D how to behave around with Direct2D
around as well and to allow the latter to render into the same back buffer surfaces.
As always, to create a rendering surface, or a bitmap, for Direct2D, a structure description must be filled out, this
time a D2D1_BITMAP_PROPERTIES1
structure:
The first parameter defines the bitmap’s pixel format
and alpha mode. We set the pixel
format to DXGI_FORMAT_B8G8R8A8_UNORM, as that is the format we used for our Direct3D back buffer, and we set the alpha
mode to D2D1_ALPHA_MODE_IGNORE. We will talk more about the alpha mode in later tutorials.
FLOAT dpiX and FLOAT dpiY
The horizontal and vertical dots per inch, or dpi, of the bitmap.
These options specify how a bitmap can be used. We will use D2D1_BITMAP_OPTIONS_TARGET, which specifies that the
bitmap can be used as a device context target and 2D1_BITMAP_OPTIONS_CANNOT_DRAW, which specifies that the bitmap
cannot be used as an input.
This represents a colour context that can be used with an ID2D1Bitmap1 object. We won’t use this, so we can set this to
be a nullptr.
To actually create a bitmap with the desired structure, the dxgi version of the backbuffer is necessary. To create the
render bitmap, we use the
ID2D1DeviceContext::CreateBitmapFromDxgiSurface
method:
IDXGISurface *surface
The first parameter is the DXGI surface from which the bitmap can be created, it must have been created from the same
Direct3D device that the Direct2D device context is associated with. As seen in the last tutorial, the GetBuffer method
can be used to retrieve the back buffer.
It takes two parameters. The first parameter specifies the colour to create in red, green, blue, and alpha format. The
second parameter contains the address of a pointer to the newly created brush. This parameter is passed uninitialized.
This is an array of characters that contains the name of the font family to use, for example, for console output we
would choose an easily readible font, such as L”Lucida Console”.
This value specifies the density, or the font weight, of a typeface, in terms of the lightness or heaviness of the
strokes. We will use DWRITE_FONT_WEIGHT_LIGHT.
This parameter represents the amount of stretching compared to a font’s normal aspect ratio. Lower values indicate
narrower widths; higher values indicate wider widths. We will use DWRITE_FONT_STRETCH_NORMAL.
This parameter is an array of characters that contains the locale name we want to use. Since we are trying to
communicate using British English, we will set this to L”en-GB”.
IDWriteTextFormat **textFormat
Once the function returns, this parameter contains the address of a pointer to the newly created text format object.
And here is the actual C++-code to set up the text format:
Text Layouts
Before actually rendering text to a bitmap, a text layout must be created. This is done using the
IWriteFactory::CreateTextLayout
function:
This function is rather easy to use, it takes a string of the text to render, the desired text format together with the
dimension of the desired output buffer, and produces an object that represents the fully analyzed and formatted text.
The fourth parameter specifies whether text snapping is suppressed or clipping to the layout rectangle is enabled. We
will leave this at D2D1_DRAW_TEXT_OPTIONS_NONE for now.
And finally, here is the code to draw the FPS information to the screen:
Putting It All Together
To manage Direct2D, a new class, called Direct2D was created:
And here is its implementation according to what we learned in this tutorial:
The DirectXApp class was changed to create text containing information about the frames per second:
The output of the FPS information can be toggled via the F1 key:
The drawing takes place in the render function of the derived game class:
I also cleaned up a bit and eliminated some warning messages.
There are a few Sol messages that we can’t do much about, so we simply ignore them:
Since at the time I wrote this, Visual Studio 15.3 wasn’t out yet, thus there was no support for C++17 and if
constexpr, I also finally suppressed the warning from the logger, it was really starting to annoy me:
Here is a screenshot of the game window with FPS information drawn by DirectWrite.
And here is the log file:
I don’t know about you, but I am starting to feel quite excited — none of this COM or DirectX stuff is alien or too
difficult any more. In the next tutorial, we will finally draw triangles on the screen!
References
Tricks of the Windows Game Programming Gurus, by André LaMothe