Direct3D ワールドでの DirectShow ムービー
Mike Borozdin
February 2003
適用対象:
Microsoft DirectShow
Microsoft Direct3D
Microsoft DirectX 9.0
要約: DirectShow または Direct3D アプリケーション開発者の観点から、Direct 3D 環境でビデオを表示する方法について説明します。
目次
はじめに
VMR-9 のアーキテクチャ
アロケータ-プレゼンタの実装
VMR とフィルタ グラフの構成
関連情報
はじめに
Microsoft® DirectX® 9.0 の最も重要な新機能の 1 つが、以前から期待されていたビデオとグラフィックスのレンダリング パイプラインの統合です。最近まで、Microsoft® DirectShow® は、ビデオを主に Microsoft DirectDraw® を通してハードウェア オーバーレイ サーフェイスにレンダリングするか、システム メモリ内で GDI を通してレンダリングするかのどちらかでした。
Microsoft Windows® XP で Video Mixing Renderer 7 (VMR-7) が導入されました。これにより、オフスクリーンの DirectDraw 7 サーフェイスへのレンダリングが可能になりましたが、これは Microsoft Direct3D® 8.0 インターフェイスとの互換性を持っていませんでした。さらに最近では、DirectX 9.0 において VMR-9 が導入されました。これは DirectX がサポートしているすべてのプラットフォーム上で利用できるもので、Direct3D サーフェイスを使ってビデオ フレームのレンダリングを行います。
VMR-9 にはいくつかの利点があります。第 1 に、Direct3D が提供しているすべてのトランスフォーム処理を利用できます。たとえば、ビデオ ストリームをピクセル シェーダに通して、カスタム ビデオ エフェクトを得る、というようなことが簡単にできます。実際、VMR-9 は強力な使いやすいデジタル信号プロセッサ (DSP) であると言えます。また、ビデオをテクスチャ上にレンダリングすることも可能です。回転する立方体の 1 つの面の上でビデオを再生するというような単純なシナリオを想像してください。ゲーム開発者にとっては、使いにくいカット シーンの形でビデオ要素を利用する必要がなくなったという利点があります。現在では、ビデオを 3D グラフィックスと組み合わせ、グラフィックスと同じようにダイナミックでインタラクティビティを備えたビデオ クリップが作成できます。
第 2 に、VMR-9 ではビデオをユーザー インターフェイス (UI) と統合するのが簡単になります。ダイナミックな UI を作成できます。カラー キーイングを使って、静的な UI 要素をビデオにオーバレイする必要はなくなりました。
Direct3D との統合に加え、VMR が提供するその他の重要な新機能には以下のものがあります。
- 複数のビデオ ストリームを同時にレンダリングする能力。
- 最新のデインターレース ハードウェアのサポート。テレビや DV ビデオなどのインターレース ソースをプログレッシブ スキャン モニタで表示するときには、プログレッシブ イメージを作るために、インターレースされたフィールドをデインターレースする必要があります。グラフィックス処理ユニット (GPU) なら、高度なデインターレース テクニックをリアルタイムで提供できます。VMR により、アプリケーションはこれらの提供されているテクニックを自由に選んで使えるようになります。
- 色相・彩度・輝度・コントラストを制御する新しいハードウェアのサポート。これらはプロセス アンプリフィケーションまたは ProcAmp と呼ばれます。ProcAmp は、ユーザーが各自のモニタ上で調節するグローバルな設定ではなくなりました。3D ワールドでは、グローバルなコントラストと輝度の機能は一般に望ましくありません。ProcAmp のサポートは、VMR-9 のミキシング、アルファ ブレンディング、およびカラー キーイング機能との組み合わせにより、ムービーのさまざまなフェードイン/フェードアウトを可能にします。
この記事では以下のトピックを扱います。
- 「VMR-9 のアーキテクチャ」。VMR-9 フィルタのプラグイン コンポーネントについて説明します。
- 「アロケータ-プレゼンタの実装」。VMR-9 用のカスタム アロケータ-プレゼンタを作成する方法について説明します。
- 「Direct3D デバイスの作成」。3-D レンダリング プロセスの最初のステップ、すなわち Direct3D デバイス オブジェクトの作成について説明します。
- 「サーフェイスの割り当て」。 3-D レンダリング プロセスの次のステップ、すなわちビデオ イメージを格納するための 1 つまたは複数の Direct3D サーフェイスの割り当てについて説明します。
- 「イメージの表示」。 3-D レンダリング プロセスの最後のステップ、すなわちイメージの表示について説明します。
- 「VMR-9 とフィルタ グラフの構成」。 アプリケーションがビデオをレンダリングするためにどのように VMR-9フィルタを構成し、フィルタ グラフを作成するかについて説明します。
VMR-9 のアーキテクチャ
VMR-7 を使ったことがある人ならば、VMR-9 にはすぐに慣れることができるでしょう。VMR-9 のアーキテクチャはVMR-7と基本的に同じです。このアーキテクチャについての詳しい説明は SDK ドキュメントにあるので、ここでは簡単に触れるだけにします。
VMR-9 はレンダラ フィルタです。これはビデオ ストリームの終端にあるフィルタ グラフの中にあります。アプリケーションのカスタム コンポーネントで置き換え可能な、2 つのプラグイン コンポーネントが含まれています。
- アロケータ-プレゼンタ。VMR-9 が使う Direct3D サーフェイスを割り当て、VMR-9 がビデオ フレームを格納したら、これらのサーフェイスを表示します。
- イメージ コンポジタ。ビデオ入力ストリームがどのようにミキシングされるかを制御します (この文章では、イメージ コンポジタについては解説しません)。
この文章では、デフォルトのアロケータ-プレゼンタをカスタム アロケータ-プレゼンタに置き換える方法について説明します。このカスタム アロケータ-プレゼンタが D3D テクスチャ サーフェイス上に描画したビデオ ストリームを、回転する正方形の平面上にレンダリングします。
アロケータ-プレゼンタの実装
カスタム アロケータ-プレゼンタを使うために、アプリケーションは以下の処理を行います。
- カスタム アロケータ-プレゼンタ オブジェクトのインスタンスを作成します。
- VMR-9 の構成を行います。
- フィルタ グラフを作成します。
ただし、これらのステップについて説明する前に、アロケータ-プレゼンタ オブジェクトそのものの実装について説明しておきましょう。
SDK のサンプル VMR9Allocator では、カスタム アロケータ-プレゼンタは CAllocator という名前の C++ クラスとして実装されています。このクラスは、3 つの基本的なタスクを実行します。
- Direct3D デバイスの作成
- サーフェイスの割り当て
- イメージの表示
Direct3D デバイスの作成
カスタム アロケータ-プレゼンタの最初のタスクは、Direct3D 環境のセットアップです。Direct3D プログラミングに慣れている人にとっては、このプロセスは比較的簡単です。まず Direct3D オブジェクトを作成し、このオブジェクトを使って Direct3D デバイスを作成します。CAllocator クラスはこれらの処理をクラス コンストラクタの中で実行します。次のコードに示すように、CreateDevice メンバ関数が呼び出されます。
CAllocator::CAllocator(HRESULT& hr, HWND wnd, IDirect3D9* d3d,
IDirect3DDevice9* d3dd)
: m_refCount(1), m_D3D(d3d), m_D3DDev(d3dd), m_window( wnd )
{
CAutoLock Lock(&m_ObjectLock);
hr = E_FAIL;
if ( IsWindow( wnd ) == FALSE )
{
hr = E_INVALIDARG;
return;
}
if ( m_D3D == NULL )
{
ASSERT( d3dd == NULL );
m_D3D.Attach( Direct3DCreate9(D3D_SDK_VERSION) );
if (m_D3D == NULL) {
hr = E_FAIL;
return;
}
}
if ( m_D3DDev == NULL )
{
hr = CreateDevice();
}
}
HRESULT CAllocator::CreateDevice()
{
m_D3DDev = NULL;
D3DPRESENT_PARAMETERS pp;
ZeroMemory(&pp, sizeof(pp));
pp.Windowed = TRUE;
pp.hDeviceWindow = m_window;
pp.SwapEffect = D3DSWAPEFFECT_COPY;
return m_D3D->CreateDevice(0, D3DDEVTYPE_HAL, m_window,
D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED,
&pp, &m_D3DDev.p);
}
この例で、IDirect3D9::CreateDevice に対して指定されているフラグは、ウィンドウ モードと D3DSWAPEFFECT_COPY を指定しています。必要ならば、別のフラグを使うこともできます。ただし、VMR-9 は別スレッド上で実行されているので、D3DCREATE_MULTITHREADED フラグは必須です。
サーフェイスの割り当て
アロケータ-プレゼンタにとっての次の大きなタスクは、VMR-9 用のサーフェイスの割り当てです。サーフェイスの割り当ては難しい場合があります。デコーダはさまざまなビデオ フォーマットを出力し、グラフィックス カードのフォーマット サポートはまちまちだからです。サーフェイスの割り当てを行うときの目標は、3-D プリミティブへの描画が可能なビデオ イメージを作成することです。このためには、上流フィルタ、ハードウェア、Direct3D のフォーマット変換能力を考慮に入れる必要があります。
サーフェイスはアロケータ-プレゼンタ オブジェクトの InitializeDevice メソッド内に作成されます。このメソッドは、VMR-9 が上流デコーダとの間でピン接続のネゴシエーションを行うときに呼び出されるもので、次のシグニチャを持っています。
STDMETHODIMP InitializeDevice(
DWORD_PTR
dwUserID,
VMR9AllocationInfo *lpAllocInfo,
DWORD *lpNumBuffers
);
dwUserID パラメータは VMR-9 インスタンスを識別します。この例では、インスタンスは 1 つしかないので、dwUserID は無視できます。VMR9AllocationInfo 構造体はいくつかの情報を含んでいます。VMR は、上流フィルタ (通常はビデオ デコーダ) から提供されたフォーマットに基づいて、これらの情報を格納します。dwWidth および dwHeight メンバはビデオのサイズを含んでおり、Format メンバは Direct3D フォーマット タイプを示し、dwFlags メンバはサーフェイスを記述します。
たとえば、VMR9AllocFlag_TextureSurface フラグはテクスチャ サーフェイスを、VMR9AllocFlag_3DRenderTarget フラグはレンダー ターゲットを指定します。lpNumBuffers パラメータは、作成するサーフェイスの数を指定します。
VMR-9 は、上流デコーダが受け付けるサーフェイスを必要とします。ビデオ デコーダと Direct3D デバイスとの互換性がある Direct3D サーフェイスを作成するときには、以下のステップの実行を推奨します。
- IVMRSurfaceAllocatorNotify::SetD3DDevice を呼び出して、VMR-9 に対して Direct3D デバイスとモニタについての情報を通知します。
- テクスチャ サーフェイスを要求するために、VMR9AllocationInfo 構造体に VMR9AllocFlag_TextureSurface フラグを追加します。
- サーフェイスを割り当てます。この操作に成功したら、S_OK を返します。
- 上記のステップが失敗し、上流フィルタがサーフェイスをレンダ ターゲットとして要求しなかった場合には、オフスクリーン サーフェイスの作成を試みます。
次のコードは、CAllocator クラスがこれらのステップをどのように実装しているかを示しています。
HRESULT CAllocator::InitializeDevice(DWORD_PTR dwUserID,
VMR9AllocationInfo *lpAllocInfo, DWORD *lpNumBuffers)
{
if (lpNumBuffers == NULL)
{
return E_POINTER;
}
if ( m_lpIVMRSurfAllocNotify == NULL )
{
return E_FAIL;
}
HRESULT hr = m_lpIVMRSurfAllocNotify->SetD3DDevice( m_D3DDev,
MonitorFromWindow( m_window, MONITOR_DEFAULTTOPRIMARY ) );
if (FAILED(hr))
{
return hr;
}
// テクスチャを作成する。さもないと、サーフェイスをプリミティブ上に
// テクスチャとして描画することができない。「テクスチャ」フラグを追加。
lpAllocInfo->dwFlags |= VMR9AllocFlag_TextureSurface;
DeleteSurfaces(); // Release any surfaces that we allocated
previously.
// 以前に割り当てたすべてのサーフェイスを解放する。
m_surfaces.resize(*lpNumBuffers);
// VMR-9 に対し、サーフェイスを割り当てるよう要求する。
hr = m_lpIVMRSurfAllocNotify->AllocateSurfaceHelper(
lpAllocInfo, lpNumBuffers, & m_surfaces.at(0) );
// この呼び出しが失敗した場合には、プライベート テクスチャを作成する。
// デコードしたビデオ フレームをプライベート テクスチャにコピーすることになる。
if (FAILED(hr) &&
!(lpAllocInfo->dwFlags & VMR9AllocFlag_3DRenderTarget))
{
DeleteSurfaces();
// フォーマットは YUV か?
if (lpAllocInfo->Format > '0000')
{
D3DDISPLAYMODE dm;
hr = m_D3DDev->GetDisplayMode(NULL, & dm );
if (SUCCEEDED(hr))
{
// プライベート テクスチャを作成する。
hr = m_D3DDev->CreateTexture(
lpAllocInfo->dwWidth,
lpAllocInfo->dwHeight,
1,
D3DUSAGE_RENDERTARGET,
dm.Format,
D3DPOOL_DEFAULT,
& m_privateTexture.p, NULL );
}
if (FAILED(hr))
{
return hr;
}
}
lpAllocInfo->dwFlags &= ~ VMR9AllocFlag_TextureSurface;
lpAllocInfo->dwFlags |= VMR9AllocFlag_OffscreenSurface;
hr = m_lpIVMRSurfAllocNotify->AllocateSurfaceHelper(
lpAllocInfo, lpNumBuffers, & m_surfaces.at(0) );
if (FAILED(hr))
{
return hr;
}
}
return m_scene.Init(m_D3DDev);
}
VMR9AllocationInfo 構造体の情報を、正しいサーフェイスを割り当てるための一連の Direct3D 呼び出しに変換するのは、簡単な作業ではありません。幸いなことに、VMR-9 はこの作業の大部分を自動的に行うヘルパ関数IVMRSurfaceAllocatorNotify9::AllocateSurfaceHelper を用意しています。利用可能なハードウェアの種類が多いため、少なくとも何らかのサーフェイスが作成されるように、1 つまたは複数のフォールバック アルゴリズムを用意しておくのがよいでしょう。サーフェイスのフォーマットが YUV フォーマットの場合、アロケータ-プレゼンタは、Direct3D プリミティブ上にテクスチャ化される RGB サーフェイスを作成しなくてはなりません。
イメージの表示
個々の入力ストリームについて、ビデオ デコーダはビデオ フレームを VMR-9 入力バッファに書き込みます。VMR-9 は各ストリームに ProcAmp 値を適用し、ストリームを合成し、必要ならばカラー キーイングとアルファ ブレディングを適用し、最後に合成されたイメージをアロケータ-プレゼンタに渡します。その後、VMR-9 はアロケータ-プレゼンタ オブジェクトの GetSurface メソッドを呼び出してデコーダのためのサーフェイスを取得し、イメージを表示するときになったら PresentImage 呼び出します。
イメージを表示するための正しい方法は、割り当てられたサーフェイスの特性に大きく依存します。このため、アロケータ-プレゼンタは割り当てと表示の両方を実行する1つのオブジェクトとして実装するのが一番です。3-D シーンを作成するという自明なタスク以外にも、アプリケーションはデバイス消失やウィンドウが別のモニタに移動したなどの状況を処理しなくてはなりません。
イメージを 3-D シーンにレンダリングするための方法としては、以下のステップを推奨します。
- ディスプレイの変更が進行中かどうかをチェックします。デバイスが変更された場合には、新しいデバイス上にサーフェイスを再作成します。
- デバイスがネイティブ ビデオ フォーマットでのテクスチャをサポートしておらず、プライベート テクスチャを作成していた場合には、次の作業を行います。
- IDirect3DTexture9::GetSurfaceLevel を呼び出してサーフェイスを取得します。.
- IDirect3DDevice9::StretchRect を呼び出して、VMR-9 のサーフェイスの拡大 縮小と色変換を行い、プライベート テクスチャに格納します。
- プライベート テクスチャを作成していなかった場合には、サーフェイスからテクスチャを取得します。
- シーンを描画し、ビデオをシーン内の何らかの頂点のセットへとテクスチャ化します。
- イメージを表示します。
- デバイス消失が起こった場合には、シーンのレンダリングを終了し、デバイスをリストアし、サーフェイスを再作成します。
次のコードは、CAllocator クラスが PresentImage メソッドの実装方法を示しています。
HRESULT CAllocator::PresentImage(
DWORD_PTR dwUserID,
VMR9PresentationInfo *lpPresInfo)
{
CAutoLock Lock(&m_ObjectLock);
// ディスプレイの変更が進行中かどうかをチェックする。
if ( NeedToHandleDisplayChange() )
{
// Direct3D デバイスを切り替える (コードは省略)。
}
HRESULT hr = PresentHelper( lpPresInfo ); // コードは下記を参照。
if ( hr == D3DERR_DEVICELOST)
{
// デバイスのリストアを試みる。
if (m_D3DDev->TestCooperativeLevel() == D3DERR_DEVICENOTRESET)
{
DeleteSurfaces();
hr = CreateDevice();
if (FAILED(hr))
{
return hr;
}
hr = m_lpIVMRSurfAllocNotify->ChangeD3DDevice( m_D3DDev,
MonitorFromWindow( m_window,MONITOR_DEFAULTTOPRIMARY ) );
if (FAILED(hr))
{
return hr;
}
}
hr = S_OK;
}
return hr;
}
HRESULT CAllocator::PresentHelper(VMR9PresentationInfo *lpPresInfo)
{
// パラメータの検証。
if ( ( lpPresInfo == NULL ) || ( lpPresInfo->lpSurf == NULL ) )
{
return E_POINTER;
}
CAutoLock Lock(&m_ObjectLock);
// サーフェイスからデバイスを取得する。
CComPtr<IDirect3DDevice9> device;
HRESULT hr = lpPresInfo->lpSurf->GetDevice(& device.p );
if (FAILED(hr))
{
return hr;
}
// プライベート テクスチャを作成していた場合には、デコードされたイメージをそこに blit する。
if ( m_privateTexture != NULL )
{
CComPtr<IDirect3DSurface9> surface;
hr = m_privateTexture->GetSurfaceLevel( 0 , & surface.p );
if (FAILED(hr))
{
return hr;
}
// サーフェイス全体をテクスチャのサーフェイスにコピーする。
hr = device->StretchRect( lpPresInfo->lpSurf, NULL, surface,
NULL, D3DTEXF_NONE );
if (FAILED(hr))
{
return hr;
}
hr = m_scene.DrawScene( device, m_privateTexture ) ;
if (FAILED(hr))
{
return hr;
}
}
else // テクスチャが VMR-9 によって割り当てられていた場合。
{
// サーフェイスからテクスチャを取得する。
CComPtr<IDirect3DTexture9> texture;
hr = lpPresInfo->lpSurf->GetContainer( IID_IDirect3DTexture9,
(LPVOID*) & texture.p ) );
if (FAILED(hr))
{
return hr;
}
hr = m_scene.DrawScene (device, texture );
if (FAILED(hr))
{
return hr;
}
}
hr = device->Present( NULL, NULL, NULL, NULL );
return hr;
}
VMR とフィルタ グラフの構成
VMR-9 は、3 つの動作モードをサポートしています。
ウィンドウ
この既定のモードでは、VMR-9 は別のウィンドウを作成し、そのウィンドウにビデオをレンダリングします。
ウィンドウレス
このモードでは、ビデオがアプリケーション ウィンドウに直接レンダリングされ、ビデオは実質的にウィンドウ上のコントロールとなるため、アプリケーションから細かい制御を行うことができます。
レンダーレス
このモードはカスタム アロケータ-プレゼンタ オブジェクトを使用します。
したがって、VMR-9 で CAllocator クラスを使用するためには、以下のステップを実行して、VMR-9 をレンダーレス モードに構成しなくてはなりません。
- VMR-9 フィルタを作成します。
- VMR-9 に対して IVMRFilterConfig9 インターフェイスを問い合わせます。
- レンダーレス モードに設定します。
- VMR-9 に対してカスタム アロケータ-プレゼンタ オブジェクトのことを通知します。また、カスタム アロケータ-プレゼンタ オブジェクトに対して VMR-9 のことを通知します。
次のコードはステップ 1~3 を示しています。
CComPtr<IBaseFilter> g_filter;
g_filter.CoCreateInstance(CLSID_VideoMixingRenderer9);
CComQIPtr<IVMRFilterConfig9> filterConfig(g_filter);
if (filterConfig)
{
filterConfig->SetRenderingMode( VMR9Mode_Renderless )
}
次のコードはステップ 4 を示しています。
HRESULT SetAllocatorPresenter(IBaseFilter *filter, HWND window)
{
HRESULT hr;
CComQIPtr<IVMRSurfaceAllocatorNotify9> lpIVMRSurfAllocNotify(filter);
if (!lpIVMRSurfAllocNotify)
{
return E_FAIL;
{
// アロケータ-プレゼンタ オブジェクトを作成する。
g_allocator.Attach(new CAllocator( hr, window ));
if ( FAILED( hr ) )
{
g_allocator = NULL;
return hr;
}
// VMR-9 に対して通知する。
hr = lpIVMRSurfAllocNotify->AdviseSurfaceAllocator(g_userId,
g_allocator );
if (FAILED(hr))
{
return hr;
}
// アロケータ-プレゼンタに対して通知する。
hr = g_allocator->AdviseNotify(lpIVMRSurfAllocNotify);
return hr;
}
DirectShow は幅広いフォーマットをサポートできるように設計されています。このため、DirectShow のグラフ作成メカニズムにはかなりの柔軟性があります。アプリケーションはフィルタを 1 つずつ接続することでグラフを作成することもできますし、インテリジェント接続を利用して DirectShow に最善のグラフを作成させることもできます。この例では、グラフに VMR-9 フィルタを手動で追加し、グラフの残りの部分はフィルタ グラフ マネージャに自動的にレンダリングさせるのが一番よいでしょう。このアプローチには、DirectShow がサポートしているほとんどのフォーマットとの互換性があります。
上のコードに示したように、VMR-9 を作成して構成を行ったら、IFilterGraph::AddFilter を呼び出してグラフにフィルタを追加します。その後、IGraphBuilder::RenderFile を呼び出してファイルをレンダリングします。
hr = g_graph->AddFilter(g_filter, L"Video Mixing Renderer 9");
if (SUCCEEEDED(hr))
{
hr = g_graph->RenderFile( wszPath, NULL )
}
RenderFile メソッドは、グラフ内の既存のレンダラを優先します。新しいレンダラを追加するのは、グラフ内のレンダらがファイルをレンダリングできない場合に限られます。この例のグラフ作成の詳細については、VMR9Allocator サンプルのコードを参照してください。
関連情報
Microsoft DirectShow の詳細については、DirectShow SDK ドキュメント(英語版)、あるいは日本語版をダウンロードして参照してください。