OpenGLで文字列を描画する

(2007.05.02. 更新: ビットマップで文字を描く方法の、最後のほうに少し説明追加。)
(2007.01.30. 更新:GlyphmetricicsFloat構造体の説明をちょっとだけ追加。)

OpenGLは3次元の図形を表示することはできますが、
文字を表示するための機能は
実はOpenGL自身は持ってなかったりします。
しかし、幸い、
WGLではOpenGLで文字を表示するための関数が提供されているので、
(Windows環境ならば)それを利用して文字や文字列を表示することができます。

・・・できるのですが、
そもそも文字は単なる三角とか四角の図形とは別物なので
若干の手順を踏む必要があったりして、
ちょっと ややこしかったりもします。
でも、まぁ、一度使えるようになってしまえば、
大して面倒なものでもないと思うので。


まず、OpenGL(WGL)で文字列を描画する方法には、
大きく分けて2通りあり、
ビットマップで文字を描く方法、
文字をポリゴンで立体的につくって表示する方法、
があります。
それぞれ、
wglUseFontBitmaps
wglUseFontOutlines
という関数を使います。
どちらの関数も、
指定した文字をディスプレイリストとして作成してくれます。

使いたい文字が半角英数字(1バイト文字)のみならば、
あらかじめ256文字分、全てディスプレイリストとして作成してしまってもよいのですが、
日本語(2バイト文字)の場合は文字の種類が多すぎて、
全てディスプレイリスト化するには、なかなか無理があるので、
今回、この場では、そのつど、各文字についてのディスプレイリストを作成することにします。


ビットマップで文字を描く

さて、
「wglUseFontBitmaps」という関数を使うワケですが、
いきなりは使えません。
いろいろ準備が必要なのです。

まず、この関数をインポートする部分の記述。

[DllImport( GL_DLL, EntryPoint = "wglUseFontBitmaps", CharSet = CharSet.Unicode )]
public static extern bool wglUseFontBitmaps( IntPtr hdc, int start, int count, uint listbase );

赤い部分、要注意です。
これは、文字(列)のマーシャリング方法を指定する記述です。
C#では基本的に文字や文字列はUnicode(2バイト)扱いですが、
アンマネージ関数ではANSI(1バイト)だったりすることも多いので、
引数として文字(列)をアンマネージ関数に渡す場合は、
デフォルトではUnicode(2バイト)→ANSI(1バイト)に変換されることなっています。
しかし、今回の場合、この関数はUnicodeをサポートしていて日本語も使えるので、
勝手にANSIに変換されてしまうと、
2バイト文字の日本語が無理矢理1バイトに変換されてしまい、
日本語でなくなって(?)しまいます。
そこで、赤い部分の記述を加えて、Unicodeのままで渡すように指定をしてやる、
と、いうわけです。

もう一つ、

[DllImport( "gdi32.dll", EntryPoint = "SelectObject" )]
public static extern IntPtr SelectObject( IntPtr hdc, IntPtr bmp );

というのを、記述しておきます。
GDI(gdl32.dll)の関数です。
(.netには、この関数と同じ働きをする関数が見当たらない・・・。)

では、
必要なものの定義が終わったので、
具体的な手順に移ります。

まずは、
「wglUseFontBitmaps」関数は、指定した文字を描くディスプレイリストを作成します。
なので、事前にディスプレイリストの識別番号を文字の数だけ用意しておかなければなりません。
表示したい文字とフォントを、

string text = "世界のみなさん、こんにちは。";
Font font = new Font( "MS 明朝", 12f, FontStyle.Regular );

であるとします。("Hello world."の日本語訳って、こんな意味であってますよね?)
まずは、各文字分のディスプレイリストの識別番号を確保しておきます。

uint listBase = gl.GenLists( text.Length );

さてさて、
ここでまた面倒なことに、GDI(gdi32.dll)の関数を使わなければなりません。
最初のほうで定義しておいた「SelectObject」という関数です。

SelectObject( hDC, font.ToHfont() );

「hDC」は、デバイスコンテキストのハンドルです。
レンダリングコンテキストの作成のときなどにも使うやつです。
「font.ToHfont()」で、フォントオブジェクトのハンドルを取得しています。

つぎに、やっと「wglUseFontBitmaps」関数の出番です。
文字列の各文字について、

bool b;
for( int i = 0; i < text.Length; ++i )
{
    b = wglUseFontBitmaps( hDC , text[i], 1, listBase + (uint)i );
    if( !b ) throw new Exception( "wglUseFontBitmaps failed." )
}

これで、各文字のディスプレイリストが作成されたことになります。
「b」がfalseの場合は、ディスプレイリストの作成に失敗した、ということです。

作成したディスプレイリストを使って文字列を描画する場合は、

for( int i=0; i < text.Length; ++i )
{
    glCallList(listBase + (uint)i );
}

こんな感じで。

ちなみに、
文字は常に同じ大きさ(フォントで指定した大きさ)で描画され、
glColor*関数で色を付けたり、glRasterPos*関数で位置座標を指定したりします。
glRasterPos*関数が呼ばれると、その時点での描画色がラスタ(ビットマップ)を描画する色として適用されます。
が、回転ができません。
glTranslate*は使えません。
画面上のピクセル単位で位置を移動したい場合は、

// ピクセル単位の移動量。
float xMove, yMove;
glBitmap( 0, 0, 0, 0, xMove, yMove, null );

とします。
この移動量は、glRasterPos*関数が呼ばれると、リセットされます。

つまり、
描画手順は、

  1. glColor*などで色を指定してから、
  2. glRasterPos*でワールド座標を指定、
  3. glBitmapでピクセル単位の位置調整をして、
  4. ディスプレイリストを使って文字列を描画。

となります。



文字をポリゴンで立体的につくって表示する

今度は、「wglUseFontOutlines」という関数を使います。
こちらの場合も、
いろいろと準備が要りまして・・・。

まずは「wglUseFontOutlines」関数のインポート。
やはり同様に、「CharSet = CharSet.Unicode 」の部分に注意です。

[DllImport( GL_DLL, EntryPoint = "wglUseFontOutlines", CharSet = CharSet.Unicode )]
public static extern bool UseFontOutlines( IntPtr hdc, int first, int count, uint listBase,
                                           float deviation, float extrusion, OutlineFontFormat format,
                                           GlyphmetricicsFloat[] glyphMetrics
                                          );

コレだと後で不便なので、もう一つ、
引数をちょっと変えてオーバーロートを用意しておきます。

[DllImport( GL_DLL, EntryPoint = "wglUseFontOutlines", CharSet = CharSet.Unicode)]
public static extern bool UseFontOutlines( IntPtr hdc, int first, int count, uint listBase,
                                           float deviation, float extrusion, OutlineFontFormat format,
                                           ref GlyphmetricicsFloat glyphMetrics
                                         );

最後の引数を、配列から参照渡しに変更してあります。

でもって、その最後の引数になっている構造体の定義も。
なにはともあれ、こんなかんじです。
とりあえず、なにはともあれ、です。

[StructLayout( LayoutKind.Sequential )]
public struct GlyphmetricicsFloat
{
    public float gmfBlackBoxX;
    public float gmfBlackBoxY;
    public PointFloat gmfptGlyphOrigin;
    public float gmfCellIncX;
    public float gmfCellIncY;
}

[StructLayout( LayoutKind.Sequential )]
public struct PointFloat
    public float x;
    public float y;
}

あと、さっきと同じく、GDIのSelectObject関数も使います。
これで準備完了。
実際の手順に移ります。

では、
さっきのビットマップの場合と同様に、表示したい文字とフォントを、

string text = "世界のみなさん、こんにちは。";
Font font = new Font( "MS 明朝", 12f, FontStyle.Regular );

であるとします。
これまた同じく、あらかじめ各文字分のディスプレイリストの識別番号を確保しておきます。

uint listBase = gl.GenLists( text.Length );

つぎは、やっぱり同じく、「SelectObject」を使います。

SelectObject( hDC, font.ToHfont() );

さて次。ここがちょっと違う。
GlyphmetricicsFloatという、さっき定義しておいた構造体を、配列として文字の数だけ用意します。

GlyphmetricicsFloat[] gmf = new GlyphmetricicsFloat[text.Length];
bool b;
for( int i = 0; i < text.Length; ++i )
{
    b = wgl.UseFontOutlines( hDC,
                             text[i],
                             1, listBase+ i ,
                             deviation, extrusion,
                             this.format,
                             ref gmf[i]
                           );

    if( !b ) throw new Exception( "wglUseFontBitmaps failed." )
}

これでエラーがなければ、無事完了。

作成したディスプレイリストを使って文字列を描画する場合は、
やはり、ビットマップの文字と同様に、

for( int i=0; i < text.Length; ++i )
{
    glCallList( listBase + (uint)i );
}

基本的には、これでOK。

作成したディスプレイリストを使って文字列を描画する場合は、

文字のサイズについては、
ディスプレイリストとして作成される文字の基本サイズは常に1.0×1.0なので、
描画時にglScale*関数で拡大縮小をして調整します。
(作成時にフォントのサイズを指定しても、まったく反映されません。)
この場合、法線ベクトルまで拡大縮小されてしまうので、
glEnable(GL_NORMALIZE);としておかないと、ライティングがおかしくなるので注意されたし。

あと、こちらはglTranslate*やglRotate*関数で移動や回転が可能です。


GlyphmetricicsFloatを利用する

さっき「なにはともあれ」といっていた「GlyphmetricicsFloat」という構造体は、
文字のサイズなどの情報を表す構造体です。
なので、これをうまく利用してやれば、各文字の配置を自分で調整することもできます。
で、下はGlyphmetricicsFloat構造体の各フィールドの説明。
(グリフ=文字を表す図形のこと。)

gmfBlackBoxX, gmfBlackBoxY
グリフを完全に囲む最小の矩形の幅、高さ。
 
gmfptGlyphOrigin
グリフを完全に囲む最小の矩形の左上角の座標。
 
gmfCellIncX, gmfCellIncY
現在の文字の原点から次の文字の原点までの距離。

サンプルプログラム(?)

とりあえず、
GLSharpの中に文字列描画のためのクラスが用意してあるので、
そっちのほうを参考にしてくださいな。

そのうち、
ここにもちゃんとサンプルプログラムを用意しときますんで・・・。

・・・と、以前に書いたものの、
だんだんめんどくさくなってきたかもしれない今日この頃。



<back to OpenGL menu>