トップコンテンツプログラムノベルゲーム>文字の表示



文字の表示 その2

前回の文字表示の続きです。今度は文字列の表示が行なえるようなプログラムを作ります。


プログラム その1

最初は楽に作れてしまうバージョン(1 行表示のみ)から書いてしまいます。
そっちの方がソースが短く済むので。


gl6_1.h (gl6_1.h のヘッダファイル) [表示・非表示]
 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.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <GL/glut.h>

/* freetypeライブラリ使用時に必要な宣言 */
#include <ft2build.h>
#include FT_FREETYPE_H

/* FreeTypeエラーメッセージを、配列へ格納 */
#include <freetype/fterrors.h>
#undef __FTERRORS_H__
#define FT_ERRORDEF( e, v, s )  { e, s },
#define FT_ERROR_START_LIST     {
#define FT_ERROR_END_LIST       { 0, 0 } };


/* 描画領域についての情報を格納する構造体(OpenGLで使用)*/
typedef struct
{
  GLint x, y;          // 原点からの(OpenGLなら左下)描画開始位置
  GLsizei width, height; // それぞれ描画領域の幅, 高さ
} area;


/* グローバル変数 */
// OpenGL用(テクスチャ)
GLuint texture;      // テクスチャの識別番号

// FreeType用
FT_Library library;
FT_Face face;            // フォント書体についての情報を含む構造体へのポインタ
FT_Bitmap bits;          // フォントのビットマップデータを含む構造体を指すポインタ
FT_GlyphSlot slot;

const struct
{
  int          err_code;
  const char*  err_msg;
} ft_errors[] =

#include FT_ERRORS_H     // エラーメッセージ表示に必要な情報を読み込む


#pragma comment(lib, "freetype244MT.lib")
#define  FONTFILE  "C:\\Windows\\Fonts\\msgothic.ttc"


/* 関数のプロトタイプ宣言 */
void FT_myInit(void);
void GL_myInit(void);
void display(void);
void loadCharFont(const FT_ULong ch);
area renderLetter(short x, short y, const FT_ULong ch);
void renderStr(short x, short y, const char* str);
void renderTexture(short x, short y, short width, short height, const GLuint id);

gl6_1.c (GUI ウィンドウ上に文字列を表示) [表示・非表示]
 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.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
#include "gl6_1.h"

int main(int argc, char* argv[])
{
  /* 初期化 */
  glutInit(&argc, argv);
  FT_myInit();
  GL_myInit();

  /* イベント待機状態に入る */
  glutMainLoop();

  return 0;
}


void FT_myInit(void)
{
  int i, j;
  int n = 0;
  FT_Error err;

  /* freetypeライブラリの初期化 */
  err = FT_Init_FreeType(&library);
  if (err != 0) 
    {
      fprintf(stderr, "FreeType error: %s", ft_errors[err].err_msg);
      exit(EXIT_FAILURE);
    }

  /* フォントフェースをロード(フォントファイルからロードする) */
  err = FT_New_Face(library, FONTFILE, 0, &face);
  if (err != 0)
    {
      fprintf(stderr, "FreeType error: %s", ft_errors[err].err_msg);
      exit(EXIT_FAILURE);
    }

  /* ピクセルサイズの指定 */
  err = FT_Set_Pixel_Sizes(face, 200, 200);
  if (err != 0)
    {
      fprintf(stderr, "FreeType error: %s", ft_errors[err].err_msg);
      exit(EXIT_FAILURE);
    }

  slot = face->glyph;
}

void GL_myInit(void)
{
  /* ウィンドウ生成前に行なうべき設定 */
  glutInitWindowSize(800, 600);                  // ウィンドウサイズの設定
  glutInitWindowPosition(200, 200);              // 表示位置の設定
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);  // ディスプレイモードの設定
  glutCreateWindow("OpenGL");                    // ウィンドウを生成
  glClearColor (0.0, 1.0, 0.0, 1.0);             // ウィンドウの背景色

  glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 2.0);

  /* イベント発生時に呼び出される関数の登録 */
  glutDisplayFunc(display);  // 描画関数(自分自身で定義)を登録
}

void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);

  glEnable(GL_BLEND);

  /* 文字列の表示 */
  glPushMatrix();
    glColor3f(1.0, 0.0, 1.0);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    renderStr(50, 300, "pVa!");
  glPopMatrix();

  glDisable(GL_BLEND);

  glutSwapBuffers();
}

/* 引数として受け取った文字のグリフ(字形)データを取得 */
void loadCharFont(const FT_ULong ch)
{
  FT_Error err;

  err = FT_Load_Char(face, ch, FT_LOAD_RENDER);
  if (err != 0)
    {
      fprintf(stderr, "FreeType error: %s", ft_errors[err].err_msg);
      exit(EXIT_FAILURE);
    }

  /* 字形のbmpデータへのポインタ値を修正 */
  bits = slot->bitmap;
}

/* 文字のレンダリング */
area renderLetter(short x, short y, const FT_ULong ch)
{
  area tmp = {x, y, 0, 0};        // 戻り値を格納

  /* 文字のビットマップデータ読込 */
  loadCharFont(ch);

  /* テクスチャ作成 */
  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_INTENSITY,
               bits.width, bits.rows, 0, GL_LUMINANCE,
               GL_UNSIGNED_BYTE, bits.buffer);

  /* レンダリング */
  glPushMatrix();
  renderTexture(x + (slot->bitmap_left), y + (slot->bitmap_top - bits.rows),
                bits.width, bits.rows, texture);
  glPopMatrix();


  /* 文字の描画範囲を格納して返却する */
  tmp.width = bits.width;
  tmp.height = bits.rows;

  return tmp;
}

/* 文字列のレンダリング */
void renderStr(short x, short y, const char* str)
{
  int i;
  int Ymax = 0; // 高さの最大値
  const int num = strlen(str);  // 文字数
  area tmp = {x, y, 0, 0};

  /* 文字列のレンダリング */
  for (i = 0; i<num; ++i)
    tmp = renderLetter((tmp.x + tmp.width), tmp.y, (FT_Long)str[i]);
}

/* 指定された位置にテクスチャを描画する */
void renderTexture(short x, short y, short width, short height, const GLuint id)
{
  glViewport(x, y, width, height);

  glEnable(GL_TEXTURE_2D);

  glBindTexture(GL_TEXTURE_2D, id);
  glBegin(GL_QUADS);
    glTexCoord2f(1.0, 0.0);  glVertex2f(1.0, 1.0);
    glTexCoord2f(1.0, 1.0);  glVertex2f(1.0, 0.0);
    glTexCoord2f(0.0, 1.0);  glVertex2f(0.0, 0.0);
    glTexCoord2f(0.0, 0.0);  glVertex2f(0.0, 1.0);
  glEnd();

  glDisable(GL_TEXTURE_2D);
}

実行結果はこちら

文字列の表示

細かいことを言えば、V と a の間が広がりすぎという事なども言えるのですが、そこらへんの調整まではしないことにします。
やりたい方は「カーニング」, 「リガチャ」といったキーワードを元に、自分自身で調べてみてください。
ついでに、renderStr 関数は 1 バイト文字専用の関数なので、日本語文字を試してみたい方は、関数の仮引数と文字列の型を変更してください。
C++ の関数テンプレート使えば楽なのですが……。


さて、新しい関数は登場していないので、このプログラムの処理を解説します。
display の中にある renderStr が文字列を描画する関数なのですが、

  1. renderStr
  2. renderLetter
  3. renderTexture

という 3 段階の関数呼び出しによって文字列描画を行なっています。
renderStr は指定されたウィンドウ座標を左下の原点として、renderLetter 関数を使って 1 文字ずつ横に並べていくだけです。
renderTexture も、指定されたウィンドウ領域にテクスチャ画像を描画するだけの関数なので説明不要でしょう。


というわけで、1 文字分の描画を担当する renderLetter ですが、処理の順番はソースコードのコメントを見ればわかるでしょう。
なので、まずテクスチャ作成ステップで必要となる bits 変数について説明します。
これは FT_Bitmap 構造体型の変数で、文字画像を配置するために必要なデータが色々格納されています。
リンク先を見ればなんとなくわかると思いますが、FT_Bitmap のメンバ変数 rows は画像の高さ, width は画像の幅, buffer は画像データを格納している unsigned char 型配列です。

ついでに言うと、テクスチャ作成時に画像の形式を GL_LUMINANCE ではなく GL_INTENSITY としています。
2 つのモードの違いは、アルファ値を常に 1 とするか, 輝度の割合をそのままアルファ値としても利用するか、です。
GL_INTENSITY モードは、文字領域部分のみを表示したい, そして文字色も色々変更したい、という場合にとても便利です。


もう 1 つは、レンダリングのステップに必要な slot 変数FT_GlyphSlotRec_ 構造体型へのポインタです。
FT_GlyphSlotRec_ はグリフスロット……じゃわからないから、1 字ごとの字形データを記憶するための単語カードみたいなものと考えていいと思います。そしてそれを束ねて出来上がっているのがフォントファイル。なんとなくですが、こんなイメージで大丈夫だと思います。
loadCharFont で字形を読み込むたびに、slot 変数の指すものは自動で変わってくれているみたいです。bits 変数の場合、放っておくとポインタの指す先がおかしくなってしまうので、アドレスの値を更新しないといけないのですが。
slot 変数の中で、今回のプログラムにとっての重要なメンバ変数は bitmap, bitmap_left, bitmap_top の 3 つです。
bitmap は先程 bits 変数の所で紹介したとおり、文字のビットマップ画像に関するデータを保持する FT_Bitmap 構造体型です。

bitmap_leftとbitmap_top また bitmap_left, bitmap_top については右の画像を見たほうがわかりやすいと思います。
bitmap_left は、基本の配置位置(画像の場合は原点)を基準にした場合、原点と画像領域左サイドとの、x 方向での距離。
bitmap_top は、基本の配置位置を基準にした場合、原点と画像領域最上部との、y 方向での距離。
結論を言ってしまうと、配置位置の調整は、

という風にしてやれば大丈夫です。

なお、この配置調整を行なわなくても、日本語の文章の場合はそこまで困りません。
しかし、欧文の場合、「p」などの下の方に少しずらすべき文字も含めて、全ての文字が上段に表示されてしまいます。
少し詳しい内容を知りたい方はこちらのページの「4.2 フォントのコンセプト」を参照してください。


そして最後に、次の文字を描画するときのために、文字画像の描画した範囲を返す、という処理を行なって renderLetter の処理は終了します。
ここまでの説明で無事理解できたでしょうか?


プログラム その2

さて、これで完成にしてもいいのですが、問題点が少しあります。
文字サイズが大きな場合はこのままでもいいのですが、20px 以下の小さな文字、特に漢字のような複雑な文字になってくると、途端に見えが悪くなることがあります。
その場合、大きい解像度の文字画像を縮小して表示すると上手くいくのですが、そのまま glScale で調整するとおかしくなります。
後は、ある一定の箇所に描画するための viewport 変換を別の所で行ないたい、といった場合も必要でしょうか。

今回のプログラムはそこらへんも考慮して作ってみます。


gl6_2.h (gl6_2.c のヘッダファイル) [表示・非表示]
 1.
 2.
 3.
 4.
 5.
 6.
 7.
 8.
 9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
~ インクルードファイル等 変更なし ~

/* グローバル変数 */
// OpenGL用(テクスチャ)
GLuint texture;      // テクスチャの識別番号
const area msgbox = {40, 30, 720, 180};  // メッセージボックスの領域

~ 以下、定数宣言のところまで変更なし ~
#define  FONTFILE  "C:\\Windows\\Fonts\\msgothic.ttc"
#define  HALFSP  0x0020  // 半角スペース
#define  FULLSP  0x3000  // 全角スペース
#define  LF      '\n'    // 改行文字

/* 関数のプロトタイプ宣言 */
void FT_myInit(void);
void GL_myInit(void);
void display(void);
void loadCharFont(const FT_ULong ch);
area renderLetter(short x, short y, const FT_ULong ch,
                  const float scale, const area* space);
void renderStr(const char* str, const float scale, const area* space);
void renderTexture(short x, short y, short width, short height,
                   const GLuint id, const area* space);

gl6_2.c (GUI ウィンドウ上に文字列を表示) [表示・非表示]
 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.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
#include "gl6_2.h"

~ main, 初期化を行なう関数 変更なし ~

void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);

  glEnable(GL_BLEND);

  /* メッセージボックスの表示 */
  glPushMatrix();
    glViewport(msgbox.x, msgbox.y, msgbox.width, msgbox.height);
    glColor4f(0.3, 0.4, 0.8, 0.8);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  // アルファブレンド(半透明)
    glRectf(0.0, 0.0, 1.0, 1.0);
    glBlendFunc(GL_ONE, GL_ZERO);
  glPopMatrix();

  /* 文字列の表示 */
  glPushMatrix();
    glViewport(msgbox.x, msgbox.y, msgbox.width, msgbox.height);
    glColor3f(1.0, 0.0, 1.0);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  // アルファブレンド(半透明)
    renderStr("pVa !\nI live in TOKYO. And you? Oh! It's marvelous! HOW-HOW!!!",
              0.2, &msgbox);
    glBlendFunc(GL_ONE, GL_ZERO);
  glPopMatrix();

  glDisable(GL_BLEND);

  glutSwapBuffers();
}

~ loadCharFont 関数 変更なし ~

/* 文字のレンダリング */
area renderLetter(short x, short y, const FT_ULong ch,
                  const float scale, const area* space)
{
  area tmp = {x, y, 0, 0};        // 戻り値を格納

  switch (ch)
  {
    case HALFSP:
      tmp.width = (GLint) (face->size->metrics.x_ppem * 0.4 * scale);
      tmp.height = tmp.width;
      break;
    case FULLSP:
      tmp.width = (GLint) (face->size->metrics.x_ppem * 0.9 * scale);
      tmp.height = tmp.width;
      break;
    default:
      /* 文字のビットマップデータ読込 */
      loadCharFont(ch);

      /* テクスチャ作成 */
      glGenTextures(1, &texture);
      glBindTexture(GL_TEXTURE_2D, texture);
      glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_INTENSITY,
                   bits.width, bits.rows, 0, GL_LUMINANCE,
                   GL_UNSIGNED_BYTE, bits.buffer);

      /* レンダリング */
      glPushMatrix();
      renderTexture(x + ((slot->bitmap_left) * scale),
                    y + ((slot->bitmap_top - bits.rows) * scale),
                    (bits.width * scale), (bits.rows * scale),
                    texture, space);
      glPopMatrix();


      /* 文字の描画範囲を格納して返却する */
      tmp.width = bits.width * scale;
      tmp.height = bits.rows * scale;
  }

  return tmp;
}

/* 文字列のレンダリング */
void renderStr(const char* str, const float scale, const area* space)
{
  int i;
  int Ymax = 0; // 高さの最大値
  const int num = strlen(str);  // 文字数
  area tmp = {0};  // 描画開始位置(画像領域の左下を基準)

  tmp.x = space->x;
  tmp.y = (space->y + space->height) - (face->size->metrics.y_ppem * scale);


  /* 文字列のレンダリング */
  for (i = 0; i < num; ++i)
    {
      /* 明示的な改行処理 */
      if (str[i] == LF)
        {
          tmp.x = space->x;
          tmp.width = 0;
          tmp.y -= face->size->metrics.y_ppem * scale * 1.2;
          continue;
        }

      tmp = renderLetter((tmp.x + tmp.width), tmp.y, (FT_Long)str[i], scale, space);

      /* 次の文字を画面出力する際、表示領域からはみ出るかの確認 */
      if ((tmp.x + tmp.width + (face->size->metrics.x_ppem * scale)) > 760.0)
        {
          /* 自動的に改行 */
          tmp.x = space->x;
          tmp.width = 0;
          tmp.y -= face->size->metrics.y_ppem * scale * 1.2;
          continue;
        }
    }
}

/* 指定された位置にテクスチャを描画する */
void renderTexture(short x, short y, short width, short height,
                   const GLuint id, const area* space)
{
  float x1, y1, x2, y2;
  x1 = (float)(x - (space->x)) / (float)(space->width);           // 左端
  x2 = (float)(x - (space->x) + width) / (float)(space->width);   // 右端
  y1 = (float)(y - (space->y)) / (float)(space->height);          // 下端
  y2 = (float)(y - (space->y) + height) / (float)(space->height); // 上端

  glEnable(GL_TEXTURE_2D);

  glBindTexture(GL_TEXTURE_2D, id);
  glBegin(GL_QUADS);
    glTexCoord2s(1, 0);  glVertex2f(x2, y2);
    glTexCoord2s(1, 1);  glVertex2f(x2, y1);
    glTexCoord2s(0, 1);  glVertex2f(x1, y1);
    glTexCoord2s(0, 0);  glVertex2f(x1, y2);
  glEnd();

  glDisable(GL_TEXTURE_2D);
}

実行結果はこちら。

ずいぶん長くなりましたが、解説はきちんとしましょう。

さて、renderStr・renderLetter・renderTexture の 3 つの関数に引数 space, renderStr・renderLetter の 2 つの関数に引数 scale が追加されています。
space は描画領域を指定するため, scale は拡大・縮小の割合を指定するために使います。

後、注意することとしては、テクスチャを描画する際はウィンドウ座標の代わりに、描画領域(space)内を基準にした座標が使われていることに注意してください。
この座標系は glOrtho で指定した範囲の値で指定していきます。
(このプログラムだと、0.0 ≦ x ≦ 1.0, 0.0 ≦ y ≦ 1.0)

ついでに、いくつか機能を付け加えています。
標準だと、空白文字や改行文字に関するフォントが入っていないため、そういった文字を無視して処理がされるのですが、今回のプログラムでは、プログラム側で文字の処理を行ない、空白や改行の効果が現れるようにしています。
また、文字列が描画領域の横幅からはみ出そうになった時、自動的に改行を行なう処理もつけています。
これくらいはつけないと、後でゲーム動かす時に困るので…。


その1 で長々と解説しちゃったし、基本的には処理の手順も変わっていないので、このくらいの説明で大丈夫でしょうか?
以上で、長くなりましたが文字表示および、OpenGL に関する内容はこれで終了とさせて頂きます。
本当はアルファベット同士の間が詰まりすぎている所なんか直したいんだけど、そこらへんは面倒っぽい気もする。
文字はほとんど日本語しか表示しないので、どうしても困るわけじゃないしね。
まあ、そのうち追加で書くことが出来れば、別にページを用意します。


「ノベルゲーム」に戻る