DirectX12(D3D12)基础教程(十三)——D2D、DWrite On D3D12与文字输出

1、 前言

  在经过了前面一系列章节的“狂轰滥炸”式的学习之后,如果你现在跟进到了这里,那么请为自己点个赞先!

  从本章开始,教程内容都会开始使用Markdown方式进行发布。一方面主要是为了练习Markdown编辑器的使用;另一方面Markdown目前已经成为比较流程的程序文档的编辑方式,我也来跟跟潮流,也是让大家习惯Markdown。还有就是Markdown的公式编辑比较简洁,后续的文章中可能会出现很多公式,这样编辑、保存、发布、显示都会比较方便了,所以权衡所有的利弊问题后,我还是选择使用Markdown来编辑和发布教程内容,请大家理解和支持!

  这一章中主要的任务就是把不愠不火的D2D和DWrite捡起来,并且和D3D12深度结合起来,为示例程序添加基本的文字支持。

  说到文字支持其实这是一个游戏引擎或者其它3D应用程序必须具备的基本功能之一。当然具体的实现方式有很多种,基本的文字渲染有两大个方向,一种是基于光栅化字体的,也就是把字体提前做出图片,然后按照像素位置索引每个字按照UI显示的一般方式进行字体显示;另一种就是使用矢量字体,进行适当的变换后,按照显示一般曲线的方法进行显示;这两种方法各有优缺点,一般的引擎里都会选择最合适的方式进行字体渲染。

  而在我们的教程中为了做到显示文字内容的目的,还是选择使用D2D+DWrite的方式。这种方式主要跟传统的Windows下的HDC显示和渲染文字的方式方法在编程接口(API)上比较类似,也是我比较熟悉的领域,同时这种方式可以方便的跟D3D12或D3D11无缝融合,在编程方式上也比较类似,学习的成本相对也不是太高,最终这种方式在显示文字信息时因为DWrite的封装,做一般的效果也足够了。所以基于这些考虑最终我还是决定使用D2D和DWrite来显示文字信息。当然这样做的也有弊端,那就是首先这种方式没法跨平台,当然因为本系列教程就是为了说清楚D3D12的,本就是Win平台下的,这样选择也没什么。

  另外对于DWrite和D2D的性能,说法不一,有些说性能差强人意,有些又说性能还行,众说纷纭,我也没时间去细究到底性能怎么样,如果各位有兴趣可以自己想办法测试下看看,毕竟实践出真知!最后呢,因为这一章为了消除大家对其性能的顾虑,所以,我依然使用了多线程+多显卡的渲染框架方式,并且果断的将D2D+DWrite显示文字信息的功能放到了辅助显卡上,这样它就不占用主显卡的资源了,也算是给大家提供一种思路。而且最后实际的例子运行中,我发现显示简单的文字信息也不会占用辅助显卡的性能,因此我想如果大家都掌握了多显卡渲染的基本编程框架,那么完全可以用DWrite+D2D在辅助显卡上显示文本信息,从而解决显示文字信息的问题,至少浪费点辅助显卡的性能还不至于影响3D渲染的大局。

  至于D2D和DWrite究竟是什么,后面我会详细介绍,这里只是说清楚目标和使用它们的根本原因。

  本章示例代码运行后的效果如下:
图片[1]-DirectX12(D3D12)基础教程(十三)——D2D、DWrite On D3D12与文字输出-卡核
  全部的例子代码可以到GitHub上下载查看:
12-D2DWriteOnD3D12

2、D2D、DWrite简介

  D2D顾名思义,就是Direct2D的缩写,因此他就是DirectX中的2D图形显示API接口,与D3D相对应,D2D和D3D合起来就组成了Win平台下完整的Graphic组件。基本就可以支持所有有性能要求的图形应用程序的图形功能,比如3D游戏等。从API角度讲,其实D2D与传统的Win GDI(DC)API很类似,但在性能上因为直接利用了图形硬件的能力,所以要远比Win GDI高的多的多。这样使用D2D,即使的我们过去学习的Win GDI API的相关知识得以保留,同时又得到了性能方面的显著提升,综合下来是一种在Win平台下处理2D图形显示的不错选择。当然从功能上来讲,D2D也比Win GDI API要扩展和高级了很多。

  同理DWrite就是DirectWrite的缩写,Write直译就是写的意思,顾名思义就是直接显示文本的组件名称。它也是Win GDI API中文字部分的功能升级与扩展。

  以上两个组件都采用了基本COM接口的封装方式,都作为了DirectX中的一部分。因此在调用方式上与DirectX中的其它组件比较类似,同时得益于DXGI功能接口的独立性,因此它们可以天然的与D3D接口进行互操作,从而实现在3D场景渲染结果的基础上进行复杂高效的2D图形或文字的渲染。本章我们就使用DWrite来显示文字。

3、添加D2D、DWrite基础支持文件

  要使用D2D和DWrite组件,即使它们是采用了COM形式封装的接口,但也是根据一般的C++中引用组件的一般方式来添加引用的。所以首先我们就要像下面这样添加头文件和Lib库文件的引用:

//--------------------------------------------------------------
#include <d3d11_4.h>
#include <d3d11on12.h>
#include <d2d1_3.h>
#include <dwrite.h>
//--------------------------------------------------------------
//......
//--------------------------------------------------------------
#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "dwrite.lib")
#pragma comment(lib, "d3d11.lib")
//--------------------------------------------------------------

  上面的代码稀松平常,唯一可能要引起我们注意的就是我们还包含了D3D11的组件,这是为什么呢?具体接下来就开始具体介绍。

4、D2D、DWrite基本编程步骤

  刚才已经提到过D2D和DWrite在编程概念上与Win GDI API很类似,其实更确切的说它更像是一个在概念上混合了D3D与Win GDI API的产物,所以它的编程概念首先就是类似D3D的渲染目标、然后就是类似GDI的字体、画笔、画刷、位图、线、线条、矩形、圆、扇形、圆弧等等。当然更高级的在D2D和DWrite中也可以使用Shader来渲染2D的画面或字体,当然目前例子教程中还用不到这个高级的功能。

  如果你对Win GDI API以及D3D编程的基本概念都很熟悉的话,D2D和DWrite的学习曲线就是很平坦的。或者说在概念上就没有什么复杂的东西,瞬间就可以理解了。最后需要强调的就是,这些编程的基本概念其实也是大多数2D图形编程包中的一般概念,或者用一种更易理解的方式来说,比如你要实现一套基于D3D UI渲染技术的2D图形模块出来,那么这些概念以及对应的基本API等,就是你需要参考实现的最基本的一组API及对应功能。

  基于这样的认知,各位就不难理解为什么在教程中要特别讲解一下如何将D2D和DWrite整合到D3D12中来,本章的重点在于怎么把它们的功能整合到D3D12中来,优先解决的是能不能显示文字的问题。相对高级的功能就先不去浪费时间了,一方面这些东西概念上并不复杂,基本的使用会了的话,剩下的就是查看MSDN的手册了;另一方面D2D、DWrite已经有很多大牛编写了很多优秀的教程,这里就不再去班门弄斧了,各位需要深入了解的就在此基础上搜素进行深度学习即可。

  有了上面的概念,再来看看具体怎么样使用D2D和DWrite。从编程模型上来说D2D和DWrite的关系很紧密,或者可以直接认为DWrite就是D2D的一个子功能接口,如果不显示文字信息的话可以单独使用D2D,而如果要显示文字信息,那就要使用DWrite组件。使用DWrite就必须要用D2D来作为支撑。

  从基本编程的过程上来说,使用D2D和DWrite的大致流程如下:

1、创建D2D和DWrite工厂(与创建DXGI的工厂接口类似,就是个原初接口的创建,没什么特殊的,就不过多详细描述了,大家可以直接看代码);

2、创建D2D设备对象及接口(D2D的设备接口,在形式上与D3D11的类似,都有一个Device接口以及一个Device Context接口,后者则主要是D2D的各种绘制功能函数的集合接口);

2、创建D2D渲染目标;

3、创建DWrite的字体、D2D的画刷、画笔、设置背景色等对象;

4、利用这些对象,使用D2D各种Draw函数绘制需要的2D几何体或者显示文字信息;

5、使用完毕、销毁对象、清理资源;

  以上过程是一个基本的过程,其实跟Win GDI的API的调用过程也是大同小异的,只是使用了D2D渲染目标的概念代替了GDI中的Device Context(简称DC)概念而已,其它也就是使用COM接口函数,代替了GDI中的分散的各种Brush、Pen、Font等等相关API。所以从这点上来说,如果之前深入学习过GDI编程的相关知识的话,现在这些知识就在D2D和DWrite中得到了保值增值(传说中,目前流行的一些非IE浏览器内部的底层2D图形支持之类的也都移植到基于D2D和DWrite上来)。这就好像如果你懂的虚拟机,那么现在再把存储和网络等等能虚拟化的设备统统都给你虚拟化了,就成了云计算。

5、基于D3D11On12设备创建D2D渲染目标

  根据前面的描述,D2D和DWrite的基本使用方法也不是很复杂,那么究竟怎样将它们和D3D12混合在一起使用呢?其实全部的秘密就在如何创建基于D3D12渲染目标的D2D渲染目标上,或者以更容易理解的方式来说,就是让D2D和D3D都绘制到同一张屏幕后缓冲中(或者同一张离屏纹理/表面上)。

  当然这说起来容易做起来稍微有点复杂,因为最早D2D、DWrite能跟D3D混用是D3D11推出不久之后,彼时微软的想法就是用一套比较新的2D图形接口来取代年代久远的DirectDraw并且在一些性能和效果要求较高的应用场景中取代传统的Win GDI 的API,因此D2D和DWrite应运而生,并且通过从D3D中拆出来的DXGI接口,实现了与D3D11的无缝集成,从而整体上形成了Win平台中的Graphic组件,用以通吃2D、3D渲染。

  后来到了D3D12的年代,D2D和DWrite貌似也就没有跟着一起升级了,看上去像两兄弟分家了,而D3D12依然是比较活跃的领域,D2D和DWrite则相对消沉一些。因此现在在D3D12中要使用D2D和DWrite的话,就需要请D3D11来牵线搭桥了,具体的方法就是使用D3D12的设备创建一个D3D11on12的向下兼容接口来过渡。详细的方法过程如下图所示:
图片[2]-DirectX12(D3D12)基础教程(十三)——D2D、DWrite On D3D12与文字输出-卡核
  图中左边的调用链路就是参与实现D3D12和D2D渲染到同一张纹理的各种组件的核心设备对象以及接口,主要有D3D12设备对象、D3D11设备对象、及D2D设备对象以及它们各自被用到的接口;右边的链路虽然别分开表示为貌似不同的几种资源对象,其实它们最终都表示的是同一张纹理,或者更直接的理解为它们本身就是同一块显存,这块显存先从它是D3D12 Resource的样子开始被一路”整容“最终变成了D2D Render Target的样子(如果你了解过韩国整形术的话,这里实质上是进行着差不多相同概念的过程!)。这个过程中关键的函数都已经标注在图上了,一目了然。理解了这个过程之后,大家就可以直接去阅读本章代码中相关部分的内容了。代码中也已经标注的很清楚了,就不在这里过多浪费篇幅粘贴代码了。只是需要提醒大家的是,代码中我都尽量使用了较高版本的相关接口,这样做是方便大家可以在此基础上进一步掌握这些高版本中扩展出来的相应组件的一些高级功能。

6、创建DWrite字体用D2D显示文字

  根据前面的描述,如果解决了D2D与D3D12渲染到同一个纹理的问题,剩下的其实就是各自去按照渲染逻辑组织代码了。它们之间也就没有过多的交互了,无非就是控制下绘制的顺序,一般都是3D场景整个渲染完了之后,再去调用D2D显示2D的图形或文本信息。

  具体的例子中主要是为了显示一段文本信息,说明下当前渲染状态中各种后处理的开关状态。根据D2D绘制文字的一般框架,那么首先就需要创建字体及格式信息对象,因为DWrite支持很多字体及相应格式,所以就必须明确程序要使用的字符集、字体、字号、风格(粗体?斜体?下划线?)等信息,而这些在DWrite中就使用一个IDWriteTextFormat接口来表示。具体代码如下:

GRS_THROW_IF_FAILED(pIDWriteFactory->CreateTextFormat(
	L"微软楷体",
	NULL,
	DWRITE_FONT_WEIGHT_NORMAL,
	DWRITE_FONT_STYLE_NORMAL,
	DWRITE_FONT_STRETCH_NORMAL,
	20,
	L"zh-cn", //中文字库
	&pIDWriteTextFormat
));
// 水平方向左对齐、垂直方向居中
GRS_THROW_IF_FAILED(pIDWriteTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_JUSTIFIED));
GRS_THROW_IF_FAILED(pIDWriteTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER));

  从代码后面两行还可以看出,我们还可以设置字体水平与垂直对齐方式,这其实是相当于稍后需要将文字显示其中的矩形框而言的。由此也可以看出用DWrite显示文字信息其本身的功能已经是很强悍了。这里要提醒大家注意的就是其中的字库参数,现在搜到的官方示例中都是英文字库,所以要显示中文,就要像这里这样使用中文字库和字体名。否则中文信息显示可能会混乱。

  为了支持显示文字信息,那么还需要创建一个D2D的画刷对象接口。这主要是因为在D2D及DWrite中支持的是矢量型字体字库,所以理论上可以实现任意大小、任意字色、任意填充方式甚至阴影效果等字体(这些其实Win GDI中也可以做到)。而在D2D中最终这些都没有提供默认值,所以就需要我们额外的创建一些对象或提供一些参数值,来完成文本信息的显示。当然这样虽然在文本信息的显示上做到了几乎最大的灵活性,但是在编码方面就给我们带来了一定的复杂性的挑战。最终如果你是一个用惯了很多“简洁即王道”软件包的程序员,很容易对这种方式产生厌恶。不过幸运的是,我们是用C++的开发人员,那么这些就自己稍微封装一下即可。当然具体怎么封装就是各位的事情了,本系列教程要做的就是把最原始最直接的编程方法、概念、模式、模型、原理等等分享给各位,而不会讲怎么封装!

  OK,多的就不啰嗦了。为了成功显示文本,那么接着我们就需要创建一个D2D的画刷对象接口,用于填充最终显示的文字,代码如下:

// 创建一个画刷,字体输出时即使用该颜色画刷
GRS_THROW_IF_FAILED(pID2D1DeviceContext6->CreateSolidColorBrush(
	D2D1::ColorF(D2D1::ColorF::Gold)
	, &pID2D1SolidColorBrush));

  上述基本的辅助对象接口创建完毕后,终于可以显示文本了。在3D渲染包括后处理渲染全部结束后,就可以像下面代码这样准备并显示文本了:

if ( 1 == g_nFunNO )
{
	StringCchPrintfW(pszUIString, MAX_PATH, _T("水彩画效果渲染:开启;"));
}
else
{
	StringCchPrintfW(pszUIString, MAX_PATH, _T("水彩画效果渲染:关闭;"));
}

if (1 == g_nUsePSID)
{
	StringCchPrintfW(pszUIString, MAX_PATH, _T("%s高斯模糊后处理渲染:开启。"),pszUIString);
}
else
{
	StringCchPrintfW(pszUIString, MAX_PATH, _T("%s高斯模糊后处理渲染:关闭。"), pszUIString);
}

pID3D11On12Device1->AcquireWrappedResources(pID3D11WrappedBackBuffers[nCurrentFrameIndex].GetAddressOf(), 1);

pID2D1DeviceContext6->SetTarget(pID2DRenderTargets[nCurrentFrameIndex].Get());
pID2D1DeviceContext6->BeginDraw();
pID2D1DeviceContext6->SetTransform(D2D1::Matrix3x2F::Identity());
pID2D1DeviceContext6->DrawTextW(
	pszUIString,
	_countof(pszUIString) - 1,
	pIDWriteTextFormat.Get(),
	&stD2DTextRect,
	pID2D1SolidColorBrush.Get()
);
GRS_THROW_IF_FAILED(pID2D1DeviceContext6->EndDraw());

pID3D11On12Device1->ReleaseWrappedResources(pID3D11WrappedBackBuffers[nCurrentFrameIndex].GetAddressOf(), 1);

pID3D11DeviceContext4->Flush();

  上述代码中核心的显示文本信息的调用就是DrawTextW,其名称中W表示显示的UNICODE字符,也就是宽字节字符集。这对显示中文信息是最大的福音!

7、D2D、D3D11on12与D3D12同步

  经过前面的步骤,最终文本信息也成功的显示了。到这里,我想细心的各位一定想到了一个问题,那就是根据我们前面各种教程中反复甚至啰嗦的强调一个概念——D3D12天生就是异步执行的情况下,那么之前代码中又是调用D3D11on12、接着又是调用D2D的情况下,它们究竟是怎么同步的?或者直白的说,D2D最终怎么知道D3D12已经画完了,它可以接着画了呢?

  其实全部的秘密就在前面代码的几个API中,首先AcquireWrappedResources就相当于在D3D12执行完毕之后,在D3D11on12设备开始工作之前,在渲染目标上设置了一个资源屏障,而接着的D2D的相关调用就被D3D11on12理解为像在D3D12中一样录制命令列表而已,最终ReleaseWrappedResources就相当于又放置了一个将渲染状态的纹理切换为可以提交状态的资源屏障。整体上所有这些调用都是在录制命令列表!

  最后D3D11设备对象的Flush()函数就相当于D3D12中的ExecuteCommandLists函数,将所有渲染的命令,包括D2D显示文本的命令提交到显卡去执行。

  这样在编程模式和运行模式上D3D12、中间过渡的D3D11、以及D2D就得到了高度的统一,只要理解了D3D12的异步运行模式,那么理解D2D的异步模式就没什么问题了。

  最终其实基于这样对几个组件间同步控制的理解,那么其实显示文本的D2D过程(含D3D11部分)就可以插入在任意的D3D12渲染过程中的位置,只是需要搞清楚Flush之后渲染目标其实变成了可提交状态,这时如果需要继续D3D12渲染,那么就需要额外插入一个将状态变成可渲染状态的资源屏障!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片