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

Last Update: 2023.3.1

Blender 2.8~3.0

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

Blender 2.8~3.0

Last Update: 2023.3.1

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

3-4節 では、gpuモジュールを利用して図形を描画する方法を説明しました。 しかし、gpuモジュールには、テキストを描画するためのAPIを用意していません。 このため、テキストを描画するためには、本節で紹介するblfモジュールを利用する必要があります。

作成するアドオンの仕様

blfモジュールを使ってテキストを描画する方法を理解するために、次の機能を備えるアドオンを作成します。

アドオンを作成する

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

import datetime

import bpy
import blf


bl_info = {
    "name": "サンプル 3-5: 日時を表示するアドオン②",
    "author": "ぬっち(Nutti)",
    "version": (3, 0),
    "blender": (2, 80, 0),
    "location": "3Dビューポート > Sidebar > サンプル 3-5",
    "description": "現在の日時を表示するアドオン",
    "warning": "",
    "support": "TESTING",
    "doc_url": "",
    "tracker_url": "",
    "category": "3D View"
}


# リージョン情報の取得
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


# 日時を表示するオペレータ
class SAMPLE35_OT_ShowDatetime(bpy.types.Operator):

    bl_idname = "object.sample35_show_datetime"
    bl_label = "日時を表示"
    bl_description = "日時を表示します"

    # 描画ハンドラ
    __handle = None

    @classmethod
    def is_running(cls):
        # 描画中はTrue
        return True if cls.__handle else False

    @classmethod
    def __handle_add(cls, context):
        if not cls.is_running():
            # 描画関数の登録
            cls.__handle = bpy.types.SpaceView3D.draw_handler_add(
                cls.__draw, (context, ), 'WINDOW', 'POST_PIXEL'
            )

    @classmethod
    def __handle_remove(cls, context):
        if cls.is_running():
            # 描画関数の登録を解除
            bpy.types.SpaceView3D.draw_handler_remove(
                cls.__handle, 'WINDOW'
            )
            cls.__handle = None

    @classmethod
    def __draw(cls, context):
        # リージョンの幅を取得するため、描画先のリージョンを得る
        region = get_region(context, 'VIEW_3D', 'WINDOW')

        # 描画先のリージョンへテキストを描画
        #   注意:本来ならタイマを併用して再描画しないと日時が更新されないが、
        #         説明簡略化のためにタイマを利用していない。
        #         このため表示の不自然さを避けるため、時間以下の情報を表示していない。
        if region is not None:
            blf.color(0, 1.0, 1.0, 1.0, 1.0)
            blf.size(0, 30, 72)
            blf.position(0, 100.0, region.height - 120.0, 0)
            date_str = datetime.datetime.now().strftime("%Y.%m.%d")
            blf.draw(0, date_str)

    def invoke(self, context, event):
        op_cls = SAMPLE35_OT_ShowDatetime

        if context.area.type == 'VIEW_3D':
            # [開始] ボタンが押された時の処理
            if not op_cls.is_running():
                self.__handle_add(context)
                print("サンプル 3-5: 日時の表示処理を開始しました。")
            # [終了] ボタンが押された時の処理
            else:
                self.__handle_remove(context)
                print("サンプル 3-5: 日時の表示処理を終了しました。")
            # エリアを再描画
            if context.area:
                context.area.tag_redraw()
            return {'FINISHED'}
        else:
            return {'CANCELLED'}


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

    bl_label = "日時を表示"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "サンプル 3-5"
    bl_context = "objectmode"

    def draw(self, context):
        op_cls = SAMPLE35_OT_ShowDatetime

        layout = self.layout
        # [開始] / [停止] ボタンを追加
        if not op_cls.is_running():
            layout.operator(op_cls.bl_idname, text="開始", icon="PLAY")
        else:
            layout.operator(op_cls.bl_idname, text="終了", icon="PAUSE")


classes = [
    SAMPLE35_OT_ShowDatetime,
    SAMPLE35_PT_ShowDatetime,
]


def register():
    for c in classes:
        bpy.utils.register_class(c)
    print("サンプル 3-5: アドオン『サンプル 3-5』が有効化されました。")


def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)
    print("サンプル 3-5: アドオン『サンプル 3-5』が無効化されました。")


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

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

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

[3Dビューポート] スペースのSidebarを表示し、パネル [サンプル 3-5] > [日時を表示] が追加されていることを確認します。

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

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

1 [3Dビューポート] スペースのSidebarに配置されたパネル [サンプル 3-5] > [日時を表示] より、[開始] ボタンをクリックします。
2 [3Dビューポート] スペースに、年月日のテキストが表示されます。
3 パネル [サンプル 3-5] > [日時を表示] に配置された [終了] ボタンをクリックすると、テキストが描画されなくなります。

アドオンを無効化する

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

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

ソースコードの解説

本節のサンプルアドオンのソースコードを見ると、描画関数の登録をはじめとして 3-4節 のサンプルアドオンのソースコードと似ています。 図形を描画する処理とテキストを描画する処理が異なることを除けば、gpuモジュールを使った図形描画とほぼ同じ手順で、blfモジュールを使ってテキストを描画できることがわかります。 そこで本節では、blfモジュールによるテキストの描画処理を中心に説明し、重複する部分については説明を省略します。

blfモジュールをインポートする

テキストを描画するために利用するAPIは、blfモジュールをインポートすることによって利用できるようになります。

import blf

描画関数を登録する

3-4節 のサンプルアドオンと同様、テキストを描画するためには、描画関数を登録する必要があります。 描画関数の登録は、クラスメソッド __handle_add で行っています。 具体的な引数の型については、3-4節 と同じであるため、説明は省略します。

@classmethod
def __handle_add(cls, context):
    if not cls.is_running():
        # 描画関数の登録
        cls.__handle = bpy.types.SpaceView3D.draw_handler_add(
            cls.__draw, (context, ), 'WINDOW', 'POST_PIXEL'
        )

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

描画関数の処理

描画関数である SAMPLE35_OT_ShowDatetime.__draw スタティックメソッドは、描画先のリージョンが更新されるたびに呼ばれます。 SAMPLE35_OT_ShowDatetime.__draw スタティックメソッドは、描画先のリージョン情報を get_region 関数で取得したあと、blfモジュールのAPIを使ってテキストを描画します。

リージョン情報の取得

リージョンの座標値は、リージョンの左下が (x, y) = (0, 0) となり、右方向がX軸正方向、左方向がY軸正方向となります。

本節のサンプルアドオンでは、[ウィンドウ] リージョンの左上の座標にテキストを表示する必要があります。 しかし、リージョンの左上の座標値は環境によって変化するため、単純に数値をそのまま入力して左上にテキストが表示されるように調整しただけでは、リージョンのサイズを変更したときに、テキストを正しい位置に表示できません。 このため、リージョンの左上の座標値を常に取得し、取得した座標値からの差分値を利用します。

さて、[ウィンドウ] リージョンの左上の座標を取得するためには、リージョン情報を取得する必要があります。 本節のサンプルアドオンでは、次の引数を受け取る get_region 関数を呼び出して、リージョン情報を取得します。

引数 型 意味
context bpy.types.Context コンテキスト
area_type str 取得対象のリージョンが属するエリアが持つスペース
region_type str 取得対象のリージョン

引数 area_type や region_type に指定する値は、それぞれ 2-7節 で説明した、パネルクラスのクラス変数 bl_space_type と bl_region_type に指定する値と同じです。

get_region 関数のソースコードを次に示します。

# リージョン情報の取得
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 に保存されているため、get_region 関数の引数に指定したスペースのタイプ area_type と、エリア情報のメンバ変数 type が一致することを確認することで、目的のエリア情報を取得できます。 本節のサンプルアドオンでは、get_region 関数の引数 area_type に 'VIEW_3D' が指定されているため、[3Dビューポート] スペースを持つエリア情報を取得できます。

取得したエリア情報のメンバ変数 regions から、エリアを構成する全てのリージョン情報を取得できます。 エリア情報と同様に、リージョン情報のメンバ変数 type と get_region 関数の引数 region_type が一致すれば、目的のリージョン情報を取得できます。 本節のサンプルアドオンでは、get_region 関数の引数 region_type に 'WINDOW' が指定されているため、[ウィンドウ] リージョンのリージョン情報を取得できます。

描画座標の計算

[ウィンドウ] リージョンのリージョン情報は、get_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) となります。

テキストの描画処理

SAMPLE35_OT_ShowDatetime.__draw 関数では、テキストを描画するために、blfモジュールの関数を4つ呼び出しています。

blf.color(0, 1.0, 1.0, 1.0, 1.0)
blf.size(0, 30, 72)
blf.position(0, 100.0, region.height - 120.0, 0)
date_str = datetime.datetime.now().strftime("%Y.%m.%d")
blf.draw(0, date_str)

1つ目の blf.color 関数は、フォントの色を指定する関数で、次に示す引数を指定します。

引数 型 意味
第1引数 int フォントID(デフォルトのフォントを指定する場合は、0を指定)
第2引数 float フォントの色(赤)
第3引数 float フォントの色(緑)
第3引数 float フォントの色(青)
第3引数 float フォントの色(アルファ)

2つ目の blf.size 関数は、フォントサイズを指定する関数で、次に示す引数を指定します。

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

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

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

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

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

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

3-4節 と同様に、不要になった描画関数は登録解除する必要があります。

@classmethod
def __handle_remove(cls, context):
    if cls.is_running():
        # 描画関数の登録を解除
        bpy.types.SpaceView3D.draw_handler_remove(
            cls.__handle, 'WINDOW'
        )
        cls.__handle = None

まとめ

blfモジュールを用いて、任意のテキストをBlender上に表示する方法を説明しました。 blfモジュールを利用するためには、描画関数の登録など面倒な部分がありますが、Blenderが提供する既存のUIにテキストを表示するよりも、描画方法に自由度があります。

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

ポイント