ChatGPTでBlenderアドオン制作に慣れてきたので、ChatGPTに細かい部分を質問しながら、アドオンの作り方についてまとめてみました。また、自分でアドオンを作ってみました。
文字コード
スクリプトはUTF-8で保存します。
Python2では、スクリプトの文字コードを明示的に指定しないとデフォルトではASCIIとして扱われます。しかし、非ASCII文字(例えば日本語など)を含むスクリプトを書く場合、スクリプトの文字コードを指定する必要があります。# -*- coding: utf-8 -*-は、そのスクリプトの文字コードがUTF-8であることを指定するものです。
しかし、Python3ではスクリプトのデフォルト文字コードはUTF-8になっているため、この指定は基本的に不要になりました。ただし、一部の環境ではデフォルトが変更されている可能性がありますので、移植性を確保するためには明示的に指定しておく方が良いかもしれません。
この行は、Pythonインタプリターではなく、エディター(またはその他のツール)に対する指示でもあります。エディターがこの行を読むことで、ファイルの内容をどの文字コードで読み込むべきかを知ることができます。このため、文字コードが異なる複数のPythonファイルを正しく表示・編集することが可能になります。
fake_module: addon missing ‘bl_info’ gives bad performance!…というエラーが出たときは、冒頭に「# -*- coding: utf-8 -*-」をつけると解決するときがあります。
# -*- coding: utf-8 -*-
bl_info
冒頭にbl_infoを書きます。
bl_info = {
"name": "My Custom Panel", # アドオンの名前
"author": "Author1", # アドオンの作者名
"version": (1, 0), # アドオンのバージョン
"blender": (3, 5, 0), # アドオンが対応するBlenderのバージョン
"location": "View3D > Sidebar > Category", # アドオンの場所や使用法
"description": "This is my custom panel", # アドオンの説明
"warning": "Warning1", # 何か注意事項があれば記述
"doc_url": "https://www.ame-name.com/", # アドオンのドキュメンテーションへのリンク(ある場合)
"category": "Interface", # アドオンのカテゴリ
"tracker_url": "https://www.ame-name.com/archives/15728", # アドオンのバグトラッキングシステムへのURL
}
bl_infoは、Blenderのアドオンに関する情報を提供するための辞書です。Pythonファイルの先頭に配置され、アドオンがBlenderに読み込まれたときに使用されます。この辞書には、アドオンの名前、バージョン、作者、サポートレベル、カテゴリ、説明、警告(必要な場合)、Blenderのバージョンの互換性など、さまざまな情報が含まれます。
アドオンをインストールして、アドオンの情報を見るとアドオン名や、説明、locationなどが表示されています。
nameはアドオンの名前で、Add-onsのリストに表示されます。
authorはアドオンの作者名、descriptionはアドオンの説明、versionはアドオンのバージョン、warningは注意事項で、全てアドオンの詳細に表示されます。
blenderはスクリプトを実行するのに必要なBlenderの最小バージョンです。
locationでは、アドオンの場所を説明します。
locationを変更しても実際にパネル等の表示位置が変わるわけではないようです。
bl_infoのlocationフィールドは、ユーザーに対してプラグインの位置情報を表示するためのものであり、Blenderに対して特定の機能や場所を指定するものではありません。
具体的には、locationフィールドはユーザーに対してこのアドオンやプラグインがどのようにアクセスでき、どのメニューやパネルから利用できるかを示す情報を提供します。
これはドキュメンテーションの一部と考えることができ、ユーザーがアドオンをどのように使うべきかを理解するのに役立ちます。例えば、”3D View > Tool Shelf > Your Addon Tab”といった情報が含まれることがあります。
「Documentation」ボタンをクリックすると、doc_urlに、「Report a Bug」ボタンを押すと、tracker_urlに指定したURLにとびます。「Remove」ボタンでアドオンが削除されます。
categoryはアドオン名の横に表示されます。
このcategoryフィールドはアドオンの整理とアクセスを容易にするためのもので、似たような機能を持つアドオンが同じカテゴリ内でグループ化されることを可能にします。
プルダウンメニューで指定したカテゴリからアドオンを探せます。
ChatGPTの解説では、doc_urlが古いバージョンの「wiki_url」になっています。
bl_info 辞書には必須のフィールドとオプションのフィールドがあります。以下のフィールドは必須であり、それぞれのアドオンに対して提供する必要があります:
以下のフィールドはオプショナルで、必要に応じて提供することができます:
- “name”: アドオンの名前。
- “author”: アドオンの作者名。
- “version”: アドオンのバージョン番号。通常はタプル形式で、メジャーとマイナーバージョンの両方を含みます。
- “blender”: アドオンが対応するBlenderのバージョン。これもタプル形式で、メジャー、マイナー、リリース番号の3つを含みます。
以上のように、”name”, “author”, “version”, “blender”は必須のフィールドで、他のフィールドはオプショナルですが、アドオンの可用性とユーザビリティを高めるためにも詳細を提供することをお勧めします。
- “location”: アドオンの操作場所や使用方法を記述します。
- “description”: アドオンの説明。
- “warning”: ユーザに対する警告メッセージ。例えば、アドオンが他のアドオンと競合する場合や、特定のBlenderバージョンでのみ動作する場合にここに記述します。
- “wiki_url”: アドオンのドキュメンテーションまたはヘルプページへのリンク。
- “category”: アドオンが所属するカテゴリ。このカテゴリはBlenderのアドオン管理画面での分類に利用されます。
bpyモジュール
PythonのAPIを使うには、bpyモジュールをインポートします。
import bpy
オペレーター
bpy.types.Operatorクラスの派生クラスを作り、executeメソッドにアドオンの処理を実装します。複数のオペレーターを定義することもできます。オペレーターでは、他に bl_idname、bl_label属性を定義します。
class DisplayTextOperator(bpy.types.Operator):
bl_idname = "object.display_text"
bl_label = "Display Text"
def execute(self, context):
global text
text = "Hello, world!"
return {'FINISHED'}
bl_idname属性は、このオペレーターの一意の識別子です。パネルにボタンを実装するとき等に指定します。
row = layout.row()
row.operator("object.display_text")
bl_labelの値は、ボタンなどのUIに表示されます。
executeメソッドにボタンを押したときの処理を実装します。
def execute(self, context):
global text
text = "Hello, world!"
return {'FINISHED'}
最後は {‘FINISHED’} を返します。
return {‘FINISHED’}はBlenderのPythonスクリプトにおける標準的な終了状態を表しています。これは、特にオペレーターのexecuteメソッドの最後でよく見られます。
Blenderではオペレータのメソッドが処理の結果を示すセット(実際にはPythonの集合型)を返すことが期待されています。その中で最も一般的なものは次の二つです:
{‘FINISHED’}: これはオペレータのアクションが成功裏に完了したことを示します。これにより、Blenderはオペレータが終了したことを知り、適切に後処理(たとえばUIの更新)を行います。
{‘CANCELLED’}: これはオペレータのアクションが何らかの理由で完了できなかったことを示します。これを返すと、Blenderはオペレータの終了を知り、処理を中止します。
これらの戻り値は、オペレータのメソッドが終了したときにBlenderに情報を提供するため、オペレータの動作を適切に制御するために重要な役割を果たします。
何も返さないとエラーになりました。
RuntimeError: class OBJECT_OT_display_text, function execute: incompatible return value , , Function.result expected a set, not a NoneType
オペレータのexecute()メソッドが終了した際にreturn {‘FINISHED’}を返さない場合、Blenderはそのオペレータが正常に完了したと認識しません。これが結果として、意図しない挙動を引き起こす可能性があります。
たとえば、オペレータがユーザーに何らかの入力を求める場合(例えばマウスのドラッグや数値の入力等)は、完了を示すためにreturn {‘FINISHED’}が一般的には必要となります。これがないと、Blenderはオペレータがまだ実行中であり、更なる入力を待っていると解釈する可能性があります。
したがって、オペレータが意図したタスクを全て完了し、これ以上の入力を必要としない場合には、return {‘FINISHED’}を返すことが推奨されます。
パネル
今回は、3Dビューポートのサイドバーにパネルを表示し、パネルの中のボタンをクリックすると処理が行われるようにしました。カスタムパネルを作るには、bpy.types.Panelクラスの派生クラスを作り、必要な属性とメソッドを定義します。
class HelloWorldPanel(bpy.types.Panel):
bl_label = "Hello World Panel"
bl_idname = "VIEW3D_PT_hello_world"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Category"
def draw(self, context):
global text
layout = self.layout
row = layout.row()
row.label(text=text)
row = layout.row()
row.operator("object.display_text")
row = layout.row()
row.operator("object.clear_text")
bl_labelはパネルの上部に表示されます。
bl_idnameは、オペレーターのものと同様に重複しないようにします。
オペレーターのbl_idnameの冒頭のobjectは、そのオペレーターが主にどのような対象に作用するか、というカテゴリーを示しています。これはオペレーターのカテゴライゼーションを助け、またBlender内での名前衝突を避ける役割も果たします。
オペレーターを作成する際、bl_idnameの冒頭部分(.の前の部分)は自由に設定できます。しかし、Blender内で定義されている標準的なカテゴリーを使用することが多いです。これには以下のようなものがあります:
これらはいくつかの例であり、他にも多数のカテゴリーがあります。カテゴリーはBlender内部で使用されているものを用いることもできますし、独自のカテゴリー名を作成することも可能です。ただし、Blender内での名前衝突を避けるために、標準的なカテゴリー名を使用することが推奨されます。また、一般的にカテゴリー名とオペレーター名は小文字とアンダースコアを使用したスネークケースで記述するのが一般的です。
- object:オブジェクトに対する操作を表します(例:object.deleteはオブジェクトの
- 削除を表します)。
- mesh:メッシュデータに対する操作を表します。
- uv:UVマッピングに対する操作を表します。
- scene:シーン全体に対する操作を表します。
- material:マテリアルに対する操作を表します。
bl_idnameの値に「_PT_」を含めないと警告されます。
Warning: ‘hello_world_panel’ does not contain ‘_PT_’ with prefix and suffixbl_space_type、bl_region_type属性でパネルの表示位置を指定します。
bl_region_typeは、Blenderのインターフェースの特定の領域(Region)にパネルを配置するための属性です。これはPanelクラスの定義の一部として必要です。
一部の領域タイプには次のようなものがあります:
あるパネルが表示される具体的な場所は、bl_space_typeとbl_region_typeの組み合わせによって決定されます。たとえば、3Dビューポートの右側のプロパティシェルフにパネルを配置したい場合は、bl_space_type = ‘VIEW_3D’とbl_region_type = ‘UI’と設定します。
- ‘TOOLS’: 3Dビューポートの左側のツールシェルフ
- ‘UI’: 3Dビューポートの右側のプロパティシェルフまたは他のエディタのプロパティパネル
- ‘WINDOW’: 主要なウィンドウの領域(通常は3Dビューポート自体)
bl_categoryはタブに表示されます。
同じ名前のタブがあればそこにまとめられます。
bl_categoryを指定しない場合「Misc」と表示されます。
drawメソッドに、パネルに表示するテキストやボタンを実装します。
def draw(self, context):
global text
layout = self.layout
row = layout.row()
row.label(text=text)
row = layout.row()
row.operator("object.display_text")
row = layout.row()
row.operator("object.clear_text")
UILayout.rowメソッドでサブレイアウト(UILayout)が返ります。labelメソッドで引数のテキストを表示できます。
operatorメソッドの引数にオペレーターのbl_idnameを渡すことで、クリックするとそのオペレーターのexecuteメソッドが実行されるボタンを表示できます。
UILayout.rowを呼ぶたびにサブレイアウトが縦に並び、そこにUI要素を追加できるようです。同じサブレイアウトに複数のUI要素を追加すると横に並びます。
def draw(self, context):
global text
layout = self.layout
row = layout.row()
row.label(text=text)
row = layout.row()
row.operator("object.display_text")
row.operator("object.clear_text")
row = layout.row()
row.operator("object.clear_text")
UILayoutのrow()メソッドは新たな行(水平のレイアウト)を作成し、その行に対する操作が可能になります。そして、その行に追加されるUI要素は左から右に向かって並べられます。
したがって、row()メソッドを複数回呼び出すと、それぞれの呼び出しごとに新たな行が作成され、それぞれの行に異なるUI要素を追加することができます。これにより、パネルのレイアウトを縦に積み重ねることが可能になります。
一方、同じ行(サブレイアウト)に対して複数のUI要素(例えば、複数のボタンやラベルなど)を追加すると、それらの要素はその行内で左から右へと並べられます。このように、水平方向に複数のUI要素を配置することも可能です。
横に並べたボタンの間にスペースを開けたいときは、row.separatorメソッドを使います。行の間にスペースを開けたい場合は、UILayout.rowを連続して呼びますが、行が細いときは空文字のラベルを表示します。
def draw(self, context):
global text
layout = self.layout
row = layout.row()
row.label(text=text)
row = layout.row()
row.operator("object.display_text")
row.separator()
row.operator("object.clear_text")
row = layout.row()
row.label(text="")
row = layout.row()
row.operator("object.clear_text")
UIレイアウトを制御するためのオプションは限られています。残念ながら、BlenderのPython APIでは行の高さを直接制御する方法は提供されていません。
しかし、スペースを作りたいときによく使われるワークアラウンドとして、行にダミーのラベルを追加し、そのラベルに空文字列を設定するという方法があります。これにより、見た目上のスペースが作られます。具体的なコードは以下の通りです。
—
ただし、これはあくまでワークアラウンドなので、適切なスペースを作るための本質的な解決策ではないことを理解しておいてください。
パネルをプロパティに表示する
bl_space_typeを「PROPERTIES」、bl_region_typeを「WINDOW」にすると、パネルをプロパティに表示できます。bl_contextでどのタブに表示するかを選択できます。
# Object Propertiesに表示
class HelloWorldPanel(bpy.types.Panel):
bl_label = "Hello World Panel"
bl_idname = "VIEW3D_PT_hello_world"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
bl_contextを「material」にするとMaterial Propertiesに、「data」にするとObject Data Propertiesに表示されます。
bl_contextを指定しないと、すべてのタブに表示されました。
プロパティに表示する場合、bl_categoryの値は無視されます。
しかし、これは3DビューポートなどのUIのサイドバー(’VIEW_3D’ space_typeと ‘UI’ region_type)においてのみ有効で、’PROPERTIES’ space_typeでパネルを表示する場合(例:物体、マテリアル、テクスチャプロパティなど)、この属性は無視されます。その場合、表示されるタブ(コンテキスト)は bl_context 属性で決定されます。
オペレーターとパネルを登録
オペレーターやパネルはbpy.utils.register_class/unregister_classメソッドで、Blenderのシステムに登録/解除する必要があります。引数にはクラス名を渡しています。
def register():
bpy.utils.register_class(DisplayTextOperator)
bpy.utils.register_class(HelloWorldPanel)
bpy.utils.register_class(ClearTextOperator)
def unregister():
bpy.utils.unregister_class(DisplayTextOperator)
bpy.utils.unregister_class(HelloWorldPanel)
bpy.utils.unregister_class(ClearTextOperator)
Blenderのアドオンやカスタムスクリプトの中で定義されたクラス(オペレーターやパネルなど)を、Blenderの本体システムに登録(レジスター)または削除(アンレジスター)するためには、register()関数とunregister()関数を使用します。
このプロセスは、Blenderがあなたのクラスを認識し、それを利用可能なツールとしてシステムに追加するために必要です。register()関数が呼ばれると、クラスはシステムに登録され、それによって各種機能が有効化されます(例えば、オペレーターがbpy.opsモジュールで使用可能になったり、パネルが指定されたUIスペースに表示されるようになります)。一方、unregister()関数は、その逆のプロセスを実行します。
—-
登録とアンレジスターのプロセスは、特にアドオンの開発や動的なスクリプトの実行において重要です。これにより、ユーザーがアドオンを有効化/無効化したときや、スクリプトが更新されたときに、その変更が正しく反映されます。
スクリプト
# -*- coding: utf-8 -*-
bl_info = {
"name": "My Custom Panel", # アドオンの名前
"author": "Author1", # アドオンの作者名
"version": (1, 0), # アドオンのバージョン
"blender": (3, 5, 0), # アドオンが対応するBlenderのバージョン
"location": "View3D > Sidebar > Category", # アドオンの場所や使用法
"description": "This is my custom panel", # アドオンの説明
"warning": "Warning1", # 何か注意事項があれば記述
"doc_url": "https://www.ame-name.com/", # アドオンのドキュメンテーションへのリンク(ある場合)
"category": "Interface", # アドオンのカテゴリ
"tracker_url": "https://www.ame-name.com/archives/15728", # アドオンのバグトラッキングシステムへのURL
}
import bpy
text = ""
class DisplayTextOperator(bpy.types.Operator):
bl_idname = "object.display_text"
bl_label = "Display Text"
def execute(self, context):
global text
text = "Hello, world!"
return {'FINISHED'}
class ClearTextOperator(bpy.types.Operator):
bl_idname = "object.clear_text"
bl_label = "Clear Text"
def execute(self, context):
global text
text = ""
return {'FINISHED'}
class HelloWorldPanel(bpy.types.Panel):
bl_label = "Hello World Panel"
bl_idname = "VIEW3D_PT_hello_world"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Category"
def draw(self, context):
global text
layout = self.layout
row = layout.row()
row.label(text=text)
row = layout.row()
row.operator("object.display_text")
row = layout.row()
row.operator("object.clear_text")
def register():
bpy.utils.register_class(DisplayTextOperator)
bpy.utils.register_class(HelloWorldPanel)
bpy.utils.register_class(ClearTextOperator)
def unregister():
bpy.utils.unregister_class(DisplayTextOperator)
bpy.utils.unregister_class(HelloWorldPanel)
bpy.utils.unregister_class(ClearTextOperator)