【Blender 2.8 アドオン開発】005 UI - Property編 -
前回、Blender へ機能を追加しました。
ですが、追加した機能は実行しかできません。
もちろん、スクリプトをリロードするなど、実行だけできればいい機能もあります。
ですが、おそらくアドオンを作ろうとした場合、指定した座標(入力)へオブジェクトを移動(機能)というように機能は入力とセットだとおもいます。
そこで、今回から追加した機能への入力と UI( User Interface )について説明できればとおもいます。
はじめに
Blender 上で UI、特にボタンや数値入力に代表される GUI は半自動的に生成、描画されます。
この、半自動というのがネックで、他のプログラミングで GUI を実装したことのある人には慣れが必要だと個人的にはおもっています。
そして、 Blender アドオン上の UI にかかせないのがbpy.props
下に定義されている Propertyクラスです。
このPropertyクラスは Blenderアドオンを開発する上でいくつかの役割を担うことになります。
その役割は大きく分けて
です。
今回、型の拡張については触れません。またいずれ説明できればと思います。
サンプルスクリプト
今回は下のサンプルスクリプトを拡張しながら説明していきたいとおもいます。
import bpy from bpy.props import * # # TUTORIAL_OT_PropertySample # class TUTORIAL_OT_PropertySample(bpy.types.Operator): bl_idname = "tutorial.propertysample" bl_label = "PropertySample" bl_options = {'REGISTER', 'UNDO'} #--- properties ---# val_x: FloatProperty() #--- execute ---# def execute(self, context): objects = [o for o in context.selected_objects if o.type == 'MESH'] for o in objects: print(o.location) return {'FINISHED'} # # register # def register(): bpy.utils.register_class(TUTORIAL_OT_PropertySample) # # unregister # def unregister(): bpy.utils.unregister_class(TUTORIAL_OT_PropertySample) # # Script Entry # if __name__ == "__main__": register()
第3回で説明していないbl_options = {'REGISTER', 'UNDO'}
が宣言してありますが、これについては後述します。
objects = [o for o in context.selected_objects if o.type == 'MESH']
は Python のリスト内記法で、
objects = [] for o in context.selected_objects: if o.type == 'MESH': objects.append(o)
と、同等です。便利ですよねー。
では、このスクリプトを「Run Script」から実行して Blender へ機能を登録してください。
次に、3D View 上にマウスポインタがある状態で「F3」キーを押して「PropertySample」を検索して実行してください。
3D View 左下に「PropertySample」と書かれたポップアップが出てきたとおもいます。
閉じている場合は、「PropertySample」の左にある三角ボタンで開閉できます。
* 何も書かれていないポップアップが出た場合は、もう一度「F3」キーを押して「PropertySample」を実行してみてください。
サンプルスクリプト内で宣言した
#--- properties ---#
val_x: FloatProperty()
が、Float値を入力できる UI として表示されていて、数値の入力とマウスによるドラッグによる入力ができることを確認してください。
次に、Pythonコンソールにbpy.ops.tutorial.propertysample(
と入力して、「Ctrl + スペース」を押してください。
すると、API の説明が出たとおもいます。
ここで注目してほしいのが、引数です。
今回の例でいうと、オペレータ内に宣言された Propertyクラスが API の引数になっていることが確認できます。
Propertyの宣言
Propertyクラスはbpy.props
に定義されています。
ですので、bpy.props.FloatProperty
とフルパスで宣言するか、今回のようにインポートしてやる必要があります。
クラス内に Propertyクラスを宣言する場合は、=(イコール)
ではなく、:(コロン)
を使用します。
=(イコール)
でも動きますが、Warning が出るので、:(コロン)
で宣言しましょう。
また、Propertyクラスは宣言するときにいくつか引数を指定することが出来ます。
引数は Propertyクラスの種類によって指定できる種類や数が異なります。
Propertyの使用
クラス内に宣言された Propertyクラスは通常のメンバ変数と同じようにアクセスできます。
execute
関数を以下のように変えてください。
#--- execute ---# def execute(self, context): objects = [o for o in context.selected_objects if o.type == 'MESH'] for o in objects: print(o.location) o.location.x += self.val_x # added return {'FINISHED'}
内容は、選択されているオブジェクトの X座標へval_x
の値を加算しているだけです。
では、もう一度「Run Script」で登録して API を実行してみましょう。
UI の値に連動して選択されたオブジェクトが動けば成功です。
しかし、この動作はよく考えると不自然です。
オブジェクトが動くということはexecute
が実行されているということです。
と、いうことは、オブジェクトの X座標が0の時に1と入力した場合、オブジェクトの X座標は1に移動します。
次に2と入力した場合、オブジェクトの X座標は3になるはずです。
しかし、X座標は2になります。
これは、
- 数値に1を設定
- オペレーターが引数
val_x = 1
で実行され、オブジェクトの X座標が1になる - 数値を2に設定
- アンドゥされてオブジェクトを初期位置に戻す
- オペレーターが引数
val_x = 2
で実行され、オブジェクトの X座標が2になる
このようにアンドゥと実行が繰り返し行われているからです。
ここで少しbl_options
を説明します。
このbl_options
は文字通りオプションを複数設定できます。
bl_options
を宣言しない場合はデフォルトでbl_options = {'REGISTER'}
となります。
今回の例では、
オプション | 説明 |
---|---|
REGISTER | Infoエリアへオペレターを表示、リピート・ヒストリーへオペレーターが追加される |
UNDO | アンドゥ・ヒストリーへオペレーターが追加される |
という、オプションを設定しています。
この2つのオプションを設定することでオペレーター単位のアンドゥ・リドゥ(リピート)が動作することになります。
また、この2つのオプションを設定していないとプロパティポップアップは開きません。
そして、プロパティポップアップが閉じた段階で、確定となりオペレータがアンドゥ・リドゥ(リピート)のヒストリーも確定されることになります。
これにより、UI とのスムーズなやり取りと、確定した動作へのアンドゥ・リドゥ・リピートを実現しています。
ここまで説明しておいてアレですが、プロパティポップアップを出したい場合はbl_options = {'REGISTER', 'UNDO'}
を脊髄反射で宣言しておけば大丈夫です。
また、bl_options
に設定できるオプションは他にもありますが、「F3」で開くサーチメニューに表示しないINTERNAL
ぐらいしか使ったことがないのでわかりません。
Propertyの初期化
Property の大きな役割に「値の保持」があると先に書きましたが、この値の保持が不都合なことがあります。
今回のサンプルがまさにそうです。
UI からオブジェクトを動かし、プロパティポップアップを閉じて確定した後、もう一度 API を実行した場合に前回の値を保持しているのでまずオブジェクトが動きます。
もちろん値を0にすると2回目の初期位置に戻るのですが、手間です。
どこかのタイミングで保持している値を初期化してやる必要があります。
bpy.types.Operator
クラスにはすでにそのためのinvoke
という関数が用意してあり、サブクラスでこの関数を定義してプロパティの初期化をすることが定型となっています。
#--- properties ---# val_x: FloatProperty() #--- invoke ---# added def invoke(self, context, event): print("invoke") self.val_x = 0.0 return self.execute(context) #--- execute ---#
このinvoke
関数はオペレーターが実行された瞬間呼ばれます。
self | 自身のインスタンス(レシーバー) |
context | execute と同様、現在の状態を保持した Context |
event | オペレーターが実行された時のマウスの座標位置など様々なパラメーターを保持。 今回は使用しません。 |
return | execute と同様に状態結果を定義されている列挙体で返します。ここで return {'FINISHED'} とすると、execute 関数は実行されません。execute を直後に実行させたい場合は今回のサンプルのように明示的に呼んでやる必要があります。 |
では、「Run Script」して、「F3」サーチメニューから実行してみましょう。
2度目の実行でオブジェクトが勝手に動かなければ成功です。
コンソールウィンドウを確認してもらえればわかりますが、invoke
関数が呼ばれるのは、サーチメニューの「SampleProperty」をクリックしたときだけで、開いたプロパティポップアップの UI を操作しても呼ばれません。
このことから、invoke
関数が初期化に適しているのが解ります。
まとめ
これで Property の説明はおわりです。本当にさわりの部分しか説明していません。
ですが、極力 Tips 的な要素を排除したのでファーストタッチとしてはいいのではないでしょうか?
とはいえ、具体的な使用例も今後 Tips として書いていけたらなぁと、おもいます。
次回は UI のキモとなるレイアウトと Panel クラスについて説明できればとおもいます。
では、最後に完成したサンプルスクリプトで〆させてもらいます。
import bpy from bpy.props import * # # TUTORIAL_OT_PropertySample # class TUTORIAL_OT_PropertySample(bpy.types.Operator): bl_idname = "tutorial.propertysample" bl_label = "PropertySample" bl_options = {'REGISTER', 'UNDO'} #--- properties ---# val_x: FloatProperty() #--- invoke ---# def invoke(self, context, event): self.val_x = 0.0 print("invoke") return self.execute(context) #--- execute ---# def execute(self, context): objects = [o for o in context.selected_objects if o.type == 'MESH'] for o in objects: print(o.location) o.location.x += self.val_x return {'FINISHED'} # # register # def register(): bpy.utils.register_class(TUTORIAL_OT_PropertySample) # # unregister # def unregister(): bpy.utils.unregister_class(TUTORIAL_OT_PropertySample) # # Script Entry # if __name__ == "__main__": register()
ここで記載されているソースコードを使用する場合は自己責任でお願いします。