SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

ムービーファイルをDirect3Dのテクスチャとして利用する

3D画像の表面に動画を表示する方法


ムービーファイルのテクスチャ化

 ムービーファイルのテクスチャ化から利用までの手順は、以下のとおりです。

  1. DirectShowでムービーファイルを読み込む
  2. 読み込んだムービーファイルを、マルチスレッドでDirect3Dのテクスチャに書き込む
  3. Direct3Dのプログラミング側で作成したテクスチャを取得する
  4. あとは通常のDirect3Dでのテクスチャ利用法を用いる

制御クラスの概要

 次のクラスで、ムービーテクスチャの生成、制御を行います。コメントの記法はDoxygenの記法を利用しています。

ムービーテクスチャクラス(movietex.h 内)
//! @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の場合、ムービーファイルに音声が入っていれば再生します。

ムービーテクスチャの初期化(movietex.cpp 内)
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で、IMediaControlIMediaPositionIMediaEventを作成します。以下が各インターフェイスの役割です。

インタフェース役割
IMediaControlムービーの再生、停止などを行います。
IMediaPositionムービーのタイムラインなどを制御します。ヘルプでは、C++ではIMedia+Seekingを使うことを推奨しています。
IMediaEventイベントの通知、オーバーライドを行います。

 なおDirect3Dでは、テクスチャのサイズを2の累乗で確保しないといけないという話がありますが、ハードウェアがD3DPTEXTURECAPS_NONPOW2CONDITIONALをサポートしている場合は、2の累乗以外でも確保できます(ただし、細かい条件があります。D3DUSAGE_DYNAMICフラグで作成しないなど)。

 そのため、一応2の累乗でしか確保できない場合を想定して、ムービーよりも大きい2の累乗サイズでテクスチャを確保し、UVをずらして解決する方法を、本サンプルでは採用します。その計算結果が、m_fum_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()関数を使います。

COMの初期化
CoInitializeEx(NULL, COINIT_MULTITHREADED);

 この関数を使うためには、「Windows.h」をインクルードする前に下記を定義して置く必要があります。

CoInitializeEx()のために必要な記述
#define _WIN32_WINNT 0x0400
#define _WIN32_DCOM

クラスの呼び出し

 クラスの呼び出しは下記のように行っています。初期化関数では、Direct3Dデバイス(ここではm_pd3dDevice)、ファイルパス、音声の再生の有無を引数にします。cm->GetStopTime()は、ムービーの終了時間を取得します(戻り値はdouble型)。終了時間を取得する理由は、ループ再生したい場合にムービーの終了をチェックするためです。cm->GetTexture(&m_pTexture)でテクスチャを取得します。引数は、IDirect3DDevce9型です。cm->Play()でムービーが再生されます。再生されると言っても、テクスチャを表示しないと画面には映りません。

HRESULT CD3DSetup9::AppLoop()内に記述
    // 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値を反転するといいと思います。

ループ再生

 ムービーファイルのループ再生は、現在の再生位置を取得して終了時間と比べ、終了時間を過ぎたら再生位置を初期位置に戻すことで実現します。その例が、下記のような方法です。

HRESULT CD3DSetup9::AppLoop()内に記述
    //ムービーの再生時間が終了した場合
    if(cm->GetCurrentPosition() >= dEndTime)
    {
        cm->Stop();       //停止
        cm->SetTime(0.0); //タイムラインを開始位置にセット
        cm->Play();       //再生
    }

終了に際して

 終了に際しては、COMプログラムの解放関数CoUninitialize()を呼びます。

COMの解放
CoUninitialize();

参考資料

  • Platform SDK付属のTexture3D9のサンプル
  • デフォルトでは「C:\Program Files\Microsoft Platform SDK\Samples\Multimedia\DirectShow\Players\Texture3D9」にあります。

おわりに

 今回の記事で扱ったテクニックを利用することで、3Dのシーンの背景に動画が使ったり、グラフィックカードのシェーダプログラミングで動画に処理をかけたりすることが可能になります。

修正履歴

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
特集記事連載記事一覧

もっと読む

この記事の著者

高橋 誠史(タカハシ マサフミ)

僻地の大学院生です。GPUプログラミングとそのニュースを集めたサイトShader.jpを開いています。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/226 2007/11/01 09:16

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー