UWP中的Direct2D


介紹

DirectX一直是Windows平台中高性能圖形的代名詞,自Win7開始,微軟又推出了Direct2D技術,包裝於Direct3D,但專注於2D圖形,並且准備取代GDI這樣的傳統2D圖形技術。對於Direct2D是怎么怎么好的具體描述,可以參考附錄1.

不過Direct2D是基於COM技術的,看上去有些老舊的氣息,而且是非托管的,似乎也和常見的.net語言有些隔閡。

不過微軟也為我們提供了一個工具,一個跨越這一邊界的工具,那就是SurfaceImageSource一族。該族中SurfaceImageSource繼承自Windows.UI.Xaml.Media.ImageSourceVirtualSurfaceImageSource則繼承自SurfaceImageSource,它們和BitmapSource在托管領域有着同樣的地位。但同時這兩個類又將觸角伸到了COM的領域,分別可以query interface至ISurfaceImageSourceNativeIVirtualSurfaceImageSourceNative,再與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

上文已經提到了,SurfaceImageSourceWindows.UI.Xaml.Media.ImageSource的子類,可以像使用BitmapSource一樣使用它,賦給BitmatBrushImageSource什么的。這有點像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類:

https://msdn.microsoft.com/zh-cn/library/windows/apps/windows.ui.xaml.media.imaging.surfaceimagesource.aspx

[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;
};
}
View Code

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

MainPage.xaml見正文。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM