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

Last Update: 2023.3.1

Blender 2.8~3.0

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

Blender 2.8~3.0

Last Update: 2023.3.1

2-6. アドオンのソースコードを
複数ファイルへ分割する

アドオンへ機能を追加し続けると、やがてソースコードのサイズが肥大化していきます。 ソースコードが肥大化することで管理が大変になってきたら、ソースコードを複数ファイルに分割することを検討しましょう。 本節では、アドオンのソースコードを複数のファイルへ分割する方法を紹介します。

ソースコードを複数ファイルに分割するメリット

前節までに紹介してきたサンプルアドオンのソースコードは、単一のファイルで構成されていました。 これまでのサンプルアドオンは、非常に単純な機能しか持たないアドオンであったため、ソースコードの行数も比較的少なく、単一のファイルでも問題なく扱うことができました。 しかし、本格的なアドオンは、本書で紹介しているアドオンよりも複雑な処理になる傾向があります。 複雑で規模が大きい処理の全てを単一のファイルに書くと、あとでソースコードを修正したいときに修正箇所を見つけるのが大変です。

一般的に物事を分割して考えることは、複雑なことを理解するときの助けになります。 アドオンの開発においても同じことが言え、ソースコードを複数のファイルに分割することで管理しやすくなります。 例えば、あとからソースコードを修正するときに、単一のファイルで構成されているとファイル全体を見る必要があるのに対し、ファイルが適切に分割されていれば、関係のないファイルのソースコードを見る必要がなくなるため、目的の修正箇所を早く見つけることができます。

作成するアドオンの仕様

本節では、次に示すアドオンを作成します。

アドオンを作成する

1-2節 で説明したBlenderアドオン用フォルダに、ディレクトリ名 sample_2-6 のディレクトリを作成します。 1-5節 を参考にして次のソースコードを入力し、作成したディレクトリの下にファイル名をそれぞれ __init__.py , forward_object.py , backward_object.py として保存してください。

__init__.py

bl_info = {
    "name": "サンプル 2-6: オブジェクトを並進移動するアドオン⑤",
    "author": "ぬっち(Nutti)",
    "version": (3, 0),
    "blender": (2, 80, 0),
    "location": "3Dビューポート > オブジェクト",
    "description": "アクティブなオブジェクトを並進移動するサンプルアドオン(ファイル分割版)",
    "warning": "",
    "support": "TESTING",
    "doc_url": "",
    "tracker_url": "",
    "category": "Object"
}


if "bpy" in locals():
    import imp
    imp.reload(forward_object)
    imp.reload(backward_object)
else:
    from . import forward_object
    from . import backward_object


import bpy


# メニューを構築する関数
def menu_fn(self, context):
    self.layout.separator()
    self.layout.operator(forward_object.SAMPLE26_OT_ForwardXObject.bl_idname)
    self.layout.operator(backward_object.SAMPLE26_OT_BackwardXObject.bl_idname)


# Blenderに登録するクラス
classes = [
    forward_object.SAMPLE26_OT_ForwardXObject,
    backward_object.SAMPLE26_OT_BackwardXObject,
]


# アドオン有効化時の処理
def register():
    for c in classes:
        bpy.utils.register_class(c)
    bpy.types.VIEW3D_MT_object.append(menu_fn)
    print("サンプル 2-6: アドオン『サンプル 2-6』が有効化されました。")


# アドオン無効化時の処理
def unregister():
    bpy.types.VIEW3D_MT_object.remove(menu_fn)
    for c in classes:
        bpy.utils.unregister_class(c)
    print("サンプル 2-6: アドオン『サンプル 2-6』が無効化されました。")


# メイン処理
if __name__ == "__main__":
    register()

forward_object.py

import bpy


# オブジェクトをX軸正方向に並進移動するオペレータ
class SAMPLE26_OT_ForwardXObject(bpy.types.Operator):

    bl_idname = "object.sample26_forward_x_object"
    bl_label = "X軸正方向へ並進移動"
    bl_description = "アクティブなオブジェクトをX軸正方向へ並進移動します"
    bl_options = {'REGISTER', 'UNDO'}

    # メニューを実行したときに呼ばれるメソッド
    def execute(self, context):
        active_obj = context.active_object
        active_obj.location[0] += 1.0
        self.report({'INFO'}, "サンプル 2-6: 『{}』をX軸正方向へ並進移動しました。".format(active_obj.name))
        print("サンプル 2-6: オペレータ『{}』が実行されました。".format(self.bl_idname))

        return {'FINISHED'}

backward_object.py

import bpy


# オブジェクトをX軸負方向に並進移動するオペレータ
class SAMPLE26_OT_BackwardXObject(bpy.types.Operator):

    bl_idname = "object.sample26_backward_x_object"
    bl_label = "X軸負方向へ並進移動"
    bl_description = "アクティブなオブジェクトをX軸負方向へ並進移動します"
    bl_options = {'REGISTER', 'UNDO'}

    # メニューを実行したときに呼ばれる関数
    def execute(self, context):
        active_obj = context.active_object
        active_obj.location[0] -= 1.0
        self.report({'INFO'}, "サンプル 2-6: 『{}』をX軸負方向へ並進移動しました。".format(active_obj.name))
        print("サンプル 2-6: オペレータ『{}』が実行されました。".format(self.bl_idname))

        return {'FINISHED'}

アドオンを使用する

アドオンを有効化する

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

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

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

アドオンの機能は、2-2節 で紹介したサンプルと同じ機能ですので、ここでは説明を省きます。

アドオンを無効化する

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

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

ソースコードの解説

モジュールとパッケージ

ソースコードの解説に入る前に、Pythonのモジュールとパッケージの概念について説明します。

Pythonでは、クラスや関数、変数が定義された1つのファイルをモジュールといいます。 ソースコード内でモジュールをインポートすることによって、そのモジュールで定義されたクラスや関数、変数が使用可能となります。

一方、複数のモジュールを1つにまとめたものをパッケージといいます。 Pythonでパッケージを作成するためには、ディレクトリを作成し、その配下にファイル __init__.py とパッケージ化するモジュール一式を置きます。 Pythonで一般的にパッケージを作る場合、__init__.py は空のファイルでもよいですが、Blenderでパッケージを作る場合は、後述する特別な処理を定義する必要があります。

モジュール名はスクリプトのファイル名、パッケージ名は __init__.py とモジュール一式をまとめたディレクトリ名となります。 このことを踏まえると、本節のサンプルアドオンにおけるモジュールとパッケージの関係は、「sample_2-6パッケージには、forward_objectモジュールとbackward_objectモジュールが含まれる」となります。

__init__.pyとは

本節で作成したサンプルアドオンのソースコードは、3つのファイルから構成されます。 3つのファイルのうち最も重要なファイルは、__init__.py です。 __init__.py には、アドオン有効化時に実行される処理やモジュールの読み込み処理を定義します。 (本節のサンプルでは、forward_objectモジュールとbackward_objectモジュールを読み込む処理を定義します。)

一般的に __init__.py には、パッケージインポート時に呼び出される処理を定義することになっています。 register 関数や unregister 関数が定義され、これらの関数がアドオンの有効化/無効化時に呼び出されることから、アドオンの有効化処理は、Blender上でのパッケージのインポート処理と同様であると考えることができます。

モジュールのインポート

モジュールのインポートは、bpyモジュールを読み込むときと同様、import 指示により行います。

import forward_object
import backward_object

しかし、上記のコードを実行すると、アドオン有効化時にforward_objectとbackward_objectが見つからないというエラーが発生します。 これは、Blenderがforward_objectモジュールとbackward_objectモジュールの場所を知らないからです。

Blenderに対して、forward_objectモジュールとbackward_objectモジュールの場所を知らせるために、次のように書き換えます。

from . import forward_object
from . import backward_object

from . を追加したことにより、ファイル __init__.py が置かれたディレクトリ、つまり自身のパッケージの中から2つのモジュールforward_objectとbackward_objectを探すようになります。 上記のコードで、アドオン有効化時のエラーは出力されなくなりました。 しかし、もう1つやるべきことがあります。

1-4節 では、Blenderの機能である『Reload Scripts』機能を用いて、Blenderを再起動せずにアドオンをアップデートする方法を紹介しました。 しかし、『Reload Scripts』機能を使っても、アドオンをアップデートできない場合があります。

結論から言うと、『Reload Scripts』機能を使ったときに、__init__.py の処理の中で、インポートしているモジュールの再読み込みに失敗していることが原因です。 from . import ... を用いて、モジュールの再読み込みができると思う人もいるかもしれませんが、すでにインポートされているモジュールに対するインポート処理は、無視されてしまうことに注意が必要です。

この問題に対応するため、次のようにインポート処理を書き換える必要があります。

if "bpy" in locals():
    import imp
    imp.reload(forward_object)
    imp.reload(backward_object)
else:
    from . import forward_object
    from . import backward_object


import bpy

上記の処理を簡単に説明します。 最初のif文では、bpyモジュールがすでにインポートされているかを判定します。 インポートされていなければ、Blender起動後に初めてアドオンが読み込まれたと判定し、関連するモジュールをインポートします。

一方、bpyモジュールがすでにインポートされていれば、Blenderが起動してから2回目以降の読み込みであると考えることができます。 このときは、impモジュールを用いて、すでにインポートされているモジュールを再度読み込みます。 2回目以降の読み込みと判定されるのは、『Reload Scripts』機能によりアドオンが再度読み込まれたときに限定されるため、これは正しく動作します。

なお、bpyモジュールのインポート処理の定義は、このif文判定のあとでなければなりません。 仮に、bpyモジュールのインポート処理を、if文判定の前に書いてしまうと、最初のif文の条件式が真と判定されてしまい、期待した動作になりません。

グローバル変数bl_infoの作成

ファイル __init__.py には、グローバル変数 bl_info を定義する必要があります。 bl_info については、2-1節 ですでに説明しているため、ここでは説明を省略します。

アドオン有効化・無効化時の処理

アドオンの有効化・無効化時に呼ばれる register 関数と unregister 関数についても、__init__.py に定義する必要があります。

register 関数と unregister 関数に定義した処理は、2-1節 で説明した内容とほぼ同じですが、1点異なることがあります。

register 関数や unregister 関数の処理内で、bpy.types.VIEW3D_MT_object.append メソッドの引数に渡す menu_fn 関数を見てください。 menu_fn 関数の中で self.layout.operator メソッドが呼ばれているのは同じですが、引数に渡すクラス名の前にモジュール名が追加されています。 bpyモジュールと同様に、forward_objectやbackward_objectもモジュールであるため、モジュール内のクラスや関数などにアクセスするためには、モジュール名を指定してあげる必要があります。

forward_object.py、backward_object.py

forward_object.py には SAMPLE26_OT_ForwardXObject クラス、backward_object.py には SAMPLE26_OT_BackwardXObject クラスが定義されています。 ソースコードの内容は、2-1節 と同じであるため、ここでは説明を省略します。

まとめ

アドオンのソースコードを、複数のファイルに分割する方法を紹介しました。

ソースコードは、規模が大きくなるにつれて管理しにくくなる傾向があるため、ある程度ソースコードの規模が大きくなってきたら、複数のファイルに分割することを検討すべきです。 ソースコードを分割する単位は、行数や機能などさまざまですが、筆者はアドオンを機能単位に分割するように心がけています。 これにより、機能にちなんだファイル名をつけることができるようになります。 また、機能ごとにモジュールが分割されることで、特定の機能を修正したいときに目的のソースコードを素早く見つけることができます。

ポイント