OpenGL Simple Frame


OpenGLアプリをさくさくたくさん作りたいと常日頃から考えているのだが、OpenGLアプリを作成するたびに、フレームワークの中であちこちに散在する決められたコールバック関数の中に、毎回同じようににOpenGLの定番コードを、思い出しながらあるいは本やサイトを見ながら挿入していくような作業をしなくてはならず、それはとてもやる気と体力を消耗するものだ。そこでそのような空しい思いを繰り返ししなくてもいいように考えた。
ようするに、VC++2008でSDIとかFormとかのプロジェクトをつくるとこから始めて、

  1. OpenGLの初期化、
  2. ビューポート
  3. 視体積
  4. ライト・マテリアル
  5. カメラ移動
  6. 選択処理

が一通り実際に動くコードとして出来ているモノがあるとそこから改変して使えばよいなど、何かと便利なんではなかろうかと思ったのでメモ。その名も「OpenGLSimpleFrame」。OpenGLSimpleAdapterがあることが前提だ。

#pragma once

#include "OpenGLSimpleAdapter.h"

class OpenGLSimpleFrame
{
// OpenGLSimpleFrame (C) 2009 nursmaul
// 
//OpenGLアプリを作成するたびに、フレームワークの中であちこちに散らばった
//コールバック関数の中に、散文的にOpenGLの定番コードを、思い出しながら
//あるいは本やサイトを見ながら毎回挿入していくような、消耗する作業をなくす
//ために作成された。
//
// フレームワーク内で本クラスのインスタンスを作成し、以下の各種
// コールバックでそれぞれのメソッドを呼ぶようにするだけで、OpenGLが
// 使えるようになるだけでなく、カメラ操作と選択処理までを実装した、動く
// 本格雛形アプリの実装が完成する。
//
//(デフォルトのカメラマニピュレータ・DefaultCameraToolを内部クラスとして
// 包含しており、これのインスタンスも作成済みな点が特徴。これを真似た外部
// 独立クラスとしてオリジナルなカメラマニピュレータを作成し、本クラスに
//  登録することも可能。)
//
//・ビューの初期化時に本クラスを作成
//・ビューの描画時に、		⇒OnViewRender()を呼ぶ
//・ビューのリサイズ時に	⇒OnViewResized(w, h)を呼ぶ
//・ピック・選択描画時に ⇒OnSelectRender() を呼ぶ
//・マウス・キーボードイベントなどで各種関数を呼ぶ
//マウス左ボタン押下⇒->DefaultCamera()->OnMouseLButtonDown()を呼ぶ// オブジェクト選択
//マウス右ボタン押下⇒->DefaultCamera()->OnMouseRButtonXXXX()を呼ぶ// 曲座標カメラ位置更新
//    ..
//
  enum OpenGLSimpleFrameEnum{
    SELECT_BUFFER_SIZE=256,
  };
public:
  OpenGLSimpleFrame( 
    OpenGLSimpleAdapter* ogl,
    const int view_width, 
    const int view_height )
    : m_ogl( ogl )
  {
    {
      // OpenGL各種初期化
      m_ogl->BeginRender();
      glSelectBuffer ( SELECT_BUFFER_SIZE, m_selbuf );// セレクションバッファ初期化
      glEnable( GL_DEPTH_TEST );			// デプステスト有効
      glDepthFunc( GL_LEQUAL );
      m_ogl->EndRender();
    }
    m_default_camera = new DefaultCameraTool( m_ogl, this );// カメラ初期化
    this->SetCamera( m_default_camera->Dispatch() );
    this->OnViewResized( view_width, view_height );// ビューポート&視積初期化
  }
  ~OpenGLSimpleFrame( void ){
    delete m_default_camera;
  }
  void BeginRender( void ){ m_ogl->BeginRender(); }
  void EndRender( void ){ m_ogl->EndRender(); }
  void EndRenderNoSwap( void ){ m_ogl->EndRenderNoSwap(); }
  void DrawCamera( void ){
    // カメラ描画
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    glMultMatrixf( this->m_camera_matrix );
    return;
  }
  void DrawInit( void ){
    glViewport( 0,0, m_view_width, m_view_height );
    glClearColor( 0, 0, 0, 0 );
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
  }
  void DrawScene( void ){
    //
    GLfloat white[]	={ 1, 1, 1, 1 };
    {
      // ライト設定
      GLfloat pos0[]	={ 0, 15, 0, 1 };// 4つ目の値は1⇒点光源
      //glLightfv(GL_LIGHT0, GL_SPECULAR, spc0);
      glLightfv(GL_LIGHT0, GL_AMBIENT_AND_DIFFUSE, white);
      glLightfv(GL_LIGHT0, GL_POSITION, pos0);
      glEnable( GL_LIGHT0 );
      glEnable( GL_LIGHTING );
    }
    // グリッド(床)の描画
    glLoadName( 22 );
    {
      // マテリアル設定
      GLfloat c[]={1, .7, .7, 1};
      GLfloat c1[]={.1, .1, .1, 1};
      //glMaterialfv( GL_FRONT_AND_BACK, GL_SPECULAR, c0 );
      glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, white );
      glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT, c1 );
      glMaterialf( GL_FRONT_AND_BACK, GL_SHININESS, 50.0 );
    }
    {
      GLfloat c[]={ 0,.4,0, 1};
      glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, c );
      const int res =10;
      glNormal3f( 0,1,0 );
      for( int i=-res; i<res; ++i ){
        glBegin( GL_TRIANGLE_STRIP );
        for( int j=-res; j<=res; ++j ){
          glVertex3f( i*10, -10, j*10 );
          glVertex3f( (i+1)*10, -10, j*10 );
        }// j
        glEnd();	
      }// i
    }

    // テクスチャボードの描画
    glLoadName( 33 );
    // マテリアル設定
    glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, white );
    // テクスチャ初期化
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glEnable( GL_TEXTURE_2D );
    GLuint texture;
    glGenTextures( 1, &texture );
    glBindTexture( GL_TEXTURE_2D, texture );
    const int w = 256;
    const int h = 256;
    GLubyte buf[ h ][ w ][ 3 ];
    {
      for( int i=0; i<h; ++i ){
        for( int j=0; j<w; ++j ){
          buf[ i ][ j ][ 0 ] = j*.5+ (( (i/20+j/20)&1 )? 128: 0 );
          buf[ i ][ j ][ 1 ] = i;
          buf[ i ][ j ][ 2 ] = 0;
        }// j
      }// i
    }
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, w, h, 
      0, GL_RGB, GL_UNSIGNED_BYTE, buf );
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glBegin( GL_TRIANGLE_STRIP );
    {
      const float a=3;
      glTexCoord2f( 0, 0 );		glVertex3f(-a, -a, 0 );
      glTexCoord2f( 1, 0 );		glVertex3f(a, -a, 0 );
      glTexCoord2f( 0, 1 );		glVertex3f(-a, a, 0 );
      glTexCoord2f( 1, 1 );		glVertex3f(a, a, 0 );
    }
    glEnd();
    glDisable( GL_TEXTURE_2D );
    return;
  }
  void OnViewRender( void ){
    m_ogl->BeginRender();
    this->DrawInit();
    this->DrawCamera();
    this->DrawScene();
    m_ogl->EndRender();
    return;
  };
  void OnSelectRender( const int pick_x, const int pick_y ){
    m_ogl->BeginRender();
    {
      glRenderMode( GL_SELECT );
      glMatrixMode( GL_PROJECTION );
      glPushMatrix();
      {
        glInitNames();
        glPushName(0);
        glLoadIdentity();
        {
          // ここでgluPickMatrixでしょ
          GLint viewport[4]; ::glGetIntegerv( GL_VIEWPORT, viewport );
          gluPickMatrix( pick_x, m_view_height-pick_y, 1, 1, viewport );
        }
        this->CreateViewVolume();
        this->DrawCamera();
        this->DrawScene();
        glMatrixMode( GL_PROJECTION );
      }
      glPopMatrix();
      int hits = glRenderMode( GL_RENDER );
      {
        char s[234];sprintf( s, "%d hits", hits );
        System::Windows::Forms::MessageBox::Show( gcnew System::String(s) );
      }
      glPopMatrix();
    }
    m_ogl->EndRender();
    return;
  }
  // ビューポート&視体積の更新(ビューのサイズ変更時に呼びます)
  void OnViewResized( const int w, const int h ){
    m_view_width = w;
    m_view_height = h;
    this->SetupViewportAndViewVolume();
    // perhaps you will need to invalidate the view after this;
  }
  // カメラのマニピュレータ(マウスやキーボードのコールバックの中で呼びます)
  class DefaultCameraTool{
  public:// デフォルトの曲座標カメラ
    DefaultCameraTool( OpenGLSimpleAdapter* ogl, OpenGLSimpleFrame* frame )
      :m_frame( frame )
    {
      m_rbutton_down=0;
      m_radii = 10;
      m_phi = 0;
      m_tht = 0;
      this->UpdateMatrix();
    }
    float* Dispatch( void ){ return m_; }
    void UpdateMatrix( void ){
      m_frame->BeginRender();
      {
        glMatrixMode( GL_MODELVIEW );
        glPushMatrix();
        glLoadIdentity();
        glTranslatef( 0, 0, -m_radii );// m_radii はマウスホイールで更新してもいいし、
        glRotatef( m_tht, 1,0,0 );// m_phiはマウス座標水平移動で更新
        glRotatef( m_phi, 0,1,0 );// m_thtはマウス座標垂直移動で更新
        ::glGetFloatv( GL_MODELVIEW_MATRIX, m_ );
        glPopMatrix();
      }
      m_frame->EndRenderNoSwap();
      return;
    }
    void OnLMouseButtonDown( const int x, const int y ){
      // マウスピック
      this->m_frame->OnSelectRender( x, y );
      return;
    }
    void OnRMouseButtonDown( const int x, const int y ){
      m_rbutton_down=!0; m_prev_x=x; m_prev_y=y;
    }
    void OnMouseMove( const int x, const int y ){
      if( this->m_rbutton_down ){
        this->m_phi += x - this->m_prev_x;
        this->m_tht += y - this->m_prev_y;
        // Invalidate the view;
        this->UpdateMatrix();
      }
      this->m_prev_x = x;
      this->m_prev_y = y;
    }
    void OnMouseWheel( const int delta ){ 
      m_radii -= delta*.01;
      this->UpdateMatrix();
    }
    void OnRMouseButtonUp( const int x, const int y ){
      m_rbutton_down=0;
    }
  private:
    float m_radii;
    float m_tht;
    float m_phi;
    float m_[16];
    // tool
    int m_prev_x, m_prev_y;
    bool m_rbutton_down;
    OpenGLSimpleFrame*	m_frame;
  };
  DefaultCameraTool* DefaultCamera( void ){ return this->m_default_camera; }
  void SetCamera( float* m ){ m_camera_matrix = m; };
  //
private:
  void CreateViewVolume(){
      float a = m_view_width / static_cast< float >( m_view_height );
      const float h = 1.0;
      const float nc = 2.0;
      const float fc = 9900;
      glFrustum( -a*h, a*h, -h, h, nc, fc );
      return;
  }
  void SetupViewportAndViewVolume( void ){
    // ビューポートと視体積の更新
    m_ogl->BeginRender();
    {
      glMatrixMode( GL_PROJECTION );
      glLoadIdentity();
      this->CreateViewVolume();
    }
    m_ogl->EndRender();
    return;
  }
  const GLuint* SelectionBuffer( void ) const { return m_selbuf; }
  const int ViewWidth( void ) const { return m_view_width; }
  const int ViewHeight( void ) const { return m_view_height; }
private:
  OpenGLSimpleAdapter* m_ogl;
  GLuint m_selbuf[SELECT_BUFFER_SIZE]; 
  int			m_view_width;
  int			m_view_height;
  float*	m_camera_matrix;
  //
  DefaultCameraTool*	m_default_camera;
};


以下は、WindowsFormアプリにおける利用例だ。このようにウィザードコードの中に、数行追加するだけで雛形アプリが完成する。

private: System::Void Form1_Load(System::Object^  sender, System::EventArgs^  e) {
 m_ogl = new OpenGLSimpleAdapter( GetDC( (HWND)panel1->Handle.ToPointer() ) );
#if 0
 // MFCとかの場合
 CRect rect;
 this->GetClientRect( &rect );
 m_frame = new OpenGLSimpleFrame( m_ogl, rect.Width(), rect.Height() );
#else
 // CLIとかだったら例えばフォーム上に panel1 が置いてあったとして、
 m_frame = new OpenGLSimpleFrame( m_ogl, panel1->Width, panel1->Height );
#endif
 this->MouseWheel += gcnew System::Windows::Forms::MouseEventHandler( this, &Form1::Form1_MouseWheel);
}					 }
private: System::Void Form1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e) {
 this->m_frame->OnViewRender();
 }
private: System::Void Form1_Resize(System::Object^  sender, System::EventArgs^  e) {
 this->m_frame->OnViewResized( panel1->Width, panel1->Height );
 }
private: System::Void panel1_MouseDown(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
 if( e->Button == Windows::Forms::MouseButtons::Left ){
	 this->m_frame->DefaultCamera()->OnLMouseButtonDown( e->X, e->Y );
 }else if( e->Button == Windows::Forms::MouseButtons::Right ){
	 this->m_frame->DefaultCamera()->OnRMouseButtonDown( e->X, e->Y );
 }
 }
private: System::Void panel1_MouseUp(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
 if( e->Button == Windows::Forms::MouseButtons::Right ){
	 this->m_frame->DefaultCamera()->OnRMouseButtonUp( e->X, e->Y );
 }
 }
private: System::Void panel1_MouseMove(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
 // ま、WindowsFormの場合はこの時点でマウスボタンステータスを知ることもできるようだけど
 this->m_frame->DefaultCamera()->OnMouseMove( e->X, e->Y );
 this->Invalidate(0);
 }
private: System::Void Form1_MouseWheel(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
  // ここが、マウスホイールのコールバック関数となります。
  //System::Windows::Forms::MessageBox::Show( gcnew System::String("hi") );
  this->m_frame->DefaultCamera()->OnMouseWheel( e->Delta );
  this->Invalidate(0);
 }
};


コード貼り付けただけではアレなのでSimpleAdapterの中身の話を少ししておくと、、内部情報として、

  1. OpenGLSimpleAdapterオブジェクト情報
  2. セレクションバッファ
  3. 外部更新されるビューポート情報
  4. 外部更新されるカメラ情報
  5. (特別サービスでつけた簡易カメラ操作&ピック用ツール(内部定義クラス))

を持っている。最後のDefaultCameraToolは、おまけだ。つまり、このようなクラスがあったとして、OpenGLSimpleFrameをこうして活用してねという、あくまで一利用例としてあえて内部クラスとしてそこにあるだけで、今回のOpenGLSimpleFrameの機能概念に属すものではない。
ウィンドウ作成時に呼ばれるコンストラクタでは、OpenGLの決まりきった初期化を行い、ウィンドウサイズ変更時に呼ばれるOnViewResized()では、ビューポートと視体積の再設定を行う。視体積の設定は、セレクション処理時にも利用される。
ウィンドウ描画時に呼ばれるOnViewRender() では、ビューポート、カメラ、シーンの順に描画される。
ビューポートの描画は、DrawInit() で行われる。ここでは画面のクリアもしている。
カメラの描画は、DrawCamera()で行われる。
カメラ情報は、マトリクス情報であるfloat[16]配列の先頭へのポインタしか持っていない。どんなすごいカメラだろうが、描画時に必要なのはそれだけで十分なためだ。SetCamera() で任意のカメラマトリクスfloat[16]を参照するようにできる。さしあたりデフォルトカメラツール内部のバッファに関連づけてある。
シーンのコンテンツの描画は、DrawScene() で行っている。これはべた書きなので、関数ポインタにして、外部設定ができるようにしてもいいかも知れない。フォント系の描画をする場合には、ビューのHDCが必要となるので、OpenGLSimpleAdapterのポインタくらいは、引数に受け取るような描画関数にすれば便利かも知れない。
ピック・選択処理には、描画選択関数OnSelectRenderを使う。今回は、SimpleFrameの利用者である、DefaultCameraの内部、左ボタン押下の処理で呼ばれていることがわかるだろうか。