とある用事で、Windows のツールを作ることになりました。 せっかくだから C++.NET で作ろうと思いました。 後々のメンテを考えて3DのAPIは OpenGL で作ろうと思いました。 あまり解説が無かったので、解説を書いてみました。
で、いつものようにプログラムです。
Windowの右側の領域がOpenGLで描画されています。
ボタンを押すと、ポーズのON/OFFが切り替わります。
ソースには、いつものように適当にファイルが入っています。 大事なファイルは次のものです。
Form1.h | フォームのソース |
MyRender.cpp | OpenGLを使った描画部のソース |
tpotGL.h | フォームから呼び出すOpenGLのインターフェイスが書かれたヘッダ |
tpotCGL.h | OpneGLのラッパクラスのヘッダ |
t-potCGL.cpp | OpneGLのラッパクラスのソース |
C++.NET の開発は、VBと同じようにフォームを作成することから始まります。
アプリケーションウィザードで「Windows フォームアプリケーション」を作成します。
そのあと、適当に部品を張り付けます。
ここでは、ボタンを置くパネルと描画するためのpictureBox、ポーズのためのボタン及びTimerを張り付けました。
panel は、DockプロパティをLeftに、pictureBox1はDockプロパティをFillにしました。これによって、フォームのサイズを変更したときにpictureBoxのサイズも自動で変わってくれます。(panelのあとにpictureBoxを配置する必要があります)
あと、プロパティウィンドウをイベント表示にして、pictureBox1の描画関数、タイマーの更新関数、ボタンをクリックしたときの関数を追加します。これらは、Form1.h に勝手にプロトタイプが追加されるので、あとは中を埋めて生きます。
例えば、ボタンを押すイベントは、下の黄色い部分以外は勝手に作られています。
Form1.h 0158: private: System::Void button1_Click(System::Object * sender, System::EventArgs * e) 0159: { 0160: bPause = !bPause; 0161: if(bPause) 0162: { 0163: this->button1->Text = "そして時は\n動き出すッ!!"; 0164: }else{ 0165: this->button1->Text = "時よッ!!\n止まれッ!!"; 0166: } 0167: }
なお、この関数の中で使っている変数「bPause」は、クラスの宣言に手で追加しています。
.NET のプログラミングは、フォームに部品をつけて、それらのプロパティを設定し、
イベントをヘッダファイルで編集するのが基本のようです。
ヘッダでは windows.h をインクルードしていません。
したがって、OpenGL のような Win32 プログラムとして用意されているものを組み込むために一寸技巧を凝らします。
今回は、OpenGLの管理を独自クラスで行うのですが、フォームとのやり取りは、クラスを直接見せずに、void* として扱います。
具体的には、下のような関数を作ります。
tpotGL.h 0010: namespace tpot 0011: { 0012: // 初期化 0013: void *InitGL(System::IntPtr hWnd); 0014: // 後片付け 0015: void DestroyGL(void*); 0016: 0017: }// namespace tpot
フォームはvoid*をOpenGLのハンドルと考えて処理します。
Form1.h 0033: Form1(void) 0035: { 0036: InitializeComponent(); 0037: 0038: // OpenGL の初期化 0039: _pGL = tpot::InitGL(pictureBox1->Handle); 0040: if(_pGL) MyInitRender(_pGL ); 0044: } 0045: 0047: void Dispose(Boolean disposing) 0048: { 0049: // OpenGLの開放 0050: tpot::DestroyGL( _pGL ); 0051: 0057: }
ここで、コンストラクタが初期化するための関数で、Dispose が開放のためのフォームの関数です。
OpenGLの初期化は描画するオブジェクトのハンドルを与えれば良い様に作りました。pictureBoxは、Handleというそのままのオブジェクトを持っているので、これを渡せばpictureBoxに描画できます(というかそのように作りました)。ここで、注意しておくことは、.NETでは、ハンドルは、HANDLE 型ではなく System::IntPtr 型で収められているということです。まぁ、キャストすれば良く、ToInt32()でint型にしてからHWND型にキャストして処理します。
t-potCGL.cpp 0091: // ----------------------------------------------------------------------- 0092: // 初期化 0093: // ----------------------------------------------------------------------- 0094: void *InitGL( System::IntPtr hWnd ) 0095: { 0096: return new CGl((HWND)hWnd.ToInt32()); 0097: } 0098: 0099: 0100: // ----------------------------------------------------------------------- 0101: // 開放 0102: // ----------------------------------------------------------------------- 0103: void DestroyGL(void*p) 0104: { 0105: delete (CGl*)p; 0106: }
で、今までが前座です。これからOpenGL 自体の処理を行います。
CGl というクラスで OpenGL の管理をしているのですが、このクラスは、windowハンドルからデバイスコンテキストのハンドルを所得して、レンダリングコンテキストを作成します。この作成には、wgl関数が使えます。
t-potCGL.cpp 0016: // ----------------------------------------------------------------------- 0017: // コンストラクタ 0018: // ----------------------------------------------------------------------- 0019: CGl::CGl( HWND hWnd ) 0020: { 0021: _hWnd = hWnd; 0022: _hDC = GetDC(_hWnd); 0023: 0024: // ピクセル フォーマットを設定する 0025: if( 0 != SetupPixelFormat( _hDC ) ) return; 0026: 0027: // レンダリング コンテキストの作成 0028: _hglrc = wglCreateContext (_hDC); 0029: }
フレームバッファのピクセル数などは、SetupPixelFormat という関数で行います。SetupPixelFormat は、PIXELFORMATDESCRIPTOR による設定を基に、OpenGL のピクセルフォーマットを指定します。
t-potCGL.cpp 0048: // ----------------------------------------------------------------------- 0049: // ピクセル フォーマットを設定する 0050: // ----------------------------------------------------------------------- 0051: int CGl::SetupPixelFormat( HDC hdc ) 0052: { 0053: int pixelformat; 0054: 0055: static PIXELFORMATDESCRIPTOR pfd = { 0056: sizeof (PIXELFORMATDESCRIPTOR), // 構造体のサイズ 0057: 1, // OpenGL バージョン 0058: PFD_DRAW_TO_WINDOW | // ウィンドウスタイル 0059: PFD_SUPPORT_OPENGL | // OpenGL を使う 0060: PFD_DOUBLEBUFFER, // ダブルバッファ 0061: PFD_TYPE_RGBA, // ピクセルのカラーデータ 0062: 32, // 色のビット数 0063: 0, 0, 0, 0, 0, 0, 0, 0, // RGBAカラーバッファのビット 0064: 0, 0, 0, 0, 0, // アキュムレーションバッファのピクセル当りのビット数 0065: 32, // デプスバッファ のピクセル当りのビット数 0066: 0, // ステンシルバッファのピクセル当りのビット数 0067: 0, // 補助バッファ のピクセル当りのビット数 0068: PFD_MAIN_PLANE, // レイヤータイプ 0069: 0, // 0070: 0, // 0071: 0, // 0072: 0 // 0073: }; 0074: 0075: if ( 0 == (pixelformat = ChoosePixelFormat (hdc, &pfd)) ) 0076: { 0077: return 1; 0078: } 0079: 0080: if ( FALSE == SetPixelFormat(hdc, pixelformat, &pfd) ) 0081: { 0082: return 2; 0083: } 0084: 0085: return 0; 0086: }
終了するときは、作成したレンダリングコンテキストを削除して、所得したデバイスコンテキストを開放します。
t-potCGL.cpp 0032: // ----------------------------------------------------------------------- 0033: // デストラクタ 0034: // ----------------------------------------------------------------------- 0035: CGl::~CGl() 0036: { 0037: // レンダリング コンテキストをカレントからはずす。 0038: wglMakeCurrent (NULL, NULL) ; 0039: 0040: // レンダリング コンテキストの削除 0041: wglDeleteContext (_hglrc); 0042: 0043: // GetDC で確保した分を開放 0044: ReleaseDC( _hWnd, _hDC ); 0045: }
ここまでくれば、後はほとんど OpenGL プログラムです。
今回は、MyRender という関数を作って、タイマーから16msごとに呼び出しています。
(MyRenderは、グローバルな関数です。これはちょとしょぼいですね。)
Form1.h 0142: private: System::Void timer1_Tick(System::Object * sender, System::EventArgs * e) 0143: { 0144: float dTime; 0145: int iNow; 0146: 0147: // カウンタの更新 0148: iNow = System::Environment::TickCount; 0149: dTime = 0.001f * (float)(iNow - iPrev); 0150: iPrev = iNow; 0151: 0152: if(bPause) dTime = 0.0f;// ポーズ処理 0153: 0154: // 再描画 0155: if(_pGL) MyRender( _pGL, dTime, pictureBox1->Width, pictureBox1->Height ); 0156: }
MyRender の中身は、ちょっと以外OpenGLプログラムです。
適当にクリアして、ポリゴンを描画しています。
MyRender.cpp 0029: void MyRender( void *p, float dt, int w, int h ) 0030: { 0031: tpot::CGl *pGL = (tpot::CGl*)p; 0032: 0033: // 描画開始 0034: pGL->BeginRender(); 0035: 0036: // 描画範囲の設定 0037: glViewport(0, 0, w, h); 0038: 0039: // 最初の塗りつぶし 0040: glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ; 0041: 0042: // 射影行列の設定 0043: glMatrixMode(GL_PROJECTION); 0044: glLoadIdentity(); 0045: gluPerspective(40, (float)w/(float)h, 0.1, 200); 0046: 0047: // カメラの設定 0048: glMatrixMode(GL_MODELVIEW); 0049: glLoadIdentity(); 0050: glTranslated( 0.0, 0.0, -5.0 ); 0051: 0052: // 回転 0053: static float angle = 0; 0054: angle += 360.0f * dt / 10.0f; 0055: if(360<angle)angle-=360; 0056: glRotatef( angle, 0.0f, 0.0f, 1.0f ); 0057: 0058: // 四角形を描く 0059: glBegin(GL_QUADS); 0060: 0061: glColor3f(1.0, 0.0, 0.0); 0062: glVertex3f(-1,-1,1); 0063: glVertex3f( 1,-1,1); 0064: glVertex3f( 1, 1,1); 0065: glVertex3f(-1, 1,1); 0066: 0067: glEnd(); 0068: 0069: // 終了 0070: glFlush(); 0071: pGL->EndRender(); 0072: }
ちょっと違う部分というのが、描画の直前と直後です。これは先ほど作成したCGlクラスのコンテキストを使う部分です。
初期化部分は、wglMakeCurrent をつかって、デバイスコンテキストにレンダリングコンテキストを割り当てます。
tpotCGL.h 0037: inline void CGl::BeginRender() 0038: { 0039: wglMakeCurrent( _hDC, _hglrc ); 0040: }
終了したら、デバイスコンテキストをスワップして画面を更新すると共に、レンダリングコンテキストを切り離します。
これらは、普通のWindowsのOpenGLプログラムなので、他のサイトを見ても同じ内容を見かけるでしょう。
tpotCGL.h 0042: inline void CGl::EndRender() 0043: { 0044: SwapBuffers( _hDC ); 0045: wglMakeCurrent( _hDC, 0); 0046: }
さて、本当のところあと少し残りがあります。
CGlクラスを作成した後に、背景色の設定や深度バッファを有効にするなど普遍な部分をあらかじめ設定しておきます。
MyRender.cpp 0014: void MyInitRender( void *p ) 0015: { 0016: tpot::CGl *pGL = (tpot::CGl*)p; 0017: 0018: // レンダリング コンテキストをカレントにする 0019: pGL->SetCurrent(); 0020: 0021: // 背景色 0022: glClearColor(1.0f, 1.0f, 1.0f, 1.0f) ; 0023: 0024: /// 深度バッファ 0025: glEnable(GL_DEPTH_TEST); 0026: glDepthFunc(GL_LEQUAL); 0027: }
CGl::SetCurrent は、描画開始時と同じようにデバイスコンテキストにレンダリングコンテキストを割り当てているだけです。
tpotCGL.h 0032: inline void CGl::SetCurrent() 0033: { 0034: wglMakeCurrent( _hDC, _hglrc ); 0035: }
なお、今回のソースをコンパイルするには、opengl32.lib と glu23.lib を「追加の依存ファイル」に追加する必要があります。
また、実行には、opengl32.dll が必要です。
もともとMFCは嫌いだったのですが、これで完全に.NET frameworkに移ってもいい気がしました。
やっぱり、C++だと、ライブラリがそのまま使えて楽ですね。
まぁ、OpenGL なら C# でもライブラリが作られているようなので、何でもいいか。