2012年

6月

26日

C#.NETでOpenGLを使った文字列描画+α

以前、OpenGLの機能を使ってAndoird上の文字列描画法をブログで紹介したことがあります。

今回はそれをC#でやってみようと思います。

 

前回の記事 AndroidでOpenGLを使った文字列描画

まずは、前回のAndroidでの処理の流れを簡潔に書いておきます。

詳しい詳細を知りたい方は上記リンクを参照してください。

 

○android.graphics.Bitmapクラスの作成

画像サイズは、文字列描画時の縦横幅を超える2のべき乗の最小値が望ましい

 

文字列描画時の幅はandroid.graphics.Paintクラス及び

android.graphics.Paint.FontMetricsクラスを使用すれば得ることが出来る

例1)640×480 → 1024×512

例2) 320×240 → 512 ×256

android.graphics.Canvasクラスの作成

コンストラクタの引数に、Bitmapクラスを渡す

Canvas.drawText()
Canvasに文字列を描画 描画結果がBitmapクラスに反映される

android.opengl.GLUtils.texImage2D()

Bitmapクラスのピクセルデータを領域に割り当てる

四角ポリゴンにテクスチャとして貼り付けて描画

 

と、このようになっていました。

今回のC#版も、ほぼ同じことをやります。違いとしては↓

android.graphics.BitmapSystem.Drawing.Bitmapへ、

android.graphics.CanvasSystem.Drawing.Graphicsに変わっただけです。

 

ところで前回の処理方法では「Bitmap、Canvasクラスの作成」と「テクスチャ領域の確保と開放」を毎フレームずつ行っていました。

 

Android上だと大きなサイズのテクスチャを複数枚画面に張るだけで処理速度に影響が出るので、テクスチャの幅を「指定文字列を描画可能な最小限の値」に設定するためにあえてこのような処理にしていました。

 

しかし、こんな頻繁にバッファの確保と開放を繰り返す処理は無駄な気がします。最初に大きな領域をあらかじめ取っておいて、そこに書き込むべきだと思います。

 

なによりメモリと処理スペックが限られているAndroidではともかく、PC上でそのようなことをする必要はありません。なので今回はこの部分を改変することにしました。

本編

今回はVC#2010とOpenTK 2012-03-15を使用しています。

 


フォントの用意


まずフォントオブジェクトを作成します。C#.NETにはSystem.Drawing.Fontクラスが用意されているのでそれを利用します。

 

System.Drawing.Fontクラス コンストラクタ

public Font(string familyNamefloat emSize,  FontStyle style)

 

familyName:フォント名

emSize:フォントサイズ

style:スタイル(斜体、太字等)

コンストラクタは他にもありますが、今回扱う分ではこれで充分です。

 

使用例)フォントはMS明朝、サイズは15でスタイルは太字+下線です

Font font = new Font("MS 明朝", 15.0f, FontStyle.Underline | FontStyle.Bold); 



 画像の用意 (System.Drawing.Bitmap、 System.Drawing.Graphics)


次にテクスチャの元になる画像を用意し、画像へ文字列を描画する手段の準備を整えます。

 

// ウィンドウの画面サイズを640×480とする

Bitmap image = new Bitmap(640, 480);

 

// BitmapイメージからGraphicsを作成

Graphics graphics = Graphics.FromImage(image);

これで、graphicsの描画メソッドを使うと描画結果がimageへ反映されるようになります。

画像サイズは640×480と指定してますが、値は2のべき乗にするのが良いかもしれません。

 


OpenGLでテクスチャの領域確保


OpenGLでテクスチャ領域を確保し、テクスチャパラメータを設定。

 

// 領域確保

GL.GenTextures(1, out texBindID);

 

// アクティブ設定

GL.BindTexture(TextureTarget.Texture2D, texBindID);

 

// ポリゴン色とテクスチャ色の合成方法

GL.TexEnv(TextureEnvTarget.TextureEnv, TextureEnvParameter.TextureEnvMode, (int)All.Modulate);

省いてはいますが、実際に処理を書く場合はエラーの確認をしておきましょう。

 


画像のピクセルデータ取得&テクスチャ貼り付け


まずはGraphicsクラスを使ってBitmapクラスへ文字列を描き込みます。

文字列の描画にはこのメソッドを使います。

 

文字列描画関数

public Graphics.DrawString(string s,  Font font,  Brush brushfloat xfloat y)


s:描画対象文字列 

font:フォントデータ 

brush:ブラシ

x:x座標

y:y座標

float x = 100, y = 50;

Color color = Color.Black;


// 描画領域を塗りつぶす(背景は透明にする)

graphics.Clear(Color.FromArgb(0));

 

// 文字列を描画

 graphics.DrawString("OpenGLで文字列描画", font, new SoldBrush(color), x, y);

次にBitmapクラスからピクセルデータのポインタ(IntPtr型)を取り出します。

ピクセルデータを取り出すには、Bitmapデータをロックする必要があります。

 

画像領域ロック関数

public BitmapData Bitmap.LockBits(

             Rectangle rect,

             ImageLockMode flags,

             PixelFormat format 

)

 

rect:画像のロック領域

flags:アクセス方法

format:取得するピクセルデータのフォーマット指定

24ビットカラーで受け取る場合→PixelFormat.Format24bppRgb

32ビットカラーで受け取る場合→PixelFormat.Format32bppArgb

 

ロックに成功すると、BitmapData.Scan0からピクセルデータの先頭アドレスにアクセスできるようになります。

// ピクセルデータの確保

System.Drawing.Imaging.BitmapData data = image.LockBits(

            new Rectangle(0, 0, image.Width, image.Height), // 画像全体をロック

            System.Drawing.Imaging.ImageLockMode.ReadOnly、// 読み取り専用

            System.Drawing.Imaging.PixelFormat.Format32bppArgb // 32bitカラー

);

 

// テクスチャ領域にピクセルデータを貼り付ける

GL.BindTexture(TextureTarget.Texture2D, texBindID);
                GL.TexImage2D(
                    TextureTarget.Texture2D,
                    0,
                    PixelInternalFormat.Rgba,
                    data.Width, data.Height,
                    0,
                    PixelFormat.Bgra, // Rgbaにすると赤と青が反転するので注意
                    PixelType.UnsignedByte,
                    data.Scan0);

 

// ロックをしたら必ずアンロックする

image.UnlockBits(data);

後はテクスチャをポリゴンに貼り付けて描画すれば文字列が画面に表示されます。

 

// カメラ行列初期化
GL.MatrixMode(MatrixMode.Projection);
GL.LoadIdentity();

 

// カメラ行列設定(gluortho2Dと同じようなもの)

Matrix4 view = Matrix4.CreateOrthographicOffCenter(0, 640, 480, 0, 0, 1000);

GL.MultMatrix(ref view);

 

// ライティングを無効化
GL.Disable(EnableCap.Normalize);

GL.Disable(EnableCap.Lighting);

 

// カリングを無効化
GL.Disable(EnableCap.CullFace);

 

// 深度バッファの無効化
GL.Disable(EnableCap.DepthTest);

 

// テクスチャ有効化

GL.Enable(EnableCap.Texture2D);

 

// 描画色は必ず白に設定

GL.Color4(Color4.White);

 

// 四角形ポリゴンの描画開始

 GL.Begin(BeginMode.Quads);

 

GL.Texcoord2(Vector2.Zero); // (0, 0)        

GL.Vertex2(Vector2.Zero); // (0, 0)


GL.Texcoord2(Vector2.UnitY); // (0, 1)

GL.Vertex2(new Vector2(0, 480)); // (0, 480)


GL.Texcoord2(Vector2.UnitX + Vector2.UnitY); // (1, 1)

GL.Vertex2(new Vector2(640, 480)); // (640, 480)


GL.Texcoord2(Vector2.UnitX); // (1, 0)

GL.Vertex2(new Vector2(640, 0)); // (640, 0)


// 描画終了

GL.End();

Bitmapクラスのロック/アンロックやピクセルデータのコピーがあり中々重そうな処理にも見えますが、実際のところ負担になるような重さではないので気軽に使うことが出来ます。

 

速度を計ってみたところ、1フレーム辺りの描画処理時間は15~30ミリでした。

C#ならもっと簡単に出来るんじゃ?

さてここまでOpenGLを利用する文字列描画の方法をやってきましたが、C#ならOpenGLの機能を使わずともGraphicsクラスだけで文字列の描画が可能です。

 

上記でBitmapクラスからGraphicsクラスを作成しましたが、Controlクラスを使っているのならわざわざそんなことをせずともPaintイベントをオーバーライドさせすればPaintEventArgsからGraphicsを取得することが出来ます。

 

Bitmapクラスを使わず、文字列をテクスチャ化せずに描画しようとするとこうなります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System;
using System.Drawing;
using System.Windows.Forms;
using OpenTK;
using OpenTK.Graphics.OpenGL;

namespace test
{
    public partial class Form1 : Form
    {
        GLControl glControl1;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Form1()
        {
            InitializeComponent();

            // OpenGLコントロール作成
            glControl1 = new GLControl();
            glControl1.BackColor = System.Drawing.Color.Black;
            glControl1.Location = new System.Drawing.Point(13, 13);
            glControl1.Name = "glControl1";
            glControl1.Size = new System.Drawing.Size(267, 241);
            glControl1.TabIndex = 0;
            glControl1.VSync = false;
            glControl1.Parent = this;

            // イベント追加
            glControl1.Load += glControl1_Load;
            glControl1.Paint += glControl1_Paint;
        }

        /// <summary>
        /// 初期化
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void glControl1_Load(object sender, EventArgs e)
        {
            // 初期化色設定
            GL.ClearColor(Color.CornflowerBlue);
        }

        /// <summary>
        /// 描画
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void glControl1_Paint(object sender, PaintEventArgs e)
        {
            // コントロールをアクティブ設定
            glControl1.MakeCurrent();

            // 画面クリア
            GL.Clear(
                ClearBufferMask.ColorBufferBit |
                ClearBufferMask.DepthBufferBit);

            // 画面更新
            glControl1.SwapBuffers();

            // 文字列描画(必ずSwapBuffersの後に呼ぶ)
            e.Graphics.DrawString(
                "C#のみで文字列描画",
                new Font("MS ゴシック", 20f),
                Brushes.White, 0, 100);
        }
    }
}

描画結果↓

さらに、Paintイベントがオーバーライド出来なくても、Controlのハンドル(hWnd)さえあればGraphicsクラスは作成可能です。デバイスコンテキストでも作れます。

 

ControlからSystem.Drawing.Graphicsを取得する関数

public  Graphics.FromHwnd(IntPtr hwnd)

public  Graphics.FromHdc(IntPtr hdc)

 

これで作成したGraphicsクラスを、上記同様SwapBuffers()の後に描画メソッドを実行すれば文字列描画が可能です。 

 

あとこれはWindows限定ですが、Controlハンドルやデバイスコンテキストが参照できない場合、アプリケーション名さえ分かればFindWindow関数でハンドルを取得することが可能です。

 

Win32APIの関数なので、アンマネージコードdllをインポートする必要があります。

フォームをアクティブにする

これまで三種類の描画方法を紹介してきました。

①Bitmapクラスを中継して文字列をテクスチャ化してポリゴンに貼り付ける

②Control.PaintイベントをオーバーライドしてPaintEventArgs.Graphicsから描画

③ウィンドウハンドル、又はデバイスコンテキストからGraphicsを作成 ハンドルが無ければFindWindowで取得する 描画法は②と同じ

 

FormクラスやControlクラスを使っているのであれば、の方法が簡単に実現可能です。

ちなみに何故手のかかる①の方法を紹介したかといいますと、の方法で描画できない場合があるからです。

 

今回OpenTKを扱うに当たって私はOpenTK.GameWindowでウィンドウを動かしているのですが、何故か②と③の方法では描画できませんでした。

 

GameWindowメンバにWindowInfoContextなどそれっぽい変数はあるんですけど、中をのぞいても肝心のハンドルがどこにも見当たりません。

 

ウィンドウタイトルはあるのでFindWindowは使えるんですが、ウィンドウ名はわかってもクラス名が違うらしく、有効な値が取得できませんでした。

 

そんな時にどうすればいいのか?その解決法がの描画方法です。

 

は簡単に実装できる反面、ラッパークラスライブラリの仕様でハンドルを参照できない場合は使い物になりません。

 

一方の方法はに比べ複雑にはなりますが、ハンドルを必要とせず描画が可能です。 

 

ならおそらく、現在出回っているOpenGLのラッパークラスライブラリ全てで使用可能なのではないでしょうか?

一応、強引ながらOpenTK.GameWindowからハンドルを取得する方法はあります。

 

調べてみたら「GameWindow.WindowInfoからハンドルの参照が可能」という情報を入手したので確認をしてみました。

 

GameWindow.WindowInfo「IWindowInfo」という要素が空っぽのインターフェースなのですがこれを覗いてみたところ「OpenTK.Platform.Windows.WinWindowInfo」というクラスが入っていて、確かにウィンドウハンドルとデバイスコンテキストの両方を所持していました。

 

ところがこのクラスが実はprivateクラスで、型キャストも出来ないわけです・・・

しかし、ToString()を覗いたところ、このような文字列を返す仕様になってました。

 

"Windows.WindowInfo: Handle 自身のウィンドウハンドル, Parent (親ハンドル)"

 

つまりどうすればいいのかというと、

①GameWindow.WindowInfo.ToString()でハンドル情報が載った文字列を取得

②ハンドル情報の部分だけ切り取り、long.Parse()でlong値を取得

③IntPtr型のコンストラクタにlong値を渡してウィンドウハンドルに変換

 

これで取得ハンドルからGraphicsクラスを作成すればと同じ描画が可能になります。

実行結果↓

只この方法、の描画法と比較すると、ある程度のチラつきが目立ちます。

GameWindowから描画したからチラつくのか、あるいはGraphicsから描画するとチラつくのかは不明ですが、綺麗に描画をしたいのであればの方法が確実そうです。

プラスα

このテクスチャ化による文字列描画ですが、応用を利かせればこのようなことも出来ます。

グラデーションをかけて、四角・円状に一部を切り取ってます。全て動的処理です。

処理をかける前の画面↓

画面のミクは、MMDモデルを読み込んでGLSLシェーダを適応したものです。

地味にスキンメッシュアニメーションもしてます。

 

というわけで、次回はOpenGL+GLSLでGPUスキンメッシュアニメーションをやります。