mieki256's diary



2024/01/13() [n年前の日記]

#1 [prog] OpenGL + glBitmapで文字描画をしたい

OpenGL で文字描画をしたい。 _昨日の実験 で、wglUseFontBitmaps() を使えば文字描画ができることは分かったけれど、Windowsに特化しているあたりがなんだかちょっと微妙になんとなく気になる。ここは一つ、Linux あたりでも使えそうな方法でやれないものか。

そんなわけで、2値画像を描画できるという glBitmap() を使えば、Windows以外でも使えるのではないかと思えてきたので、そのあたりを試してみた。

glBitmapで画像を描画 :

まずは glBitmap() の基本的な使い方を把握したい。以下のページが参考になった。ありがたや。

_ビットマップ


その前に、画像をビットパターンの配列に変換するツールが欲しいなと…。png画像を読み込んで、unsigned char の配列にして出力する Pythonスクリプトを書いてみた。環境は、Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0。

_png2bits.py
"""
png image to c header file

Usage: python png2bits.py -i input.png --label label_name > image.h

Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0
"""

import argparse
import os
import sys
from PIL import Image


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-i", "--infile", required=True, help="PNG image file")
    parser.add_argument("--label", help="symbol name")
    args = parser.parse_args()
    infile = args.infile

    if os.path.isfile(infile):
        print("/* infile: %s */" % infile)
    else:
        print("Error: Not found %s" % infile)
        sys.exit()

    if not args.label:
        label = infile.replace(".", "_")
        label = label.replace(" ", "_")
    else:
        label = args.label

    im = Image.open(infile).convert("L")
    im.point(lambda x: 0 if x < 128 else x)
    w, h = im.size
    print("static unsigned int %s_width = %d;" % (label, w))
    print("static unsigned int %s_height = %d;\n" % (label, h))
    print("static unsigned char %s[] = {" % (label))

    count = 0
    for y in range(h - 1, -1, -1):
        buf = []
        for i in range(int(w / 8)):
            buf.append(0)
        if (w % 8) != 0:
            buf.append(0)

        for x in range(w):
            v = im.getpixel((x, y))
            if v > 128:
                buf[int(x / 8)] |= 1 << (7 - (x % 8))

        s = ""
        for v in buf:
            s += "0x%s," % format(v, "02x")
            count += 1
        print("  %s // %d" % (s, y))

    print("};\n")

    print("static unsigned int %s_len = %d;" % (label, count))


if __name__ == "__main__":
    main()

使い方は以下。
python png2bits.py -i INPUT.png --label LABEL_NAME > OUTPUT.h

やってることは、PIL(Pillow)でpng画像を読み込んでグレースケール画像に変換して、各ドットをチェックして値が128より大きければ1に、小さければ0にして、8ドット=1byteにまとめて並べて出力しているだけ。OpenGLは下から上に画像を描画していくらしいので、png画像をスキャンしていくときも下から上に見ていくようにした。

このスクリプトを使って、以下の画像を C言語の .h の形に変換。

image_32x32.png
_image_32x32.png

image_lena.png
_image_lena.png

以下のような .h が得られた。

_image_32x32.h
_image_lena.h

内容は以下のような感じ。
/* infile: image_32x32.png */
static unsigned int image_32x32_width = 32;
static unsigned int image_32x32_height = 32;

static unsigned char image_32x32[] = {
  0xff,0xff,0x00,0x00, // 31
  0xff,0xff,0x00,0x00, // 30

// ...

  0x01,0x80,0x55,0xcc, // 1
  0x00,0x00,0xaa,0xcc, // 0
};


これで描画すべきビットパターンの配列は得られた。OpenGL の glBitmap() で描画するプログラムを書いてみる。

ビルドは、Windows10 x64 22H2上で、MinGW (gcc 6.3.0) / MSYS2 MINGW64 (gcc 13.2.0) + freeglut を使った。

_01_glbitmap.c
// draw bitmap sample on OpenGL

#include <windows.h>
#include <stdio.h>
#include <GL/gl.h>
#include <GL/freeglut.h>

// include bitmap pattern
#include "image_32x32.h"
#include "image_lena.h"

void display(void)
{
  GLfloat xorig, yorig, xmove, ymove;
  GLsizei w, h;

  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);

  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

  // draw arrow
  glColor4f(1, 1, 1, 1);     // set color
  glRasterPos2f(-0.5, -0.5); // set position
  xorig = 0.0;
  yorig = 0.0;
  xmove = 0.0;
  ymove = 0.0;
  w = image_32x32_width;
  h = image_32x32_height;
  glBitmap(w, h, xorig, yorig, xmove, ymove, image_32x32); // draw bitmap

  // draw lena
  glColor4f(1.0, 0.8, 0.5, 1);
  glRasterPos2f(0.0, 0.0);
  xorig = 0.0;
  yorig = 0.0;
  xmove = 0.0;
  ymove = 0.0;
  w = image_lena_width;
  h = image_lena_height;
  glBitmap(w, h, xorig, yorig, xmove, ymove, image_lena);

  glFlush();
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key)
  {
  case '\x1B':
  case 'q':
    // Exit on escape or 'q' key press
    glutLeaveMainLoop();
    // exit(EXIT_SUCCESS);
    break;
  }
}

int main(int argc, char **argv)
{
  glutInit(&argc, argv);
  glutInitWindowSize(512, 288);
  glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA | GLUT_DEPTH);

  glutCreateWindow("glBitmap sample");
  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);

  glutMainLoop();
  return EXIT_SUCCESS;
}

_Makefile
PROGRAM = 01_glbitmap.exe
SRC = $(PROGRAM:%.exe=%.c)

ifeq ($(MSYSTEM),MINGW64)

# --------------------
# MSYS2 MINGW64

$(PROGRAM): $(SRC) image_32x32.h image_lena.h  Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -static

else

# --------------------
# MinGW

$(PROGRAM): $(SRC) image_32x32.h image_lena.h  Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

endif

image_32x32.h: image_32x32.png Makefile
    python png2bits.py -i $< --label image_32x32 > $@

image_lena.h: image_lena.png Makefile
    python png2bits.py -i $< --label image_lena > $@

.PHONY: clean
clean:
    rm -f *.exe
    rm -f *.o
    rm -f image_32x32.h
    rm -f image_lena.h

make と打って、01_glbitmap.exe が生成された。実行結果は以下。

01_glbitmap_ss.png

両方とも描画できた。

glBitmapで文字描画をしてみる :

glBitmap() を使えば2値画像を描画できることが分かったので、コレを使って文字描画ができないか試してみる。ひとまず、英数字のみを等幅フォントで描画する方向で考える。

その前に、フォントデータを作らないといけない…。コレも、Python を使って、フォントを敷き詰めた画像からビットパターンを作って、unsigned char 配列として出力するスクリプトを書いてみた。環境は、Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0。

_fontpng2bits.py
"""
font png image to c header file

Usage: python fontpng2bits.py -i input.png --label label_name > image.h

Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0
"""

import argparse
import os
import sys
from PIL import Image


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-i", "--infile", required=True, help="PNG image file")
    parser.add_argument("--label", help="symbol name")
    args = parser.parse_args()
    infile = args.infile

    if os.path.isfile(infile):
        print("/* infile: %s */" % infile)
    else:
        print("Error: Not found %s" % infile)
        sys.exit()

    if not args.label:
        label = infile.replace(".", "_")
        label = label.replace(" ", "_")
    else:
        label = args.label

    im = Image.open(infile).convert("L")
    im.point(lambda x: 0 if x < 128 else x)
    imgw, imgh = im.size
    w = int(imgw / 16)
    h = int(imgh / 6)
    print("/* source image size = %d x %d */\n" % (imgw, imgh))

    print("static int %s_width = %d;" % (label, w))
    print("static int %s_height = %d;" % (label, h))

    alen = int(w / 8) + (1 if (w % 8) != 0 else 0)
    clen = alen * h
    cnum = 16 * 6
    print("static int %s_chr_len = %d;\n" % (label, clen))

    print("static unsigned char %s[%d][%d] = {" % (label, cnum, clen))

    for c in range(16 * 6):
        bx = int(c % 16) * w
        by = int(c / 16) * h

        print("  {")

        print("    // code = 0x%02x" % (c + 0x20))

        for y in range(h - 1, -1, -1):
            buf = []
            for i in range(alen):
                buf.append(0)

            for x in range(w):
                v = im.getpixel((bx + x, by + y))
                if v > 128:
                    buf[int(x / 8)] |= 1 << (7 - (x % 8))

            s = ""
            for v in buf:
                s += "0x%s," % format(v, "02x")
            print("    %s" % s)

        print("  },")

    print("};")


if __name__ == "__main__":
    main()

使い方は以下。
python fontpng2bits.py -i INPUT.png --label LABEL_NAME > OUTPUT.h


フォント画像は以下のものを用意した。ASCIIコード 0x20 - 0x7f までを、横16文字、縦6行に、等幅で敷き詰めた画像を用意する。各フォントの入手先については後述する。

font_pet2015.png
_font_pet2015.png

font_courR18.png
_font_courR18.png

font_shnm8x16r.png
_font_shnm8x16r.png

font_shnm8x16rx2.png
_font_shnm8x16rx2.png

font_profont.png
_font_profont.png

font_ter-u24b.png
_font_ter-u24b.png


これを先ほどのPythonスクリプトに渡して、C言語の .h(ヘッダーファイル)の形にした。

_fontdata.h
_fontdata_courr18.h
_fontdata_shnm8x16r.h
_fontdata_shnm8x16rx2.h
_fontdata_profont.h
_fontdata_ter-u24b.h

中身は以下のような感じになっている。文字1つ毎に配列になってる、2次元配列として用意した。
/* infile: font_pet2015.png */
/* source image size = 256 x 96 */

static int fontdata_width = 16;
static int fontdata_height = 16;
static int fontdata_chr_len = 32;

static unsigned char fontdata[96][32] = {
  {
    // code = 0x20
    0x00,0x00,
    0x00,0x00,
    0x00,0x00,
    0x00,0x00,

// ...

    0x00,0x00,
    0x00,0x00,
  },
  {
    // code = 0x21
    0x00,0x00,

// ...

    0x0c,0x30,
    0x03,0xc0,
    0x03,0xc0,
  },
};


フォントデータが用意できたので、glBitmap() で描画してみる。ソースは以下。文字描画の処理をしているのは、drawText() の部分。

環境は、Windows10 x64 22H2 + MinGW (gcc 6.3.0) / MSYS2 MINGW64 (gcc 13.2.0) + freeglut。

_02_drawbitmapfont.c
// bitmap font draw sample on OpenGL

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <GL/gl.h>
#include <GL/freeglut.h>

// include bitmap pattern
// --------------------
// #include "fontdata.h"
// #include "fontdata_courr18.h"
// #include "fontdata_shnm8x16r.h"
// #include "fontdata_shnm8x16rx2.h"
// #include "fontdata_profont.h"
#include "fontdata_ter-u24b.h"

#define SCRW 512
#define SCRH 288

static int framerate = 60;

static int count = 0;

// draw text
void drawText(char *str)
{
  GLsizei w = (GLsizei)fontdata_width;
  GLsizei h = (GLsizei)fontdata_height;
  int clen = fontdata_chr_len;
  GLfloat xorig, yorig;
  GLfloat xmove, ymove;
  xorig = 0;
  yorig = 0;
  xmove = w;
  ymove = 0;

  glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // コレが無いと表示が崩れる

  int slen = strlen(str);
  for (int i = 0; i < slen; i++)
  {
    int c = str[i];
    if (c == 0)
      break;
    if (c < 0x20 || c > 0x7f)
      c = 0x20;
    c -= 0x20;
    glBitmap(w, h, xorig, yorig, xmove, ymove, &(fontdata[c][0]));
  }
}

// draw OpenGL
void display(void)
{
  GLfloat xorig, yorig, xmove, ymove;
  GLsizei w, h;

  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);

  // draw text
  glColor4f(1, 1, 1, 1);    // set color
  glRasterPos2f(-0.8, 0.5); // set position
  drawText("Hello World !!");

  {
    char buf[256];
    sprintf(buf, "count %d", count);
    glRasterPos2f(-0.8, 0.0);
    drawText(buf);
  }

  glutSwapBuffers();
  glFlush();

  count++;
}

void onTimer(int value)
{
  glutPostRedisplay(); // redraw
  glutTimerFunc((1000 / framerate), onTimer, 0);
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key)
  {
  case '\x1B':
  case 'q':
    // Exit on escape or 'q' key press
    glutLeaveMainLoop();
    // exit(EXIT_SUCCESS);
    break;
  }
}

int main(int argc, char **argv)
{
  count = 0;
  
  glutInit(&argc, argv);
  glutInitWindowSize(SCRW, SCRH);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);

  glutCreateWindow("bitmap font draw sample");
  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);

  glutTimerFunc((1000 / framerate), onTimer, 0);

  glutMainLoop();
  return EXIT_SUCCESS;
}

最初、表示がグチャグチャになってハマってしまった。glPixelStorei(GL_UNPACK_ALIGNMENT, 1); を入れないと思った通りに描画されなかった。


_Makefile
PROGRAM = 02_drawbitmapfont.exe
SRC = $(PROGRAM:%.exe=%.c)
FONTS = fontdata.h fontdata_courr18.h fontdata_shnm8x16r.h fontdata_shnm8x16rx2.h fontdata_profont.h fontdata_ter-u24b.h

ifeq ($(MSYSTEM),MINGW64)

# --------------------
# MSYS2 MINGW64

$(PROGRAM): $(SRC) $(FONTS) Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -static

else

# --------------------
# MinGW

$(PROGRAM): $(SRC) $(FONTS) Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

endif

fontdata.h: font_pet2015.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_courr18.h: font_courR18.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_shnm8x16r.h: font_shnm8x16r.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_shnm8x16rx2.h: font_shnm8x16rx2.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_profont.h: font_profont.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_ter-u24b.h: font_ter-u24b.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

.PHONY: clean
clean:
    rm -f *.exe
    rm -f *.o
    rm -f fontdata.h

Makefile には、先ほど用意した全フォントデータ(.h) を生成する行も記述しちゃってるけど、不要なら該当行は消してしまって構わない。

make でビルド。得られた 02_drawbitmapfont.exe を実行すると以下のような結果になった。

ss_fontdata.png

文字(文字列)を描画することができた。

ソースの最初のほうのコメントアウトを修正して、別のフォントを描画してみると、以下のような感じになる。印象が結構違うなと…。

courR18.bdf の例。
ss_fontdata_courr18.png

東雲フォント(shnm8x16r.bdf)の例。
ss_fontdata_shnm8x16r.png

東雲フォント(shnm8x16r.bdf)を2倍に拡大した例。
ss_fontdata_shnm8x16rx2.png

ProFontの例。
ss_fontdata_profont.png

Terminus Font の例。
ss_fontdata_ter-u24b.png


ということで、glBitmap() を使って、OpenGL で文字描画をすることができる、と分かった。

余談。これらのフォントデータと、glBitmap() を使って描画する関数を、一つの .h(ヘッダーファイル)にまとめてしまって、「この .h を1つ #include すれば文字描画できますよ」という形にしてしまえば少しは便利に使えるかもしれないなと…。

もっとも…。
  • glut を使っていたら、既に文字描画できる関数 glutBitmapCharacter() があったりするし。
  • Windowsに特化すれば wglUseFontBitmaps() があるし。
  • Linux なら FTGLライブラリがあるし。
学習用途ならそれらを使うほうが手っ取り早いか…。

フォントの入手先 :

前述のように、ASCIIコード 0x20 - 0x7f を、横16文字 x 縦6行で、等幅で並べた画像さえあれば、こうしてフォントデータを用意できるので、元になるフォントは画像でもbdfフォントでもなんでも使えるはず。要は、以下の文字さえ用意してあればいい。

 !"#$%&'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~ 


とりあえず、今回用意したフォント画像の元フォントの入手先をメモしておく。


一番最初に使った pet2015 というフォントは、自分が作ったビットマップフォント。ライセンスは CC0 / Public Domain。画像しか存在していない。1文字8x8ドット。8bit PC MZ-700のフォントに似せつつドット単位では別物になるようにドットを打った。 *1

_mieki256's diary - HGIMG3とビットマップフォント画像の作り方を勉強中


courR18.bdf は、ググったら Apple のサイトにあったのでそこから入手。bdfファイルの最初のほうにライセンスその他が書いてあると思うのだけど、英語分からん…。実験用なら使ってもいいのではないかと安易に思って使ってみたけど、実際はどうなんだろう…。

_courR18.bdf


東雲フォントは以下から入手。ライセンスは Public Domain。shinonome-0.9.11p1.tar.bz2 を入手して解凍。bdfフォルダの中に .bdfフォントが入っている。英数字部分は、6x12, 7x14, 8x16, 9x18 が用意されてる。

_shinonome font family


ProFont は以下から入手。ライセンスは _MIT License になっているらしい。profont-x11.zip を入手して解凍。pcfフォントしか入ってない。*.pcf を任意のフォルダに置いて、GIMP 2.8.22 Portable のフォントフォルダとして追加して、GIMP上で画像化してみた。 *2

_ProFont for Windows, for Macintosh, for Linux


Terminus Font は以下から入手。ライセンスは SIL Open Font License, Version 1.1。terminus-font-4.49.1.tar.gz を入手して解凍。bdfフォントが入っている。

_Terminus Font Home Page

bdfフォントの画像化 :

bdfフォントを画像化したい時は、bdf2bmp というツールが使える。

_bdf2bmp

使い方は以下。
bdf2bmp input.bdf output.bmp

デフォルトだと境界線(?)が入るけれど、オプションをつければ、境界線を入れない状態で出力できたり、横に並ぶ文字の個数を指定できたりする。以下は、境界線無し(0ドット幅)、横に16文字並べる指定。
bdf2bmp input.bdf output.bmp -s0 -c16

こうして画像化してしまえば、任意のグリッドサイズを指定、かつ、グリッド単位で選択や移動ができる画像編集ソフトを使うことで、文字の並び替えもできるかなと。自分の場合は _ドットエディタ EDGE2 (シェアウェア) で作業してしまうことが多いけれど、GIMP のグリッド表示と吸着を有効化して作業できなくもないかなと…。

*1: 余談。MZシリーズのフォントは PET 2001 というPCのフォントとクリソツで…。当時はなんというか、大らかだったんだなと…。
*2: _最近のバージョンのGIMPは一部のビットマップフォントが使えない という話があったので、今回、念のために、あえて GIMP 2.8.22 Portable を使って作業した。

以上です。

過去ログ表示

Prev - 2024/01 - Next
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

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project