描画結果を画像情報として取得しよう




やあ子供たち、元気にしてたかい。2010年になってまたまた新しいスマートフォンタブレットバイスが目白押しで楽しそうだね!今年はとくに楽しくなりそうな予感がするね予感どころじゃないかな。
今日はWindowsのメモリデバイスコンテキストというものに描画した絵柄を、画像情報として簡単に取り出すための汎用テンプレート、DrawAndBitmapの紹介だ。
せっかくGDIやOpenGL駆使して描画してるのに、いきなりフレームバッファに転送して表示させておしまい、再利用できませんてんじゃとても切ないよな。描画した結果はいろいろなFace概念にテクスチャ画像として貼り付け再利用したい場合もあるだろうし、OpenGLにしたって画面の解像度じゃなくってプリンタの解像度でレンダリングさせたい場合だってあるわけだし、wgl系関数を使わずに3D空間に文字を書く方法ってままならないから文字列をテクスチャにしてしまえ!なんてことがあるようだけど、じゃそれってどうやるの?っていう話にもなりがちだ。(wgl系関数がないiPhoneAndroidなんかでOpenGLESですなんて場合はどうなってるんだろうね興味深いね!)
そんなときWindowsではメモリデバイスコンテキストにDIBを関連付けてそこに描画を行い、最後に画像情報を取り出して活用する、ということをするのだけどこれが何やら七面倒くさくて複雑で、せっかくできたコードでも変数のスコープが入り組んでわけわかんない感じになりがちだ。しかも覚えても絶対また忘れるから抽象化が楽なように自分用にまとめておきたいということもあった。
今回のDrawAndBitmapは、一連の処理を前述のような予備知識なしでも簡単にできるように関数テンプレートとして用意したものだよ。これを使えばその利用者は

  1. 描画処理そのもの
  2. 描画結果である画像情報の利用コード

だけを記述すればよくなるんだ。
以下が今回のDrawAndBitmapのソースコードだよ。

template < class FuncDraw, class FuncRetrieve >
void DrawAndBitmap( 
                 const int width,
                 const int height, 
                 const int depth, //24or32
                 FuncDraw& __funcDraw__, 
                 FuncRetrieve& __funcRetrv__ )
{
  // GDIで描画した図形、テキストなどを、画像情報として
  // 取得し処理するための手段を提供します。
  // width, height, depth で指定したサイズのメモリデバイスコンテキストに対し、
  // FuncDrawで指定される描画処理を実行し、
  // FuncRetrieveにて指定される、画像情報処理を実行します。
  //
  // concepts:
  //
  // ●void FuncDraw::operator()( CDC& memdc ) const;
  // memdcには、本テンプレート内で管理している
  // メモリデバイスコンテキストが渡されますので、これを使って、
  // GDIやOpenGLなどを用いてコンテンツの描画処理を記述します。
  // 
  // ●void FuncRetrv::operator()( CDC&, BITMAP& ) const;
  // FuncDrawにて描画した画像を利用する処理を記述します。
  //
  CDC memDC;
  memDC.CreateCompatibleDC( NULL );
  {
    HBITMAP memBmp;
    {
      DWORD* dummy_bmbits;
      BITMAPINFO bi;
      ZeroMemory( &bi.bmiHeader, sizeof(BITMAPINFOHEADER) );
      bi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
      bi.bmiHeader.biWidth       =  width;
      bi.bmiHeader.biHeight      =  height;
      bi.bmiHeader.biPlanes      = 1;
      bi.bmiHeader.biCompression = BI_RGB;
      bi.bmiHeader.biBitCount    = depth;
      memBmp = CreateDIBSection( memDC, &bi, DIB_RGB_COLORS, (void**)&dummy_bmbits, 0, 0 );
    }
    {
      HGDIOBJ pOldBmp = memDC.SelectObject( memBmp );
      __funcDraw__( memDC );  // ★
      BITMAP bitmap;
      GetObject( memBmp, sizeof(BITMAP), &bitmap );
      __funcRetrv__( memDC, bitmap ); // ★
      memDC.SelectObject(pOldBmp);
    }
    DeleteObject( memBmp );
  }
  memDC.DeleteDC();
  return;
}

そして使い方は以下のようだ。例えばMFCのSDIアプリのOnDrawの中で、いきなり

  // ★ここがDrawAndBitmap呼び出し
  ::DrawAndBitmap( 640, 480, 24, FuncDraw(), FuncRetrv( pDC ) );

と、これだけでOK。FuncRetrvがpDCを引数にとっているのは、そのようにFuncRetrvを自分で設計したからというだけの話だよ。FuncDrawとFuncRetrvは自分で書くものなので、実際の呼び出しの構造は以下のようにかんたんな話になってるよ。誤解のないように。

  ::DrawAndBitmap( 640, 480, 24, 自分が書いたFuncDraw, 自分が書いたFuncRetrv );

これ一行で、4つ目の引数として渡しているFuncDrawファンクタインスタンスの中に定義された、描画コードで描画した結果を、5つ目の引数として渡した、FuncRetrvファンクタインスタンスの中に定義された、画像処理コードで利用する、ということをやってくれる。
では具体例に行ってみよう。
例えば日の丸と新年の挨拶をGDIで描画した結果を、ビューのクライアント領域に表示して、かつBMP画像ファイルとして出力したいなんていう場合を考えてみよう。
まずはFuncDrawの定義は以下のようになる。FuncDrawは、描画対象となるメモリデバイスコンテキストを引数としてDrawAndBitmapから呼び出されるようになっているので、これを使ってコンテンツの描画をすきなだけ行うコードを記述すればよいよ。

  struct FuncDraw
  {
    void operator()( CDC& memDC ) const
    {
      //コンテンツをGDIで描画
      {
        // 幾何図形の描画
        memDC.FillSolidRect( 0, 0, 320, 240, RGB( 255 ,255 ,255 ) );
        CPen pen(PS_SOLID, 1, RGB( 255, 200, 200) );
        memDC.SelectObject( pen );
        CBrush br( RGB( 255, 200, 200 ) );
        memDC.SelectObject( br );    
        memDC.Ellipse( CRect( 60, 20, 260, 220 ) );
        CBrush br2( RGB( 255, 12, 12 ) );
        memDC.SelectObject( br2 );    
        memDC.Ellipse( CRect( 70, 30, 250, 210 ) );
        // フォント系の描画
        SetBkMode(memDC,TRANSPARENT);
        HFONT       hFont,hFontOld;
        {
          hFont= hFont=  CreateFont(
            24,                     //★font height
            0,                      // 
            0,                      //★ text angle
            0,                      //angle between baseline from horiz
            FW_REGULAR,             //is bold
            FALSE,                  //is italic
            FALSE,                  //is underline
            FALSE,                  //is lined out
            SHIFTJIS_CHARSET,       //charset
            OUT_DEFAULT_PRECIS,     //presision
            CLIP_DEFAULT_PRECIS,    //clipping presigion
            PROOF_QUALITY,          //quality
            FIXED_PITCH|FF_MODERN,  //pitch and family
            "みかちゃん");          //★ face
        }
        hFontOld = (HFONT)SelectObject( memDC, hFont);
        SetTextColor( memDC, BLACK_BRUSH );
        {
          CString str = "謹賀新年";
          for( int i=0; i<12; ++i ){
            TextOut( memDC.GetSafeHdc(), i*20, i*20, str, str.GetLength() );  
          }// i
        }
        SelectObject( memDC, hFontOld);
        DeleteObject(hFont);
      }
      return;
    }
  };

次にFuncRetrvの定義。FuncRetrvは、FuncDrawにて描画されたメモリデバイスコンテキストと、関連付けられた画像の情報を含むBITMAP構造体がそれぞれ引数として、DrawAndBitmapから呼び出されるので、この画像情報をどう利用するかというコードを書いてあげればいいだけだ。

struct FuncRetrv
  {
    FuncRetrv( CDC* pdc ): _pdc(pdc){}
    void operator()( CDC& memDC, BITMAP& bitmap ) const
    {
      //メモリDCの画像(即ちビットマップの内容)を実際の画面に転送するもよし
      _pdc->BitBlt( 0, 0, bitmap.bmWidth, bitmap.bmHeight, &memDC, 0, 0, SRCCOPY );
      //メモリDCの画像(即ちビットマップの内容)をBMPファイルとして書き出すもよし
      write_bmp( "c:\\temp\\hello.bmp", bitmap.bmWidth+bitmap.bmWidth%4, bitmap.bmHeight, 
        3, (unsigned char*)bitmap.bmBits );
      return;
    }
    mutable CDC* _pdc;
  };

ここでは、ビューのクライアント領域への表示と、簡易BMPファイル出力のwrite_bmpを使った、画像ファイル出力を行っているね。おっと、write_bmpについてはこっちの過去日記を参照してくれよな。
また、FuncDrawの中でOpenGL描画を行うこともできる。このとき、注意しなくてはならないのは、GLの初期化時にPFD_DRAW_TO_WINDOWのかわりにPFD_DRAW_TO_BITMAPを指定するようにすることと、また、ピクセルのビット深さを、メモリDCのビット深さと合わせてあげることだ。以下では、OpenGLSimpleAdapterをそのように改変して使っているね。おっと、OpenGLSimpleAdapterについてはこちらの過去日記も参考にしてくれ。また、GLで描画したキャンバスの上に、もう一回GDIで新年の挨拶を描画しているね。

class OpenGLSimpleAdapter 
  // ↑CLRでやる場合は、class の前に、public ref を入れてね
{ 
  // 
  // OpenGL Simple Adaptor loOGLHost (C) 2008 nurs 
  // 
  // 使い方: 
  // 1)本クラスのインスタンスを、ターゲットビューのメンバとして作成する。 
  //   コンストラクタのHDCは、Win32なら
  //    ⇒  ::GetDC( this->GetSafeHwnd() ) );
  //   CLRのFormなら、
  //     ⇒ ::GetDC( (HWND)parentForm->Handle.ToPointer() );
  //   などとして取ってきます。 
  // 2)ターゲットビューの、適切な箇所(初期化、描画、リサイズ)にて、
  //   本ホストの、BeginRender()と、EndRender() を呼び出し、その間に、
  //   目的のOpenGL描画コードを記述します。
  // *)ちなみに利用側コードのどこかのcpp内にて、
  //#pragma comment( lib, "opengl32.lib" )
  //#pragma comment( lib, "glu32.lib" )
  //#pragma comment( lib, "gdi32.lib" )
  //#pragma comment( lib, "User32.lib" )
  //  の記述も、忘れないで下さい。
  //
  // ★RenderPolicyを予め作成しておき、必要なときに必要な描画
  //   ポリシーで、Render() をかける、という使い方もできます。
  // ★注意:WindowsForm、.NET環境の場合は、プロジェクトの共通言語
  //   ランタイムサポートを、/clr:pure ではなく、/clr にする。
  //
  //
public:
  OpenGLSimpleAdapter( HDC dc ){ 
    if(dc==0) return;
    m_hdc = dc;
    { 
      // メモリデバイスコンテキストの場合は、
      // PFD_DRAW_TO_WINDOWのかわりに、PFD_DRAW_TO_BITMAPを、
      // また、ピクセルのビット深さ(以下では24となってるとこ)
      // を、メモリDCのビット深さとあわせてやる必要があるよ
      static PIXELFORMATDESCRIPTOR pfd={ 
        sizeof(PIXELFORMATDESCRIPTOR), 1, 
        PFD_DRAW_TO_BITMAP | //PFD_DRAW_TO_WINDOW | 
        PFD_SUPPORT_OPENGL //| 
        /*PFD_DOUBLEBUFFER*/, PFD_TYPE_RGBA, 
        24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
        16, 0, 0, PFD_MAIN_PLANE, 0, 0, 0, 0 
      }; 
      GLint iPixelFormat; 
      if( (iPixelFormat = ChoosePixelFormat(m_hdc, &pfd) ) == 0) 
        return; 
      if(SetPixelFormat(m_hdc, iPixelFormat, &pfd) == FALSE) 
        return; 
    }
    if( (m_hglrc = wglCreateContext(m_hdc) )==0) 
      return; // pure Managed だとランタイムでエラーに 
    if( (wglMakeCurrent(m_hdc, m_hglrc) )==0) 
      return; 
    wglMakeCurrent(0, 0);
    return;
  }
  ~OpenGLSimpleAdapter( void ){} 
  template< class RenderPolicy > void Render( RenderPolicy& po ){
    wglMakeCurrent( this->m_hdc, this->m_hglrc );
    po();
    wglMakeCurrent( this->m_hdc, 0 );
    SwapBuffers( this->m_hdc );
  }
  HDC BeginRender( void ){		
    wglMakeCurrent( this->m_hdc, this->m_hglrc );
    return this->m_hdc;
  }
  void EndRender( void ){		
    wglMakeCurrent( this->m_hdc, 0 );
    SwapBuffers( this->m_hdc );
  }
  void EndRenderNoSwap( void ){		
    wglMakeCurrent( this->m_hdc, 0 );
  }
private:
  HDC m_hdc;
  HGLRC m_hglrc;
};



  struct FuncDraw2
  {
    void operator()( CDC& memDC ) const
    {
      OpenGLSimpleAdapter* ogl = new OpenGLSimpleAdapter( memDC );
      ogl->BeginRender();
      //
      // ●OpenGL初期化glBegin()までのデフォルトコード
      //
      glClearColor( 0, 0, 0, 0 );
      glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
      // ビューポートと視体積
      {
        float aspect_ratio;
        {
#if 01
          // MFCとかの場合
          //CRect rect;
          //{
          //  const CWnd& w = this->m_picbox;//←ここ、ダイアログなのか何かその中の部品なのかで変わってきます。
          //  w.GetClientRect( &rect );
          //}
          //glViewport( rect.left, rect.top, rect.Width(), rect.Height() );
          glViewport( 0,0,640,480 );
          aspect_ratio = 640.0 / (float)480;
#else
          // CLIとかだったら例えばフォーム上に panel1 が置いてあったとして、
          float width = panel1->Width;
          float height = panel1->Height;
          glViewport( 0 ,0, width, height );
          aspect_ratio = width/height;
#endif
        }

        glMatrixMode( GL_PROJECTION );
        float h = 1.0;
        float nc = 2.0;
        float fc = 9900;
        glLoadIdentity();
        glFrustum( -aspect_ratio*h, aspect_ratio*h, -h, h, nc, fc );
      }
      // モデルビューマトリクス
      glMatrixMode( GL_MODELVIEW );
      glLoadIdentity();
      // カメラ描画
      glTranslatef( 0,0,-10 );
      //glTranslatef( 0, 0, -m_radii );// m_radii はマウスホイールで更新してもいいし、
      //glRotatef( m_tht, 1,0,0 );// m_phiはマウス座標水平移動で更新
      //glRotatef( m_phi, 0,1,0 );// m_thtはマウス座標垂直移動で更新

      // デプステスト有効
      glEnable( GL_DEPTH_TEST );
      glDepthFunc( GL_LEQUAL );
      //
      {
        // ライト設定
        GLfloat spc0[]	={1.0f, 1.0f, 1.0f, 1.0f};
        GLfloat diff0[]	={0.99f, 0.99f, 0.99f, 1.0f};
        GLfloat amb0[]	={0.99f, 0.99f, 0.99f, 1.0f};
        //		GLfloat pos0[]	={ 0,-1,0, 1};// 4つ目の値は1⇒点光源
        GLfloat pos0[]	={ 0,15,0, 1};// 4つ目の値は1⇒点光源
        //glLightfv(GL_LIGHT0, GL_SPECULAR, spc0);
        glLightfv(GL_LIGHT0, GL_AMBIENT, amb0);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, diff0);
        glLightfv(GL_LIGHT0, GL_POSITION, pos0);
        glEnable( GL_LIGHT0 );
      }
      //
      {
        // マテリアル設定
        GLfloat c0[]={1, 1, 1, 1};
        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, c );
        glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT, c1 );
        glMaterialf( GL_FRONT_AND_BACK, GL_SHININESS, 50.0 );
      }


      // ここから描画開始します
      // 
      glEnable( GL_LIGHTING );
      // グリッド
      {
        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
      }

      {
        // マテリアル設定
        GLfloat c[]={1, 1, 1, 1};
        glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, c );
      }


      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 );
      ogl->EndRenderNoSwap();

      delete ogl;

// フォント系の描画
        SetBkMode(memDC,TRANSPARENT);
        HFONT       hFont,hFontOld;
        {
          hFont= hFont=  CreateFont(
            24,                     //★font height
            0,                      // 
            0,                      //★ text angle
            0,                      //angle between baseline from horiz
            FW_REGULAR,             //is bold
            FALSE,                  //is italic
            FALSE,                  //is underline
            FALSE,                  //is lined out
            SHIFTJIS_CHARSET,       //charset
            OUT_DEFAULT_PRECIS,     //presision
            CLIP_DEFAULT_PRECIS,    //clipping presigion
            PROOF_QUALITY,          //quality
            FIXED_PITCH|FF_MODERN,  //pitch and family
            "みかちゃん");          //★ face
        }
        hFontOld = (HFONT)SelectObject( memDC, hFont);
        SetTextColor( memDC, RGB(255,255,0) );
        {
          CString str = "謹賀新年";
          for( int i=0; i<12; ++i ){
            TextOut( memDC.GetSafeHdc(), i*20, i*20, str, str.GetLength() );  
          }// i
        }
        SelectObject( memDC, hFontOld);
        DeleteObject(hFont);


      return;
    }
  };

以上の結果のレンダリング結果が、冒頭の画像ということになるわけさ。