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究竟是什麼,後面我會詳細介紹,這裏只是說清楚目標和使用它們的根本原因。

  本章示例代碼運行後的效果如下:

  全部的例子代碼可以到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的向下兼容接口來過渡。詳細的方法過程如下圖所示:

  圖中左邊的調用鏈路就是參與實現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渲染,那麼就需要額外插入一個將狀態變成可渲染狀態的資源屏障!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章