ムービーファイルのテクスチャ化
ムービーファイルのテクスチャ化から利用までの手順は、以下のとおりです。
- DirectShowでムービーファイルを読み込む
- 読み込んだムービーファイルを、マルチスレッドでDirect3Dのテクスチャに書き込む
- Direct3Dのプログラミング側で作成したテクスチャを取得する
- あとは通常のDirect3Dでのテクスチャ利用法を用いる
制御クラスの概要
次のクラスで、ムービーテクスチャの生成、制御を行います。コメントの記法はDoxygenの記法を利用しています。
//! @class CMovieToTexture //! @brief ムービーテクスチャクラス class CMovieToTexture { CComPtr<IDirect3DDevice9> m_pd3dDevice; // Our rendering device CComPtr<IDirect3DTexture9> m_pTexture; // Our texture CComPtr<IGraphBuilder> m_pGB; // GraphBuilder CComPtr<IMediaControl> m_pMC; // Media Control CComPtr<IMediaPosition> m_pMP; // Media Position CComPtr<IMediaEvent> m_pME; // Media Event CComPtr<IBaseFilter> m_pRenderer; // our custom renderer //! @param 幅 LONG m_lWidth; //! @param 高さ LONG m_lHeight; //! @param ピッチ LONG m_lPitch; /*! @param 元のムービーのサイズ(幅、高さ) / テクスチャのサイズ(幅、高さ)で算出するUV値 */ FLOAT m_fu, m_fv; void CheckMovieStatus(void); void CleanupDShow(void); public: CMovieToTexture(); //コンストラクタ ~CMovieToTexture(); //デストラクタ /*! @fn HRESULT InitDShowTextureRenderer(WCHAR* wFileName, const BOOL bSound) @brief DirectShowからテクスチャへのレンダリングへの初期化 @param *pd3dDevice (in) Direct3Dデバイス @param *wFileName (in) ムービーファイルのパス @param bSound (in) サウンド再生フラグ @return 関数の成否 */ HRESULT InitDShowTextureRenderer(IDirect3DDevice9 * pd3dDevice, WCHAR* wFileName, const BOOL bSound); //! @fn IDirect3DTexture9 * GetTexture() //! @brief ムービーテクスチャの取得 //! @return ムービーがレンダリングされたテクスチャ HRESULT GetTexture(IDirect3DTexture9** pTexture){ return m_pTexture.CopyTo(pTexture); }; //! @fn VOID Play() //! @brief ムービーの再生 VOID Play(){ m_pMC->Run(); }; //! @fn VOID Stop() //! @brief ムービーの停止 VOID Stop(){ m_pMC->Stop(); }; //! @fn VOID SetSpeed(double time) //! @brief 再生スピードの変更 //! @param time (in) 再生スピードの倍率 VOID SetSpeed(double time){ m_pMP->put_Rate(time); }; //! @fn double GetStopTime() //! @brief 終了時間の取得 //! @return 終了時間 double GetStopTime() { REFTIME time; m_pMP->get_StopTime(&time); return time; }; //! @fn double GetDuration() //! @brief ストリームの時間幅の取得 //! @return 全ストリーム長 double GetDuration() { REFTIME time; m_pMP->get_Duration(&time); return time; } //! @fn double GetCurrentPosition() //! @brief 現在の再生位置の取得 //! @return 現在の再生位置 double GetCurrentPosition() { REFTIME time; m_pMP->get_CurrentPosition(&time); return time; } //! @fn VOID SetTime(double time) //! @brief 現在の再生位置を指定位置にセット //! @param time (in) セットしたい再生位置 VOID SetTime(double time){ m_pMP->put_CurrentPosition(time); }; /*! @fn VOID GetUV(FLOAT* u, FLOAT* v) @brief 再生するテクスチャのサイズとムービーの サイズが合わない場合の余剰分をカットす るためのUV値取得 @param *u (out) テクスチャ座標U @param *v (out) テクスチャ座標V */ VOID GetUV(FLOAT* u, FLOAT* v) { *u = m_fu; *v = m_fv; } /*! @fn VOID GetEvent(long* lEventCode, long* lParam1, long* lParam2, long msTimeout) @brief ムービーのイベント取得 @param lEventCode (out) イベント コードを受け取る変数へのポインタ @param lParam1 (out) 第 1 イベント引数を受け取る変数へのポインタ。 @param lParam2 (out) 第 2 イベント引数を受け取る変数へのポインタ。 @param msTimeout (in) タイムアウト時間 (ミリ秒単位)。 イベントが到着するまで動作を停止するには、INFINITE を使う。 */ VOID GetEvent(long* lEventCode, long* lParam1, long* lParam2, long msTimeout); };
InitDShowTextureRenderer()関数に、読み込むムービーテクスチャのファイル名を指定します。Play()とStop()関数で、ムービーファイルの再生と停止を行うことができます。そのほかの関数に関しては、後述の実装例で説明します。
初期化
初期化は、次のメンバ関数で行っています。引数には、Direct3Dのデバイスと、テクスチャ化したいムービーファイルのファイルパスを記述します。第3引数のBOOL値がTRUEの場合、ムービーファイルに音声が入っていれば再生します。
HRESULT CMovieToTexture::InitDShowTextureRenderer(
IDirect3DDevice9 * pd3dDevice, WCHAR* wFileName, const BOOL bSound)
{
HRESULT hr = S_OK;
CComPtr<IBaseFilter> pFSrc; // Source Filter
CComPtr<IPin> pFSrcPinOut; // Source Filter Output Pin
CMovieTexture *pCTR=0; // DirectShow Texture renderer
m_pd3dDevice = pd3dDevice;
// フィルタグラフの作成
if (FAILED(m_pGB.CoCreateInstance(
CLSID_FilterGraph, NULL, CLSCTX_INPROC)))
return E_FAIL;
#ifdef REGISTER_FILTERGRAPH
// Register the graph in the Running Object Table
// (for debug purposes)
AddToROT(m_pGB);
#endif
// Create the Texture Renderer object
pCTR = new CMovieTexture (NULL, &hr);
pCTR->SetDevice(m_pd3dDevice);
if (FAILED(hr) || !pCTR)
{
Msg(TEXT("Could not create texture renderer object! hr=0x%x"),
hr);
return E_FAIL;
}
// Get a pointer to the IBaseFilter on the TextureRenderer,
// add it to graph
m_pRenderer = pCTR;
if (FAILED(hr = m_pGB->AddFilter(
m_pRenderer, L"TextureRenderer")))
{
Msg(TEXT("Could not add renderer filter to graph! hr=0x%x"),
hr);
return hr;
}
// フィルタグラグにソースを追加
hr = m_pGB->AddSourceFilter (wFileName, L"SOURCE", &pFSrc);
// If the media file was not found, inform the user.
if (hr == VFW_E_NOT_FOUND)
{
Msg(TEXT("Could not add source filter to graph! ")
TEXT("(hr==VFW_E_NOT_FOUND)\r\n\r\n")
TEXT("This sample reads a media file from ")
TEXT("the DirectX SDK's media path.\r\n")
TEXT("Please install the DirectX 9 SDK on this machine."));
return hr;
}
else if(FAILED(hr))
{
Msg(TEXT("Could not add source filter to graph! hr=0x%x"),
hr);
return hr;
}
if (SUCCEEDED(hr = pFSrc->FindPin(L"Output", &pFSrcPinOut)))
{
//AVIとかMPEG1とかVFWでいけるやつ
//オーディオトラックを再生しない場合 NO_AUDIO_RENDERER を定義
if(!bSound)
{
// If no audio component is desired,
// directly connect the two video pins instead of allowing
// the Filter Graph Manager to render all pins.
CComPtr<IPin> pFTRPinIn; // Texture Renderer Input Pin
// Find the source's output pin and
// the renderer's input pin
if (FAILED(hr = pCTR->FindPin(L"In", &pFTRPinIn)))
{
Msg(TEXT("Could not find input pin! hr=0x%x"), hr);
return hr;
}
// Connect these two filters
if (FAILED(hr = m_pGB->Connect(pFSrcPinOut, pFTRPinIn)))
{
Msg(TEXT("Could not connect pins! hr=0x%x"), hr);
return hr;
}
}
else
{
// Render the source filter's output pin.
// The Filter Graph Manager will connect the video stream
// to the loaded CTextureRenderer and will load
// and connect an audio renderer (if needed).
if (FAILED(hr = m_pGB->Render(pFSrcPinOut)))
{
Msg(TEXT("Could not render source output pin! hr=0x%x"),
hr);
return hr;
}
}
}
else
{
CComPtr<IBasicAudio> IAudio;
hr = m_pGB.QueryInterface(&IAudio);
//WMVとかMPEG2とか
hr = m_pGB->RenderFile(wFileName,NULL);
if(FAILED(hr))
return hr;
if(!bSound)
{
// 無音
hr = IAudio->put_Volume(-10000);
}
else
{
//音の出力
hr = IAudio->put_Volume(0);
}
}
// Get the graph's media control, event & position interfaces
m_pGB.QueryInterface(&m_pMC);
m_pGB.QueryInterface(&m_pMP);
m_pGB.QueryInterface(&m_pME);
// Start the graph running;
if (FAILED(hr = m_pMC->Run()))
{
Msg(TEXT("Could not run the DirectShow graph! hr=0x%x"), hr);
return hr;
}
//テクスチャの取得
m_pTexture = pCTR->GetTexture();
pCTR->GetVideoDesc(&m_lWidth, &m_lHeight, &m_lPitch);
D3DSURFACE_DESC desc;
m_pTexture->GetLevelDesc(0, &desc);
//テクスチャが2の累乗でしか確保できない場合に
//余剰分を表示しないためのUV計算
m_fu = (FLOAT)m_lWidth / (FLOAT)desc.Width;
m_fv = (FLOAT)m_lHeight / (FLOAT)desc.Height;
return S_OK;
}
とりあえず、まずグラフビルダーを作成し、そこからベースフィルタを作成していきます。そのあとはCMovieTextureで、実際のムービーファイルをテクスチャに書き出す処理を行います。
QueryInterfaceで、IMediaControl、IMediaPosition、IMediaEventを作成します。以下が各インターフェイスの役割です。
| インタフェース | 役割 |
IMediaControl | ムービーの再生、停止などを行います。 |
IMediaPosition | ムービーのタイムラインなどを制御します。ヘルプでは、C++ではIMedia+Seekingを使うことを推奨しています。 |
IMediaEvent | イベントの通知、オーバーライドを行います。 |
なおDirect3Dでは、テクスチャのサイズを2の累乗で確保しないといけないという話がありますが、ハードウェアがD3DPTEXTURECAPS_NONPOW2CONDITIONALをサポートしている場合は、2の累乗以外でも確保できます(ただし、細かい条件があります。D3DUSAGE_DYNAMICフラグで作成しないなど)。
そのため、一応2の累乗でしか確保できない場合を想定して、ムービーよりも大きい2の累乗サイズでテクスチャを確保し、UVをずらして解決する方法を、本サンプルでは採用します。その計算結果が、m_fuとm_fvに格納されます。
それと、もう一つムービーをテクスチャ化する際の注意点があります。それはムービーの1フレームの画像が、上下逆さに入ってくることです。これはWindowsのBMPの仕様に沿った(と言うよりも、1フレームの画像をBMPで取り出している)ためです。したがって、マッピング時にはVを反転する必要があります。これはテクスチャマッピングの方で意識的に変えた方が、動作速度が速そうです。
この初期化が済めば、m_pTextureにムービーがレンダリングされていきます。
テクスチャへの書き込み
ムービーファイルのフレーム画像をテクスチャに書き込む処理の例が、次の部分です。
引数のpSampleからGetPointerすることで、現在の再生してるフレームのピクセルデータが取得できるので、書き込み先のテクスチャのビット幅に応じて書き込みをしていきます。
HRESULT CMovieTexture::DoRenderSample( IMediaSample * pSample )
{
BYTE *pBmpBuffer, *pTxtBuffer; // ビットマップ、テクスチャ
LONG lTxtPitch; // Pitch of bitmap, texture
BYTE * pbS = NULL;
DWORD * pdwS = NULL;
DWORD * pdwD = NULL;
UINT row, col, dwordWidth;
CheckPointer(pSample,E_POINTER);
CheckPointer(m_pTexture,E_UNEXPECTED);
// ビデオからビットマップを取り出す
pSample->GetPointer( &pBmpBuffer );
// テクスチャをロックする
D3DLOCKED_RECT d3dlr;
if( m_bUseDynamicTextures )
{
if( FAILED(m_pTexture->LockRect(0, &d3dlr, 0, D3DLOCK_DISCARD)))
return E_FAIL;
}
else
{
if (FAILED(m_pTexture->LockRect(0, &d3dlr, 0, 0)))
return E_FAIL;
}
// テクスチャのピッチ幅の取得
pTxtBuffer = static_cast<byte *>(d3dlr.pBits);
lTxtPitch = d3dlr.Pitch;
// コピー
// 32bitカラーのテクスチャの場合
if (m_TextureFormat == D3DFMT_X8R8G8B8)
{
// Instead of copying data bytewise,
// we use DWORD alignment here.
// We also unroll loop by copying 4 pixels at once.
//
// original BYTE array is
// [b0][g0][r0][b1][g1][r1][b2][g2][r2][b3][g3][r3]
//
// aligned DWORD array is
// [b1 r0 g0 b0][g2 b2 r1 g1][r3 g3 b3 r2]
//
// We want to transform it to
// [ff r0 g0 b0][ff r1 g1 b1][ff r2 g2 b2][ff r3 b3 g3]
// below, bitwise operations do exactly this.
dwordWidth = m_lVidWidth / 4;
// aligned width of the row, in DWORDS
// (pixel by 3 bytes over sizeof(DWORD))
for( row = 0; row< (UINT)m_lVidHeight; row++)
{
pdwS = ( DWORD*)pBmpBuffer;
pdwD = ( DWORD*)pTxtBuffer;
for( col = 0; col < dwordWidth; col ++ )
{
pdwD[0] = pdwS[0] | 0xFF000000;
pdwD[1] = ((pdwS[1]<<8) | 0xFF000000) | (pdwS[0]>>24);
pdwD[2] = ((pdwS[2]<<16) | 0xFF000000) | (pdwS[1]>>16);
pdwD[3] = 0xFF000000 | (pdwS[2]>>8);
pdwD +=4;
pdwS +=3;
}
// we might have remaining (misaligned) bytes here
pbS = (BYTE*) pdwS;
for( col = 0; col < (UINT)m_lVidWidth % 4; col++)
{
*pdwD = 0xFF000000 |
(pbS[2] << 16) |
(pbS[1] << 8) |
(pbS[0]);
pdwD++;
pbS += 3;
}
pBmpBuffer += m_lVidPitch;
pTxtBuffer += lTxtPitch;
}// for rows
}
// 16bitのテクスチャの場合
if (m_TextureFormat == D3DFMT_A1R5G5B5)
{
for(int y = 0; y < m_lVidHeight; y++ )
{
BYTE *pBmpBufferOld = pBmpBuffer;
BYTE *pTxtBufferOld = pTxtBuffer;
for (int x = 0; x < m_lVidWidth; x++)
{
*(WORD *)pTxtBuffer = (WORD)
(0x8000 +
((pBmpBuffer[2] & 0xF8) << 7) +
((pBmpBuffer[1] & 0xF8) << 2) +
(pBmpBuffer[0] >> 3));
pTxtBuffer += 2;
pBmpBuffer += 3;
}
pBmpBuffer = pBmpBufferOld + m_lVidPitch;
pTxtBuffer = pTxtBufferOld + lTxtPitch;
}
}
// アンロック
if (FAILED(m_pTexture->UnlockRect(0)))
return E_FAIL;
return S_OK;
}
ムービーテクスチャクラスの実装例
実際に先のクラスを実装したものが、「d3dsetup9.h」と「d3dsetup9.cpp」で呼び出されています。
必要なこと
DirectShowは、COMを利用するのでその初期化が必要になります。通常はCoInitialize()関数を使うのですが、マルチスレッドで利用するオプションを付けるため、CoInitializeEX()関数を使います。
CoInitializeEx(NULL, COINIT_MULTITHREADED);
この関数を使うためには、「Windows.h」をインクルードする前に下記を定義して置く必要があります。
#define _WIN32_WINNT 0x0400 #define _WIN32_DCOM
クラスの呼び出し
クラスの呼び出しは下記のように行っています。初期化関数では、Direct3Dデバイス(ここではm_pd3dDevice)、ファイルパス、音声の再生の有無を引数にします。cm->GetStopTime()は、ムービーの終了時間を取得します(戻り値はdouble型)。終了時間を取得する理由は、ループ再生したい場合にムービーの終了をチェックするためです。cm->GetTexture(&m_pTexture)でテクスチャを取得します。引数は、IDirect3DDevce9型です。cm->Play()でムービーが再生されます。再生されると言っても、テクスチャを表示しないと画面には映りません。
// CMovieTexture* cm;
//ムービーテクスチャ
cm = new CMovieToTexture;
// 初期化
// 引数は、Direct3Dデバイス,ファイル名,音声再生の有無
cm->InitDShowTextureRenderer(m_pd3dDevice, L"movie.wmv", FALSE);
dEndTime = cm->GetStopTime(); //ムービーの終了時間
cm->GetTexture(&m_pTexture); //ムービーをレンダリングするテクスチャ
cm->Play(); //再生
取得したテクスチャの扱いは通常のDirect3Dのプログラミングのものと同じですが、上下逆さに入ってくるので、テクスチャマッピングする際にはv値を反転するといいと思います。
ループ再生
ムービーファイルのループ再生は、現在の再生位置を取得して終了時間と比べ、終了時間を過ぎたら再生位置を初期位置に戻すことで実現します。その例が、下記のような方法です。
//ムービーの再生時間が終了した場合
if(cm->GetCurrentPosition() >= dEndTime)
{
cm->Stop(); //停止
cm->SetTime(0.0); //タイムラインを開始位置にセット
cm->Play(); //再生
}
終了に際して
終了に際しては、COMプログラムの解放関数CoUninitialize()を呼びます。
CoUninitialize();
参考資料
- Platform SDK付属のTexture3D9のサンプル
おわりに
今回の記事で扱ったテクニックを利用することで、3Dのシーンの背景に動画が使ったり、グラフィックカードのシェーダプログラミングで動画に処理をかけたりすることが可能になります。
