介紹
DirectX一直是Windows平台中高性能圖形的代名詞,自Win7開始,微軟又推出了Direct2D技術,包裝於Direct3D,但專注於2D圖形,並且准備取代GDI這樣的傳統2D圖形技術。對於Direct2D是怎么怎么好的具體描述,可以參考附錄1.
不過Direct2D是基於COM技術的,看上去有些老舊的氣息,而且是非托管的,似乎也和常見的.net語言有些隔閡。
不過微軟也為我們提供了一個工具,一個跨越這一邊界的工具,那就是SurfaceImageSource一族。該族中SurfaceImageSource繼承自Windows.UI.Xaml.Media.ImageSource,VirtualSurfaceImageSource則繼承自SurfaceImageSource,它們和BitmapSource在托管領域有着同樣的地位。但同時這兩個類又將觸角伸到了COM的領域,分別可以query interface至ISurfaceImageSourceNative和IVirtualSurfaceImageSourceNative,再與Direct2D技術接軌。至此,XAML快速的界面技術,Direct2D高效的圖形功能,得以合二為一。
本文將簡單的介紹一下SurfaceImageSource的使用,為大家呈現一個高效圖形應用的小例子(演示代碼使用XAML和C++/CX)。
准備
代碼主要是C++的(略有C++/CX擴展),因為要操作Direct2D和COM。大家可以根據需要自行包裝自己的組件來調用。
用到了WIC(Windows Imaging Component)等技術,不過不是本文重點。
問題
熟悉WPF的讀者可能想到,在classical desktop中使用的WPF,里面的一部分組件有一個神奇的屬性,OpacityMask,利用它可以給控件的渲染顯示加上一個蒙版,實現各種透明漸變和不規則輪廓等等。
不過到了UWP(更早的從Windows Store App出現開始),雖然大家寫的還是一樣的XAML,但是OpacityMask屬性沒了。估計是為了性能考慮吧,不給用這么繁瑣的東西了。
但難免有時要用到這樣的功能,我們可以依靠高效的Direct2D圖形技術來實現它。
看看效果先:


托管的SurfaceImageSource
上文已經提到了,SurfaceImageSource是Windows.UI.Xaml.Media.ImageSource的子類,可以像使用BitmapSource一樣使用它,賦給BitmatBrush的ImageSource什么的。這有點像WriteableBitmap,不過它卻是操作Direct2D的入口。
我們先用XAML做一個這樣的界面:
1 <Page x:Class="App2.MainPage" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:local="using:App2" 5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 6 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 7 mc:Ignorable="d"> 8 9 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> 10 <Grid.RowDefinitions> 11 <RowDefinition Height="50" /> 12 <RowDefinition Height="*" /> 13 </Grid.RowDefinitions> 14 15 <Grid.Resources> 16 <Style TargetType="Button"> 17 <Setter Property="Margin" Value="10,0,0,0" /> 18 </Style> 19 </Grid.Resources> 20 21 <StackPanel HorizontalAlignment="Center" 22 Orientation="Horizontal" 23 x:Name="btns"> 24 <Button Tag="Assets/star.png">Star</Button> 25 <Button Tag="Assets/ellipse.png">Ellipse</Button> 26 </StackPanel> 27 28 <Rectangle Grid.Row="1" x:Name="canvas" /> 29 </Grid> 30 </Page>
Button的Tag記載的是用來做蒙版的圖片,我們的例子里使用的圖片,為了方便都是400*400的。並且這兩個蒙版圖片都是用黑白表示的。黑色表示沒有,白色表示全有,灰色就是半透明了,操作的是Alpha通道。

這分別是要顯示的圖片,和兩種蒙版。
先看看MainPage聲明了哪些成員:
1 Microsoft::WRL::ComPtr<IWICImagingFactory> m_factory; // WIC工廠,因為多處使用,可以復用一下 2 Microsoft::WRL::ComPtr<IDXGIDevice> m_dxgiDevice; // DXGI Device 3 Microsoft::WRL::ComPtr<ID2D1DeviceContext> m_d2dDeviceContext; // D2D Device Context 4 Microsoft::WRL::ComPtr<IWICBitmapSource> m_img; // 這個就是要被蒙版處理的原始圖片了
我們善用ComPtr,讓C++的RAII機制(資源獲取就是初始化)來幫我們實現簡單的“垃圾回收”。
在MainPage::CreateDevice函數中,我們初始化Direct2D的相關設備:
void MainPage::CreateDevice() { static D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1 }; HRESULT hr; ComPtr<ID3D11Device> d3dDevice; HR(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, 0, D3D11_CREATE_DEVICE_BGRA_SUPPORT, // 注意,Direct2D畫圖是BGRA通道順序,不是常見的RGB featureLevels, extent<decltype(featureLevels)>::value, // C++ type traits,獲取array長度 D3D11_SDK_VERSION, &d3dDevice, nullptr, nullptr)); HR(d3dDevice.As(&m_dxgiDevice)); HR(D2D1CreateDevice(m_dxgiDevice.Get(), nullptr, &m_d2dDevice)); HR(m_d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &m_d2dDeviceContext)); }
然后我們看看主要的畫圖流程:
這是那兩個有蒙版設置的button,它們的Tag屬性記錄了要應用的蒙版的路徑。
void MainPage::OnButtonClick(Object^ sender, RoutedEventArgs^ e) { HRESULT hr; auto btn = safe_cast<Button^>(sender); auto maskPath = btn->Tag->ToString(); SurfaceImageSource^ sis = ref new SurfaceImageSource(400, 400); // SurfaceImageSource創建時就必須指定大小,這個大小相當於畫紙的大小 ComPtr<ISurfaceImageSourceNative> sisn; // 跨越托管和非托管的邊界 // 轉換成IUnknown*也可以 HR(reinterpret_cast<IInspectable*>(sis)->QueryInterface(IID_PPV_ARGS(&sisn))); HR(sisn->SetDevice(m_dxgiDevice.Get())); // 這里需要注意,盡管SurfaceImageSource本身是個ImageSource,但是我們也應該在ImageBrush的層面上完成操作。如果我們將ImageBrush留在XAML上,而只新建和替換(在之后的流程里)SurfaceImageSource,會發生SurfaceImageSource賦值給ImageSource后引用計數增加量,和將ImageSource設為nullptr后引用計數減少量不相等的情況,發生“內存泄漏”。 auto brush = ref new ImageBrush(); brush->ImageSource = sis; canvas->Fill = brush; Draw(sisn.Get(), maskPath->Data()); }
Draw函數。畫圖的操作我們需要在UI線程上完成:
void MainPage::Draw(ISurfaceImageSourceNative* sisn, const wchar_t* mask) { HRESULT hr; ComPtr<IDXGISurface> surface; RECT rect = { 0, 0, 400, 400 }; POINT renderTargetOffset; // 可視區域在surface中的偏移量 // 可以想象成surface是一張大畫板,比我們的顯示區域400*400要大。每次Direct2D會選擇一個區域來畫,不一定是(0,0),因為可能有一些緩沖策略,使得每次畫圖的區域都不一樣 HR(PrepareDraw(sisn, rect, &surface, &renderTargetOffset)); // 創建所有我們需要的圖形 ComPtr<IWICBitmapSource> maskSrc = GetMask(LoadImageByWIC(mask).Get()); ComPtr<ID2D1Bitmap> maskBmp; ComPtr<ID2D1Bitmap> imgBmp; ComPtr<ID2D1BitmapBrush> imgBrush; ComPtr<ID2D1Bitmap1> tgrBmp; // Note ID2D1Bitmap1 HR(m_d2dDeviceContext->CreateBitmapFromWicBitmap(maskSrc.Get(), &maskBmp)); HR(m_d2dDeviceContext->CreateBitmapFromWicBitmap(m_img.Get(), &imgBmp)); HR(m_d2dDeviceContext->CreateBitmapBrush(imgBmp.Get(), &imgBrush)); HR(m_d2dDeviceContext->CreateBitmapFromDxgiSurface(surface.Get(), nullptr, &tgrBmp)); m_d2dDeviceContext->SetTarget(tgrBmp.Get()); m_d2dDeviceContext->BeginDraw(); m_d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Translation(renderTargetOffset.x, renderTargetOffset.y)); // 應用可視區域的偏移量來調整device context的位置 m_d2dDeviceContext->Clear({0, 0, 1, 1}); // 將畫布填充成藍色,讓我們的改變變得明顯一些 m_d2dDeviceContext->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); // 必須先設置這個,才能調用下面的函數 m_d2dDeviceContext->FillOpacityMask(maskBmp.Get(), imgBrush.Get()); // 應用蒙版 HR(m_d2dDeviceContext->EndDraw()); m_d2dDeviceContext->SetTarget(nullptr); HR(sisn->EndDraw()); }
以下是我們的一些輔助函數:
嘗試開始畫圖。這會確定我們需要畫圖的區域(在surface上)。如果這個開始調用失敗了,我們檢測一下原因,嘗試第二次。
HRESULT MainPage::PrepareDraw(ISurfaceImageSourceNative* sisn, const RECT& updateRect, IDXGISurface** surface, POINT* offset) { HRESULT hr; hr = sisn->BeginDraw(updateRect, surface, offset); if ((hr == DXGI_ERROR_DEVICE_REMOVED) || (hr == DXGI_ERROR_DEVICE_RESET)) { CreateDevice(); // 設備有更改,並不是單純的失敗,重新創建設備,再試一次 return PrepareDraw(sisn, updateRect, surface, offset); } else { return hr; } }
ComPtr<IWICBitmapSource> MainPage::LoadImageByWIC(const wchar_t* file)函數,通過WIC加載圖片,既加載我們的原圖像,也加載蒙版圖像。
ComPtr<IWICBitmapSource> MainPage::LoadImageByWIC(const wchar_t* file) { ComPtr<IWICBitmapDecoder> decoder; ComPtr<IWICBitmapFrameDecode> frame; ComPtr<IWICFormatConverter> converter; HRESULT hr; HR(m_factory->CreateDecoderFromFilename(file, nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder)); HR(decoder->GetFrame(0, &frame)); HR(m_factory->CreateFormatConverter(&converter)); HR(converter->Initialize( frame.Get(), GUID_WICPixelFormat32bppPBGRA, // 這個預處理的BGRA,因為兩個透明圖層疊加時需要把RGB通道和Alpha通道相乘相加,而預處理就是預先把相乘的步驟完成了,可以增加一點效率 WICBitmapDitherTypeNone, nullptr, 0, WICBitmapPaletteTypeCustom)); return converter; }
MainPage::GetMask函數把黑白色的圖片處理成用alpha通道表示透明度的bitmap。因為只有黑白色的話,顏色是通過RGB通道確定的,alpha通道一直是1.0。而direct2D提供的API呢,卻是使用的alpha通道來進行蒙版應用。這樣看似合理一些,但我們生成一張利用alpha通道來表現透明度的蒙版圖片,肯定是要比我們用單純的黑白色來表現蒙版要麻煩一些的。
ComPtr<IWICBitmapSource> MainPage::GetMask(IWICBitmapSource* src) { uint32_t width, height; src->GetSize(&width, &height); size_t len = width * 4 * height; unique_ptr<byte[]> pixels(new byte[len]); src->CopyPixels(nullptr, width * 4, len, pixels.get()); for (size_t i = 0; i < width * height; i++) { pixels[i * 4 + 3] = (pixels[i * 4] + pixels[i * 4 + 1] + pixels[i * 4 + 2]) / 3; } ComPtr<IWICBitmap> bmp; m_factory->CreateBitmapFromMemory(width, height, GUID_WICPixelFormat32bppPBGRA, width * 4, len, pixels.get(), &bmp); return bmp; }
用Direct2D的好處
圖形應用(包括圖像處理,地圖),游戲,這些特殊的應用需要一個強悍的圖形技術來支撐它們的運作和體驗,而Direct2D無疑為我們提供了這樣的可能,讓我們能在XAML之中,發揮圖形技術的強大威力。
擴展
對於SurfaceImageSource,除了轉換成ISurfaceImageSourceNative接口外,還能轉換成ISurfaceImageSourceNativeWithD2D接口,區別就在於withD2D的這一個,可以在后台線程上畫圖,只要在UI線程上刷新就可以了。
SurfaceImageSource還有一個子類,VirtualSurfaceImageSource,它主要是起虛擬化的作用,用於圖像區域比可視區域大的情況,比如地圖。
附錄
[1] 關於Direct2D:
https://msdn.microsoft.com/zh-cn/library/windows/desktop/dd370987(v=vs.85).aspx
[2] DirectX 和 XAML 互操作
https://msdn.microsoft.com/zh-cn/library/windows/apps/hh825871.aspx
[3] SurfaceImageSource類:
[4] ISurfaceImageSourceNative接口:
https://msdn.microsoft.com/zh-cn/library/windows/apps/hh848322.aspx
完整代碼
MainPage.xaml.h
// // MainPage.xaml.h // Declaration of the MainPage class. // #pragma once #include "MainPage.g.h" namespace App2 { /// <summary> /// An empty page that can be used on its own or navigated to within a Frame. /// </summary> public ref class MainPage sealed { public: MainPage(); void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; private: void OnButtonClick(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void CreateDevice(); Microsoft::WRL::ComPtr<IWICBitmapSource> LoadImageByWIC(const wchar_t* file); Microsoft::WRL::ComPtr<IWICBitmapSource> GetMask(IWICBitmapSource* src); void Draw(ISurfaceImageSourceNative* sisn, const wchar_t* mask); HRESULT PrepareDraw(ISurfaceImageSourceNative* sisn, const RECT& updateRect, IDXGISurface** surface, POINT* offset); Microsoft::WRL::ComPtr<IWICImagingFactory> m_factory; Microsoft::WRL::ComPtr<IDXGIDevice> m_dxgiDevice; Microsoft::WRL::ComPtr<ID2D1DeviceContext> m_d2dDeviceContext; Microsoft::WRL::ComPtr<IWICBitmapSource> m_img; }; }
MainPage.xaml.cpp
// // MainPage.xaml.cpp // Implementation of the MainPage class. // #include "pch.h" #include "MainPage.xaml.h" using namespace App2; using namespace Platform; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Media::Imaging; using namespace Windows::UI::Xaml::Navigation; using namespace Microsoft::WRL; using namespace std; #define HR(exp) hr = exp; assert(SUCCEEDED(hr)) // The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 MainPage::MainPage() { InitializeComponent(); } void MainPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) { for (UIElement^ uiElem : btns->Children) { auto btn = safe_cast<Button^>(uiElem); btn->Click += ref new RoutedEventHandler(this, &MainPage::OnButtonClick); } CreateDevice(); HRESULT hr; HR(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_factory))); m_img = LoadImageByWIC(L"Assets/img.png"); } void MainPage::CreateDevice() { static D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1 }; HRESULT hr; ComPtr<ID3D11Device> d3dDevice; ComPtr<ID2D1Device> d2dDevice; HR(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, 0, D3D11_CREATE_DEVICE_BGRA_SUPPORT, featureLevels, extent<decltype(featureLevels)>::value, D3D11_SDK_VERSION, &d3dDevice, nullptr, nullptr)); HR(d3dDevice.As(&m_dxgiDevice)); HR(D2D1CreateDevice(m_dxgiDevice.Get(), nullptr, &d2dDevice)); HR(d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &m_d2dDeviceContext)); } ComPtr<IWICBitmapSource> MainPage::LoadImageByWIC(const wchar_t* file) { ComPtr<IWICBitmapDecoder> decoder; ComPtr<IWICBitmapFrameDecode> frame; ComPtr<IWICFormatConverter> converter; HRESULT hr; HR(m_factory->CreateDecoderFromFilename(file, nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder)); HR(decoder->GetFrame(0, &frame)); HR(m_factory->CreateFormatConverter(&converter)); HR(converter->Initialize( frame.Get(), GUID_WICPixelFormat32bppPBGRA, // Pre-multipled BGRA WICBitmapDitherTypeNone, nullptr, 0, WICBitmapPaletteTypeCustom)); return converter; } ComPtr<IWICBitmapSource> MainPage::GetMask(IWICBitmapSource* src) { uint32_t width, height; src->GetSize(&width, &height); size_t len = width * 4 * height; unique_ptr<byte[]> pixels(new byte[len]); src->CopyPixels(nullptr, width * 4, len, pixels.get()); for (size_t i = 0; i < width * height; i++) { // BGRA. The average of RGB channels is used as Alpha channel. pixels[i * 4 + 3] = (pixels[i * 4] + pixels[i * 4 + 1] + pixels[i * 4 + 2]) / 3; } ComPtr<IWICBitmap> bmp; m_factory->CreateBitmapFromMemory(width, height, GUID_WICPixelFormat32bppPBGRA, width * 4, len, pixels.get(), &bmp); return bmp; } void MainPage::OnButtonClick(Object^ sender, RoutedEventArgs^ e) { HRESULT hr; auto btn = safe_cast<Button^>(sender); auto maskPath = btn->Tag->ToString(); SurfaceImageSource^ sis = ref new SurfaceImageSource(400, 400); ComPtr<ISurfaceImageSourceNative> sisn; HR(reinterpret_cast<IInspectable*>(sis)->QueryInterface(IID_PPV_ARGS(&sisn))); HR(sisn->SetDevice(m_dxgiDevice.Get())); // set device as DXGI device. // Note this auto brush = ref new ImageBrush(); brush->ImageSource = sis; canvas->Fill = brush; Draw(sisn.Get(), maskPath->Data()); } void MainPage::Draw(ISurfaceImageSourceNative* sisn, const wchar_t* mask) { HRESULT hr; ComPtr<IDXGISurface> surface; RECT rect = { 0, 0, 400, 400 }; POINT renderTargetOffset; // view port offset in surface. HR(PrepareDraw(sisn, rect, &surface, &renderTargetOffset)); ComPtr<IWICBitmapSource> maskSrc = GetMask(LoadImageByWIC(mask).Get()); ComPtr<ID2D1Bitmap> maskBmp; ComPtr<ID2D1Bitmap> imgBmp; ComPtr<ID2D1BitmapBrush> imgBrush; ComPtr<ID2D1Bitmap1> tgrBmp; // Note ID2D1Bitmap1 HR(m_d2dDeviceContext->CreateBitmapFromWicBitmap(maskSrc.Get(), &maskBmp)); HR(m_d2dDeviceContext->CreateBitmapFromWicBitmap(m_img.Get(), &imgBmp)); HR(m_d2dDeviceContext->CreateBitmapBrush(imgBmp.Get(), &imgBrush)); HR(m_d2dDeviceContext->CreateBitmapFromDxgiSurface(surface.Get(), nullptr, &tgrBmp)); m_d2dDeviceContext->SetTarget(tgrBmp.Get()); m_d2dDeviceContext->BeginDraw(); m_d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Translation(renderTargetOffset.x, renderTargetOffset.y)); m_d2dDeviceContext->Clear({0, 0, 1, 1}); // Clear with blue color. m_d2dDeviceContext->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); // Must set antialias mode. m_d2dDeviceContext->FillOpacityMask(maskBmp.Get(), imgBrush.Get()); HR(m_d2dDeviceContext->EndDraw()); m_d2dDeviceContext->SetTarget(nullptr); HR(sisn->EndDraw()); } HRESULT MainPage::PrepareDraw(ISurfaceImageSourceNative* sisn, const RECT& updateRect, IDXGISurface** surface, POINT* offset) { HRESULT hr; hr = sisn->BeginDraw(updateRect, surface, offset); if ((hr == DXGI_ERROR_DEVICE_REMOVED) || (hr == DXGI_ERROR_DEVICE_RESET)) { CreateDevice(); // Device changed, try again. return PrepareDraw(sisn, updateRect, surface, offset); } else { return hr; } }
MainPage.xaml見正文。
