Date

Direct3D を使ったゲームなどに動画再生を取り入れる場合、Direct3Dサーフェイスに対してレンダリングし、画面へ表示することになる。Video Mixing Renderer 9 に対してプレゼンタを提供すれば Direct3D9 サーフェイスへレンダリングする機能が実現できる。

  • 3Dシーンと動画再生のレンダリングタイミングを独立させる。
  • Direct3D10を使う

これらを実現するために、Direct3D10 サーフェイスへ画像をレンダリングするフィルタを製作してみたい。

サンプルコードは GitHub からダウンロード願います → https://github.com/mahorigahama/dx10surf

  • Direct3D10を使うので Windows XP以下では動作しない。動作にはDirectX10が動作するグラフィックチップが搭載されたコンピュータが必要です。GMA950,GMAX3100など内蔵グラフィックスでは動かない。

フィルタの仕様

本ブログでも既に取り上げたレンダラーフィルタを基本とし、それに対してレンダリング先となるDirect3D10サーフェイスを渡すためのインターフェイスIDX10Surfを拡張する。

フィルタ
フィルタ名 dx10surf renderer
フィルタクラス名 `CDX10SurfRenderer`
GUID {12A23AC0-3167-49f5-978D-76B8F387DB00}
フィルタタイプ レンダラーフィルタ
入力ピンの数 1
入力タイプ `MEDIATYPE_Video`,`MEDIASUBTYPE_RGB32`
出力ピンの数 0

拡張するインターフェイス

インターフェイス名 IDX10Surf
GUID {03D0B0C9-46AE-46ce-BB9A-05D10ED2556B}

拡張インターフェイス IDX10Surf を定義

拡張インターフェイスIDX10Surfを定義する。

1
2
3
4
5
__interface
_declspec(uuid("{03D0B0C9-46AE-46ce-BB9A-05D10ED2556B}"))
IDX10Surf : public IUnknown {
    void SetGrabProp(GrabProp *grab);
};

SetGrabPropメソッドを、アプリケーション側から呼び出すことにより、レンダリング先サーフェイスを渡す。

_declspec(uuid("..."))は、インターフェイスID (IID) を指定している。

次に、SetGrabPropの引数であるGrabPropクラスを定義する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class GrabProp {
public:
    GrabProp() : texture(NULL)
    {
        InitializeCriticalSection(&crit);
    }
    ~GrabProp() {
        DeleteCriticalSection(&crit);
    }
    void Enter() {
        EnterCriticalSection(&crit);
    }
    void Leave() {
        LeaveCriticalSection(&crit);
    }
    CRITICAL_SECTION crit;
    ID3D10Texture2D* texture;
};

Direct3D10サーフェイスに対してレンダリングするとき、サーフェイスに対して同時にアクセスしないように排他制御する必要がある。というのも、フィルタグラフがレンダリングするスレッド「ストリーミングスレッド」と、アプリケーションが実行している「アプリケーションスレッド」という複数のスレッドが存在しているからだ。

そこでクリティカルセクションを宣言して、アプリケーションとフィルタとで共有するようにする。

ついでにフィルタのGUIDも定義する。

1
2
3
class
_declspec(uuid("{12A23AC0-3167-49f5-978D-76B8F387DB00}"))
CDX10SurfRenderer;

これらをまとめて、dx10render_if.hとして保存しておく。アプリケーション側とフィルタ側共通で使用する。

フィルタクラス CDX10SurfRenderer の実装

フィルタクラスCDX10SurfRendererを実装する。IDX10Surfインターフェイスを継承する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class CDX10SurfRenderer :
    public CBaseVideoRenderer, public IDX10Surf
{
public:
    DECLARE_IUNKNOWN;
...
    // IDX10Surf
    virtual void SetGrabProp(GrabProp *grab);
private:
    CCritSec m_CritSec;
    GrabProp * m_pGrab;
};

CUnknown::NonDelegatingQueryInterfaceを、IDX10Surfインターフェイスを取得できるように拡張する。

1
2
3
4
5
6
7
STDMETHODIMP CDX10SurfRenderer::NonDelegatingQueryInterface(REFIID riid,void **ppv) {
    CheckPointer(ppv,E_POINTER);
    if(riid==__uuidof(IDX10Surf)) {
        return GetInterface((LPUNKNOWN)dynamic_cast<IDX10Surf*>(this), ppv);
    }
    return CBaseFilter::NonDelegatingQueryInterface(riid,ppv);
}

IDX10Surf::SetGrabPropを実装する。引数で渡されてきたGrabPropへのポインタを記憶しておく。これでアプリケーション側とフィルタ側でGrabPropを共有するというわけだ。

1
2
3
4
void CDX10SurfRenderer::SetGrabProp(GrabProp *grab) {
    CAutoLock mylock(&m_CritSec);
    m_pGrab=grab;
}

最後にDoRenderSampleを実装する。m_pGrab->textureに画像を書き込む。ポイントは次の3つ。

1.m_pGrabが設定されていないときは、何もしない。 2. クリティカルセクションを使って排他制御をしている。そうしないと「アプリケーション側のレンダリング処理」と「フィルタのレンダリング処理」が同時実行されてしまう。 3.サーフェイスの1ライン辺りのサイズmapped.RowPitchと、フレームの1ライン辺りのサイズ(m_Pitch)は異なるため、forループを使って書き込んでいる。

基本的に、サーフェイスの横幅は2^nピクセルに対してムービーは320や640など2^nではないので、1ラインごとに書きこむ必要がある。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
HRESULT CDX10SurfRenderer::DoRenderSample(IMediaSample *pMediaSample) {
    CAutoLock mylock(&m_CritSec);
    LPBYTE pbSrc=NULL;
    pMediaSample->GetPointer(&pbSrc);
    const long lActualSize=pMediaSample->GetActualDataLength();
    HRESULT hr=S_OK;
    if(pbSrc && m_pGrab) {
        m_pGrab->Enter();
        D3D10_MAPPED_TEXTURE2D mapped={NULL};
        if(m_pGrab->texture->Map(
            D3D10CalcSubresource(0, 0, 1),
            D3D10_MAP_WRITE_DISCARD, 0, &mapped)==S_OK && mapped.pData!=NULL)
        {
            LPBYTE p=(LPBYTE)mapped.pData;
            for(int y=0;y<m_Vih.bmiHeader.biHeight;++y) {
                CopyMemory(p, pbSrc, m_Pitch);
                p+=mapped.RowPitch;
                pbSrc+=m_Pitch;
            }
            m_pGrab->texture->Unmap(D3D10CalcSubresource(0, 0, 1));
        }
        m_pGrab->Leave();
    }
    return hr;
}

※ 行を無視して連続して書き込み、レンダリングするときにテクスチャを参照するU,V座標で調整するという手もありますが…。

レンダラーフィルタが完成したので、それを使って実際にWMVファイルを再生するサンプルコードを書いていきたいと思う。

作成するサンプルコードには3つのクラスがある。

クラス名 役割
DxPtr DirectX用のスマートポインタ(テンプレートクラス) CComPtrから派生したもの。
D3d10 Direct3D 10 によるレンダリング準備、レンダリングを行う。
Player DirectShow のフィルタグラフ構築、ムービーを再生する。

ここではDxPtrというCComPtrから派生したスマートポインタを使っている。CComPtrをそのまま使うと一部メソッドで例外が発生するためである。

Direct3D 10の準備

Direct3D 10でレンダリングを行うための準備をする。初期化するコードについてはDirectX SDKにサンプルがいくらでもあるので、ここではポイントだけ説明する。

エフェクトファイルのロード

1つめのポイントは、ムービーレンダリング用のエフェクトファイルを用意することだ。というのも「ちょっとした変換」が必要だからである(後述)。D3D10CreateDeviceAndSwapChainなどの基本的な初期化が終わったら、D3DX10CreateEffectFromFileでロードする。ここではファイル名を Default.fx とする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DWORD dwShaderFlags=D3D10_SHADER_ENABLE_STRICTNESS;
#ifdef _DEBUG
    dwShaderFlags|=D3D10_SHADER_DEBUG;
#endif
    hr=D3DX10CreateEffectFromFile(L"Default.fx"
        ,NULL,NULL,"fx_4_0",dwShaderFlags,0
        ,Device,NULL,NULL,&Effect,NULL,NULL);
    CHK_HR_D3D(hr);
    Technique = Effect->GetTechniqueByName("Default");
    if(!Technique)
        return E_FAIL;

テクスチャの作成

2つめのポイントは、レンダリング先のDirect3D10テクスチャをCPUが書き込み可能な2Dテクスチャとして作成することだ。

詳しくは以下の表を満たす条件となる。CPUはレンダリング前にテクスチャへ書き込み、GPUはレンダリング時に参照できれば良い。

アクセス元読み込み可能?書き込み可能?
GPUYN
CPUNY

これをコードで表すと次のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
HRESULT D3d10::CreateDynamicTexture(SIZE sz,
        DxPtr<ID3D10Texture2D> &texture)
{
    HRESULT hr;
    D3D10_TEXTURE2D_DESC desc={0};
    desc.Width=sz.cx;
    desc.Height=sz.cy;
    desc.MipLevels=1;
    desc.ArraySize=1;
    desc.SampleDesc.Count=1;
    desc.SampleDesc.Quality=0;
    desc.Format=DXGI_FORMAT_R8G8B8A8_UNORM;
    desc.BindFlags=D3D10_BIND_SHADER_RESOURCE;
    desc.Usage=D3D10_USAGE_DYNAMIC;
    desc.CPUAccessFlags=D3D10_CPU_ACCESS_WRITE;
    hr=Device->CreateTexture2D(&desc,NULL,&texture);
    CHK_HR_D3D(hr);
    return hr;
}

エフェクトファイルの記述

ムービーレンダリング用のエフェクトを Default.fx に記述する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Texture2D txDiffuse;
SamplerState samLinear
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = Wrap;
    AddressV = Wrap;
};
struct VS_INPUT
{
    float4 Pos : POSITION;
    float2 Tex : TEXCOORD;
};
struct PS_INPUT
{
    float4 Pos : SV_POSITION;
    float2 Tex : TEXCOORD0;
};
PS_INPUT VS(VS_INPUT input)
{
    PS_INPUT output = (PS_INPUT)0;
    output.Pos=input.Pos;
    output.Tex=input.Tex;
    return output;
}

頂点シェーダVSを見ればわかるとおり、今回はシンプルにするため投影変換は行っていません。次にピクセルシェーダを実装する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
float4 PS( PS_INPUT input) : SV_Target
{
    float4 color=txDiffuse.Sample(samLinear, input.Tex);
    float4 outcolor;
    outcolor.x=color.z; // z->x (R)
    outcolor.y=color.y; // y->y (G)
    outcolor.z=color.x; // x->z (B)
    outcolor.w=1.0;
    return outcolor;
}

ここが「ちょっとした変換」である。dx10surf rendererフィルタで受け取った画像データと、作成したDirect3D10サーフェイスとで、RGBの配列が異なるため、このような変換をしている。

Direct3D10サーフェイスは先ほどのコードを見ればわかるとおり、DXGI_FORMAT_R8G8B8A8_UNORM形式で作成している。これはABGRで配置されている。一方、画像データの方はARGBなので、BとRを入れ替えている。

最後にテクニックを実装する。

1
2
3
4
5
6
7
8
9
technique10 Default
{
    pass P0
    {
        SetVertexShader( CompileShader( vs_4_0, VS() ) );
        SetGeometryShader( NULL );
        SetPixelShader( CompileShader( ps_4_0, PS() ) );
    }
}

DirectShow の準備

フィルタグラフの構築

Direct3D10の準備が終わったら、フィルタグラフを構築する。ひとつひとつやっていくのは面倒なので、シンプルにRenderFileを使う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HRESULT Player::Init(DxPtr<ID3D10Texture2D> &texture) {
    HRESULT hr;
    pGraphBuilder.CreateInstance(CLSID_FilterGraph);
    pGraphBuilder.QueryInterface<IMediaControl>(&pMediaControl);
    pRenderer.CreateInstance<CDX10SurfRenderer>();
    hr=pGraphBuilder->AddFilter(pRenderer,NULL);
    CHK_HR_D3D(hr);
    hr=pGraphBuilder->RenderFile(
        _T("C:\\Users\\Public\\Videos\\Sample Videos\\lake.wmv"),NULL);
    CHK_HR_D3D(hr);

レンダリング先テクスチャを設定

次に、dx10surf rendererに実装したIDX10Surfインターフェイスを使って、レンダリング先となるテクスチャを渡す。

1
2
3
4
5
6
7
// set render target
    DxPtr<IDX10Surf> pDxSurfFilter;
    pRenderer.QueryInterface<IDX10Surf>(&pDxSurfFilter);
    grab.texture=texture;
    pDxSurfFilter->SetGrabProp(&grab);
    return S_OK;
}

メイン部分の作成

Direct3DとDirectShowの準備ができたので、あともう一息です。

2つのクラスのインスタンスをグローバル変数(無名名前空間)で宣言する。

1
2
3
4
5
6
7
namespace {
    typedef struct {
        D3d10 d3d10;
        Player player;
    } GVars;
    GVars *g_Vars;
};

D3d10クラスとPlayerクラスを初期化するメソッドを呼び出す。(サンプルでは、Dx10Play.cpp の InitInstance 関数に記述。)

初期化が終わったら、メッセージループにレンダリング処理を呼び出すようにする。レンダリング中にテクスチャが書き換わることのないように、クリティカルセクションで排他制御しているところがポイントである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
while( WM_QUIT != msg.message )
{
    if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
    {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }
    g_Vars->player.Enter(); // クリティカルセクション開始
    g_Vars->d3d10.Render();
    g_Vars->player.Leave(); // クリティカルセクション終了
    Sleep(0);
}

Comments

comments powered by Disqus