WPFとDirect3D9Exの連携 --- 導入

WPFDirect3Dレンダリング結果を貼り付けたい場合、D3DImageクラスが利用できます。

CPU経由でBitmapSourceを作成するのに比べ、D3DImageはGPU内で完結するため、より効率的です。

今回はD3DImageを利用するところまでを実装したいと思います。

C++/CLI用ComPtr

C++/CLIのクラスではC++のクラスをメンバーに持てず、生のポインタを自分で管理する必要があります。

そこでComPtrをC++/CLIクラスのメンバで管理できるクラスを作成します。

このクラスをスタックで利用(gcnewなし)することにより、スコープから抜けた時や、親クラスのDisposeが呼ばれたときに破棄されるようになります。

最低限のものですので、コピーコンストラクタなどは適宜実装してください。

template <class T>
ref class CliComPtr
{
public:
    !CliComPtr() { Release(); }
    ~CliComPtr() { CliComPtr::!CliComPtr(); }
    T* operator->() { return _p; }
    void operator = (T* p)
    {
        Release();
        _p = p;
        _p->AddRef();
    }
private:
    T* _p;
    void Release()
    {
        if (_p != nullptr)
        {
            _p->Release();
            _p = nullptr;
        }
    }
};


Device作成

WPFはDirect3D9しか利用できない為、IDirect3DDevice9Exを作成しています。

バックバッファは利用しない為、サイズは適当です。

HResult確認用に、AssertHRマクロを定義してあります。

// CComPtr用
#include <atlcomcli.h>
#pragma comment(lib, "oleaut32.lib")

// D3D9Ex用
#include <d3d9.h>
#pragma comment(lib, "d3d9.lib")

using namespace System;

#define AssertHR(expr) { HRESULT hr = expr; if (FAILED(hr)) throw gcnew ComponentModel::Win32Exception(hr, #expr); }

CComPtr<IDirect3DDevice9Ex> CreateDevice()
{
    CComPtr<IDirect3D9Ex> d3d9Ex;
    AssertHR(Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9Ex.p));

    CComPtr<IDirect3DDevice9Ex> device;
    D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = TRUE;
    d3dpp.BackBufferHeight = 1;
    d3dpp.BackBufferWidth = 1;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    AssertHR(d3d9Ex->CreateDeviceEx(
        D3DADAPTER_DEFAULT,
        D3DDEVTYPE_HAL,
        NULL,
        D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED | D3DCREATE_FPU_PRESERVE,
        &d3dpp,
        NULL,
        &device.p));

    return device;
}


RenderTarget作成

フォーマットはX8R8G8B8で作成しています。(byte列だとBGRA)

CComPtr<IDirect3DSurface9> CreateRenderTarget(IDirect3DDevice9Ex* device, int width, int height)
{
    CComPtr<IDirect3DSurface9> surface;
    AssertHR(device->CreateRenderTarget(
        width,
        height,
        D3DFMT_X8R8G8B8,
        D3DMULTISAMPLE_NONE,
        0,
        FALSE,
        &surface.p,
        NULL));

    return surface;
}


Renderer作成

今回は赤く塗りつぶすだけの実装になっています。

public ref class Renderer
{
public:
    property IntPtr Surface { IntPtr get() { return IntPtr(_surface); } }
    Renderer(int width, int height)
    {
        _device = CreateDevice();
        _surface = CreateRenderTarget(_device, width, height);

        AssertHR(_device->SetRenderTarget(0, _surface));
        AssertHR(_device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0xff, 0, 0), 1.0f, 0));
    }
private:
    CliComPtr<IDirect3DDevice9Ex> _device;
    CliComPtr<IDirect3DSurface9> _surface;
};


WPF

Xaml上では、Imageコントロールのみを置いています。

C#側でD3DImageを作成し、Image.Sourceにセットしています。

D3DImageではフロントバッファの有効時にも画像が更新されるように実装しています。

Xaml

<Window x:Class="D3DSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="250">
    <Grid>
        <Image x:Name="image" />
    </Grid>
</Window>

C#

int width = 100;
int height = 100;
var renderer = new Renderer(width, height);

image.Source = CreateD3DImage(width, height, renderer.Surface);
D3DImage CreateD3DImage(int width, int height, IntPtr surface)
{
    var d3dImage = new D3DImage();
    Action updateImage = () =>
    {
        if (d3dImage.IsFrontBufferAvailable == false)
            return;
        d3dImage.Lock();
        d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface);
        d3dImage.AddDirtyRect(new Int32Rect(0, 0, width, height));
        d3dImage.Unlock();
    };
    d3dImage.IsFrontBufferAvailableChanged += (s, e) => updateImage();
    updateImage();

    return d3dImage;
}

f:id:any-programming:20170502152556p:plain