ゆとりーなの日記

日記的な事を書いて行くと思はれる

第3回〜四角形を描画する〜

前回描画までの準備が整いましたが、今回からいよいよ画面に何かを描画していくことにします。取り敢えずは基本となる四角形を描画していくことにします。
というわけで色々な図形を描画するクラスを作ります。

// drawobject.h
#pragma once
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include <d3d9.h>
#include <d3dx9.h>
#include "deleter.h"

class DrawObject : private boost::noncopyable {
public:
    void drawBox(float left, float top, float right, float bottom, const D3DXCOLOR &color);
private:
    friend class Application;
    explicit DrawObject(boost::intrusive_ptr<IDirect3DDevice9> graphic_device);
    boost::intrusive_ptr<IDirect3DDevice9> graphic_device_;
};

安直なクラス名ですが気にしないでください。取り敢えず四角形を描画するインターフェイスを用意してみました。コピーを禁止にして、生成を自身とApplicationクラスしかできないようにしていますが、これにはちょっとした意味があります。まあそれについては後ほどということで。
実装はこんなかんじです。

// drawobject.cc
#include "drawobject.h"
#include <cassert>

namespace {
// 2D図形を描画するのに必要なバーテクス構造体
struct VertexBox2D {
    float x;
    float y;
    float z;
    float rhw;
    DWORD color;
};
// 2D図形を描画するのに必要なFVF
const DWORD kFvfVertexBox(D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
}

DrawObject::DrawObject(boost::intrusive_ptr<IDirect3DDevice9> graphic_device) : graphic_device_(graphic_device) {
}

void DrawObject::drawBox(float left, float top, float right, float bottom, const D3DXCOLOR &color) {
    assert(graphic_device_);
    // 四角形の座標情報を入れる
    const VertexBox2D vertex[4] = {
        {left, top, 0.f, 1.f, color},
        {right, top, 0.f, 1.f, color},
        {left, bottom, 0.f, 1.f, color},
        {right, bottom, 0.f, 1.f, color}
    };
    // 2D図形を描画するのに必要なFVFに設定
    graphic_device_->SetFVF(kFvfVertexBox);
    // 描画転送する
    graphic_device_->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, vertex, sizeof(vertex[0]));
}

コンストラクタで描画に必要なIDirect3DDevice9*を保存します。この値を使ってDrawObject::drawBoxは描画を行います。
DirectXで2D図形を描画するのには様々な方法があるのですが、今回は簡単だと思われる方法で実装してみました。まずは、描画に必要な情報を集めたバーテクス構造体を宣言します。2D図形であれば、x、y、z、値とrhw値とそれに色情報があれば取り敢えずなんとかなります。2Dなのにzとかありますが、これで一応描画順にかかわらず図形をzの大小で前後関係ソートとか出来ます。が、半透明図形等を描くときに残念な結果になってしまうので基本使わなくていいと思います。因みに座標の指定順は、左上、右上、左下、右下の順です。rhwは取り敢えず1でいいです。そして色はRGBAで指定します。RGBは赤緑青、Aはα値(透明度みたいなもの)を表します。たとえば0xFFFFFFFFがR:255、G:255、B:255:、A:255で白を表し、0x000000FFは黒です。色情報がDWORDを取っているのに引数ではD3DXCOLORという謎の構造体になっていますが、これは色情報を表す便利クラスです。コンストラクタでRGBA値をそれぞれ0〜1までの値を使って指定できます。上の例でいえば0が0で、1が255という感じで対応します。また、D3DXCOLORはDWORDに色情報を勝手に変換してくれるので、割と便利です。
ところでせっかくα値が指定できるのに、Direct3Dはデフォルトではアルファブレンド(半透明とか)が扱えません。そこでアルファブレンドが扱えるようにGraphicDeviceクラスで設定してやります。

// graphicdevice.h
#pragma once
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include <d3d9.h>
#include "deleter.h"

class GraphicDevice : private boost::noncopyable {
public:
    // インターフェイス
private:
    void init();
    // 内部実装関係
};

// graphicdevice.cc
void GraphicDevice::init() {
    assert(graphic_device);
    // αブレンドが有効になる設定
    graphic_device_->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
    graphic_device_->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
    graphic_device_->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
}

GraphicDevice::GraphicDevice(HWND window_handle, int client_width, int client_height, bool windowed) : present_parameters_(initPresentParameters(window_handle, client_width, client_height, windowed)), direct3d_(Direct3DCreate9(D3D_SDK_VERSION)), graphic_device_(createDevice(window_handle, direct3d_, &present_parameters_)) {
    if (!direct3d_) {
        std::runtime_error("Direct3Dの作成に失敗");
    }
    if (!graphic_device_) {
        std::runtime_error("Direct3Dデバイスの作成に失敗");
    }
    // 初期化直後にαブレンドが有効になる設定を呼ぶ
    init();
}

void GraphicDevice::endScene() {
    assert(graphic_device_);
    graphic_device_->EndScene();
    switch (graphic_device_->Present(nullptr, nullptr, nullptr, nullptr)) {
    case D3DERR_DEVICELOST:
	if (graphic_device_->TestCooperativeLevel() == D3DERR_DEVICENOTRESET) {
	    if (FAILED(graphic_device_->Reset(&present_parameters_))) {
                throw std::runtime_error("デバイスロストからの復旧に失敗");
	    }
            // リセット直後にαブレンドが有効になる設定を呼ぶ
            init();
	}
        break;
    case D3DERR_DRIVERINTERNALERROR:
	throw std::runtime_error("内部ドライバエラーが発生");
	break;
    default:
	break;
    }
}

初期化時はもちろん、デバイスロスト時のリセット後も設定が戻ってしまうので、αブレンドを有効にする設定を呼び出してやります。
それではDrawObjectクラスをApplicationクラスに組み込みましょう。

#include "graphicdevice.h"
#include "drawobject.h"

int Application::run() {
    init();
    MSG message;
    GraphicDevice graphic_device(window_handle_, client_width_, client_height_, windowed_);
    DrawObject draw_object(graphic_device.graphic_device_);
    for (;;) {
        // メッセージキューにメッセージがあるかを調べる
        if (PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE)) {
            // メッセージ取得
            const BOOL result(GetMessage(&message, nullptr, 0, 0));
            // 終了、あるいはエラーならループを抜ける
            if ((!result) || (!(~result))) {
                break;
            }
            // キー入力等のメッセージの変換
            TranslateMessage(&message);
            // メッセージの処理
            DispatchMessage(&message);
        } else {
            if (graphic_device.beginScene()) {
                update(&draw_object);
                graphic_device.endScene();
            }
        }
    }
    // メッセージループに入った後のWinMainの返り値はこれである必要がある
    return static_cast<int>(message.wParam);
}

これで、DrawObjectクラスを使って四角形が描画する準備が整いました。そろそろなんでこんなクラスを作ったかについての話に移りましょう。
前回GraphicDevice::beginSceneメソッドとGraphicDevice::endSceneメソッドの間でのみ描画が行えるという設計にしました。この間に仮想関数メソッドを入れて引数としてApplicationクラスで作ったDrawObjectの参照を渡してこれを使ってのみ描画できるようにすれば、間違ってGraphicDevice::beginSceneメソッドとGraphicDevice::endSceneメソッドで描画してしまうという事態を防ぐことができるのです。コピーやApplicationクラス以外では生成できないので、不正に作って描画しようという悪しき試みも防げます。
というわけでApplicationクラスに次のように仮想関数メソッドを追加します。

// application.h
#pragma once
#define _WIN32_WINNT 0x0501
#include <string>
#include <boost/noncopyable.hpp>
#include <windows.h>
#include "drawobject.h"

class Application : private boost::noncopyable {
public:
    // インターフェイス
protected:
    virtual void update(DrawObject *draw_object);
private:
    // 内部実装関係
};

// application.cc
void Application::update(DrawObject *draw_object) {
    assert(draw_object);
}

実際にApplicationクラスを継承したクラスを作り四角形を描画するテストコードを書いてみます。

// testapplication.h
#pragma once
#include "application.h"

class TestApplication : public Application {
public:
    TestApplication(const std::string &caption, int client_width, int client_height, bool windowed);
protected:
    virtual void update(DrawObject *draw_object);
};

// testapplication.cc
#include "testapplication.h"
#include <cassert>

TestApplication::TestApplication(const std::string &caption, int client_width, int client_height, bool windowed) : Application(caption, client_width, client_height, windowed) {
}

void TestApplication::update(DrawObject *draw_object) {
    assert(draw_object);
    draw_object->drawBox(32.f, 32.f, 608.f, 448.f, D3DXCOLOR(1.f, 1.f, 1.f, 0.5f));
}

TestApplication::updatateメソッドで、真ん中の方に半透明の白い四角形を描画するコードを書きます。
メイン関数で生成するアプリケーション管理クラスを新しく作ったTestApplicationクラスに置き換えれば、四角形が描画できるようになります。

#include "testapplication.h"

namespace {
const LPCSTR kCaption("C++とwin32とDirectXとBoostとLuaで作る弾幕シューテイング講座");
const int kClientWidth(640);
const int kClientHeight(480);
const bool kWindowed(true);
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    TestApplication application(kCaption, kClientWidth, kClientHeight, kWindowed);
    return application.run();
}

実行画面

半透明の白い四角形が描画できました。