程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 一個用於 DirectX 編程的現代 C++ 庫

一個用於 DirectX 編程的現代 C++ 庫

編輯:關於C++

我寫過很多 DirectX 代碼,也寫過很多關於 DirectX 的文章。我甚至還編寫過關於 DirectX 的在線培訓課程。它其實並不像某些開發人員所說的那麼難以理解。學習曲線一定會有,但一旦您過了這道坎,就不難理解 DirectX 的工作方式及其為何要如此工作的原因了。不過我也承認,DirectX 系列 API 的易用性應該更高些。

幾天前,我決定著手修補一下這個缺陷。我熬了一整夜,編寫了一個小頭文件。隨後幾晚,我又將代碼行擴展到了近 5,000 行。我的目標是提供一些可借助 Direct2D 更方便地構建應用程序的東西,並向如今盛行的所有“C++ 很難”或“DirectX 很難”之類的論斷發起挑戰。我並沒打算再開發一個笨重的 DirectX 包裝。實際上,我決定借助 C++11 制作一套更簡便的 DirectX API,同時不在核心 DirectX API 之上產生任何空間和時間開銷。您可以在下面的網址找到我開發的這個庫:dx.codeplex.com。

這個庫本身只包含一個名為 dx.h 的頭文件,CodePlex 上的其余源文件提供了有關該頭文件使用方法的示例。

在本專欄文章中,我將向您展示如何利用這個庫更方便地執行各種與 DirectX 相關的常見活動。此外,我還將介紹這個庫的設計方法,以便您了解 C++11 如何幫助提高傳統 COM API 的易用性,而不必求助於 Microsoft .NET Framework 等會對性能產生很大影響的包裝。

顯然,我們的重點是 Direct2D。要借助 DirectX 開發類別最為廣泛的應用程序和游戲,Direct2D 仍是最簡單也最有效的方式。許多開發人員似乎加入到了兩個對立的陣營中。他們中有 DirectX 鐵桿開發人員:他們不斷學習各種版本的 DirectX API。他們在 DirectX 多年來的發展中歷經磨練,並且樂於成為這一進入門檻極高的“貴賓俱樂部”的一員(很少有開發人員能加入該俱樂部)。而在另一陣營的開發人員聽到了 DirectX 很難的消息,不想跟 DirectX 扯上一丁點關系。不用說,他們往往會拒絕使用 C++。

我不屬於任一陣營。我相信 C++ 和 DirectX 不必如此困難。在上月的專欄文章 (msdn.microsoft.com/magazine/dn198239) 中,我介紹了 Direct2D 1.1 和作為先決條件的 Direct3D 和 DirectX 圖形基礎結構 (DXGI) 代碼,以創建一個設備並管理交換鏈。該代碼利用 D3D11CreateDevice 函數創建 Direct3D 設備,適用於 GPU 或 CPU 呈現,長度約 35 行。不過,在我提供的小頭文件的幫助下,可將其精簡為:

auto device = CreateDevice();

CreateDevice 函數返回一個 Device1 對象。 由於所有 Direct3D 定義都位於 Direct3D 命名空間內,所以也可以這樣寫(更加明確):

Direct3D::Device1 device = Direct3D::CreateDevice();
Device1 對象不過是對 ID3D11­Device1 COM 接口指針(DirectX 11.1 版本引入的 Direct3D 設備接口)的包裝。 Device1 類派生自 Device 類,後者是對原 ID3D11Device 接口的包裝。 它代表一個引用,與直接獲取該接口指針本身相比,不會帶來任何額外開銷。 請注意,Device1 及其父類 Device 是常規的 C++ 類,而不是接口。 您可以將它們看作智能指針,但這有些過於簡單化。 當然,它們能夠處理引用計數並提供“->”運算符直接調用您選擇的方法,但在開始使用 dx.h 庫提供的諸多非虛方法時,才是它們真正大放異彩之時。

例如: 通常,您可能需要 Direct3D 設備的 DXGI 接口來傳遞其他某種方法或函數。 不怕麻煩的話,可以這樣做:

          auto device = Direct3D::CreateDevice();
wrl::ComPtr<IDXGIDevice2> dxdevice;
HR(device->QueryInterface(dxdevice.GetAddressOf()));

這當然可行,但現在您還必須直接處理 DXGI 設備接口。另外,您還需要牢記,IDXGIDevice2 接口是 DirectX 11.1 版本的 DXGI 設備接口。實際上,也可簡單地調用 AsDxgi 方法:

          auto device = Direct3D::CreateDevice();
auto dxdevice = device.AsDxgi();

返回的 Device2 對象(此次是在 Dxgi 命名空間中定義的)包裝 IDXGIDevice2 COM 接口指針,提供自己的一組非虛方法。再舉一個例子,您可能需要使用 DirectX“對象模型”訪問 DXGI 工廠:

          auto device   = Direct3D::CreateDevice();
auto dxdevice = device.AsDxgi();
auto adapter  = dxdevice.GetAdapter();
auto factory  = adapter.GetParent();

當然,這是一種常見模式,是 Direct3D Device 類提供 GetDxgiFactory 方法作為捷徑的一種手段:

          auto d3device = Direct3D::CreateDevice();
auto dxfactory = d3device.GetDxgiFactory();

因此,除了少量便捷的方法和函數(如 GetDxgiFactory)外,非虛方法以一對一形式映射到底層的 DirectX 接口方法和函數。為了構建異常簡便且高效的 DirectX 編程模型,我綜合利用了幾種方法(雖然看似不多)。第一種方法是使用范圍枚舉。DirectX 系列 API 定義了一個令人眼花缭亂的常量數組,其中有很多是傳統的枚舉、標志或常量。它們不是強類型,很難找到並使用,而且與 Visual Studio IntelliSense 之間的配合不太好。下面是創建 Direct2D 工廠所需的代碼(請先忽略工廠選項):

          wrl::ComPtr<ID2D1Factory1> factory;
HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                     factory.GetAddressOf()));

D2D1CreateFactory 函數的第一個參數是一個枚舉,但由於它並非范圍枚舉,很難被 Visual Studio IntelliSense 發現。這些老舊的枚舉提供了些微的類型安全,但並不多。充其量,您會在運行時遇到 E_INVALIDARG 結果代碼。我不知道您會怎麼做,但我寧願在編譯時捕獲此類錯誤,或者,更好的辦法是根本不用它們:

auto factory = CreateFactory(FactoryType::MultiThreaded);
           

之前的優點再次顯現出來:我無需花費時間四處查找調用了哪個最新版本的 Direct2D 工廠接口。 當然,這裡最大的優勢還是效率。 DirectX API 不僅僅是創建和調用 COM 接口方法。 為了將各種各樣的屬性和參數包裝到一起,DirectX API 采用了許多簡陋的老式數據結構。 交換鏈的說明就是一個很好的示例。 它包含很多容易混淆的成員,我從來記不住該如何准備這個結構,更不用說和平台有關的細節了。 這裡,我編寫的庫再次派上了用場,它提供了另一個結構,用來代替令人恐懼的 DXGI_SWAP_CHAIN_DESC1 結構:

             SwapChainDescription1 description;
           

在本例中,我提供了一個二進制兼容的替代品,以確保 DirectX 將其視作相同的類型,但您在使用時應該提供更具實用性的替代品。 它與 Microsoft .NET Framework 為其 P/Invoke 包裝提供的替代品相似。 該默認構造函數提供的默認值適用於大多數桌面和 Windows 應用商店應用程序。 但您可能需要重寫這些值,例如針對桌面應用程序作出如下改動,以便在調整大小時實現更平滑的呈現效果:

          SwapChainDescription1 description;
description.SwapEffect = SwapEffect::Discard;

順便提一下,在編寫 Windows Phone 8 應用程序時,也需要使用這種交換效果,但不允許在 Windows 應用商店應用程序中使用。自己去想吧。

許多最好的庫能夠引領您快速、方便地找到可行的解決方案。讓我們看一個具體的示例。Direct2D 提供了一個線性漸變畫筆。該畫筆的創建過程包含三個邏輯步驟: 定義漸變停止點,創建漸變停止點集合,然後創建該集合的線性漸變畫筆。圖 1 是直接使用 Direct2D API 的示例。

圖 1 創建線性漸變畫筆(比較麻煩的方式)

          D2D1_GRADIENT_STOP stops[] =
{
  { 0.0f, COLOR_WHITE },
  { 1.0f, COLOR_BLUE },
};
wrl::ComPtr<ID2D1GradientStopCollection> collection;
HR(target->CreateGradientStopCollection(stops,
   _countof(stops),
   collection.GetAddressOf()));
wrl::ComPtr<ID2D1LinearGradientBrush> brush;
HR(target->CreateLinearGradientBrush(D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES(),
   collection.Get(),
   brush.GetAddressOf()));

借助 dx.h 就直觀了許多:

          GradientStop stops[] =
{
  GradientStop(0.0f, COLOR_WHITE),
  GradientStop(1.0f, COLOR_BLUE),
};
auto collection = target.CreateGradientStopCollection(stops);
auto brush = target.CreateLinearGradientBrush(collection);

雖然這並未大幅縮短圖 1 所示代碼的長度,但這段代碼顯然更易編寫,首次使用也不易出錯,何況還有 IntelliSense 的幫助。該庫采用了多種方法來構建令人愉悅的編程模型。在本例中,我們使用了一個函數模板來重載 CreateGradientStopCollection 方法,從而縮減了編譯時的 GradientStop 數組的大小,因此,無需使用 _countof 宏。

如何進行錯誤處理?構建此類精簡編程模型的先決條件之一就是采用實現錯誤傳播的異常。請考慮我之前提到的 Direct3D Device AsDxgi 方法的定義:

          inline auto Device::AsDxgi() const -> Dxgi::Device2
{
  Dxgi::Device2 result;
  HR(m_ptr.CopyTo(result.GetAddressOf()));
  return result;
}

這是庫中的一種極為常見的模式。首先,請注意,該方法為常量類型。庫中的幾乎所有方法都為常量類型,這是因為唯一的數據成員是底層的 ComPtr,我們無需修改它。在方法體中,您可以看到最終的 Device 對象是如何產生的。庫中的所有類都提供移動語義,因此,即使這看似需要執行一系列復制操作(通過推理,是一系列 AddRef/Release 對),但實際上不會在運行時產生任何此類操作。代碼中間將表達式括起來的 HR 是一個內聯函數,它會在結果不為 S_OK 時引發異常。最後,庫總是嘗試返回最具體的類,以避免調用方不得不執行額外的 QueryInterface 調用來公開更多功能。

上面的示例使用了 ComPtr CopyTo 方法,它實際上只是調用了 QueryInterface。下面是來自 Direct2D 的另一個示例:

          inline auto BitmapBrush::GetBitmap() const -> Bitmap
{
  Bitmap result;
  (*this)->GetBitmap(result.GetAddressOf());
  return result;
}

這個示例有些不同之處:它直接調用了位於底層 COM 接口上的方法。事實上,這種模式構成了庫中的大部分代碼。此處,我返回的是畫筆繪制所用的位圖。許多 Direct2D 方法與此處的示例一樣返回 void,因此,無需借助 HR 函數檢查結果。但是,對 GetBitmap 方法的間接調用可能並不那麼明顯。

我在開發這個庫的早期版本時,不得不在依靠編譯器取巧還是依靠 COM 取巧之間作出選擇。我的早期嘗試包括借助 C++ 的模板取巧,特別是類型特性,但也包括編譯器類型特性(也稱作內部類型特性)。起初,這很有趣,但事實很快證明,我是在自找麻煩。

您會發現,該庫將 COM 接口之間的從屬關系模型構建為具體的類。COM 接口只能直接繼承另一個接口。除 IUnknown 自身之外,每個 COM 接口都必須直接繼承另一個接口。最終,這導致所有方式又回到了 IUnknown 類型層次結構。開始時,我為每個 COM 接口定義了一個類。RenderTarget 類包含 ID2D1RenderTarget 接口指針。DeviceContext 類包含 ID2D1DeviceContext 接口指針。這看似十分合理,但當您需要將 DeviceContext 用作 RenderTarget 時,情況發生了改變。畢竟,ID2D1DeviceContext 接口派生自 ID2D1RenderTarget 接口。因此,將 DeviceContext 作為引用參數傳遞給接收 RenderTarget 的方法順理成章。

但遺憾的是,C++ 類型系統不這樣認為。使用這種方法時,DeviceContext 實際上無法派生自 RenderTarget,否則它會保有兩個引用。我曾試圖將移動語義和內部類型特性結合起來,以根據需要正確地移動引用。我差點就取得了成功,但有些情況會引入額外的 AddRef/Release 對。最終,事實證明這太過復雜,我需要一種更簡潔的解決方案。

與 C++ 不同,COM 擁有定義明確的二進制協定。畢竟,這才是 COM 的真正意義。只要遵守約定規則,COM 絕不會讓您失望。可以這麼說,您可以通過 COM 取巧,並使用 C++ 來增強您的優勢,而不是跟它對著干。這意味著,每個 C++ 類並不保有強類型化的 COM 接口指針,而只是一個指向 IUnknown 的泛型引用。C++ 會將類型安全這一特性補回來,包括其針對類繼承的規則(以及最新的移動語義),讓您能夠再次將這些 COM“對象”用作 C++ 類。從概念上講,我開始時這樣做:

class RenderTarget { ComPtr<ID2D1RenderTarget> ptr; };
class DeviceContext { ComPtr<ID2D1DeviceContext> ptr; };

但最終得到的是:

          class Object { ComPtr<IUnknown> ptr; };
class RenderTarget : public Object {};
class DeviceContext : public RenderTarget {};

由於 COM 接口及其關系所暗示的邏輯層次結構現在被一個 C++ 對象模型具體化了,因此,這整個編程模型將極其自然而實用。關於這個庫,還有很多功能值得研習,因此,我鼓勵您仔細查閱一下源代碼。到撰寫本文時為止,該庫已涵蓋幾乎所有 Direct2D 和 Windows 動畫管理器,以及一些 DirectWrite、Windows 圖像處理組件 (WIC)、Direct3D 和 DXGI 的有用代碼塊。另外,我會定期添加更多功能,所以也請您不時回來看看。請盡情體驗吧!

下載代碼示例

查看本欄目

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved