WPFのコントロールにDirectX12で描画する
モチベーション
モーショングラフィックスエンジン的なものを作りたい。
グラフィックスの描画はDXRを見越してDirectX12が使いたい。
でも、周りのUIはグラフィックスとは分離して、MVC的なフレームワークで作りたい。
Unityでいいじゃんって話なのですが…
やりたいこと
WPFで色々なパラメータやリソースを表示編集できるWindowの中に、DirextXが描画するコントロールを置きたい。
ポリ一枚表示しただけですし、スライダーで動くわけでもなければリサイズにも対応していませんが、
一応上記画像のようにできました。
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の矩形領域内に描画することになります。
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」の付録のサンプルコードを流用させていただきました。
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);
}
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;
}
<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.dll
がdxcompiler.dll
などを参照していてハマりました。
全文は こちら(Github)
技術領域問題
WPFのコントロールは一枚のDirectX9で描かれています。そこに無理やりWin32のウインドウを乗っけてそこにDirextX12で描画している訳です。
なのでWPFコントロールでサンドイッチすることができません。
詳しくは 技術領域の概要(MSDN)
余談
DirectX12の情報…少なすぎ!!
最近のMSの動向としてはDirectXを触るのは本当に限られた場合だけで、基本的にはUnityとか使ってね!君たちはDirectXなんて知らなくていいよ!ってスタンスですし、いまだにDirectX9が現役(?)ですからね。ゲームの専門学校などでもいまだにDirectX9と聞きます。確かにシンプルで初学向けなのかもしれませんが。
MSDNは情報「量」だけは結構ありますし、「DirectX12 Programming Vol.1」のおかげで基本的な使い方は理解できましたが、少し外れたことを、応用したいと思うと似たような例が見つかりませんね。
参考
Author And Source
この問題について(WPFのコントロールにDirectX12で描画する), 我々は、より多くの情報をここで見つけました https://qiita.com/up-hash/items/085314d0bab70bfb20bf著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .