読者です 読者をやめる 読者になる 読者になる

OpenGLをC++11でラップしてみんとてするなり 第7回

OpenGL C++

今回のテーマはテクスチャ。今までと比べると単に作って破棄する機能が増えただけに見えて、実はかなり厄介です。
というのも、インスタンスの作成から破棄までの間でコンテキストが切り替わらない今までの機能とは異なり、コンテキストが切り替わってもデータをずっと保持しておく必要があるためです。しかも、特別に設定をしない限りはテクスチャIDはコンテキストごとに独立しているので、複数コンテキストで同じ値を使い回したい時にどうするか、少々工夫が必要になります。

なお、OpenGLのテクスチャには何種類かありますが、ここではGL_TEXTURE_2DとGL_TEXTURE_RECTANGLEにだけ対応することにしますので、ご了承下さい。またミップマップへの対応も省略します。

●テクスチャの処理

MVCモデルで考えると、コンテキストはViewで、それぞれのコンテキストに表示される物体はModelになりますが、テクスチャ情報は物体の属性として用意されるはずなのでModel側になります。よって、テクスチャクラスは、コンテキストに依存しないクラスとして作るべきでしょう。
そしてテクスチャを描画する際は、コンテキストに対して「今からこのテクスチャを使え」と指示することになります。今までと同様に考えると、コンテキストにテクスチャを渡すと何らかのオブジェクトが返ってきて、そのオブジェクトのデストラクタで元々指示されていたテクスチャに戻すことになるでしょう。

そこで問題になるのは、コンテキストごとのテクスチャIDを、どのように管理するか、ということになります。
最も単純な実装では、テクスチャを使い始める瞬間にコンテキストごとのテクスチャIDを生成し、使い終わったら削除する、となりますが、GPUにテクスチャの画像データを送り込むのは極めて重い処理ですので、なるべく避けたいところです。
ですので、初回に使う時には仕方ありませんが、一度作ったコンテキストごとのテクスチャIDは、ずっと保持したままにしておきましょう。

不要になったデータを削除する際の処理も考えると、具体的な挙動は次のようになるかと思います:

・コンテキスト非依存のテクスチャの作成
 →特別な処理は不要
・コンテキスト非依存のテクスチャの更新
 →各コンテキストに対し、コンテキストごとのテクスチャIDに紐付けられた情報を更新するよう要求する
・コンテキスト非依存のテクスチャの削除
 →各コンテキストから、コンテキストごとのテクスチャIDを削除する
・新規コンテキストの生成
 →特別な処理は不要
・コンテキストの削除
 →テクスチャIDを全て削除する
・コンテキスト非依存のテクスチャを、あるコンテキストで初めて描画
 →コンテキストごとのテクスチャIDを作成する
 →コンテキストごとのテクスチャIDを、描画中のみ有効化する
・コンテキスト非依存のテクスチャを、あるコンテキストで2回目以降描画
 →コンテキストごとのテクスチャIDを、描画中のみ有効化する

テクスチャIDは、コンテキストが削除されてもテクスチャが削除されても削除される必要があるため、なかなかややこしそうに見えます。
ついでに言うと、FBOを用いてテクスチャメモリ上に描画を行った際のことを考えるとさらに頭が痛くなるのですが、今は置いておくことにしましょう。

●テクスチャ関連クラスの設計

 とりあえず、テクスチャ関連のクラスが保持するべき情報を考えましょう。

(1) コンテキスト非依存なテクスチャ
 更新や削除を各コンテキストに通知するためには、どのコンテキストにテクスチャIDを持っているか、という情報が必要になります。
 テクスチャオブジェクトが(コンテキスト, テクスチャID)の組をすべて持っている、という方法も考えられますが、コンテキストとテクスチャの数が増えすぎると、かなりのデータを保持しなければなりません。
 通常は、コンテキストの数は限られているはずですし、更新や削除が頻繁に行われることはないでしょうから、全てのコンテキストに「自分に関連するテクスチャIDがあれば、処理してくれ」と通知すれば十分かと思います。
 テクスチャは、コピー不能にしておくといろいろ後が楽そうではありますが、「このテクスチャをコピーして、画像のこの辺りだけ修正して別のテクスチャーを作る」みたいなことは頻繁にありそうですので、コピーはできるようにしておきましょう(ただし、今回は実装を割愛します)。
 ムーブは、(2)のことを考えて禁止します。ムーブを許可する仕組みも作れますが、このクラスはOpenGLのリソースを使用するわけではないので、あまり気にすることもありません。

(2) コンテキスト
 まずは(1)に従い、全てのコンテキストを順に追えるような仕組みを追加しましょう。コンストラクタSTLコンテナに追加して、デストラクタで除去することにします。コンテキストを作成順に追ったり、検索したりする必要はなく、とりあえず全てが高速に列挙できれば十分ですので、std::vectorに入れておけばよいでしょう。
 次に、コンテキストは(テクスチャ, テクスチャID管理クラス)の組を持つようにします。テクスチャのムーブは(1)で禁止しましたので、キーはテクスチャオブジェクトへのポインタで十分でしょう。マッピングの変更は少ないですが検索頻度は高いため、C++11から登場したstd::unordered_mapを用いて速度を稼ぎます。

(3) テクスチャID管理クラス
 必要な情報は全てOpenGL側で管理してくれるため、基本はテクスチャIDの生成/破棄を行えば十分です。
 ただ、テクスチャの内容の更新処理はコンテキスト外で行われる可能性がありますが、それを反映させるのはコンテキストがアクティブになった時でなければなりません。
 そのため、更新処理が必要かどうかのフラグを追加で持っておき、コンテキスト内でテクスチャを使用する必要が生じた際に、フラグが立っていたらデータを更新する必要があります。

●サンプルソース
 以上を元に作ったサンプルソースがこれです。

実のところ、C++11らしい部分として重要なところは新しくは出てきていません。極めて普通のC++のコードです。
gl/Texture.hに固定長配列の参照を返している妙な書式があったりもしますが(const GLfloat (&getBorderColor() const)[4] { return m_border_color ; })、これもまあ昔からできたコードです。

強いて1点だけ挙げるとすれば、flag_util.hでしょうか?

flag_util.h

#include <type_traits>

#define define_flag_operators(NAME)                                                 \
inline NAME operator | (const NAME& x, const NAME& y) {                             \
  typedef std::underlying_type<NAME>::type base_type ;                              \
  return static_cast<NAME>(static_cast<base_type>(x) | static_cast<base_type>(y)) ; \
}                                                                                   \
inline NAME operator & (const NAME& x, const NAME& y) {                             \
  typedef std::underlying_type<NAME>::type base_type ;                              \
  return static_cast<NAME>(static_cast<base_type>(x) & static_cast<base_type>(y)) ; \
}                                                                                   \
inline NAME operator ^ (const NAME& x, const NAME& y) {                             \
  typedef std::underlying_type<NAME>::type base_type ;                              \
  return static_cast<NAME>(static_cast<base_type>(x) ^ static_cast<base_type>(y)) ; \
}                                                                                   \
inline NAME& operator |= (NAME& x, const NAME& y) {                                 \
  return x = x | y ;                                                                \
}                                                                                   \
inline NAME& operator &= (NAME& x, const NAME& y) {                                 \
  return x = x & y ;                                                                \
}                                                                                   \
inline NAME& operator ^= (NAME& x, const NAME& y) {                                 \
  return x = x ^ y ;                                                                \
}                                                                                   \
inline NAME operator ~ (const NAME& x) {                                            \
  typedef std::underlying_type<NAME>::type base_type ;                              \
  return static_cast<NAME>(~static_cast<base_type>(x)) ;                            \
}

C++11のenum classで型安全性が確保できたはいいのですが、enum classをビットフラグとして使うことは逆に困難になりました。何故ならenum classにはビット演算が定義されていないからです。
ならば演算子オーバーロードで自作してしまえ、というコンセプトで作ったのが、上記の長ったらしいマクロです。
どこか適当な場所でdefine_flag_operators(型名)とやってやるだけで各種のビット演算が定義できます。まあ、あまり推奨できるコードではないのは確かですが。

そう言いながらも多少解説を行いますと、enum classは特に何も指定しなければintと同サイズですが、enum class Hoge : short {}のように継承に似た書式を用いる事で、任意の整数型と同サイズになることができます。
ですので、ビット演算のためにstatic_castで整数型に変換する際、変換後の型はenum classと同サイズの整数型を取得しなければなりません。
そこで登場するのがstd::underlying_type。enum classに対してstd::underlying_type::typeとすると、Hogeと同サイズの整数型が得られます。ですので、この型にstatic_castしてからビット演算して、最後にenum classに戻せばOK、ということです。


これにて、当面予定していた3つの機能(glEnable()/行列操作/テクスチャ)の実装を終えましたので、OpenGLC++11の話については一旦筆を置きます。
とはいえ、OpenGLはいろいろとラッパーの作り甲斐のあるライブラリですので、また何か別のネタを思いついたらいつの間にか第8話が出てくるかもしれません。
とりあえずは、次回からは別のことについて書こうと思っています。