WPFDXInterop - HexagramNM/NM_WindowCaptureVirtualCamera GitHub Wiki

WPFDXInteropの使い方

今回、ウィンドウキャプチャのトリミング設定画面を作るために、DirectXのテクスチャをWPFの画面に貼り付ける必要がありました。DirectX 9であれば、WPFの標準の機能で実現できるらしいですが、WinRTのWindowsGraphicsCapture APIがDirectX 11を前提としており、標準の機能では対応できません。探したところ、WPFDXInteropというDirectX 11用にMicrosoft公式から出ているMITライセンスのWPFの拡張を見つけたので、今回これを採用しました。nuget経由でインストールできます。

WPFDXInterop

DirectXのテクスチャをWPFに貼り付けるまでの流れ

WPFDXInteropを導入するとD3D11Imageというクラスを追加できます。このクラスのインスタンスはWPFのImageクラスの画像ソースに設定できます。設定するとD3D11Imageに描画された内容が画像として表示されます。

コードの詳細についてはNM_WindowCaptureVirtualCamera/MainWindow.xaml.csをご覧ください。

D3D11ImageImageのソースに設定する

D3D11Imageクラスのインスタンスを作り、これをImageクラスのSourceプロパティに代入すれば設定できます。また、D3D11ImageSetPixelSizeメソッドで画像サイズの設定もできます。

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のレンダリングイベント設定

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へのレンダリング

D3D11Imageのレンダリングイベントでは、引数からIDXGIResourceにアクセスするためのポインタがnintとして取得できます。

private void PreviewWindow_OnRender(nint resourcePtr, bool isNewSurface)
{
    NM_WindowCapture.CopyCapturePreviewToDXGIResource(captureObj, resourcePtr);
}

取得できたIDXGIResourceへの書き込みはNM_WindowCapture(C++側)で行います。NM_WindowCaptureにはすでにプレビュー用にキャプチャしたウィンドウを描画したテクスチャをもっています。以下のように、共有ハンドルを用いてIDXGIResourceID3D11Resourceとして扱えるようにし、この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);
}

戻る

⚠️ **GitHub.com Fallback** ⚠️