はじめてのBlenderアドオン開発

Last Update: 2023.3.1

Blender 2.7

はじめてのBlenderアドオン開発

Blender 2.7

Last Update: 2023.3.1

3-5. blfモジュールを使ってテキストを描画する

3-4節 ではOpenGLにアクセスするAPIを利用して図形描画を行う方法を説明しましたが、同様にテキストも描画したいと思うかもしれません。 しかし、OpenGLはテキストを描画するためのAPIを用意していないことから、独自のフォント画像を用意してテキストを描画する必要があります。 このため、簡単なテキストを出力したい場合であっても、非常に手間がかかります。 しかし幸いなことに、Blenderはテキストを描画するためのAPIを提供していますので、本節で紹介します。

作成するアドオンの仕様

テキストを描画するためのAPIを使ったテキストの描画方法を理解するために、次の機能を持つアドオンを作成します。

アドオンを作成する

1-5節 を参考にして以下のソースコードを入力し、ファイル名 sample_3_5.py として保存してください。

import bpy
from bpy.props import BoolProperty, PointerProperty
import blf


bl_info = {
    "name": "サンプル3-5: 3Dビューエリアにテキストを描画する",
    "author": "Nutti",
    "version": (2, 0),
    "blender": (2, 75, 0),
    "location": "3Dビュー > プロパティパネル > テキスト描画",
    "description": "3Dビューエリアにテキストを描画するアドオン",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "3D View"
}


# テキスト描画
class RenderText(bpy.types.Operator):

    bl_idname = "view_3d.render_text"
    bl_label = "テキスト描画"
    bl_description = "テキストを描画します"

    __handle = None           # 描画関数ハンドラ

    def __handle_add(self, context):
        if RenderText.__handle is None:
            # 描画関数の登録
            RenderText.__handle = bpy.types.SpaceView3D.draw_handler_add(
                RenderText.__render, (self, context), 'WINDOW', 'POST_PIXEL')

    def __handle_remove(self, context):
        if RenderText.__handle is not None:
            # 描画関数の登録を解除
            bpy.types.SpaceView3D.draw_handler_remove(
                RenderText.__handle, 'WINDOW'
            )
            RenderText.__handle = None

    @staticmethod
    def __render_text(size, x, y, s):
        # フォントサイズを指定
        blf.size(0, size, 72)
        # 描画位置を指定
        blf.position(0, x, y, 0)
        # テキストを描画
        blf.draw(0, s)

    @staticmethod
    def __get_region(context, area_type, region_type):
        region = None
        area = None

        # 指定されたエリアを取得する
        for a in context.screen.areas:
            if a.type == area_type:
                area = a
                break
        else:
            return None
        # 指定されたリージョンを取得する
        for r in area.regions:
            if r.type == region_type:
                region = r
                break

        return region

    @staticmethod
    def __render(self, context):
        # リージョン幅を取得するため、描画先のリージョンを得る
        region = RenderText.__get_region(context, 'VIEW_3D', 'WINDOW')

        # 描画先のリージョンへテキストを描画
        if region is not None:
            # 影の効果を設定
            blf.shadow(0, 3, 0.0, 1.0, 0.0, 0.5)
            # 影の位置を設定
            blf.shadow_offset(0, 2, -2)
            # 影の効果を有効化
            blf.enable(0, blf.SHADOW)
            RenderText.__render_text(
                40, 40, region.height - 120, "Hello Blender world!!"
            )
            # 影の効果を無効化
            blf.disable(0, blf.SHADOW)
            RenderText.__render_text(
                30, 40, region.height - 180, "Suzanne on your lap"
            )

    def invoke(self, context, event):
        sc = context.scene
        if context.area.type == 'VIEW_3D':
            # 開始ボタンが押された時の処理
            if sc.rt_running is False:
                sc.rt_running = True
                self.__handle_add(context)
                print("サンプル3-5: テキストの描画を開始しました。")
            # 終了ボタンが押された時の処理
            else:
                sc.rt_running = False
                self.__handle_remove(context)
                print("サンプル3-5: テキストの描画を終了しました。")
            # 3Dビューの画面を更新
            if context.area:
                context.area.tag_redraw()
            return {'FINISHED'}
        else:
            return {'CANCELLED'}


# UI
class OBJECT_PT_RT(bpy.types.Panel):

    bl_label = "テキスト描画"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        sc = context.scene
        layout = self.layout
        # 開始/停止ボタンを追加
        if sc.rt_running is False:
            layout.operator(RenderText.bl_idname, text="開始", icon="PLAY")
        else:
            layout.operator(RenderText.bl_idname, text="終了", icon="PAUSE")


# プロパティの作成
def init_props():
    sc = bpy.types.Scene
    sc.rt_running = BoolProperty(
        name="実行中",
        description="実行中か?",
        default=False
    )


# プロパティの削除
def clear_props():
    sc = bpy.types.Scene
    del sc.rt_running


def register():
    bpy.utils.register_module(__name__)
    init_props()
    print("サンプル3-5: アドオン「サンプル3-5」が有効化されました。")


def unregister():
    clear_props()
    bpy.utils.unregister_module(__name__)
    print("サンプル3-5: アドオン「サンプル3-5」が無効化されました。")


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

1-5節 を参考にして作成したアドオンを有効化すると、コンソールウィンドウに以下の文字列が出力されます。

サンプル3-5: アドオン「サンプル3-5」が有効化されました。

[3Dビュー] エリアのプロパティパネルを表示し、項目 [テキスト描画] が追加されていることを確認します。

アドオンの機能を使用する

有効化したアドオンの機能を使い、動作を確認します。

1 [3Dビュー] エリアのプロパティパネルの項目 [テキスト描画] に配置されている [開始] ボタンを押します。
2 [3Dビュー] エリアに文字列「Hello Blender world!!」「Suzanne on your lap」が表示されます。
3 プロパティパネルの項目 [テキスト描画] に配置されている [終了] ボタンを押すとテキスト描画処理が終了し、テキストが表示されなくなります。

アドオンを無効化する

1-5節 を参考にして有効化したアドオンを無効化すると、コンソールウィンドウに以下の文字列が出力されます。

サンプル3-5: アドオン「サンプル3-5」が無効化されました。

ソースコードの解説

本節のサンプルを見ると、描画関数の登録をはじめとして 3-4節 のサンプルのソースコードと非常に似ており、テキストを描画する処理を除き、OpenGLを使った図形描画とほぼ同じ手順でテキストを描画できることがわかります。 本節では、テキストの描画処理を中心に説明し、重複する部分については説明を省略します。 サンプルのソースコードに関して、ポイントとなる点は以下のとおりです。

テキスト描画APIを利用する

テキストを描画するためのAPIは、blf と呼ばれるモジュールに含まれています。 このため、blf モジュールをインポートする必要があります。

import blf

アドオン内で利用するプロパティを定義する

複数のクラス間で共有するプロパティを次に示します。 本節のサンプルでは、共有するプロパティが1つであるため bpy.types.PropertyGroup によるプロパティのグループ化を行っていません。

変数 意味
rt_running テキスト描画中の場合は True

描画関数を登録する

3-4節bgl モジュールを使って図形を描画した時と同様、テキストを描画するためには描画関数を登録する必要があります。 描画関数の登録は、__handle_add メソッド内の bpy.types.SpaceView3D.draw_handler_add 関数で行います。 具体的な引数の型については、3-4節 と同じであるため、説明は省略します。

# 描画関数の登録
RenderText.__handle = bpy.types.SpaceView3D.draw_handler_add(
    RenderText.__render, (self, context), 'WINDOW', 'POST_PIXEL')

本節のサンプルでは、描画関数が RenderText.__render スタティックメソッド、描画するリージョンが WINDOW であることから、第1引数に RenderText.__render 、第3引数に WINDOW を指定します。 第2引数には、自身のインスタンスとコンテキスト情報を渡します。

描画関数の処理

描画関数である RenderText.__render スタティックメソッドは、描画先のリージョンが更新されるたびに呼ばれます。RenderText.__render スタティックメソッドは、描画先のリージョン情報を RenderText.__get_region スタティックメソッドで取得した後、RenderText.__render_text スタティックメソッドを使ってテキストを描画します。

リージョンの取得

3-4節 で説明したように、リージョンの座標値は左下が (x, y) = (0, 0) となります。 本節のサンプルでは、[ウィンドウ] リージョンの左上の座標にテキストを表示する必要があります。 しかし、リージョンの左上の座標は環境によって変化するため、単純に数値をそのまま入力して左上にテキストが表示されるように調整しただけでは、リージョンのサイズを変更したときに正しい位置にテキストを表示することができません。 このため左上の座標値を常に取得しておき、取得した座標値からの差分値を指定することで、常にリージョンの左上に表示するようにします。

[ウィンドウ] リージョンの左上の座標を取得するためには、リージョン情報を取得する必要があります。 本節のサンプルでは次の引数を受け取る RenderText.__get_region スタティックメソッドにより、リージョン情報を取得します。

引数 意味
context コンテキスト
area_type 取得するエリア
region_type 取得するリージョン

area_typeregion_type に指定する値は、それぞれ 2-8節 で説明したパネルクラスのクラス変数 bl_space_typebl_region_type に指定したものと同じです。

RenderText.__get_region スタティックメソッドのコードを次に示します。

@staticmethod
def __get_region(context, area_type, region_type):
    region = None
    area = None

    # 指定されたエリアを取得する
    for a in context.screen.areas:
        if a.type == area_type:
            area = a
            break
    else:
        return None
    # 指定されたリージョンを取得する
    for r in area.regions:
        if r.type == region_type:
            region = r
            break

    return region

Blender上で開いている全てのエリア情報は、context.screen.areas に保存されているため、RenderText.__get_region スタティックメソッドの引数に指定したエリアのタイプ area_typearea.type が一致することを確認することで、目的のエリア情報を取得することができます。 本節のサンプルでは、RenderText.__get_region スタティックメソッドの引数 area_type'VIEW_3D' が指定されているため、[3Dビュー] エリアのエリア情報を取得することができます。

取得したエリア情報から area.regions により、エリアを構成する全てのリージョン情報を取得することができます。 エリア情報と同様に、r.typeRenderText.__get_region スタティックメソッドの引数 region_type が一致すれば、目的のリージョン情報を取得することができます。 本節のサンプルでは、RenderText.__get_region スタティックメソッドの引数 region_type'WINDOW' が指定されているため、 [ウィンドウ] リージョンのリージョン情報を取得することができます。

描画座標の計算

[ウィンドウ] リージョンのリージョン情報は、RenderText.__get_region スタティックメソッドの戻り値 region に保存されています。 region からリージョンに関する情報を取得することができ、作業時間を描画する座標値を求めるために利用します。

本節のサンプルで必要となる情報は、[ウィンドウ] リージョンの左上の座標値です。 リージョンの左下の座標値が (x, y) = (0, 0) であることから、左上の座標は (x, y) = (0, リージョンの高さ)となります。 リージョン情報 region はリージョンの高さや幅の情報を持ち、region.height でリージョンの高さを、region.width でリージョンの幅を取得することができます。 このため、リージョンの左上の座標は (x, y) = (0, region.height) で取得できます。 同様に、右上の座標は (x, y) = (region.width, region.height)、右下の座標は (x, y) = (region.width, 0) となります。

テキストの描画処理

テキストの描画処理は、RenderText.__render_text スタティックメソッドで行います。

@staticmethod
def __render_text(size, x, y, s):
    # フォントサイズを指定
    blf.size(0, size, 72)
    # 描画位置を指定
    blf.position(0, x, y, 0)
    # テキストを描画
    blf.draw(0, s)

RenderText.__render_text スタティックメソッドに指定する引数を次に示します。

引数 意味
size フォントサイズ
x 描画座標(X座標)
y 描画座標(Y座標)
s 描画するテキスト

RenderText.__render_text スタティックメソッドでは、テキストを描画するために blf モジュールの関数を3つ使っています。 1つ目の blf.size 関数はフォントサイズを指定する関数で、次に示す引数を指定します。

引数 意味
第1引数 フォントID(デフォルトのフォントを使う場合は、0を指定)
第2引数 フォントサイズ
第3引数 DPI

次に blf.position 関数を使って、テキストを描画する位置を指定します。 blf.position 関数には次に示す引数を指定します。

引数 意味
第1引数 フォントID(デフォルトのフォントを使う場合は、0を指定)
第2引数 描画座標(X座標)
第3引数 描画座標(Y座標)
第4引数 描画座標(Z座標)

最後に、次に示す引数を blf.draw 関数に渡して呼び出し、引数に指定された文字列を描画します。

引数 意味
第1引数 フォントID(デフォルトのフォントを使う場合は、0を指定)
第2引数 描画する文字列
フォントIDは、Blenderに読み込まれているフォントの識別子で、デフォルトのフォントには0が割り当てられています。 デフォルトのフォント以外のフォントに変えたい場合は、blf.load 関数を使ってフォントを読み込み、フォントIDに blf.load の戻り値を指定することで、読み込んだフォントを使ってテキストを描画することができます。

描画関数である RenderText.__render スタティックメソッドは、RenderText.__get_region スタティックメソッドで取得したリージョン情報から計算した描画座標を引数に指定して、RenderText.__render_text スタティックメソッドを呼びます。

ここまでの処理でテキストを描画することができますが、本節のサンプルでは blf モジュールのフォント装飾機能を使ってテキストに少し飾りつけを行っています。

# 描画先のリージョンへテキストを描画
if region is not None:
    # 影の効果を設定
    blf.shadow(0, 3, 0.0, 1.0, 0.0, 0.5)
    # 影の位置を設定
    blf.shadow_offset(0, 2, -2)
    # 影の効果を有効化
    blf.enable(0, blf.SHADOW)
    RenderText.__render_text(
        40, 40, region.height - 120, "Hello Blender world!!"
    )
    # 影の効果を無効化
    blf.disable(0, blf.SHADOW)
    RenderText.__render_text(
        30, 40, region.height - 180, "Suzanne on your lap"
    )

テキスト "Hello Blender world!!" は、blf モジュールのフォント装飾機能を使って強調表示しています。 blf.shadow 関数は描画するテキストに影の効果を加えるための関数で、以下の引数を与えることで好みの影を加えることができます。 本節のサンプルでは、影の色が緑色でアルファ成分が0.5、影の大きさが3の影を加えています。

引数 意味
第1引数 フォントID(デフォルトのフォントを使う場合は、0を指定)
第2引数 影の大きさ
第3引数 影の色(赤成分)
第4引数 影の色(緑成分)
第5引数 影の色(青成分)
第6引数 影の色(アルファ値)

blf.shadow_offset 関数を用いると、影の表示位置を変更することができます。 本節のサンプルでは、テキストの描画位置から右に2ピクセル、下に2ピクセルずらして影を表示するように設定します。

引数 意味
第1引数 フォントID(デフォルトのフォントを使う場合は、0を指定)
第2引数 テキスト本体からのオフセットピクセル数(X軸)
第3引数 テキスト本体からのオフセットピクセル数(Y軸)
blf.shadow_offset 関数の第2引数は画面右方向がX軸正方向、第3引数は画面上方向が正方向です。

ここまでの処理で、影の大きさや色、表示位置を設定しましたが、このままでは影が有効化されません。 影を有効化するためには、blf.enable 関数の引数に blf.SHADOW を指定して呼び出す必要があります。 また、影を有効化した場合は描画関数終了前に無効化することを忘れないでください。 影を無効化する場合は blf.disable 関数の引数に blf.SHADOW を指定して呼び出します。

blf.enable 関数は影の描画を有効化する時以外に、テキストの回転やテキストの一部分を切り出して表示する効果を有効化する時にも使います。 blf.enable に指定可能な値を以下に示します。

効果
ROTATION 回転
CLIPPING 切り抜き
SHADOW

テキストの回転の効果では、blf.rotation 関数に次の引数を指定することで、回転量を調整できます。

効果
第1引数 フォントID(デフォルトのフォントを使う場合は、0 を指定)
第2引数 回転量(ラジアン)

テキストの切り抜き効果では、blf.clipping 関数に以下の引数を指定することで切り抜き領域を指定できます。 なお、切り抜き領域を示す座標値には、リージョンの左下を (x, y) = (0, 0) とした座標を指定することに注意してください。

効果
第1引数 フォントID(デフォルトのフォントを使う場合は、0を指定)
第2引数 切り抜く領域のX座標の最小値
第3引数 切り抜く領域のY座標の最小値
第4引数 切り抜く領域のX座標の最大値
第5引数 切り抜く領域のY座標の最大値

影の有効化のところでも書きましたが、blf.enable を有効化した効果は、描画関数を終える前に必ず無効化してください。 無効化していない状態で描画関数を終えてしまうと、blf.enable で設定した効果が、Blender内のUI上に表示されているすべてのテキストに対して適用されてしまいます。

描画関数内では、bgl モジュールと blf モジュールが提供するAPIを同時に使用することができます。 このため、描画関数を複数登録する必要はありません。

描画関数の登録を解除する

3-4節 と同様に、描画処理を停止するときに登録した描画関数の登録を解除する必要があります。

if RenderText.__handle is not None:
    # 描画関数の登録を解除
    bpy.types.SpaceView3D.draw_handler_remove(
        RenderText.__handle, 'WINDOW'
    )
    RenderText.__handle = None

まとめ

Blenderが提供するテキスト描画モジュール blf を用いて、任意のテキストをBlender上に表示する方法を説明しました。 blf モジュールを利用するためには、描画関数の登録やテキストに適用した効果の管理など面倒な部分がありますが、Blenderが提供する既存のUIに表示するよりも、描画方法に自由度があります。 また bgl モジュールを使ってテキストを描画するよりも、比較的簡単にテキストを描画することができます。

Blenderのアドオンの中には bgl モジュールと組み合わせることで少し変わったUIを構築しているアドオンがあるため、いろいろなアドオンを使ってソースコードを見ながら、使い方を学んでいくとよいと思います。 例えば、クリックしたマウスのボタンや押したキーボードのキーを表示するアドオン『Screencast Keys』は、bgl モジュールや blf モジュールを使っているため、bgl モジュールや blf モジュールの使い方を学ぶための取っ掛かりとしてよい参考資料になると思います。

ポイント