D3DImageを使って黒い画像を表示してみる
はじめに
WPFで動画や高速な画像を表示する方法を調べると、Media FoundationやEVRなど、さまざまな技術が登場します。しかし、それらの記事では最初から複雑な構成になっていることが多く、「結局D3DImageって何なの?」という疑問を持ったまま読み進めることになりがちです。
そこで本シリーズでは、できるだけシンプルなサンプルから順番に説明していきます。
第1回では、Direct3D9Exで黒い画像を作成し、それをWPFのD3DImageへ表示するだけのサンプルを作成します。
まずは、「DirectXで作成した画像がWPFへ表示される仕組み」を理解することを目的とします。
D3DImageとは?
D3DImageは、Direct3D9で作成した画像をWPF上へ表示するためのクラスです。
通常、WPFで画像を表示する場合は、次のようなクラスを利用します。
- Image
- BitmapSource
- WriteableBitmap
これらはCPU上の画像データを表示するための仕組みです。
一方、動画や高速な画像処理ではGPU上のテクスチャをそのまま表示したい場合があります。
そのような用途で利用されるのがD3DImageです。
D3DImageは画像データを保持しているわけではありません。
IDirect3DSurface9を参照し、その内容をWPFへ表示します。
イメージとしては次のようになります。
Direct3D9
│
▼
Texture
│
▼
IDirect3DSurface9
│
▼
D3DImage
│
▼
Imageコントロール
つまり、WPFはGPU上のサーフェスを直接表示していることになります。
なぜDirect3D9を使うのか?
最近ではDirect3D11やDirect3D12を利用する機会が多くなっています。
しかし、WPFのD3DImageが表示できるのはDirect3D9のサーフェスです。
そのため、最終的にD3DImageへ表示する場合は、Direct3D9Exを利用する必要があります。
Direct3D11を利用する場合でも、共有テクスチャを介してDirect3D9へ受け渡す構成になるのが一般的です。
今回作成するプログラム
今回は非常にシンプルな構成です。
C++
│
├─ Direct3D9Ex 初期化
├─ Texture作成
├─ 黒で塗りつぶす
└─ Surface取得
│
▼
D3DImage
│
▼
Image表示
単に黒い画像を作成して表示するだけです。
XAMLによる画面レイアウトの作成とD3DImageの設定
MainWindow.xaml
表示用のImageコントロールを配置します。
<Window x:Class="D3DImageSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Image x:Name="preview" Stretch="Fill"/>
</Grid>
</Window>
MainWindow.xaml.cs
コンストラクタではD3DImageを生成し、ImageコントロールのSourceへ設定します。
public partial class MainWindow : Window
{
private D3DImage? d3dimg;
private bool backBufferInitialized = false;
public MainWindow()
{
InitializeComponent();
d3dimg = new D3DImage();
d3dimg.IsFrontBufferAvailableChanged += d3dimg_IsFrontBufferAvailableChanged;
preview.Source = d3dimg;
}
private void d3dimg_IsFrontBufferAvailableChanged(
object? sender,
DependencyPropertyChangedEventArgs e)
{
if (d3dimg!.IsFrontBufferAvailable)
{
// フロントバッファが無効になった
backBufferInitialized = false;
}
}
}
この時点では、まだ何も表示されません。
D3DImageは表示先となるIDirect3DSurface9が設定されて初めて画像を表示できるようになります。
今回は、Direct3D9Exを扱う部分はC++/CLIで作成したライブラリへ実装します。
WPF(C#)からは、このC++/CLIライブラリのクラスを呼び出すことで、Direct3D9Exの初期化やIDirect3DSurface9の生成を行います。
役割を分けると、次のような構成になります。
WPF (C#)
│
│ D3DImage
│
▼
C++/CLI ライブラリ
│
├─ Direct3D9Ex 初期化
├─ Device 作成
├─ Texture 作成
├─ Surface 作成
└─ Surface を C# へ渡す
この構成にすることで、WPF側は画面表示に専念でき、Direct3Dに関する処理をC++側へまとめることができます。
以降は、このC++/CLIライブラリを実装しながら、Direct3D9Exの初期化と表示用のSurfaceを作成していきます。
Direct3Dの初期化、表示用のSurface作成
D3DPreview.cpp
まずは、Direct3D9Exの初期化と表示用のSurfaceを作成します。
今回は、D3DPreviewクラスのInitialize()関数で初期化を行います。
bool D3DPreview::Initialize(int width, int height)
{
// Direct3D9Exを生成
Direct3DCreate9Ex(D3D_SDK_VERSION, &m_d3d_ex);
// Device作成
D3DPRESENT_PARAMETERS pp = {};
pp.Windowed = TRUE;
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
pp.BackBufferFormat = D3DFMT_UNKNOWN;
pp.BackBufferWidth = 1;
pp.BackBufferHeight = 1;
pp.BackBufferCount = 1;
m_d3d_ex->CreateDeviceEx(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
GetDesktopWindow(),
D3DCREATE_HARDWARE_VERTEXPROCESSING |
D3DCREATE_MULTITHREADED |
D3DCREATE_FPU_PRESERVE,
&pp,
nullptr,
&m_d3d_device_ex);
// GPU上のレンダリングターゲットを作成
m_d3d_device_ex->CreateTexture(
width,
height,
1,
D3DUSAGE_RENDERTARGET,
D3DFMT_A8R8G8B8,
D3DPOOL_DEFAULT,
&m_d3d_texture9,
nullptr);
// D3DImageへ渡すSurfaceを取得
m_d3d_texture9->GetSurfaceLevel(
0,
&m_d3d_surface9);
// CPUからアクセス可能なSurfaceを作成
m_d3d_device_ex->CreateOffscreenPlainSurface(
width,
height,
D3DFMT_A8R8G8B8,
D3DPOOL_SYSTEMMEM,
&m_d3d_sys_surface9,
nullptr);
m_width = width;
m_height = height;
return true;
}
ここでは、表示に必要となる3つのオブジェクトを作成しています。
- GPU上のレンダリングターゲット (
Texture) -
D3DImageへ渡すSurface - CPUから書き込みを行うための
SYSTEMMEM Surface
GPU上のTextureはCPUから直接アクセスできないため、一旦SYSTEMMEM Surfaceへ画像を書き込み、後からGPUへ転送する構成としています。
黒い画像を作成する
GPU上のレンダリングターゲットはCPUから直接アクセスできません。
そのため、一旦CPUアクセス可能なSYSTEMMEM Surfaceへ画像を書き込み、その後GPUへ転送します。
流れは次のようになります。
SYSTEMMEM Surface
│
│ LockRect()
▼
CPUで黒く塗りつぶす
│
▼
UpdateSurface()
│
▼
GPU Surface
今回はSYSTEMMEM Surfaceを黒で塗りつぶし、UpdateSurface()でGPUへ転送します。
D3DPreview.cpp
void D3DPreview::ClearSurface()
{
D3DLOCKED_RECT lockedRect;
m_d3d_sys_surface9->LockRect(
&lockedRect,
nullptr,
0);
for (UINT y = 0; y < m_height; y++)
{
BYTE* line =
static_cast<BYTE*>(lockedRect.pBits) +
y * lockedRect.Pitch;
for (UINT x = 0; x < m_width; x++)
{
// A8R8G8B8 (1画素4バイト)
line[x * 4 + 0] = 0x00; // B
line[x * 4 + 1] = 0x00; // G
line[x * 4 + 2] = 0x00; // R
line[x * 4 + 3] = 0xff; // A
}
}
m_d3d_sys_surface9->UnlockRect();
// GPU上のSurfaceへ転送
m_d3d_device_ex->UpdateSurface(
m_d3d_sys_surface9,
nullptr,
m_d3d_surface9,
nullptr);
}
A8R8G8B8形式では、アルファ値(A)も1バイト含まれています。
今回は黒色を表示したいので、RGBは0x00、アルファ値は0xff(不透明)を設定しています。
最後にUpdateSurface()を呼び出すことで、CPUから書き込んだ画像をGPU上のSurfaceへ転送できます。
作成したSurfaceをD3DImageへ渡す
ここまでで、Direct3D9Ex上に表示用のIDirect3DSurface9を作成し、黒い画像を書き込むことができました。
次に、このSurfaceをD3DImageへ渡します。
D3DPreview.cpp
まず、作成したSurfaceを取得するための関数を用意します。
void* D3DPreview::GetSurface()
{
return m_d3d_surface9;
}
MainWindow.xaml.cs
UpdatePreview()では、D3DImageへSurfaceを設定し、表示の更新を通知します。
private void UpdatePreview()
{
if (d3dimg.IsFrontBufferAvailable)
{
unsafe
{
d3dimg.Lock();
if (!backBufferInitialized)
{
IntPtr surface = (IntPtr)previewControl.GetSurface();
d3dimg.SetBackBuffer(
D3DResourceType.IDirect3DSurface9,
surface,
true);
backBufferInitialized = true;
}
d3dimg.AddDirtyRect(
new Int32Rect(
0,
0,
d3dimg.PixelWidth,
d3dimg.PixelHeight));
d3dimg.Unlock();
}
}
}
SetBackBuffer()では、C++側で作成したIDirect3DSurface9をD3DImageへ関連付けます。
一度関連付けが完了すると、以降はAddDirtyRect()を呼び出すだけで、Surfaceの内容がWPFへ反映されます。
これで、C++側で作成した黒い画像がWPFのImageコントロールへ表示されます。
まとめ
今回は、D3DImageの基本的な仕組みを理解するために、WPFでDirectXの画像を表示するシンプルなサンプルを作成しました。
内容は次の通りです。
-
D3DImageとは何か - Direct3D9Exで画像を作成する
- CPUで画像を書き込む方法
- GPUへ転送する方法
- WPFへ表示を反映する方法
本記事では、D3DImageの動作の流れを分かりやすくすることを目的としているため、エラー処理やリソースの解放処理、C++/CLIのインターフェース実装など、一部のコードは省略しています。
実際のアプリケーションでは、適切なエラー処理やリソース管理を実装してください。
次回予告
今回作成した黒い画像の代わりに、USBカメラから取得したNV12画像を表示してみます。
Media Foundationで取得した画像をDirect3Dへ転送し、WPFへ表示するところまでを紹介する予定です。