🍤

Ebitengineで文字描画 - text/v2でより便利に+多言語・縦書きにも対応!

2024/04/07に公開

祝・Ebitengine v2.7 リリース!🎉 今バージョンの目玉機能である text/v2 により、

  • 文字を簡単に整列して描画できるように
  • ベクター(パス)変換でさらに高度な変形が可能に
  • アラビア語などの筆記体や、日本語やモンゴル語の縦書きなど、幅広い言語に対応
  • フォント機能(font feature. 後述)が利用できるように
  • 複数のフォントを組み合わせられるフォントフォールバックに対応

といった機能が追加されました!もちろん、以前からあった特徴もそのままです。

  • 事前にフォント画像を生成したりする必要はなく、手軽に描画できます。
  • グリフ単位で *ebiten.Image を取り出して、文字送りしたり動かしたり色を変えたりといった演出を適用できます。

これらの特徴により、普通に文字を描画する分には、使い方の面でもパフォーマンスの面でも、まったく困ることはないでしょう。

ただし、縁取りやグラデーションなど高度な装飾をするには、パフォーマンスに気をつけて実装方法を考える必要があります。これはフォント描画の本質的な難題[1]なので、頑張っていきましょう。この記事にも可能な限りTipsを載せるので、ぜひ参考にしてください。

text/v2 の基本

まずは簡単に使い方を説明します。詳しくは godoc を参照してください。

初期化

OpenType, TrueType フォントを読み込むには NewGoTextFaceSource[2]を使います。(ただしビットマップフォントや過去コードの移植には別の手段を使うので後述します)

続けて、読み込んだデータとフォントサイズを与えてフォントフェイス text.GoTextFace を作成しておきます。

var (
	fontFace *text.GoTextFace
)

func init() {
	// ファイルオープンなどで io.Reader を得る
	f, err := os.Open("KiwiMaru-Regular.ttf")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	// フォントを読み込む
	src, err := text.NewGoTextFaceSource(f)
	if err != nil {
		panic(err)
	}

	// フォントフェイスを作る
	fontFace = &text.GoTextFace{Source: src, Size: 48}
}

描画する

初期化したらあとは text.Draw に必要な情報を渡して描画するだけです。

// お馴染み `ebiten.DrawImageOptions` に、
// テキストレイアウト機能が足された `text.DrawOptions` を作る。
op := &text.DrawOptions{}
op.GeoM.Translate(3, 3)
op.LineSpacing = 48 * 1.5
text.Draw(screen, "Hello,\n世界!", fontFace, op)
コード例全体
package main

import (
	"image/color"
	"os"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/text/v2"
)

var (
	fontFace *text.GoTextFace
)

type game struct{}

func init() {
	f, err := os.Open("KiwiMaru-Regular.ttf")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	src, err := text.NewGoTextFaceSource(f)
	if err != nil {
		panic(err)
	}

	fontFace = &text.GoTextFace{Source: src, Size: 48}
}

func main() {
	if err := ebiten.RunGame(&game{}); err != nil {
		panic(err)
	}
}

func (g *game) Update() error {
	return nil
}

func (g *game) Draw(screen *ebiten.Image) {
	screen.Fill(color.RGBA{0x76, 0xbe, 0xd0, 255})

	op := &text.DrawOptions{}
	op.ColorScale.Scale(0, 0, 0, 1)
	op.LineSpacing = 48 * 1.5
	text.Draw(screen, "Hello,\n世界!", fontFace, op)

	op.GeoM.Reset()
	op.GeoM.Translate(3, 3)
	op.ColorScale.Reset()
	text.Draw(screen, "Hello,\n世界!", fontFace, op)
}

func (g *game) Layout(ow, oh int) (int, int) {
	return ow, oh
}

結果はこのようになります。

ね、簡単でしょう?

中央揃え、右揃え

op.PrimaryAlign で書字方向の、op.SecondaryAlign で改行方向の整列を指定できます。

終端揃えなら終端が、中央揃えなら中央が、指定した座標に来るように描画されます。日本語の横書きで画面右端に描画したいときは op.PrimaryAlign = text.AlignEnd して GeoM.Translate(画面サイズ, 0) とすればいいわけです。大抵はこの挙動で満足できると思いますが、もしテキストサイズを考慮したい場合は、text.Measure 関数を使います。

w, h := text.Measure("Hello, 世界!", fontFace, lineSpacing)

ビットマップフォントの利用

ビットマップフォントを正しく読み込むのは現状難しいです。したがって、bitmapfont パッケージに収録されているフォントを利用するのが今のところ唯一の確実な方法となります。ビットマップフォントへの最新の対応状況が気になる方は Ebitengine Discord Server を覗いてみてください。

bitmapfont パッケージを利用する場合、既に読み込み済みの bitmapfont.Face があるのでとても簡単です。これを text.NewGoXFace 関数に渡せば初期化完了です。あとは同じように描画するだけです。

var fontFace = text.NewGoXFace(bitmapfont.Face)

op := &text.DrawOptions{}
text.Draw(screen, "Hello, 世界!", fontFace, op)

過去コードの移植

text.NewGoXFace 関数は過去コードの移植にも使います。今までテキスト描画するには opentype.NewFace 関数などを使ってフォントフェイスを作っていたはずなので、それをビットマップフォント同様に text.NewGoXFace に渡すだけです。具体的なコードは割愛します。

ちなみに

GoTextFace は描画のたびに作り直しても問題ないですが、GoXFace はそうではありません。考えるのが面倒ならどちらも保存しておいた値を使い回せばいいと思います。

凝ったこと (1) グリフ単位の描画

v2 になる前から可能でしたが、グリフ単位で画像を取り出し、文字送りしたり、それぞれに違う演出を適用するようなことができます。ただしこれは英語や日本語などグリフ同士がバラバラになっても問題ない言語でのみ通用し、アラビア語などでは容易には適用できないことに留意してください。

グリフを取り出すには text.AppendGlyphs 関数を使います。examples/text にも使用例があるので参照してみてください。

// AppendGlyph の第一引数に既に持っている []text.Glyph を渡せば、
// 追記する感じの挙動になるのでいくらか効率的です
for _, glyph := range text.AppendGlyphs(nil, "Hello, 世界!", fontFace, &op.LayoutOptions) {
	op.GeoM.Reset()
	op.GeoM.Translate(glyph.X, glyph.Y)
	screen.DrawImage(glyph.Image, &op.DrawImageOptions)
}

実のところ、text.Draw も中では AppendGlyph して各グリフを描画しているだけです。

凝ったこと (2) ベクター(パス)変換

複雑な変形をしたり、滑らかにズームするにはベクター(パス)に変換するのがよいでしょう。ベクターに変換するには text.AppendVectorPath 関数を使います。こちらも examples/fontvector に使用例があるので参照してみてください。

フォントをベクターに変換する処理も、そのベクターを描画する処理もそこそこ負荷のある処理なので、多用は避けたほうがよいでしょう。あるいは、適切な描画結果のキャッシュを自分で用意するのもよいでしょう。また負荷の他にも、アンチエイリアスが効かないか、効いても微妙な品質になる問題もあります。

細い縁取りなら、古典的な「上下左右に同じ文字を少しずらして描画する」の方が効率がいいかもしれません。

その一方で、縁取りの細かな形状の制御(丸みを帯びるか、角張るか)や、サイズの変更に強いというメリットもあります。時と場合に合わせて使い分けましょう。

凝ったこと (3) フォントのバリエーションや機能

一つのフォントファイルから、さまざまな太さや斜体を生成できる「バリエーション」、リガチャ(合字)など組版的な機能を提供する「機能 (feature)」も利用できます。フォントによってサポート状況は異なるので、あらかじめ調べておきましょう。

凝ったこと (4) シェーダーを使う

これ以上に凝ったエフェクト、たとえばガウスぼかしの要領で影や綺麗な縁取りを描画するなども、シェーダーを使えばなんでも実現できるでしょう。もちろん、負荷には注意する必要はありますが、極めて見るのも面白いかもしれません。

まとめ

というわけで、駆け足で Ebitengine の text/v2 の機能を紹介してまいりましたが、いかがでしょうか。正直もっと魅力的なサンプルをご用意したかったのですが、筆者の開発力が足りませんでした!

text/v2 はまだリリースされたばかりの新たなパッケージです。旧 text パッケージでは解決が難しい問題を解決し、新たな基盤として生まれましたが、今後もまだまだ新機能が追加される可能性はあります。これからの Ebitengine にも乞うご期待!

脚注
  1. フォントはふぉんとうに難しい ↩︎

  2. GoText とは、ベースになっている github.com/go-text/typesetting パッケージを意味します。同様に、GoX とは golang.org/x/image/font を意味します。 ↩︎

Discussion