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のプログラムと大きくは変わらない気がしてきました。ただ、仕事ではインタラクティブを求められているので、マウスクリック感知が必要です。次回はその辺りを調査です。