2008年9月12日金曜日

OpenGLでアンチエイリアスが掛かった文字を描く

Windows環境において
OpenGLでテキスト描画をしようと思うなら
"wglUseFontBitmaps" でビットマップの文字を描くという方法があるわけだが、
この方法だと、描画される文字はビットマップ(文字通りの0か1のみのbitのマップ)ゆえに、
アンチエイリアスが掛からない。

そこで、
アンチエイリアスが掛かった文字を描くにはどうしたらいいのか? 
と。

以前、文字をポリゴン化したメッシュを作成しようとしたときに、
アウトラインフォントの輪郭線を取得するのに
"GetGlyphOutline"
というwin32 API を使ったのだが、
たしか、この関数って、アンチエイリアスの掛かったグリフのビットマップなんかも
これで取得できるんだったよなー、
というのを思い出したので、
その辺りをググって調べつつ、やってみた。

おおざっぱな方針としては、
まず、アンチエイリアスの掛かったグリフのイメージを取得して、
それをDrawPixelsで描画、をディスプレイリスト化。
という感じ。

・・・これだと、
glBitmapみたいにglColor*で
テキストの色を変えることができないけど、
まぁ、しかたがないか。


<手順その1>
まずは、バッファを渡すところにnullを渡してやると、
必要なバッファのサイズを返してくれるので、
そうして取得したサイズぶんだけbyte配列(バッファ)を作成。

IntPtr prevObj = win32.SelectObject( hDC, font.ToHfont() );

uint bufferSize = win32.GetGlyphOutline( hDC, (uint)character, format, out metrics, 0, IntPtr.Zero, ref matrix );

byte[] buffer = new byte[bufferSize];


<その2>
さっきのバッファを渡してもう一度GetGlyphOutlineを呼び出せば、
バッファにアンチエイリアスの掛かったグリフのイメージが書き込まれる。


GCHandle gchBuffer = GCHandle.Alloc( buffer, System.Runtime.InteropServices.GCHandleType.Pinned );

uint ret = win32.GetGlyphOutline( hDC, (uint)character, format, out metrics, bufferSize, gchBuffer.AddrOfPinnedObject(), ref matrix );

gchBuffer.Free();


<その3>
バッファに書き込まれた値は0~255にはなっていなくて、
formatフラグでい指定した階調の値そのままが入ってる。
つまり、17階調なら0~16の値が入ってるので、
255/16とかでスケーリングする必要がある。
(注:MSDNには、GGO_GRAY8_BITMAP(65階調)を指定すると
0~255の値が入るって書いてあるけど、それは嘘。
実際にやってみると、0~64の値が入ってた。)

さらに、グリフイメージの左上から、という並びで書き込まれているので、
このままでは上下反転した文字になってしまうから、
これもまた、ひっくり返しておかないといけない。

あと、グリフイメージの幅は「4バイト境界」になってて、
metrics.blackBoxXと一致してないので、そこも要注意。
(これ、MSDNのリファレンスに書いてなくて、結構悩まされた・・・。)
つまり、バッファに入ってるイメージの幅は、
metrics.blackBoxXが収まるのに必要十分な4の整数倍、ということ。

<その4>
さて、グリフイメージが取得できたので、
ここからはOpenGLの部分。

glDrawPixelを使うわけだが、
文字が描かれていない部分はα値を0にして透明にするのが賢明であろうから、
フォーマットとして使用できそうなのは、
GL_RGBA、GL_LUMINANCE_ALPHA、GL_ALPA
の3つだろう。
GL_RGBAなら色を指定できるが1pixel辺り32bitのメモリが必要、
GL_LUMINANCE_ALPHAだと、1pixelあたり16bitだが色が白のみになる。
GL_ALPHAだと1pixelあたり8bitだけど色は黒になる。
どれにするかは、用途によりけり、といったところだろうか。
普通はRGBAだけど、白か黒の時はLUMINANCE_ALPHAやALPHAにする、
とかが、いいのだろうか。

後は、glDrawPixelをディスプレイリストにしてしまえばできあがり。

くれぐれも、毎フレームごとにglDrawPixelを呼び出したりしないこと。
これをやってしまうと、
いちいちプロセッサメモリからビデオメモリにピクセルデータを転送することになり、
非常に遅くなる。
それに対して、ディスプレイリスト化すると、
イメージデータはビデオメモリにコピーされるので、転送する手間が無くなり、
割と高速に描画されるようになる。

ちなみに、
"glPixelTransfer"関数と"glPixelZoom"関数を
glDrawPixelの手前で使えば、
<その3>でやる値のスケーリングや上下反転の操作を
ピクセル転送処理として行うことができる。
が、そのぶん、パフォーマンスが悪くなる。

0 件のコメント: