ホーム < ゲームつくろー! < IKD備忘録

COM
ATL版DirectXの再描画を促すには?

(2009. 7. 24)


 前章でとりあえずSphereが描画されましたが、再描画がされていないので止まったまま。再描画を行わせる最も簡単な方法は、クライアント側の再描画を実行する事です。例えばVBならばReflesh関数を呼ぶ。実際これでATLのOnDraw関数が呼ばれるので再描画されます。しかし・・・これだと例えばブラウザに貼り付けた場合に対処できません。

 そこで、OnDraw関数は「使わずに」、ATLの内部で再描画を促すように工夫します。


○ 再描画スレッドの作成

 Direct3Dは「IDirect3DDevice9::Presentメソッド」を呼ぶとフリップしてくれます。ですから、これをバンバン呼ぶような仕組みを作ってしまえば良いんです。これを実現する確実な方法は再描画スレッドを作ることです。スレッドの中で「drawメソッド」を呼び続ければOKです。

 で、どう作るかですが、今回はこうしてみました:

UpdateThread.h
#ifndef IKD_UPDATETHREAD_H
#define IKD_UPDATETHREAD_H

template< class T >
class UpdateThread {
    struct ThreadInfo {
        T* m_pObj;
        bool isEnd;

        ThreadInfo() : m_pObj( 0 ), isEnd( false ) {}
    };
    uintptr_t m_threadHandle;
    ThreadInfo m_info;

public:
    UpdateThread(T* pObj) : m_threadHandle( 0 ) {
        m_info.m_pObj = pObj;
    }

    ~UpdateThread() {
        waitEnd();
    }

    void start() {
        m_threadHandle = _beginthread( func, 0, &m_info );
        m_info.isEnd = false;
    }

    void stop() {
        m_info.isEnd = true;
    }

    void waitEnd() {
        unsigned roopCount = 0;
        if (m_threadHandle != 0) {
            stop();
            while( WaitForSingleObject( (HANDLE)m_threadHandle, 10 ) != WAIT_OBJECT_0 ) {
                if (roopCount++ >= 50)
                break;
            }
        }
        m_threadHandle = 0;
    }

    static void __cdecl func( void* pInfo ) {
        ThreadInfo* info = (ThreadInfo*)pInfo;
        if ( info->m_pObj == 0 ) {
            _endthread();
            return;
        }

        // ガンガン回すぜ!
        while( info->isEnd == false ) {
            info->m_pObj->update();
            info->m_pObj->draw();
        }

        _endthread();
    }
};

#endif

 UpdateThreadというupdateとdrawを内部でガンガン回すスレッドクラスです。startメソッドでスレッドを作ります。func関数がスレッドで回される本体で、info->isEndがfalseの間はガンガン回します。スレッドをとめるにはwaitEndメソッドを呼び出します。これは内部でスレッドを止めて、スレッドハンドルが破棄されたかどうかをWaitForSingleObject関数で監視しています。ただし、ずっと回し続けるとアプリケーションがハングアップしてしまいますので、ある程度待った後は抜けてしまいます。たぶん・・・大丈夫です。

 このクラスはテンプレートになっています。テンプレートの引数に渡すクラスには「draw()」と「update()」が宣言されている必要があります。上の赤い部分で呼び出しているためです。この2つのメソッドをDirectXを扱うATLコントロールクラスに追加します。


○ OnDraw、さようなら!

 上のスレッドに更新と再描画をしてもらうと、OnDrawメソッドが必要なくなります。よって、OnDrawは空実装、変わりにdrawメソッドにDirect3Dの描画部分がみっしりと書き込まれます:

CDXWindow.cpp
HRESULT CDXWindow::OnDraw(ATL_DRAWINFO& di)
{
    return S_OK;
}

void CDXWindow::draw() {
    if ( pD3DDev ) {
        // ライト
        D3DLIGHT9 light;
        ZeroMemory(&light, sizeof(D3DLIGHT9) );
        light.Direction = D3DXVECTOR3(-1, -1, 1);
        light.Type = D3DLIGHT_DIRECTIONAL;
        light.Diffuse.r = 1.0f;
        light.Diffuse.g = 1.0f;
        light.Diffuse.b = 1.0f;
        light.Ambient.r = 0.5f;
        light.Ambient.g = 0.5f;
        light.Ambient.b = 0.5f;
        light.Range = 1000;

        // マテリアル
        D3DMATERIAL9 material = {
            {1.0f, 1.0f, 1.0f, 1.0f},
            {0.0f, 0.0f, 0.0f, 0.0f},
            {0.0f, 0.0f, 0.0f, 0.0f},
            {0.0f, 0.0f, 0.0f, 0.0f},
            50.0f
        };

        // ビュー変換
        // 視点は原点固定ですが、カメラの位置は適当です
        static float a = 0.0f;
        D3DXMATRIX View;
        D3DXMatrixLookAtLH(
            &View,
            &D3DXVECTOR3(10.0f * cosf( a ), 0.0f, 10.0f * sinf( a )),
            &D3DXVECTOR3(0, 0, 0),
            &D3DXVECTOR3(0, 1, 0)
        );
        a += m_speed;

        // 射影変換
        D3DXMATRIX Proj;
        float w = (float)(rect.right - rect.left);
        float h = (float)(rect.bottom - rect.top);
        D3DXMatrixPerspectiveFovLH( &Proj, D3DXToRadian(45), w/h, 1.0f, 10000.0f);

        pD3DDev->SetLight( 0, &light );
        pD3DDev->LightEnable( 0, true );
        pD3DDev->SetRenderState( D3DRS_LIGHTING, TRUE );
        pD3DDev->SetRenderState( D3DRS_AMBIENT, 0x00808080 ); // アンビエントライト
        pD3DDev->SetMaterial( &material );
        pD3DDev->SetTransform( D3DTS_VIEW, &View );
        pD3DDev->SetTransform( D3DTS_PROJECTION, &Proj );

        pD3DDev->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );
        pD3DDev->BeginScene();
        sphere->DrawSubset(0);
        pD3DDev->EndScene();
        if ( FAILED(pD3DDev->Present( NULL, NULL, NULL, NULL ) ) ) {
        }
    }
}


○ スレッドメソッドをどこで呼ぶか?

 せっかく作ったスレッドをどこで作り、どこで破棄するか?これ、結構重要です。UpdateThread自体はCDXWindowATLコントロールのメンバに持たせます。

CDXWindow.h
class CDXWindow {
public:
    UpdateThread<CDXWindow> updateThread;
    ...
}

ATLコントロールがアクティブになった時「InPlaceActivateメソッド」が自動的に呼ばれます。ここでDirect3Dの初期化も行っているのですが、Updateスレッドはこの初期化の後で作ります。

CDXWindow.cpp
HRESULT CDXWindow::InPlaceActivate(LONG iVerb, const RECT* prcPosRect)
{
    HRESULT hr;

    hr = CComControlBase::InPlaceActivate(iVerb, prcPosRect);
    if ( !m_hOurWnd ) {
        getOurHWnd();
        initD3D();
        updateThread.start();
    }
    return hr;
}

これでCDXWindow::updateメソッドとdrawメソッドがガンガン呼ばれます。

 ATLコントロールが破棄される時は「CDXWindow::FinalReleaseメソッド」というのが呼ばれます。これはCDXWindowのデストラクタの前に呼ばれます。ここでスレッドを終了させます:

CDXWindow.h
void FinalRelease()
{
    updateThread.waitEnd();
    if ( pD3DDev ) pD3DDev->Release();
    if ( pD3D ) pD3D->Release();
    if ( sphere ) sphere->Release();
}

これでこのATLコントロールを持っているアプリケーションが終わる時に描画スレッドも終了します。この実装をしたATL版DirectXをIEに貼り付けてみると、こうなりました:

わ〜い、ちゃんと再描画されて勝手に動いてます〜。本当はページに貼りたいのですが作りかけのCOMを公開してインストールしてもらうのもどうかと思うので割愛です。IEを終了させると、スレッドもちゃんと終了してくれているようです。

 ここまでできれば、もう通常のDirectXのプログラムと大きくは変わらない気がしてきました。ただ、仕事ではインタラクティブを求められているので、マウスクリック感知が必要です。次回はその辺りを調査です。