DX11 Without DirectX SDK--08 Direct2D与Direct3D互操作性以及利用DWrite显示文字

前言

在DX11,要显示文字可以说是一件比较麻烦的事情。DX9诸如Id3dXFont用于显示文字的接口类都已经被抛弃掉了。目前行之有效的两种显示文字的方法如下:

  1. 使用包含文字的位图/矢量图,然后通过一定的方式来获取对应文字的矩形区域,最后渲染出来。

  2. 通过实现Direct2D与Direct3D互操作性,然后配合DWrite在程序写入文字。

对于个人来说,第一种方式做起来比较麻烦。对于第二种方法,我通过查阅MSDN文档,并进行了一定尝试,很快就实现了文字显示。因此接下来将围绕第二种方法进行讨论(这里不关注贴位图和绘制几何体等在Direct2D的其余操作,这些都可以在Direct3D做到)

项目源码点此:https://github.com/MKXJun/DX11-Without-DirectX-SDK

通过DXGI进行互操作

从 Direct3D 10.1开始, Direct3D Runtime使用DXGI进行资源管理。DXGI Runtime提供了跨进程共享视频内存图面的功能,并且可用作其他基于视频内存的运行时平台的基础。Direct2D 使用 DXGI 与 Direct3D 交互。

为了实现Direct2D和Direct3D互操作,并显示文字,需要经历下面的准备步骤:

  1. d3dApp.h添加头文件d2d1.hdwrite.h,并添加静态库d2d1.libdwrite.lib
  2. 修改创建ID3D11DeviceIDXGISwapChain时的一些配置参数
  3. 创建ID2D1Factory
  4. 通过IDXGISwapChain获取接口类IDXGISurface,并通过它来创建ID2D1RenderTarget以进行绑定。这样就可以通过该渲染目标进行具体操作了。

D3D11设备和DXGI交换链的创建属性修改

由于Direct2D需要支持BGRA的数据格式,因此在创建D3D11设备前需要修改如下部分:

// 创建D3D设备 和 D3D设备上下文
UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;  // Direct2D需要支持BGRA格式
#if defined(DEBUG) || defined(_DEBUG)  
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

然后在创建DXGI交换链的时候也要将DXGI格式修改为DXGI_FORMAT_B8G8R8A8_UNORM

// 检测 MSAA支持的质量等级
md3dDevice->CheckMultisampleQualityLevels(
    DXGI_FORMAT_B8G8R8A8_UNORM, , &m4xMsaaQuality);    // 注意此处DXGI_FORMAT_B8G8R8A8_UNORM
assert(m4xMsaaQuality > );
    
...

// 如果包含,则说明支持DX11.1
if (dxgiFactory2 != nullptr)
{
    ...
    // 填充各种结构体用以描述交换链
    DXGI_SWAP_CHAIN_DESC1 sd;
    ZeroMemory(&sd, sizeof(sd));
    ...
    sd.Format = DXGI_FORMAT_B8G8R8A8_UNORM;     // 注意此处DXGI_FORMAT_B8G8R8A8_UNORM
    ...
}
else
{
    // 填充DXGI_SWAP_CHAIN_DESC用以描述交换链
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    ...
    sd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;  // 注意此处DXGI_FORMAT_B8G8R8A8_UNORM
    ...
}

D2D1CreateFactory函数--创建D2D工厂对象

在创建D2D渲染目标前,还需要先创建一个ID2D1Factory对象,可以用来创建各种资源:

template<class Factory>
HRESULT D2D1CreateFactory(
    D2D1_FACTORY_TYPE factoryType,  // [In]枚举值
    Factory **factory               // [Out]获取的工厂对象
);

创建操作如下:

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

注意这里用了HR宏,以及md2dFactoryComPtr<ID2D1Factory>类型

ID2D1Factory::CreateDxgiSurfaceRenderTarget方法--创建一个DXGI表面渲染目标

现在我们要创建的是ID2D1RenderTarget对象。

接下来的操作需要在每次窗口大小变化且调用了IDXGISwapChain::ReSizeBuffers方法只会进行。通常建议写在GameApp::OnReSize内调用D3DApp::OnReSize之后。

首先释放之前创建的D2D资源(如果有的话),通过IDXGISwapChain::GetBuffer方法来获取后备缓冲区的IDXGISurface接口:

md2dRenderTarget.Reset();

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

然后填充D2D1_RENDER_TARGET_PROPERTIES结构体属性:

typedef struct D2D1_RENDER_TARGET_PROPERTIES
{
    D2D1_RENDER_TARGET_TYPE type;   // 渲染目标类型枚举值
    D2D1_PIXEL_FORMAT pixelFormat;  
    FLOAT dpiX;                     // X方向每英寸像素点数,设为0.0f使用默认DPI
    FLOAT dpiY;                     // Y方向每英寸像素点数,设为0.0f使用默认DPI
    D2D1_RENDER_TARGET_USAGE usage; // 渲染目标用途枚举值
    D2D1_FEATURE_LEVEL minLevel;    // D2D最小特性等级

} D2D1_RENDER_TARGET_PROPERTIES;

typedef struct D2D1_PIXEL_FORMAT
{
    DXGI_FORMAT format;             // DXGI格式
    D2D1_ALPHA_MODE alphaMode;      // 混合模式

} D2D1_PIXEL_FORMAT;

可以借用D2D1::RenderTargetProperties函数来创建,这里使用默认DPI:

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

最后ID2D1Factory::CreateDxgiSurfaceRenderTarget方法如下:

HRESULT ID2D1Factory::CreateDxgiSurfaceRenderTarget(
    IDXGISurface *dxgiSurface,          // [In]DXGI表面
    const D2D1_RENDER_TARGET_PROPERTIES *renderTargetProperties,    // [In]D2D渲染目标属性
    ID2D1RenderTarget **renderTarget    // [Out]得到的D2D渲染目标
);

具体调用如下:

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

surface.Reset();

至此,Direct2D就可以和Direct3D通过DXGI实现互操作了。通过ID2D1RenderTarget,你可以创建各种类型的颜色刷子,并进行绘制操作。但由于我们需要绘制文字,下面会介绍DWrite

使用DWrite显示文字

要显示文字,需要经过下面的步骤:

  1. 创建IDWriteFactory工厂对象
  2. 通过DWrite工厂对象创建IDWriteTextFormat文本格式对象
  3. 为文本格式对象设置好文本格式
  4. 通过ID2D1RenderTarget创建颜色刷
  5. 在绘制完3D部分后以及最终呈现之前进行文本绘制

DWriteCreateFactory函数--创建DWrite工厂对象

函数原型如下:

HRESULT DWRITE_EXPORT DWriteCreateFactory(
    DWRITE_FACTORY_TYPE factoryType,    // [In]工厂类型枚举
    const IID & iid,                    // [In]接口标识ID
    IUnknown **factory                  // [Out]获得工厂对象
    );

下面演示了创建过程:

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

IDWriteFactory::CreateTextFormat方法--创建文本格式对象

HRESULT IDWriteFactory::CreateTextFormat(
    const WCHAR * fontFamilyName,           // [In]字体系列名称
    IDWriteFontCollection * fontCollection, // [In]通常用nullptr来表示使用系统字体集合 
    DWRITE_FONT_WEIGHT  fontWeight,         // [In]字体粗细程度枚举值
    DWRITE_FONT_STYLE  fontStyle,           // [In]字体样式枚举值
    DWRITE_FONT_STRETCH  fontStretch,       // [In]字体拉伸程度枚举值
    FLOAT  fontSize,                        // [In]字体大小
    const WCHAR * localeName,               // [In]区域名称
    IDWriteTextFormat ** textFormat);       // [Out]创建的文本格式

字体系列的名称可以用中文来引用,比如L"宋体"L"微软雅黑"等。

字体粗细看个人喜好,用DWRITE_FONT_WEIGHT_NORMAL就差不多了吧

字体样式如下:

枚举值样式
DWRITE_FONT_STYLE_NORMAL默认
DWRITE_FONT_STYLE_OBLIQUE斜体
DWRITE_FONT_STYLE_ITALIC意大利体

字体拉伸程度用DWRITE_FONT_STRETCH_NORMAL就可以了

字体大小建议在Word文档提前感受一下

区域名称这里默认用L"zh-cn"

创建演示如下:

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

创建了IDWriteTextFormat对象后,可以调用它的一系列Get方法获取文本格式的详细信息,也可以用一系列Set方法来设置。这里不展开说明。

ID2D1RenderTarget::CreateSolidColorBrush方法--创建单色刷对象

虽然ID2D1RenderTarget对象提供了多种刷子供创建,但最常用的还是创建ID2D1SolidColorBrush单色刷。

该方法是经过重载的,现在只讨论其中一种重载方法:

HRESULT ID2D1RenderTarget::CreateSolidColorBrush(
    const D2D1_COLOR_F &color,  // [In]颜色
    ID2D1SolidColorBrush **solidColorBrush // [Out]输出的颜色刷
);

这里会默认指定Alpha值为1.0

D2D1_COLOR_F是一个包含r,g,b,a浮点数的结构体,但其实还有一种办法可以指定颜色,就是利用它的继承类D2D1::ColorF中的构造函数,以及D2D1::ColorF::Enum枚举类型来指定要使用的颜色,可以进里面去查看,这里就不给出所有的颜色枚举了。

下面演示了怎么创建一个单色刷:

// 创建固定颜色刷和文本格式
HR(md2dRenderTarget->CreateSolidColorBrush(
    D2D1::ColorF(D2D1::ColorF::White),
    mColorBrush.GetAddressOf()))

D3DApp类、GameApp类的变化以及开始文本绘制

这里以上一个项目为例,进行修改。

D3DApp类中,新增了D3DApp::InitDirect2D方法用于创建D2D工厂和DWrite工厂:

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;
}

该方法在D3DApp::Init中被调用。

而在GameApp::OnReSize方法中也进行了修改:

void GameApp::OnResize()
{
    assert(md2dFactory);
    assert(mdwriteFactory);
    // 释放D2D的相关资源
    mColorBrush.Reset();
    md2dRenderTarget.Reset();

    // 调用父类方法
    D3DApp::OnResize();

    // 为D2D创建DXGI表面渲染目标
    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();
    // 创建固定颜色刷和文本格式
    HR(md2dRenderTarget->CreateSolidColorBrush(
        D2D1::ColorF(D2D1::ColorF::White),
        mColorBrush.GetAddressOf()));
    HR(mdwriteFactory->CreateTextFormat(L"宋体", nullptr, DWRITE_FONT_WEIGHT_NORMAL,
        DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 20, L"zh-cn",
        mTextFormat.GetAddressOf()));
    
}

在这里D2D的相关资源需要在D3D相关资源释放前先行释放掉,然后在D3D重设后备缓冲区后重新创建D2D渲染目标。至于D2D后续的相关资源也需要重新创建好来。

最后在GameApp::DrawScene方法中,绘制2D部分需要在3D部分绘制完后,呈现之前进行。

首先需要调用ID2D1RenderTarget::BeginDraw方法,开始D2D绘制。该方法没有参数

绘制完成后,就调用ID2D1RenderTarget::EndDraw方法,结束D2D绘制。该方法的返回值为HRESULT,若之前绘制出现问题,在EndDraw才会进行反馈。可以用HR宏包住。

ID2D1RenderTarget::DrawTextW方法--绘制文本

DrawText在这里进行了宏定义:

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

我们的项目是只能使用Unicode字符集的(dxerr.h只允许该字符集),所以直接讨论DrawTextW方法

该方法也经过了重载。现在只讨论其中一种,且使用默认参数:

void ID2D1RenderTarget::DrawTextW(
    WCHAR *string,                      // [In]要输出的文本
    UINT stringLength,                  // [In]文本长度,用wcslen函数或者wstring::length方法获取即可
    IDWriteTextFormat *textFormat,      // [In]文本格式
    const D2D1_RECT_F &layoutRect,      // [In]布局区域
    ID2D1Brush *defaultForegroundBrush, // [In]使用的前景刷
    D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE,
    DWRITE_MEASURING_MODE measuringMode = DWRITE_MEASURING_MODE_NATURAL);

D2D1_RECT_F结构体包含了left,top,right,bottom四个成员。

现给出GameApp::DrawScene方法Direct2D部分的实现:

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

    // 绘制Direct3D部分
    ...

    // 绘制Direct2D部分
    md2dRenderTarget->BeginDraw();
    static const WCHAR* textStr = L"切换灯光类型: 1-平行光 2-点光 3-聚光灯\n"
         "切换模型: Q-立方体 W-球体 E-圆柱体";
    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(, ));
}

最终效果如下: