OpenGLの初期化


(2008.10.22 更新 : 「Tao Framework」を使用した初期化方法を追加・・・するだけのつもりだったけど、結局ほぼ全部書き換えた。)

OpenGLを使用する場合、
なにはともあれ、まずは、
「初期化」というものを行わなければならないわけです。

で、その具体的な内容はと言うと、
もっぱら、「レンダリングコンテキストの取得・初期化」で、
ウィンドウのハンドルを取得して・・・とか、
デバイスコンテキストのハンドルがうんぬんとか、
ピクセルフォーマットなんとかがなんだかんだとか、
なんだかよくわからなくてややこしそうなものをいじらなければならないのです。

OSに依存せずにウィンドウの作成とOpenGLの初期化をやってくれる
GLUTという便利なものもあるのですが、
簡単なウィンドウしか作れないし、
GLUTが勝手にウィンドウを作成するので、
Visual Studioのデザイナでこれををいじることはできないし、
.NET Frameworkに用意されているFormクラスも使えないので、
.NETとは相性がよろしくない。

で、この初期化を行う他の方法としては、
自前で一通りコードを書くのと、
既存のラッパーなどに用意されている初期化用のルーチンを利用するのと、
2つの選択肢があります。
当然、後者のほうが楽と言えば楽ですが、
自前での初期化方法を知っていて損はないはず。
ということで、
両方の初期化方法について、説明していきたいと思います。
(言語はC#、OSはWindow、Visual Studio(Express Editionも可、2005以降のバージョン)を使用、という環境を前提とします。)


すべて自分で初期化コードを書く

具体的なサンプルコードを書いていこうと思うのですが、
ここでは、ラッパーとして「Tao Framework(これを書いている時点でのバージョンは2.1.0。)
を使用することにします。
このTao FrameworkはたくさんのDLLがセットになっていますが、
必要なのは、
"Tao.OpenGl.dll" : 名前の通り、OpenGL関数を呼び出すためのラッパー
"Tao.Platform.Windows.dll" : WGLとwin32関数などのラッパー
の2つです。

サンプルプログラムを用意しておきましたので、
ページの最下部においておきます。

で、概要ですが、
今回はユーザーコントロールを作成してそこにOpenGLで描画する、
という方針でいきます。

まずは、
上記の2つのDLLを参照設定に追加し、
(ソリューションエクスプローラで「参照設定」を右クリックして「参照の追加」を選択し、「参照」タブでDLLのファイルを選択する。)
プロジェクトに新規のユーザーコントロールを追加します。
このユーザーコントロールにOpenGLで描画することを目標とします。

で、初期化は、
protected void OnHandleCreated( EventArgs e );
メソッドをオーバーライドして、ここで行います。
ここに、まずは、
ちょっと「オマジナイ」を書き加えておきます。
(コントロールに関する設定をOpenGLを使うのに都合の良いものにしておきます。)

this.SetStyle( ControlStyles.UserPaint, true );
this.SetStyle( ControlStyles.AllPaintingInWmPaint, true );
this.SetStyle( ControlStyles.DoubleBuffer, false );
this.SetStyle( ControlStyles.Opaque, true );
this.SetStyle( ControlStyles.ResizeRedraw, true );

あ、それと、コードの先頭に
using Tao.OpenGl;
using Tao.Platform.Windows;
の2行を追加しておくのをお忘れなく。

using System;
using System.Drawing;
using System.Windows.Forms;

using Tao.OpenGl;
using Tao.Platform.Windows;

namespace Tao_Sample
{
    public partial class GLControl : UserControl
    {
        public GLControl()
        {
            InitializeComponent();
        }

        protected override void OnHandleCreated( EventArgs e )
        {
            base.OnHandleCreated( e );

            this.SetStyle( ControlStyles.UserPaint, true );
            this.SetStyle( ControlStyles.AllPaintingInWmPaint, true );
            this.SetStyle( ControlStyles.DoubleBuffer, false );
            this.SetStyle( ControlStyles.Opaque, true );
            this.SetStyle( ControlStyles.ResizeRedraw, true );

            //ここでOpenGLの初期化を行う。
        }
    }
}

さて次。
OpenGLの初期化に取りかかります。
まずは、"PIXELFORMATDESCRIPTOR"構造体というものを用意します。
これは、描画するバッファのフォーマット(ピクセルフォーマット)を指定する構造体。
今回は、RGBA32bitで、アルファチャンネル有り、デプスバッファ16bitでダブルバッファ、
というフォーマットにしておきます。

で、この構造体を、コントロールのデバイスコンテキストのハンドルと一緒に、
次の関数に渡すと、指定したものに最も近いピクセルフォーマットの番号が返される。
0が返ってきたら、エラーです。
int Tao.Platform.Windows.Gdi.ChoosePixelFormat( IntPtr deviceContext, ref Gdi.PIXELFORMATDESCRIPTOR pixelFormatDescriptor );
デバイスコンテキストのハンドルは、
IntPtr Tao.Platform.Windows.User.GetDC( IntPtr windowHandle );
という関数に、コントロールのHandleプロパティを渡すことで取得できます。
また、取得したハンドルは、後であちこちで必要になるので、
コントロールに変数として持たせておくことにします。

・・・デバイスコンテキストってなんだ、ってのは、
まぁ、ここでは説明が長くなるので省略ということで。


ピクセルフォーマットの番号が選択されたら、
念のために正しいフォーマット内容を
Tao.Platform.Windows.Wgl.wglDescribePixelFormat 関数で取得し直しておきます。

// デバイスコンテキストのハンドル
IntPtr hDC;

protected override void OnHandleCreated( EventArgs e )
{
    base.OnHandleCreated( e );

    Gdi.PIXELFORMATDESCRIPTOR pfd = new Gdi.PIXELFORMATDESCRIPTOR();
    // OpenGLをサポート、ウィンドウに描画、ダブルバッファ。
    pfd.dwFlags = Gdi.PFD_SUPPORT_OPENGL | Gdi.PFD_DRAW_TO_WINDOW | Gdi.PFD_DOUBLEBUFFER; 
    pfd.iPixelType = Gdi.PFD_TYPE_RGBA; //RGBAフォーマット
    pfd.cColorBits = 32; // 32bit/pixel
    pfd.cAlphaBits = 8; // アルファチャンネル8bit (0にするとアルファチャンネル無しになる)
    pfd.cDepthBits = 16; // デプスバッファ16bit
    
    // デバイスコンテキストのハンドルを取得。
    this.hDC = User.GetDC( this.Handle );

    // ピクセルフォーマットを選択。
    int pixFormat = Gdi.ChoosePixelFormat( this.hDC, ref pfd );
    if( pixFormat <= 0 ) throw new Exception( "ChoosePixelFormat failed." );
    
    // (PIXELFORMATDESCRIPTOR構造体のサイズは40byte。)
    Wgl.wglDescribePixelFormat( this.hDC, pixFormat, 40, ref pfd ); 
    
}

今度は、コントロールのデバイスコンテキストに
先ほどのピクセルフォーマットを設定します。
これには、この関数を使用します。

bool Tao.Platform.Windows.Gdi.SetPixelFormat( IntPtr deviceContext, int pixelFormat, ref Gdi.PIXELFORMATDESCRIPTOR pixelFormatDescriptor );

ちなみに、ピクセルフォーマットの設定は
一つのデバイスコンテキストに一度限りしか使用できないらしいので注意。

ここでもう一つ注意。
なぜかはわからないが、この SetPixelFormat() を呼び出す直前に、

Wgl.wglMakeCurrent( IntPtr.Zero, IntPtr.Zero );

としてwglMakeCurrent()関数を呼び出しておかないと、
SetPixelFormat()が失敗する場合がある様子。

最後に、OpenGLのレンダリングコンテキストを作成して、初期化完了。
レンダリングコンテキストの作成には、次の関数を使用します。

IntPtr Tao.Platform.Windows.Wgl.wglCreateContext( IntPtr hDC );

作成されたレンダリングコンテキストのハンドルが返されるので、
これもコントロールのフィールドとして持っておきます。

以降、このコンテキストを
Tao.Platform.Windows.Wgl.wglCreateContext 関数でカレントに設定して、
これに対して、OpenGLの命令を出していくことになります。

実は、Tao.OpenGl と Tao.Platform.Windows を使用する場合はもうひと手間ありまして、

void Tao.OpenGl.Gl.ReloadFunctions();
void Tao.Platform.Windows.Wgl.ReloadFunctions();

この2つを、レンダリングコンテキストをカレントに設定した後で
1度だけ呼び出しておいた方が良いらしい。
どうも、呼び出しておかないと、
エクステンションの関数を呼び出したときにエラーが発生することがあるっぽい。

// デバイスコンテキストのハンドル
IntPtr hDC;

// レンダリングコンテキストのハンドル
IntPtr hRC;

protected override void OnHandleCreated( EventArgs e )
{

    // ... (省略) ...

    //よくわからんが、これがないとエラーになることがある。
    Wgl.wglMakeCurrent( IntPtr.Zero, IntPtr.Zero );

    // デバイスコンテキストにピクセルフォーマットを設定。
    bool valid = Gdi.SetPixelFormat( this.hDC, pixFormat, ref pfd );
    if( !valid ) throw new Exception( "SetPixelFormat failed" );

    // レンダリングコンテキストを作成。
    this.hRC = Wgl.wglCreateContext( this.hDC );

    if( this.hRC == IntPtr.Zero ) throw new Exception( "wglCreateContext failed." );

    Wgl.wglMakeCurrent( this.hDC, this.hRC );

    Gl.ReloadFunctions();
    Wgl.ReloadFunctions();
}

初期化があれば、終了処理もあるわけで。
OpenGLのレンダリングコンテキストは、
いわゆるアンマネージリソースという部類に入ります。
つまり、自分で明示的にリソースを解放するコードを書かないといけない、ということ。

レンダリングコンテキストの削除・解放には、次の関数を使用します。
bool Tao.Platform.Windows.Wgl.wglDeleteContext( IntPtr oldContext )
引数にレンダリングコンテキストのハンドルを渡せばOK。

で、
アンマネージリソースの解放はDisposeメソッドでやるのがC#のルールなので、
UserControl.Disposeメソッドをオーバーライドしたいところだが、
実は既に、デザイナがこのDisposeをオーバーライドしたコードを自動的に書いてるので、
そこをいじる事になります。
デザイナが自動生成したコードは、
ソリューションエクスプローラのコントロールの左側に表示されている + をクリックすると出てくる
"~.Designer.cs" というファイルです。
このファイルに書かれている
protected override void Dispose( bool disposing ) ...
と書かれているメソッドをいじります。

protected override void Dispose( bool disposing )
{
    if( disposing && ( components != null ) )
    {
        components.Dispose();

    }

    //レンダリングコンテキストを解放。
    Tao.Platform.Windows.Wgl.wglDeleteContext( this.hRC );

    base.Dispose( disposing );
}

くれぐれも、
レンダリングコンテキストの解放を忘れないように。
忘れると、
プログラムを終了してもプロセスが残って、さらになぜかCPU使用率が100%になり
タスクマネージャから強制終了させるハメになります。
(Visual Studioのデバッグで実行してるときは、ここまでひどいことにはならないなようですが・・・)


ラッパーの初期化ルーチンを使う

.NET用のOpenGLラッパーには、
上で説明したような初期化の作業を勝手にやってくれる
初期化用のルーチンが用意されているものが幾つかあり、例を挙げると、
OpenTK
Tao Framework (Tao.Platform.Windows.dll)
C# wrapper for OpenGL
GLSharp
あたりでしょうか。
いずれも、OpenGLで描画するコントロールが用意されていて、
それをメインのフォームに貼り付けるだけでOKだったりしますが、
レンダリングコンテキストの初期化のタイミングとか、指定できるオプションとか、それなりに違いがあるようです。
ちょっと、この4つを私なりに比較してみました。

OpenTK : OpenTK.GLControl
マルチプラットフォーム。
だけど、WGLのラッパークラスは提供されていない。
(ソースコードをみると、WGLのラッパークラスはあるがinternalになっていてDLLの外からは参照できない。)
System.Windows.Forms.UserControlクラスから派生している。
カラーバッファやデプスバッファのbit数とかいろいろ指定できるようになっている。
マルチサンプリングを指定するオプションも用意されているようだが、未実装。
OnHandleCreatedメソッドをオーバーライドして、そこでレンダリングコンテキストを初期化している。
そのままフォームに貼り付けるよりも、
このGLControlクラスを継承するユーザーコントロールを作成して使用した方がよさげ。
 
Tao Framework : Tao.Platform.Windows.SimpleOpenGlControl
名前のとおり、Windows用。
ピクセルフォーマットの指定は、各バッファのbit数のみ指定できる。
デザイナからもプロパティとして設定することができる。
SimpleOpenGlControl.InitializeContexts というメソッドをユーザーが呼び出すことで
レンダリングコンテキストの初期化を行う(自動的には初期化されない。)。
 
C# wrapper for OpenGL : GRV11.GRControl
Windowのみに対応。
System.Windows.Forms.Controlクラスから派生。
特に指定できるオプションなどは無いっぽい。
ピクセルフォーマットは、カラーバッファRGBA32bit、デプスバッファ24bit、それ以外指定無しで初期化している。
OnPaintメソッドをオーバーライドして、初回呼出時にレンダリングコンテキストを初期化している。
 
GLSharp : GLSharp.GLSControl
Windowのみに対応。
System.Windows.Forms.UserControlクラスから派生している。
ピクセルフォーマットをデザイナから設定できる。
マルチサンプリングもサポートしていて、これもデザイナから設定できる。
OnHandleCreatedメソッドをオーバーライドして、そこでレンダリングコンテキストを初期化している。

私としては、おすすめはOpenTK。
これを書いている時点(2008年10月)では、まだバージョンが0.9.1と開発中なので、
今後、未実装の機能が実装される事に期待したい。
GLSharpは私が作っているやつですが、
まぁ、使いやすいことを目標にしているので、
もしお気に召しましたら使ってやってくださいな。

初期化さえできてしまえば、あとはほとんど、
ほかの言語でのOpenGLプログラミングと大差はないです。
ま、ラッパーによってwglSwapBuffersの呼び出し方が微妙に違ったりしますが、
些細なことかと。


サンプルプログラム

ラッパーとしてTaoを使った、
すべて自分でレンダリングコンテキストの初期化を行うサンプルプログラムです。
Visual Studio 2008 Express Editoin のプロジェクトです。
なお、"Tao.OpenGl.dll"と"Tao.Platform.Windows.dll"は含まれていませんので、
各自で入手して組み込んでください。

Tao_Sample.zip
 
 
<back to OpenGL menu>