LoginSignup
14
16

More than 3 years have passed since last update.

WPFのコントロールにDirectX12で描画する

Last updated at Posted at 2020-02-27

モチベーション

モーショングラフィックスエンジン的なものを作りたい。
グラフィックスの描画はDXRを見越してDirectX12が使いたい。
でも、周りのUIはグラフィックスとは分離して、MVC的なフレームワークで作りたい。

Unityでいいじゃんって話なのですが…

やりたいこと

WPFで色々なパラメータやリソースを表示編集できるWindowの中に、DirextXが描画するコントロールを置きたい。
コメント 2020-02-27 144421.png

ポリ一枚表示しただけですし、スライダーで動くわけでもなければリサイズにも対応していませんが、
一応上記画像のようにできました。

D3DImage/D3D11Image(今回は不採用)

WPFにはD3DImage/D3D11Imageというクラスが用意されており、それぞれDirextX9/11向けのRenderTargetを提供しています。
WPFDXInterop

DirectX12向けのクラスは提供されていませんが、DirectX11-12間でテクスチャリソースの共有が可能です。
DirectX11のWPFDXInteropでは、WPFControlからIDXGIResourceが手に入るので、それからID3D11Texture2Dを取得し、RenderTargetViewを作成し描画します。
DirectX12の場合も同様にIDXGIResourceからID3D12Resource1を取得してRenderTargetViewを作成して描画すればいいはずです。
(DirectX12ではTextureやConstantBufferなどのインターフェイスとしての垣根がなくなり、ID3D12Resource1に統一されました。)

しかし、いざ実装してみようとすると、SwapChain使わない場合のやり方などがよくわからなかったので、確実なHwndHostを使ってみました。
ID3D12CommandQueue::ExecuteCommandListsの後、IDXGISwapChain::Presentを呼ばずに、描画完了も待たずにリターンすればいいのでしょうか?WPFコントロールから渡されるIDXGIResourceはバックバッファなんですかね?とすると、描き切れなかったら半端なRTが表示されちゃう???

HwndHost(WPF Control)とは

DirectX12のサンプルなどでは生Win32でGUIを生成して、ルートのWindowに対して描画をしています。
Win32のGUIというのは、以下のようなもので前半部がWindowの初期化、後半部がいわゆるUpdateやイベント(メッセージ)処理です。
HWNDという変数が出てきますが、これはFormやWPFでいうWindow/FormやControlに相当するもので文字通りのウインドウからボタンまでWindowとして扱い、HWNDで管理します。
DirectXにHWNDを指定して描画させる場合、このWindowの矩形領域内に描画することになります。

main.cpp
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nCmdShow)
{
    WNDCLASS winc; // WNDCLASSの設定
    HWND hwnd = CreateWindow(
        TEXT("MYCLASSNAME"), TEXT("Title"),
        WS_OVERLAPPEDWINDOW | WS_VISIBLE,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, hInstance, NULL
    );

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        DispatchMessage(&msg);
        theApp.Render();
    }
    return msg.wParam;
}

それで、HwndHostはこのWINAPIの枠を提供してくれます。
実質的にはWindowの矩形領域を提供してくれる&HWNDを保持管理してくれます。

class MyControl : HwndHost
{
    protected virtual HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        // 初期化処理(ただし、この時点ではまだ矩形情報が取れない!)
        // CreateWindowとかして、帰ってきたHWNDを返す
    }
    protected virtual IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // Win32の後半のメッセージループの部分
        // DirectXのUpdate(Render)処理はここで
    }
    protected virtual void DestroyWindowCore(HandleRef hwnd)
    {
        // 終了処理
    }
}

ここにうまいことDirectX12の初期化処理とUpdate(Render)処理を挟みます。
この方法の最大の利点は、Win32で作る場合とほとんど変わらない…というかWin32との互換性維持のための機能という点です。
DirextXはそもそもゲームを作るためのAPIなので外部GUIは不要でWindowいっぱいに描けばいいので、手に入るサンプルやノウハウも大抵の場合は生Win32のルートWindowに描いています。それらを参考にできます。

ということで、このHwndHostとWin32とDirectX12合体させてみたのがこちらになります。
DirectX12の処理は「DirectX12 Programming Vol.1」の付録のサンプルコードを流用させていただきました。

DX.cs
class DX : HwndHost
{
    [Flags] enum WindowStyle : int { /* 省略 */ }

    IntPtr app = IntPtr.Zero;

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        // Win32のWindowの初期化
        IntPtr hwnd = CreateWindowEx(
            0, "STATIC", "",
            WindowStyle.WS_CHILD | WindowStyle.WS_VISIBLE,
            0, 0,
            (int)ActualWidth, (int)ActualHeight,
            hwndParent.Handle,
            (IntPtr)WindowStyle.HOST_ID,
            IntPtr.Zero, 0);
        return new HandleRef(this, hwnd);
    }

    protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (app == IntPtr.Zero)
        {
            // DirectX12の初期化
            // BuildWindowCoreでInitしたかったが、は矩形が0のままなのでDepthBufferが作れない。
            // 本当はリサイズも考慮してデバイスのInitとRenderTarger/DepthBufferの生成を分けるべき。
            app = Init(hwnd, (int)ActualWidth, (int)ActualHeight);
        }

        // DirectX12の描画(のリクエスト)処理
        Render(app);

        handled = false;
        return IntPtr.Zero;
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        // Win32のWindowとDirectX12の終了処理
        DestroyWindow(hwnd.Handle);
        Dispose(app);
    }

    [DllImport("user32.dll")]
    static extern IntPtr CreateWindowEx( /* 省略 */ );
    [DllImport("user32.dll")]
    static extern bool DestroyWindow(IntPtr hwnd);

    [DllImport("02_SimpleTriangle.dll")]
    static extern IntPtr Init(IntPtr hwnd, int width, int height);
    [DllImport("02_SimpleTriangle.dll")]
    static extern void Render(IntPtr app);
    [DllImport("02_SimpleTriangle.dll")]
    static extern IntPtr Dispose(IntPtr app);
}
Export.cpp
TriangleApp* Init(HWND hwnd, int width, int height)
{
    auto app = new TriangleApp(); // TriangleApp.h
    app->Initialize(hwnd, width, height);
    return app;
}

void Render(TriangleApp* app)
{
    app->Render();
}

void Dispose(TriangleApp* app)
{
    app->Cleanup();
    delete app;
}
MainWindow.xaml
<Window> <!-- 属性は省略 -->
    <Grid>
        <local:DX Margin="18,43,342,122"></local:DX>
        <WrapPanel Margin="527,101,53,281">
            <TextBlock Text="Hogehoge"></TextBlock>
        </WrapPanel>
        <Slider Margin="517,149,39,231"></Slider>
    </Grid>
</Window>

DLLImportするときはImportするDLLが参照しているDLLもexeのディレクトリに並べないとDllNotFoundExceptionが出ます。
この例では、02_SimpleTriangle.dlldxcompiler.dllなどを参照していてハマりました。

全文は こちら(Github)

技術領域問題

WPFのコントロールは一枚のDirectX9で描かれています。そこに無理やりWin32のウインドウを乗っけてそこにDirextX12で描画している訳です。
なのでWPFコントロールでサンドイッチすることができません。

詳しくは 技術領域の概要(MSDN)

余談

DirectX12の情報…少なすぎ!!
最近のMSの動向としてはDirectXを触るのは本当に限られた場合だけで、基本的にはUnityとか使ってね!君たちはDirectXなんて知らなくていいよ!ってスタンスですし、いまだにDirectX9が現役(?)ですからね。ゲームの専門学校などでもいまだにDirectX9と聞きます。確かにシンプルで初学向けなのかもしれませんが。
MSDNは情報「量」だけは結構ありますし、「DirectX12 Programming Vol.1」のおかげで基本的な使い方は理解できましたが、少し外れたことを、応用したいと思うと似たような例が見つかりませんね。

参考

14
16
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
16