ゆとりーなの日記

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

コラム?2D図形なんでも描画

今のところ当口座に於いて図形描画にインターフェイスは次のようになっています。

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

四角形を書くメソッドが用意されているわけです。線やドットとかが書きたくなったらこのクラスに次のようにメソッドを追加していくことになると思います。

class DrawObject : private boost::noncopyable {
public:
    void drawBox(float left, float top, float right, float bottom, std::uint32_t &color);
    void drawLine(float left, float top, float right, float bottom, std::uint32_t &color);
    void drawPixel(float x, float y, std::uint32_t color);
private:
    const boost::intrusive_ptr<IDirect3DDevice9> graphic_device_;

こいつらを全部実装するとすると次の様なコードになると思います。

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

void DrawObject::drawBox(const float left, const float top, const float right, const float bottom, const std::uint32_t color) {
    assert(graphic_device_);
    // 四角形の座標情報を入れる
    const std::array<Vertex2D, 4> vertex = {{
        {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.front(), sizeof(vertex.front()));
}

void DrawObject::drawLine(const float left, const float top, const float right, const float bottom, const std::uint32_t &color) {
    assert(graphic_device_);
    // 線の座標情報を入れる
    const std::array<Vertex2D, 2> vertex = {{
        {left, top, 0.f, 1.f, color},
        {right, bottom, 0.f, 1.f, color},
    }};
    // 2D図形を描画するのに必要なFVFに設定
    graphic_device_->SetFVF(kFvfVertexBox);
    // 描画転送する
    graphic_device_->DrawPrimitiveUP(D3DPT_LINELIST, 2, &vertex.front(), sizeof(vertex.front()));

void DrawObject::drawPixel(const float x, const float y, const std::uint32_t color) {
    assert(graphic_device_);
    // 点の座標情報を入れる
    const std::array<Vertex2D, 1> vertex = {{
        {x, y, 0.f, 1.f, color},
    }};
    // 2D図形を描画するのに必要なFVFに設定
    graphic_device_->SetFVF(kFvfVertexBox);
    // 描画転送する
    graphic_device_->DrawPrimitiveUP(D3DPT_POINTLIST, 1, &vertex.front(), sizeof(vertex.front()));
}

これを見ると図形が違ってもやることは殆ど変わらないことが分かります。頂点配列とプリミティブの種類、数、配列要素のサイズが違うだけです。呼び出すIDirect3DDevice9のメドッドは全く変わりません。
というわけで次のようにしてやるとより拡張性の高いものが出来るのではないかと思いだしました。

class DrawObject : private boost::noncopyable {
public:
    template <typename T>
    void draw(const T &t) {
        assert(graphic_device_);
        // 図形の頂点情報を得る
        const auto vertex(t.vertex());
        // 2D図形を描画するのに必要なFVFに設定
        graphic_device_->SetFVF(kFvfVertexBox);
        // 描画転送する
        graphic_device_->DrawPrimitiveUP(t.primitiveType(), t.primitiveCount, &vertex.front(), sizeof(vertex.front());
    }
private:
    const boost::intrusive_ptr<IDirect3DDevice9> graphic_device_;
};

テンプレートで図形型を受けるようにして、図形型が座標やプリミティブタイプ等の必要な情報を返すメソッドを実装するようにします。

class Box {
public:
    Box(float width, float height) : width_(width), height_(height) {}
    Box(const Box &rhs) : width_(rhs.width_), height_(rhs.height_) {}
    std::array<Vertex2D, 4> vertex() const {
        const std::array<Vertex2D, 4> vertex = {{
            {0.f, 0.f, 0.f, 1.f, 0xFFFFFFFF},
            {width_, 0.f, 0.f, 1.f, 0xFFFFFFFF},
            {0.f, height_, 0.f, 1.f, 0xFFFFFFFF},
            {width_, height_, 0.f, 1.f, 0xFFFFFFFF}
        }};
        return vertex;
    }
    D3DPRIMITIVETYPE primitiveType() const {
        return D3DPT_TRIANGLESTRIP;
    }
    std::uint32_t primitiveCount() const {
        return 2;
    }
private:
    Box& operator =(const Box &);
    const float width_;
    const float height_;
};

class Line {
public:
    Line(float x, float y) : x_(x), y_(y) {}
    Line(const Line &rhs) : x_(rhs.x_), y_(rhs.y_) {}
    std::array<Vertex2D, 2> vertex() const {
        const std::array<Vertex2D, 2> vertex = {{
            {0.f, 0.f, 0.f, 1.f, 0xFFFFFFFF},
            {x_, y_, 0.f, 1.f, 0xFFFFFFFF},
        }};
        return vertex;
    }
    D3DPRIMITIVETYPE primitiveType() const {
        return D3DPT_LINELIST;
    }
    std::uint32_t primitiveCount() const {
        return 2;
    }
private:
    Line& operator =(const Line &);
    const float x_;
    const float y_;
};

class Pixel {
public:
    Pixel(float x, float y) : x_(x), y_(y) {}
    Pixel(const Pixel &rhs) : x_(rhs.x_), y_(rhs.y_) {}
    std::array<Vertex2D, 1> vertex() const {
        const std::array<Vertex2D, 1> vertex = {{
            {x_, y_, 0.f, 1.f, 0xFFFFFFFF},
        }};
        return vertex;
    }
    D3DPRIMITIVETYPE primitiveType() const {
        return D3DPT_POINTLIST;
    }
    std::uint32_t primitiveCount() const {
        return 1;
    }
private:
    Pixel& operator =(const Pixel &);
    const float x_;
    const float y_;
};

これだけだと色の変更や図形の移動ができないので、次のようなテンプレートクラスを用意します。

template <typename T>
class Color {
public:
    Color(const T &t, const std::uint32_t color) : t_(t), color_(color) {}
    auto vertex() -> decltype(t_.vertex()) const {
        auto vertex(t_.vertex());
        std::for_each(vertex.begin(), vertex.end(), [](Vertex2D &v) {v.color = color_;});
        return vertex;
    }
    D3DPRIMITIVETYPE primitiveType() const {
        return t_.primitiveType();
    }
    std::uint32_t primitiveCount() const {
        return t_.primitiveCount();
    }
private:
    const T &t_;
    const std::uint32_t color_;
};

template <typename T>
class Move {
public:
    Move(const T &t, const float x, const float y) : t_(t), x_(x), y_(y) {}
    auto vertex() -> decltype(t_.vertex()) const {
        auto vertex(t_.vertex());
        std::for_each(vertex.begin(), vertex.end(), [](Vertex2D &v) {v.x += x_; v.y += y_});
        return vertex;
    }
    D3DPRIMITIVETYPE primitiveType() const {
        return t_.primitiveType();
    }
    std::uint32_t primitiveCount() const {
        return t_.primitiveCount();
    }
private:
    const T &t_;
    const float x_;
    const float y_;
};

これらを以下の様に組み合わせれば、色々な図形が書けるようになる気がします。

void TestApplication::update(DrawObject *draw_object) {
    assert(draw_object);
    const Box box(32.f, 32.f);
    // (0,0)を基点に32*32の白い四角形を描画
    draw_object->draw(box);
    const Line line(32.f, 32.f);
    // (0, 0)から(32,32)を結ぶ白い線分を描画
    draw_object->draw(line);
    const Pixel pixel(64.f, 64.f);
    // (64,64)に白い点を描画
    draw_object->draw(pixel);
    // (32,32)を基点に32*32の白い四角形を描画
    draw_object(Move(box, 32.f, 32.f));
    // (32,32)を基点に32*32の赤い四角形を描画
    draw_object(Color(Move(box, 32.f, 32.f), 0xFF0000FF));
}

新しく図形を書きたいときは、適当な頂点情報とプリミティブタイプと数を返すクラスを作ればいいだけので拡張もしやすくなるのではないかと思います。また、図形を回転させたいときは頂点情報を回転変換するクラスを書けば実現できるのでこちらも拡張しやすいのではないかと勝手に思った次第です。仕様上Vertex2Dの値コピーが多くなってしまいますがそこはコンパイラのNRVOを期待しましょう。
追記:
ごめんなさい。decltype(t_.vertex())のところでコンパイルエラーが出ることが判明したので現在対処法を審議中です。で、30分ほどで審議は終わり、結論としてはBoxなりLineなりに頂点配列と頂点の型をtypedefして持たせ、decltype(t_.vertex())の代わりにそいつらを使ってやることでひとまず解決しました。更に問題が見つかって、実はMoveとかColorはテンプレート引数を取らないと駄目みたいです。よって上のコードは

Color<Move<Box>>(Move<Box>(box, 32.f, 32.f), 0xFF0000FF));

とかしないといけません。なんだか急に面倒な様相を見せてみました。なにかこの長ったらしいのを避ける方法がないかを審議する必要がありそうです。あと、脳内コンパイルだけでコードを載せると結構悲惨なのが続いているので、これからはできる限りコンパイル通してから載せます。たぶん。