DX11 Without DirectX SDK-08 Direct2D and Direct3D interoperability and using DWrite to display text

Keywords: C++ github SDK

Preface

In DX11, to display text can be said to be a relatively troublesome thing. DX9 interfaces such as Id3dXFont for displaying text have been discarded. At present, two effective methods of displaying text are as follows:

  1. Bitmap / vector map containing text is used, and then rectangular areas of corresponding text are obtained in a certain way, and finally rendered.

  2. By realizing the interoperability between Direct2D and Direct3D, and then cooperating with DWrite to write text in the program.

For individuals, the first way is more cumbersome. For the second method, I consulted the MSDN document and made some attempts, and soon realized the text display. So next we will discuss the second method (not focusing on the other operations in Direct2D, such as bitmap and geometry rendering, which can be done in Direct3D).

Project source point here: https://github.com/MKXJun/DX11-Without-DirectX-SDK

Interoperability through DXGI

Beginning with Direct3D 10.1, Direct3D Runtime uses DXGI for resource management. DXGI Runtime provides the ability to share video memory graphics across processes, and can be used as the basis for other video memory-based runtime platforms. Direct2D uses DXGI to interact with Direct3D.

In order to realize the interoperability of Direct2D and Direct3D and display text, we need to go through the following preparatory steps:

  1. Add header files d2d1.h and dwrite.h in d3dApp.h, and add static libraries d2d1.lib and dwrite.lib
  2. Modify some configuration parameters when creating ID3D11Device and IDXGIS wapChain
  3. Create ID2D1Factory
  4. The interface class IDXGIS urface is acquired through IDXGIS wapChain and used to create ID2D1 RenderTarget for binding. In this way, the specific operation can be carried out through the rendering target.

Creation Property Modification of D3D11 Device and DXGI Switching Chain

Because Direct2D needs to support BGRA data format, the following parts need to be modified before creating D3D11 devices:

// Create the context of D3D devices and D3D devices
UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;  // Direct2D needs to support BGRA format
#if defined(DEBUG) || defined(_DEBUG)  
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

Then the DXGI format should be changed to DXGI_FORMAT_B8G8R8A8_UNORM when creating DXGI exchange chain:

// Detect the quality level supported by MSAA
md3dDevice->CheckMultisampleQualityLevels(
    DXGI_FORMAT_B8G8R8A8_UNORM, 4, &m4xMsaaQuality);    // Note here that DXGI_FORMAT_B8G8R8A8_UNORM
assert(m4xMsaaQuality > 0);
    
...

// If included, DX11.1 is supported.
if (dxgiFactory2 != nullptr)
{
    ...
    // Fill in various structures to describe switching chains
    DXGI_SWAP_CHAIN_DESC1 sd;
    ZeroMemory(&sd, sizeof(sd));
    ...
    sd.Format = DXGI_FORMAT_B8G8R8A8_UNORM;     // Note here that DXGI_FORMAT_B8G8R8A8_UNORM
    ...
}
else
{
    // Fill DXGI_SWAP_CHAIN_DESC to describe switching chains
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    ...
    sd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;  // Note here that DXGI_FORMAT_B8G8R8A8_UNORM
    ...
}

D2D1 CreateFactory Function -- Create D2D Factory Objects

Before creating a D2D rendering target, you need to create an ID2D1Factory object that can be used to create various resources:

template<class Factory>
HRESULT D2D1CreateFactory(
    D2D1_FACTORY_TYPE factoryType,  // [In] Enumeration Value
    Factory **factory               // [Out] Obtained Factory Objects
);

The creation operation is as follows:

HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, md2dFactory.GetAddressOf()));

Note that HR macros are used here, and md2dFactory is ComPtr < ID2D1Factory > type

ID2D1Factory:: CreateDxgiSurface RenderTarget method -- Create a DXGI surface rendering target

Now we are going to create the ID2D1 RenderTarget object.

Next, you need to change the size of each window and call the IDXGIS wapChain:: ReSizeBuffers method only. It is usually recommended to write after calling D3DApp::OnReSize in GameApp::OnReSize.

First, release the previously created D2D resources (if any), and obtain the IDXGIS urface interface of the backup buffer through the IDXGIS wapChain:: GetBuffer method:

md2dRenderTarget.Reset();

ComPtr<IDXGISurface> surface;
HR(mSwapChain->GetBuffer(0, __uuidof(IDXGISurface), reinterpret_cast<void**>(surface.GetAddressOf())));

Then fill in the D2D1_RENDER_TARGET_PROPERTIES structure property:

typedef struct D2D1_RENDER_TARGET_PROPERTIES
{
    D2D1_RENDER_TARGET_TYPE type;   // Rendering target type enumeration values
    D2D1_PIXEL_FORMAT pixelFormat;  
    FLOAT dpiX;                     // X-Direction Pixel Points per inch, set to 0.0f, using the default DPI
    FLOAT dpiY;                     // Set the number of pixels per inch in the Y direction to 0.0f using the default DPI
    D2D1_RENDER_TARGET_USAGE usage; // Rendering Target Use Enumeration Value
    D2D1_FEATURE_LEVEL minLevel;    // Minimum Characteristic Level of D2D

} D2D1_RENDER_TARGET_PROPERTIES;

typedef struct D2D1_PIXEL_FORMAT
{
    DXGI_FORMAT format;             // DXGI format
    D2D1_ALPHA_MODE alphaMode;      // Mixed mode

} D2D1_PIXEL_FORMAT;

You can borrow the D2D1:: RenderTarget Properties function to create, here using the default DPI:

D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties(
    D2D1_RENDER_TARGET_TYPE_DEFAULT,
    D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED));

Finally, the ID2D1Factory:: CreateDxgiSurface RenderTarget method is as follows:

HRESULT ID2D1Factory::CreateDxgiSurfaceRenderTarget(
    IDXGISurface *dxgiSurface,          // [In]DXGI Surface
    const D2D1_RENDER_TARGET_PROPERTIES *renderTargetProperties,    // [In]D2D Rendering Target Properties
    ID2D1RenderTarget **renderTarget    // D2D Rendering Target Obtained by [Out]
);

Specific calls are as follows:

HR(md2dFactory->CreateDxgiSurfaceRenderTarget(surface.Get(), &props, md2dRenderTarget.GetAddressOf()));

surface.Reset();

So far, Direct2D and Direct3D can interoperate through DXGI. With ID2D1 RenderTarget, you can create various types of color brushes and draw them. But because we need to draw text, DWrite is introduced below.

Use DWrite to display text

To display text, the following steps are required:

  1. Create IDWriteFactory object
  2. Creating IDWriteTextFormat Text Format Objects through DWrite Factory Objects
  3. Setting text format for text format objects
  4. Create color brushes through ID2D1 RenderTarget
  5. Text rendering after rendering the 3D part and before final rendering

DWriteCreateFactory Function -- Create DWriteFactory Objects

The prototype of the function is as follows:

HRESULT DWRITE_EXPORT DWriteCreateFactory(
    DWRITE_FACTORY_TYPE factoryType,    // Enumeration of [In] Factory Types
    const IID & iid,                    // [In] Interface ID
    IUnknown **factory                  // [Out] Get factory objects
    );

The following demonstrates the creation process:

HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory),
        reinterpret_cast<IUnknown**>(mdwriteFactory.GetAddressOf())));

IDWriteFactory::CreateTextFormat Method -- Creating Text Format Objects

HRESULT IDWriteFactory::CreateTextFormat(
    const WCHAR * fontFamilyName,           // [In] font family name
    IDWriteFontCollection * fontCollection, // [In] Usually nullptr is used to represent the use of system font sets 
    DWRITE_FONT_WEIGHT  fontWeight,         // [In] Enumeration value of font thickness
    DWRITE_FONT_STYLE  fontStyle,           // [In] font style enumeration values
    DWRITE_FONT_STRETCH  fontStretch,       // Enumeration value of font stretching degree
    FLOAT  fontSize,                        // [In] font size
    const WCHAR * localeName,               // [In] Area Name
    IDWriteTextFormat ** textFormat);       // Text format created by [Out]

The names of font series can be quoted in Chinese, such as L "Song Style" and L "Microsoft Yahei".

Font carefully look at personal preferences, using DWRITE_FONT_WEIGHT_NORMAL is almost right.

The font style is as follows:

enum style
DWRITE_FONT_STYLE_NORMAL default
DWRITE_FONT_STYLE_OBLIQUE Italics
DWRITE_FONT_STYLE_ITALIC Italy style

Font stretching is done with DWRITE_FONT_STRETCH_NORMAL

Font Size Recommendation: Feel it in advance in Word Documents

The default area name here is L"zh-cn"

Create a demonstration as follows:

HR(mdwriteFactory->CreateTextFormat(L"Song style", nullptr, DWRITE_FONT_WEIGHT_NORMAL,
        DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 20, L"zh-cn",
        mTextFormat.GetAddressOf()));

After creating the IDWriteTextFormat object, you can call a series of Get methods to get detailed information about the text format, or you can use a series of Set methods to set it up. There is no explanation here.

ID2D1 RenderTarget:: CreateSolidColorBrush method -- Create monochrome brush objects

Although the ID2D1 RenderTarget object provides a variety of brushes for creation, the most commonly used is to create ID2D1 SolidColorBrush monochrome brushes.

This method has been overloaded and only one of them is discussed.

HRESULT ID2D1RenderTarget::CreateSolidColorBrush(
    const D2D1_COLOR_F &color,  // [In] color
    ID2D1SolidColorBrush **solidColorBrush // [Out] Output Color Brush
);

The default value of Alpha is 1.0.

D2D1_COLOR_F is a structure containing r,g,b,a floating-point numbers, but there is actually another way to specify the color, that is, to use the constructor in its inheritance class D2D1::ColorF, and the D2D1::ColorF::Enum enumeration type to specify the color to be used, you can go inside to see, and not give all the color enumerations here.

The following demonstrates how to create a monochrome brush:

// Create fixed color brushes and text formats
HR(md2dRenderTarget->CreateSolidColorBrush(
    D2D1::ColorF(D2D1::ColorF::White),
    mColorBrush.GetAddressOf()))

D3DApp class, GameApp class changes and start text rendering

Take the above project as an example to modify it.

In the D3DApp class, the D3DApp::InitDirect2D method is added to create D2D factories and DWrite factories:

bool D3DApp::InitDirect2D()
{
    HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, md2dFactory.GetAddressOf()));
    HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory),
        reinterpret_cast<IUnknown**>(mdwriteFactory.GetAddressOf())));

    return true;
}

This method is called in D3DApp::Init.

The GameApp::OnReSize method has also been modified:

void GameApp::OnResize()
{
    assert(md2dFactory);
    assert(mdwriteFactory);
    // Release of D2D related resources
    mColorBrush.Reset();
    md2dRenderTarget.Reset();

    // Calling parent class methods
    D3DApp::OnResize();

    // Creating DXGI Surface Rendering Target for D2D
    ComPtr<IDXGISurface> surface;
    HR(mSwapChain->GetBuffer(0, __uuidof(IDXGISurface), reinterpret_cast<void**>(surface.GetAddressOf())));
    D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties(
        D2D1_RENDER_TARGET_TYPE_DEFAULT,
        D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED));
    HR(md2dFactory->CreateDxgiSurfaceRenderTarget(surface.Get(), &props, md2dRenderTarget.GetAddressOf()));

    surface.Reset();
    // Create fixed color brushes and text formats
    HR(md2dRenderTarget->CreateSolidColorBrush(
        D2D1::ColorF(D2D1::ColorF::White),
        mColorBrush.GetAddressOf()));
    HR(mdwriteFactory->CreateTextFormat(L"Song style", nullptr, DWRITE_FONT_WEIGHT_NORMAL,
        DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 20, L"zh-cn",
        mTextFormat.GetAddressOf()));
    
}

Here, D2D related resources need to be released before D3D related resources are released, and then the D2D rendering target is recreated after resetting the backup buffer in D3D. As for the follow-up related resources of D2D, they need to be recreated.

Finally, in GameApp::DrawScene method, drawing the 2D part needs to be done before rendering after the 3D part has been rendered.

First, you need to call the ID2D1 RenderTarget:: BeginDraw method to start D2D drawing. This method has no parameters.

When the drawing is completed, the ID2D1 RenderTarget:: EndDraw method is called to end the D2D drawing. The return value of this method is HRESULT. If there is a problem with the previous drawing, feedback will be given in EndDraw. It can be wrapped in HR macros.

ID2D1 RenderTarget:: DrawTextW Method -- Drawing Text

DrawText defines macros here:

#ifdef UNICODE
#define DrawText  DrawTextW
#else
#define DrawText  DrawTextA
#endif // !UNICODE

Our project can only use Unicode character sets (dxerr.h only allows that character set), so we will discuss the DrawTextW method directly.

The method has also been overloaded. Now we will discuss only one of them and use the default parameters:

void ID2D1RenderTarget::DrawTextW(
    WCHAR *string,                      // Text to be output by [In]
    UINT stringLength,                  // [In] text length can be obtained by using wcslen function or wstring::length method
    IDWriteTextFormat *textFormat,      // [In] text format
    const D2D1_RECT_F &layoutRect,      // [In] Layout Area
    ID2D1Brush *defaultForegroundBrush, // Foreground Brush for [In]
    D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE,
    DWRITE_MEASURING_MODE measuringMode = DWRITE_MEASURING_MODE_NATURAL);

The D2D1_RECT_F structure contains four members: left, top, right and bottom.

This paper presents the implementation of GameApp::DrawScene method Direct2D:

void GameApp::DrawScene()
{
    assert(md3dImmediateContext);
    assert(mSwapChain);

    // Draw the Direct3D part
    ...

    // Draw the Direct2D section
    md2dRenderTarget->BeginDraw();
    static const WCHAR* textStr = L"Switching Lighting Types: 1-Parallel light 2-Point light 3-Spotlight\n"
         "Switching model: Q-Cube W-sphere E-cylinder";
    md2dRenderTarget->DrawTextW(textStr, wcslen(textStr), mTextFormat.Get(),
        D2D1_RECT_F{ 0.0f, 0.0f, 400.0f, 40.0f }, mColorBrush.Get());
    HR(md2dRenderTarget->EndDraw());

    HR(mSwapChain->Present(0, 0));
}

The final results are as follows:

Posted by dv90 on Thu, 10 Jan 2019 08:21:11 -0800