WPFDXInterop - HexagramNM/NM_WindowCaptureVirtualCamera GitHub Wiki
今回、ウィンドウキャプチャのトリミング設定画面を作るために、DirectXのテクスチャをWPFの画面に貼り付ける必要がありました。DirectX 9であれば、WPFの標準の機能で実現できるらしいですが、WinRTのWindowsGraphicsCapture APIがDirectX 11を前提としており、標準の機能では対応できません。探したところ、WPFDXInteropというDirectX 11用にMicrosoft公式から出ているMITライセンスのWPFの拡張を見つけたので、今回これを採用しました。nuget経由でインストールできます。
WPFDXInteropを導入するとD3D11Image
というクラスを追加できます。このクラスのインスタンスはWPFのImage
クラスの画像ソースに設定できます。設定するとD3D11Image
に描画された内容が画像として表示されます。
コードの詳細についてはNM_WindowCaptureVirtualCamera/MainWindow.xaml.cs
をご覧ください。
D3D11Image
クラスのインスタンスを作り、これをImage
クラスのSource
プロパティに代入すれば設定できます。また、D3D11Image
のSetPixelSize
メソッドで画像サイズの設定もできます。
NM_WindowCaputureVirtualCameraでは、以下の設定をメインウィンドウのコンストラクタで行っています。
windowPreview = new D3D11Image();
// D3D11Imageの画像サイズの設定
int previewWidth = NM_WindowCapture.GetCapturePreviewWidth();
int previewHeight = NM_WindowCapture.GetCapturePreviewHeight();
windowPreview.SetPixelSize(previewWidth, previewHeight);
// image_windowPreview: WPFのImageクラス(xaml側で設定)
image_windowPreview.Source = windowPreview;
D3D11Image
のインスタンスにウィンドウオーナーやイベントを設定する必要があります。 この設定はメインウィンドウのLoaded
イベントで行う必要がありました。 コンストラクタで設定すると、ウィンドウ周りの設定が完了していないためか例外が発生しました。
// ウィンドウハンドルを取得し、ウィンドウオーナーの設定
var helper = new System.Windows.Interop.WindowInteropHelper(this);
windowPreview.WindowOwner = helper.Handle;
// D3D11Imageのレンダリング時のイベントの設定
windowPreview.OnRender += PreviewWindow_OnRender;
// WPFの画面がレンダリングされる直前に呼び出されるイベントの設定
CompositionTarget.Rendering += CompositionTarget_Render;
CompositionTarget.Rendering
に設定したイベントでは、以下のようにD3D11Image
のインスタンスにレンダリング要求を送る必要があります。
private void CompositionTarget_Render(object? sender, EventArgs e)
{
RenderingEventArgs args = (RenderingEventArgs)e;
if (lastRender != args.RenderingTime)
{
// D3D11Imageにレンダリング要求を投げる。
windowPreview.RequestRender();
// 中略
lastRender = args.RenderingTime;
}
}
D3D11Image
のレンダリングイベントでは、引数からIDXGIResource
にアクセスするためのポインタがnint
として取得できます。
private void PreviewWindow_OnRender(nint resourcePtr, bool isNewSurface)
{
NM_WindowCapture.CopyCapturePreviewToDXGIResource(captureObj, resourcePtr);
}
取得できたIDXGIResource
への書き込みはNM_WindowCapture(C++側)で行います。NM_WindowCaptureにはすでにプレビュー用にキャプチャしたウィンドウを描画したテクスチャをもっています。以下のように、共有ハンドルを用いてIDXGIResource
をID3D11Resource
として扱えるようにし、このID3D11Resource
にテクスチャの内容をコピーすることで描画しています。このコピーにはDirectXのデバイスコンテキストにあるCopySubresourceRegion
メソッドを使用します。
また、 複数スレッドで同時にテクスチャにアクセスすることがないよう、std::lock_guard
で排他処理もしておきます。 コードの詳細はNM_WindowCapture/NM_WindowCapture.cpp
をご覧ください。
void NM_WindowCapture::CopyCapturePreviewToDXGIResource(void* resourcePtr)
{
HRESULT hr = S_OK;
if (resourcePtr == nullptr)
{
return;
}
// 取得したポインタを一度IUnknownのポインタにstatic_castし、QueryInterfaceでIDXGIResourceのポインタに変換
IUnknown* pUnk = static_cast<IUnknown*>(resourcePtr);
winrt::com_ptr<IDXGIResource> pDxgiResource;
hr = pUnk->QueryInterface(IID_PPV_ARGS(pDxgiResource.put()));
if (FAILED(hr))
{
return;
}
// IDXGIResourceから共有ハンドルを取得
HANDLE sharedHandle;
hr = pDxgiResource->GetSharedHandle(&sharedHandle);
if (FAILED(hr))
{
return;
}
// 共有ハンドルからID3D11Resourceのポインタを取得。
// これによりD3D11Imageの描画用バッファに、DirectX 11の仕組みで書き込むことができるようになる。
winrt::com_ptr<ID3D11Resource> pD3D11Resource;
hr = _dxDevice->OpenSharedResource(sharedHandle, IID_PPV_ARGS(pD3D11Resource.put()));
if (FAILED(hr))
{
return;
}
D3D11_BOX range;
range.front = 0;
range.back = 1;
range.left = 0;
range.right = VCAM_VIDEO_WIDTH;
range.top = 0;
range.bottom = VCAM_VIDEO_HEIGHT;
// ID3D11Resourceにテクスチャの内容をコピー
// _captureWindowLock: std::mutexの変数
std::lock_guard lock(_captureWindowLock);
_dxDeviceContext->CopySubresourceRegion(pD3D11Resource.get(), 0, 0, 0, 0,
_capturePreviewTexture.get(), 0, &range);
}