めもてう

忘れっぽいTIPs、HowToのメモ帳です。

【Blender 2.8 アドオン開発】005 UI - Property編 -

前回Blender へ機能を追加しました。
ですが、追加した機能は実行しかできません。

もちろん、スクリプトをリロードするなど、実行だけできればいい機能もあります。
ですが、おそらくアドオンを作ろうとした場合、指定した座標(入力)へオブジェクトを移動(機能)というように機能は入力とセットだとおもいます。

そこで、今回から追加した機能への入力と UI( User Interface )について説明できればとおもいます。

はじめに

Blender 上で UI、特にボタンや数値入力に代表される GUI は半自動的に生成、描画されます。
この、半自動というのがネックで、他のプログラミングで GUI を実装したことのある人には慣れが必要だと個人的にはおもっています。
そして、 Blender アドオン上の UI にかかせないのがbpy.props下に定義されている Propertyクラスです。

このPropertyクラスは Blenderアドオンを開発する上でいくつかの役割を担うことになります。
その役割は大きく分けて

  1. 値の保持
  2. UI の素
  3. API の引数
  4. 型の拡張(カスタムプロパティー

です。

今回、型の拡張については触れません。またいずれ説明できればと思います。

サンプルスクリプト

今回は下のサンプルスクリプトを拡張しながら説明していきたいとおもいます。

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」を検索して実行してください。

f:id:Hobbyist:20190502190025p:plain

3D View 左下に「PropertySample」と書かれたポップアップが出てきたとおもいます。
閉じている場合は、「PropertySample」の左にある三角ボタンで開閉できます。

* 何も書かれていないポップアップが出た場合は、もう一度「F3」キーを押して「PropertySample」を実行してみてください。

サンプルスクリプト内で宣言した

#--- properties ---#
val_x: FloatProperty()

が、Float値を入力できる UI として表示されていて、数値の入力とマウスによるドラッグによる入力ができることを確認してください。

次に、Pythonコンソールにbpy.ops.tutorial.propertysample(と入力して、「Ctrl + スペース」を押してください。
すると、API の説明が出たとおもいます。

f:id:Hobbyist:20190502190022p:plain

ここで注目してほしいのが、引数です。
今回の例でいうと、オペレータ内に宣言された Propertyクラスが API の引数になっていることが確認できます。

Propertyの宣言

Propertyクラスはbpy.propsに定義されています。
ですので、bpy.props.FloatPropertyとフルパスで宣言するか、今回のようにインポートしてやる必要があります。

クラス内に Propertyクラスを宣言する場合は、=(イコール)ではなく:(コロン)を使用します。
=(イコール)でも動きますが、Warning が出るので、:(コロン)で宣言しましょう。

また、Propertyクラスは宣言するときにいくつか引数を指定することが出来ます。
引数は Propertyクラスの種類によって指定できる種類や数が異なります。

公式リファレンス - FloatProperty

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 の値に連動して選択されたオブジェクトが動けば成功です。

f:id:Hobbyist:20190504210502g:plain

しかし、この動作はよく考えると不自然です。

オブジェクトが動くということはexecuteが実行されているということです。
と、いうことは、オブジェクトの X座標が0の時に1と入力した場合、オブジェクトの X座標は1に移動します。
次に2と入力した場合、オブジェクトの X座標は3になるはずです。
しかし、X座標は2になります。

これは、

  1. 数値に1を設定
  2. オペレーターが引数val_x = 1で実行され、オブジェクトの X座標が1になる
  3. 数値を2に設定
  4. アンドゥされてオブジェクトを初期位置に戻す
  5. オペレーターが引数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ぐらいしか使ったことがないのでわかりません。

公式リファレンス - Operator#bl_options

Propertyの初期化

Property の大きな役割に「値の保持」があると先に書きましたが、この値の保持が不都合なことがあります。
今回のサンプルがまさにそうです。

UI からオブジェクトを動かし、プロパティポップアップを閉じて確定した後、もう一度 API を実行した場合に前回の値を保持しているのでまずオブジェクトが動きます。
もちろん値を0にすると2回目の初期位置に戻るのですが、手間です。

f:id:Hobbyist:20190507121952g:plain

どこかのタイミングで保持している値を初期化してやる必要があります。

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」サーチメニューから実行してみましょう。

f:id:Hobbyist:20190507121920g:plain

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()

ここで記載されているソースコードを使用する場合は自己責任でお願いします。

プライバシーポリシー (Privacy policy)