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

ゆとりーなの日記

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

Linuxでgtkmm+OpenGLでゲームっぽいものを作るのには情報量が少なすぎる

Windowswin32+DirectXは沢山情報があるのにそれに比べるとほんと情報ないんですね。
まぁクラスリファレンスやら解説サイトやらサンプルやらを読み漁りからの試行錯誤の結果ちょっとそれっぽいものが出来たのでメモしておきます。
gtkmmからOpenGLをいい感じに使うラッパーとしてgtkextmmというライブラリがあるみたいなのでこれを使うことにしました。apt-getとかすれば一発で入れられると思うんで、そう考えればWindowsインストーラーとかよりも楽かもですね。あと、g++でコンパイルする際には、

`pkg-config --cflags --libs gtkglextmm-1.2`

って付けておけば必要なインクルードパスなり依存ライブラリのリンク設定なりを勝手にやってくれるのでこれもVC++でいちいちインクルードパスを通したりリンカオプション付けたりするのよりも楽かもですね。
で、コードの方に入ります。

#include <functional>
#include <string>
#include <stdexcept>
#include <gtkmm.h>
#include <gtkglmm.h>

// 頂点構造体
// DirectX風にしてあるが、2Dでもrhwは不要だし色設定がrgbaそれぞれがfloatなのは割とどうしようもない説がある
struct vertex {
  float x;
  float y;
  float z;
  float r;
  float g;
  float b;
  float a;
};

// OpenGLでの描画エリア的な感じのもの
class drawing_area : public Gtk::GL::DrawingArea {
 public:
  drawing_area(const int width, const int height) : width_(width), height_(height) {
    // OpenGLの初期化的な感じのもの
    Glib::RefPtr<Gdk::GL::Config> glconfig;
    glconfig = Gdk::GL::Config::create(Gdk::GL::MODE_RGB | Gdk::GL::MODE_DEPTH | Gdk::GL::MODE_DOUBLE);
    if (!glconfig) {
      throw std::runtime_error("OpenGLの初期化に失敗しました");
    }
    set_gl_capability(glconfig);
  }
  
  // OpenGLでの描画はこれと
  bool begin_scene() {
    Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();
    return glwindow->gl_begin (get_gl_context());
  }
  
  // これの間に行うっぽい
  void end_scene() {
    Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();
    glwindow->gl_end();
  }
  
  // 描画を反映?
  void present() {
    Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();
    glwindow->swap_buffers ();
  }
  
  // 画面を現在の色で塗りつぶす
  // 多分デフォルトだと黒
  void clear() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  }
  
 protected:  
  // 起動時に呼び出される
  virtual void on_realize() {
    // これを呼ばないとセグるらしい
    Gtk::DrawingArea::on_realize();
    // ここでOpenGLの初期設定をしとくと良さげかも
    // 取り敢えず現在の画面で2D描画を行えるように設定しておく
    Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();
    glwindow->gl_begin (get_gl_context());
    glViewport(0, 0, width_, height_);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.f, static_cast<float>(width_), static_cast<float>(height_), 0.f, 0.f, 1.f);
    glwindow->gl_end();
  }
  
  // 他のウィンドウで隠れていた場合などに呼び出されるらしい
  // これをオーバーライドしておかないと画面が表示されないくさい
  virtual bool on_expose_event(GdkEventExpose * const event) {
    Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();
    glwindow->gl_begin (get_gl_context());
    glwindow->swap_buffers ();
    glwindow->gl_end();
    return true;
  }
  
 private:
  const int width_;
  const int height_;
};

// ゲームのメインウィンドウ
class main_window : public Gtk::Window {
 public:
  main_window(const int width, const int height, const std::string &caption) : func_([] {}) {
    // 最大化ボタンや窓の端っこをいじってのサイズ変更を禁止する
    set_resizable(false);
    Gdk::Geometry size;
    size.min_width = width;
    size.min_height = height;
    // サイズを設定する
    // これを設定しておかないと多分画面サイズを0から目的のサイズに変更できないっぽい
    set_geometry_hints(*this, size, Gdk::HINT_MIN_SIZE);
    resize(width, height);
    // ウィンドウタイトルを設定
    set_title(caption);
  }
  
  // OpenGLでの描画エリア的な感じのものをウィンドウに登録
  void set_drawing_area(drawing_area &area) {
    add(area);
    // これを忘れるとOpenGLでの描画エリア的な感じのものが表示されない
    show_all_children();
  }
  
  // 16ms毎に呼ばれる関数を登録
  template <typename Pred>
  void set_func(const Pred pred) {
    func_ = pred;
    // 16ms毎に呼ばれるシグナルみたいなものにメソッドを登録
    Glib::signal_timeout().connect(sigc::mem_fun(*this, &main_window::update), 16);
  }
  
 private:
  // 16ms毎に呼ばれるシグナルみたいなものに登録するメソッド
  bool update() {
    func_();
    return true;
  }
  
  std::function<void ()> func_;
};

int main() {
  // コマンドライン引数をごまかす
  char empty_str[] = "";
  char *nul[1] = {empty_str};
  char **tempc = {nul};
  int tempv = 1;
  // gtkmmの初期化
  Gtk::Main kit(tempv, tempc);
  // gtkglextmmの初期化
  Gtk::GL::init(tempv, tempc);
  main_window window(640, 480, "test");
  drawing_area area(640, 480);
  window.set_drawing_area(area);
  window.set_func([&area] {
    if (area.begin_scene()) {
      area.clear();
      // OpenGLでDirectXみたいな描画をするときは多分頂点配列とかカラー配列を使うのがよさげな感じ
      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);
      const vertex triangle[3] = {
        {320.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f}, {640.f, 480.f, 0.f, 0.f, 1.f, 0.f, 1.f}, {0.f, 480.f, 0.f, 0.f, 0.f, 1.f, 1.f}
      };
      glVertexPointer(3, GL_FLOAT, sizeof(triangle[0]), &triangle[0].x);
      glColorPointer(4, GL_FLOAT, sizeof(triangle[0]), &triangle[0].r);
      glDrawArrays(GL_TRIANGLES, 0, 3);
      glDisableClientState(GL_VERTEX_ARRAY);
      glDisableClientState(GL_COLOR_ARRAY);
    }
    area.end_scene();
    area.present();
  });
  kit.run(window);
  return 0;
}

今回は頑張ってコメントを書いておきましたが、一応補足的なものも。
まずは毎フレーム毎に呼ばれる関数について。OpenGLで垂直同期をとるには拡張関数を呼ばないといけない感じがして面倒そうだったので、なんか指定時間毎に呼ばれるシグナルみたいなものがあるみたいなのでそれに60FPSの大体の1フレームの時間である16msをぶち込んで、それが指定した関数を呼ぶようにしてみました。WM_TIMERはわりと精度が残念な事で有名ですが、こちらはどれくらいの精度なのかちょっと分からなかったのでこれで大丈夫なのかどうかはそれなりに審議です。余談ですが、主に弾幕クラスタを中心に?垂直同期での1フレーム管理を嫌う方がいるみたいなので、Windows版もいずれはタイマーで管理することになるのかもしれません。
続いてdrawing_areaですが、解説サイトなんかだとよく

// ウィンドウサイズが変化したときなどに呼び出されるらしい
virtual bool on_configure_event (GdkEventConfigure * const event);

をオーバーライドする例を見かけますが、なんか試した感じだとこれは別にオーバーライドしなくても良さげな感じでした。
あとは、コマンドライン引数をごまかしているところですかね。Windows版ではコマンドライン引数が不要なのにLinux版で要求するのはあれだからなんとか誤魔化せないものかと余計な試行錯誤した結果がこれです。最初は偽argvを0、偽argcをNULLにしてたのですが、これではどうも上手く起動しないのでまぁ大抵のGUIアプリは引数なしで起動するだろうからargvは1でargcは実行パスだろうなという安易な考えから、偽argvには1、偽argcには実行パス取るのも面倒そうなので空の文字列をぶちこんだところそれなりに特に問題なく動いたっぽいのでこれで放置しています。どうも試した感じだとウィンドウの初期タイトルに".実行ファイル名"が入るみたいで、これに使われてるんじゃないかと勝手に推測しています。ちなみに余分なtemp変数が沢山あるのはC++0x以降では文字列をconst char *でしかとれないのでそれの回避です。
そして実行結果はこんな感じになります。

まぁわりと結構エスパーが入っているので詳しい情報待ち感がありますので悪しからず。