今はこれが精一杯-3

前回の続きです。

ここからが本番。
今度は同じテクスチャで表現可能な複数のオブジェクトをまとめて描画するように設計します。

プログラムの設計

登場人物を3人とします。

  • Renderer
    画面にグラフィックスを描画する部門の責任者。テクスチャを持っている。どのTextureObjectがどのテクスチャで描画されるのかを知っている。必要に応じて、テクスチャのロード、アンロードを行う。
  • TextureObject
    Rendererによって表現される世界(今のところ二次元)の住人。自身の色、座標、その他の情報を持つ。自分自身のテクスチャマッピング方法を知っているが、実際に使用するテクスチャは保持しておらず、Rendererによって、他のTextureObjectとテクスチャを共有する。
  • TextureDrawer
    描画実行担当者。Rendererによって毎フレーム生成される。Rendererから1つのテクスチャと複数のTextureObjectを指定される。TextureDrawerは、TextureObjectの座標の情報は知らないので、どこにどうやって描画すれば良いのかは、各TextureObjectにその都度問い合わせる。
このように考えてみました。
ポイントは、TextureDrawerとTextureObjectとの連絡の取り方になりそうです。


TextureDrawerの処理ステップ

次に、具体的なTextureDrawerの処理内容を考えてみます。

  1. Rendererからテクスチャと対応するTextureObjectの一覧を受け取る
  2. 必要なメモリサイズを調べる
    1度に描画するすべてのTextureObjectに対して、必要なメモリサイズを問い合わせる。
  3. 必要なメモリを確保する
    調べたメモリサイズを合計して、必要な分のメモリ領域を実際に確保する。
  4. 座標データを構築する
    すべてのTextureObjectに対して、座標を問い合わせる。
    TextureDrawerは、「あなたの座標をここに書き込んでおいてください」と、メモリのポインタをTextureObjectに渡す。TextureObjectは、指定されたポインタに自分の座標を書き込む。TextureDrawerはTextureObject個々の座標を知る必要はない。
  5. 描画を実行する
    最後に、Rendererから受け取ったテクスチャと座標セットを使ってOpenGLに描画命令を出す。
これで理論上は無駄なくうまく行くと思います。


情報受け渡し用のデータ構造を定義する

必要なメモリサイズを知るためのデータ構造と、座標を格納するメモリのセットがあれば良さそうです。
あまり意味はないかもしれませんが、一応テンプレートを使ってデータ型を汎用化します。

// テクスチャ描画のために必要なメモリサイズを表す構造体
template <typename S>
struct TextureVerticesHeader 
{
    S vertexArraySize;		// 頂点座標のメモリサイズ
    S colorArraySize;		// 色情報のメモリサイズ
    S coordArraySize;		// テクスチャマッピングのメモリサイズ
    int vertexNum;		// 頂点の数
};

// テクスチャ表示のためのポインタをまとめた構造体
template <typename T, typename C>
struct TextureVerticesPointers 
{
    T* vertexPointer;		// 頂点座標を格納するポインタ
    C* colorPointer;		// 色情報を格納するポインタ
    T* coordPointer;		// テクスチャマッピング座標を格納するポインタ
};

TextureObjectクラスを定義する

SimpleTextureObject以外のTextureObjectは以下のクラスを継承することとします。

// テクスチャによって表示されるオブジェクトの基底クラス
// Tは頂点座標、及びテクスチャマッピング座標に使う型で、通常はGLfloat
// Cは色情報に使う型で、通常はGLubyte
// Sはメモリサイズを表す整数型で、通常はsize_t
template <typename T, typename C, typename S>
class TextureObject
{
public:
    virtual void GetDrawSize(TextureVerticesHeader<S>*) = 0;
    virtual void PrepareDraw(TextureVerticesPointers<T, C>*) = 0;
};

デフォルトの実装クラスを作っておく

メモリの効率が悪くなることを無視すれば、多くのTextureObjectは、以下のように実装できると思います。
1オブジェクトの頂点が6個でない場合は、これとは別の独自の実装を行うこととします。

// TextureObjectのデフォルトの実装
// 1オブジェクトが2ポリゴンで表現可能な場合に適用可能。
// 派生クラスは、protectedな属性に適切な値を管理してやるだけで、GetDrawSizeとPrepareDrawの実装をしないで済む。
// ただしメモリの使用効率は良くない。
template <typename T, typename C, typename S>
class TextureObjectDefaultImplement : public TextureObject<T, C, S>
{
    typedef TextureObject<T, C, S> inherited;
	
public:
    TextureObjectDefaultImplement();
    virtual ~TextureObjectDefaultImplement();
    virtual void GetDrawSize(TextureVerticesHeader<S>*) override;
    virtual void PrepareDraw(TextureVerticesPointers<T, C>*) override;
	
protected:
    T _x, _y, _width, _height, _u, _v, _tw, _th;
    C _r, _g, _b, _a;
	
    void Clear();
};

template <typename T, typename C, typename S>
TextureObjectDefaultImplement<T, C, S>::TextureObjectDefaultImplement()
{
    Clear();
}

template <typename T, typename C, typename S>
TextureObjectDefaultImplement<T, C, S>::~TextureObjectDefaultImplement()
{
}

template <typename T, typename C, typename S>
void TextureObjectDefaultImplement<T, C, S>::GetDrawSize(TextureVerticesHeader<S>* header)
{
    header->vertexArraySize = sizeof(T) * 6 * 2;	// 三角形2個、x, y座標
    header->colorArraySize = sizeof(C) * 6 * 4;		// 三角形2個、r, g, b, a
    header->coordArraySize = sizeof(T) * 6 * 2;		// 三角形2個、x, y座標
    header->vertexNum = 6;				// 三角形2個
}

template <typename T, typename C, typename S>
void TextureObjectDefaultImplement<T, C, S>::PrepareDraw(TextureVerticesPointers<T, C>* ptrs)
{
    // 頂点座標
    *ptrs->vertexPointer++ = _x - _width / 2;	// 左上
    *ptrs->vertexPointer++ = _y + _height / 2;
	
    *ptrs->vertexPointer++ = _x + _width / 2;	// 右上
    *ptrs->vertexPointer++ = _y + _height / 2;
	
    *ptrs->vertexPointer++ = _x - _width / 2;	// 左下
    *ptrs->vertexPointer++ = _y - _height / 2;
	
    *ptrs->vertexPointer++ = _x + _width / 2;	// 右上
    *ptrs->vertexPointer++ = _y + _height / 2;
	
    *ptrs->vertexPointer++ = _x - _width / 2;	// 左下
    *ptrs->vertexPointer++ = _y - _height / 2;
	
    *ptrs->vertexPointer++ = _x + _width / 2;	// 右下
    *ptrs->vertexPointer++ = _y - _height / 2;
	
	
    // 色情報
    *ptrs->colorPointer++ = _r;
    *ptrs->colorPointer++ = _g;
    *ptrs->colorPointer++ = _b;
    *ptrs->colorPointer++ = _a;

    *ptrs->colorPointer++ = _r;
    *ptrs->colorPointer++ = _g;
    *ptrs->colorPointer++ = _b;
    *ptrs->colorPointer++ = _a;

    *ptrs->colorPointer++ = _r;
    *ptrs->colorPointer++ = _g;
    *ptrs->colorPointer++ = _b;
    *ptrs->colorPointer++ = _a;

    *ptrs->colorPointer++ = _r;
    *ptrs->colorPointer++ = _g;
    *ptrs->colorPointer++ = _b;
    *ptrs->colorPointer++ = _a;

    *ptrs->colorPointer++ = _r;
    *ptrs->colorPointer++ = _g;
    *ptrs->colorPointer++ = _b;
    *ptrs->colorPointer++ = _a;

    *ptrs->colorPointer++ = _r;
    *ptrs->colorPointer++ = _g;
    *ptrs->colorPointer++ = _b;
    *ptrs->colorPointer++ = _a;
	
    // テクスチャ座標
    *ptrs->coordPointer++ = _u;		// 左上
    *ptrs->coordPointer++ = _v;
	
    *ptrs->coordPointer++ = _u + _tw;	// 右上
    *ptrs->coordPointer++ = _v;
	
    *ptrs->coordPointer++ = _u;		// 左下
    *ptrs->coordPointer++ = _v + _th;
	
    *ptrs->coordPointer++ = _u + _tw;	// 右上
    *ptrs->coordPointer++ = _v;
	
    *ptrs->coordPointer++ = _u;		// 左下
    *ptrs->coordPointer++ = _v + _th;
	
    *ptrs->coordPointer++ = _u + _tw;	// 右下
    *ptrs->coordPointer++ = _v + _th;
}

template <typename T, typename C, typename S>
void TextureObjectDefaultImplement<T, C, S>::Clear()
{
    _x = _y = _u = _v = 0.0f;
    _width = _height = _tw = _th = 1.0f;
    _r = _g = _b = _a = 255;
}

描画クラスを実装する

TextureObjectとの連絡手段が定義できたので、後は実際の描画コードを書くだけです。

// テクスチャオブジェクトをまとめて描画するクラス
template <typename T, typename C, typename S, int t_type, int c_type>
class TextureObjectsDrawer
{
    typedef TextureObject<T, C, S> Object;
	
public:
    TextureObjectsDrawer();
    TextureObjectsDrawer(std::vector<Object*>&);
    void Add(Object*);
    void AddAll(std::vector<Object*>&);
    void Draw(GLuint texture);
    void Clear();
	
private:
    std::vector<Object*> _array;
};

template <typename T, typename C, typename S, int t_type, int c_type>
TextureObjectsDrawer<T, C, S, t_type, c_type>::TextureObjectsDrawer()
{
}

template <typename T, typename C, typename S, int t_type, int c_type>
TextureObjectsDrawer<T, C, S, t_type, c_type>::TextureObjectsDrawer(std::vector<Object*>& v)
{
    _array = v;
}

template <typename T, typename C, typename S, int t_type, int c_type>
void TextureObjectsDrawer<T, C, S, t_type, c_type>::Add(Object* p)
{
    _array.push_back(p);
}

template <typename T, typename C, typename S, int t_type, int c_type>
void TextureObjectsDrawer<T, C, S, t_type, c_type>::AddAll(std::vector<Object*>& v)
{
    _array.insert(_array.end(), v.begin(), v.end());
}

template <typename T, typename C, typename S, int t_type, int c_type>
void TextureObjectsDrawer<T, C, S, t_type, c_type>::Draw(GLuint texture)
{
    // メモリサイズを計算する
    S vert_size = 0;	// 頂点座標メモリサイズ
    S color_size = 0;	// 色情報メモリサイズ
    S coord_size = 0;	// テクスチャマッピング座標メモリサイズ
    int vert_num = 0;	// 頂点の数
	
    TextureVerticesHeader<S> sizes;
	
    std::for_each(_array.begin(), _array.end(), [&](Object* p){
        p->GetDrawSize(&sizes);
        vert_size += sizes.vertexArraySize;
        color_size += sizes.colorArraySize;
        coord_size += sizes.coordArraySize;
        vert_num += sizes.vertexNum;});
	
    // メモリを確保する
    T* vertices = (T*)malloc(vert_size);
	
    if (!vertices) {
        return;
    }
	
    C* colors = (C*)malloc(color_size);
	
    if (!colors) {
        free(vertices);
        return;
    }
	
    T* coords = (T*)malloc(coord_size);
	
    if (!coords) {
        free(vertices);
        free(colors);
        return;
    }
	
    // 座標計算を実行
    TextureVerticesPointers<T, C> ptrs;
    ptrs.vertexPointer = vertices;
    ptrs.colorPointer = colors;
    ptrs.coordPointer = coords;

    std::for_each(_array.begin(), _array.end(), [&](Object* p){p->PrepareDraw(&ptrs);});
	
    // 描画実行
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, texture);
    glVertexPointer(2, t_type, 0, vertices);
    glEnableClientState(GL_VERTEX_ARRAY);
    glColorPointer(4, c_type, 0, colors);
    glEnableClientState(GL_COLOR_ARRAY);
    glTexCoordPointer(2, t_type, 0, coords);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	
    glDrawArrays(GL_TRIANGLES, 0, vert_num);
	
    // メモリ解放
    free(vertices);
    free(colors);
    free(coords);
	
    // 描画が済んだオブジェクトは自動的に捨ててしまう
    _array.clear();
}

できました!


実装クラスを作ってみる

一応、完成したと思うので、きちんと表示できるか試してみます。
レースゲームのチェックポイントを8個表示してみます。

class CpCheckPoint : public CpTextureObjectDefaultImplement
{
    typedef CpTextureObjectDefaultImplement inherited;
	
public:
    CpCheckPoint(float, float, float);
    float GetX();
    float GetY();
    float GetRadius();
    void SetNext(bool);
	
    void PrepareDraw(CpTextureVerticesPointers*) override;
	
private:
    float _radius;
    bool _next;
};

CpCheckPoint::CpCheckPoint(float x, float y, float radius) : inherited()
{
    _x = x;
    _y = y;
    _radius = radius;
    _width = _height = _radius * 2;
    _next = false;
}

float CpCheckPoint::GetX()
{
    return _x;
}

float CpCheckPoint::GetY()
{
    return _y;
}

float CpCheckPoint::GetRadius()
{
    return _radius;
}

void CpCheckPoint::SetNext(bool onoff)
{
    _next = onoff;
}

void CpCheckPoint::PrepareDraw(CpTextureVerticesPointers* ptrs)
{
    _a = (_next) ? 255 : 128;
    inherited::PrepareDraw(ptrs);
}

うん。
ライブラリというほどではないですが、自分の中で実装の方式を明確に定義できたので、自分なりに分かりやすく、それなりに短いコードで実装できました。そして前よりも効率良く処理できていると思う(実際にフレームレート確認とかはしてないので、気分だけw)。満足!

あとは表示確認。

おっけーおっけー


これでほぼ目的は達成できたのですが、最初の目的は、数字表示クラスの失敗を克服することでしたので、もうちょっとだけ続きます。


続く