はじめてのBlenderアドオン開発
Last Update: 2023.3.1
Blender 2.7
はじめてのBlenderアドオン開発
Blender 2.7
Last Update: 2023.3.1
3-3. タイマのイベントを扱う
3-1節 と 3-2節 では、マウスやキーボードといった、ユーザからの入力イベントを扱う方法を説明しました。 イベントを発生させる手段としては、ある時間が経過したときにイベントを発生させるタイマを設定する方法があります。 一定間隔で処理を実行するアドオンを作るためには、本節で説明する、タイマを使ったイベント処理を理解する必要があります。
タイマのイベントを扱う方法を理解するため、定期的にイベントが発生することを利用した次の機能を持つアドオンを作成します。
1-5節 を参考にして以下のソースコードを入力し、ファイル名を sample_3_3.py
として保存してください。
import bpy
from bpy.props import BoolProperty, PointerProperty
from mathutils import Vector
import math
bl_info = {
"name": "サンプル3-3: メッシュ型のオブジェクトを一定間隔で動かす",
"author": "Nutti",
"version": (2, 0),
"blender": (2, 75, 0),
"location": "3Dビュー > プロパティパネル > 一定間隔でオブジェクトを移動",
"description": "選択中のメッシュ型オブジェクトが一定間隔ごとに円を描くように移動するアドオン",
"warning": "",
"support": "TESTING",
"wiki_url": "",
"tracker_url": "",
"category": "Object"
}
# プロパティ
class MOI_Properties(bpy.types.PropertyGroup):
running = BoolProperty(
name="一定間隔でオブジェクト移動中",
description="一定間隔でオブジェクト移動中か?",
default=False
)
# オブジェクト移動の処理
class MoveObjectInterval(bpy.types.Operator):
bl_idname = "object.move_object_interval"
bl_label = "一定間隔でオブジェクトを移動"
bl_description = "一定間隔でオブジェクトを移動します"
def __init__(self):
self.__timer = None # タイマのハンドラ
self.__count = 0.0 # タイマイベントが発生した回数
self.__orig_obj_loc = {} # 初期のオブジェクトの位置
def __handle_add(self, context):
if self.__timer is None:
# タイマを登録
self.__timer = context.window_manager.event_timer_add(
0.1, context.window)
# モーダルモードへの移行
context.window_manager.modal_handler_add(self)
def __handle_remove(self, context):
if self.__timer is not None:
# タイマの登録を解除
context.window_manager.event_timer_remove(self.__timer)
self.__timer = None
# オブジェクトの位置を更新
def __update_object_location(self, context):
self.__count = self.__count + 1
radius = 5.0 # 回転半径
angular_velocity = 3.0 # 角速度
angle = angular_velocity * self.__count * math.pi / 180
for obj, loc in self.__orig_obj_loc.items():
obj.location = loc + Vector(
(radius * math.sin(angle), radius * math.cos(angle), 0.0)
)
def modal(self, context, event):
props = context.scene.moi_props
# タイマイベント以外の場合は無視
if event.type != 'TIMER':
return {'PASS_THROUGH'}
# 3Dビューの画面を更新
if context.area:
context.area.tag_redraw()
# オブジェクトの移動を停止
if props.running is False:
self.__handle_remove(context)
# オブジェクトを初期の位置に移動する
for obj, loc in self.__orig_obj_loc.items():
obj.location = loc
return {'FINISHED'}
# オブジェクトの位置を更新
self.__update_object_location(context)
return {'PASS_THROUGH'}
def invoke(self, context, event):
props = context.scene.moi_props
if context.area.type == 'VIEW_3D':
# 開始ボタンが押された時の処理
if props.running is False:
self.__orig_obj_loc = {
obj: obj.location.copy()
for obj in bpy.data.objects
if obj.type == 'MESH' and obj.select
}
props.running = True
self.__handle_add(context)
print("サンプル3-3: 一定間隔でオブジェクトが移動するようになります。")
return {'RUNNING_MODAL'}
# 終了ボタンが押された時の処理
else:
props.running = False
print("サンプル3-3: 一定間隔でオブジェクトが移動しなくなります。")
return {'FINISHED'}
else:
return {'CANCELLED'}
# UI
class OBJECT_PT_MOI(bpy.types.Panel):
bl_label = "一定間隔でオブジェクトを移動"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@classmethod
def poll(cls, context):
objs = [
obj
for obj in bpy.data.objects
if obj.type == 'MESH' and obj.select and obj.mode == 'OBJECT'
]
if len(objs) == 0:
return False
return True
def draw(self, context):
sc = context.scene
layout = self.layout
props = sc.moi_props
# 開始/停止ボタンを追加
if props.running is False:
layout.operator(
MoveObjectInterval.bl_idname, text="開始", icon="PLAY"
)
else:
layout.operator(
MoveObjectInterval.bl_idname, text="終了", icon="PAUSE"
)
# プロパティの作成
def init_props():
sc = bpy.types.Scene
sc.moi_props = PointerProperty(
name="プロパティ",
description="本アドオンで利用するプロパティ一覧",
type=MOI_Properties
)
# プロパティの削除
def clear_props():
sc = bpy.types.Scene
del sc.moi_props
def register():
bpy.utils.register_module(__name__)
init_props()
print("サンプル3-3: アドオン「サンプル3-3」が有効化されました。")
def unregister():
clear_props()
bpy.utils.unregister_module(__name__)
print("サンプル3-3: アドオン「サンプル3-3」が無効化されました。")
if __name__ == "__main__":
register()
1-5節 を参考に、作成したアドオンを有効化するとコンソールウィンドウに以下の文字列が出力されます。
サンプル3-3: アドオン「サンプル3-3」が有効化されました。
プロパティパネルを表示し、項目 [一定間隔でオブジェクトを移動] が追加されていることを確認します。
有効化したアドオンの機能を使い、動作を確認します。
1 | [3Dビュー] エリアのプロパティパネルの項目 [一定間隔でオブジェクトを移動] に配置されている [開始] ボタンを押します。 |
2 | 選択中のオブジェクトが約0.1秒ごとに、開始ボタンを押したときにオブジェクトが配置されていた位置を中心として、円を描くように移動します。 |
3 | [終了] ボタンを押すとオブジェクトが移動しなくなり、[開始] ボタンを押したときの位置にオブジェクトが移動します。 |
1-5節 を参考にして有効化したアドオンを無効化すると、コンソールウィンドウに以下の文字列が出力されます。
サンプル3-3: アドオン「サンプル3-3」が無効化されました。
本節では、タイマイベントを扱う処理と作業時間計測の処理に絞り、サンプルのソースコードを解説します。 これまでに説明してきた内容については説明を省いています。 処理がわからなくなってしまった時は、ソースコード中のコメントやこれまでの説明を参考にしてください。 本節のサンプルのソースコードに関して、ポイントとなる点を次に示します。
本節では、オブジェクトを一定間隔で移動するモードをモーダルモードと書いている部分があります。 以降、モーダルモードと書かれていたら、オブジェクトを一定間隔で移動するモードとして読み進めても問題ありません。
本節のサンプルでも、複数のクラス間でデータを共有します。 サンプルで定義しているプロパティ一覧を次に示します。
プロパティ | 意味 |
---|---|
running |
オブジェクトを一定間隔で移動するモード中のときに、値が True となる |
タイマイベントを発生させるためには、タイマを登録する必要があります。 タイマの登録処理は、次に示す __handle_add
メソッドで行います。
def __handle_add(self, context):
if self.__timer is None:
# タイマを登録
self.__timer = context.window_manager.event_timer_add(
0.1, context.window)
# モーダルモードへの移行
context.window_manager.modal_handler_add(self)
タイマは、context.window_manager.event_timer_add
関数を呼び出すことで登録することができます。 context.window_manager.event_timer_add
関数は次に示す引数を受け取り、戻り値としてタイマのハンドラを返します。
引数 | 値の意味 |
---|---|
第1引数 | タイマイベントを発生させる間隔を秒単位で指定 |
第2引数 | タイマの登録先ウィンドウ |
本節のサンプルでは第1引数に 0.1
を指定することで、タイマによるイベントを0.1秒ごとに発生させます。 作業時間の測定を開始した時に押したボタンが存在するウィンドウでタイマイベントを発生させたいため、第2引数には context.window
を指定します。
戻り値として返されたハンドラはタイマの登録を解除するときに使用するため、インスタンス変数 __timer
に保存します。
最後にモーダルモードへ移行しますが、必ずしも __handle_add
メソッド内で行う必要はありません。 __handle_add
メソッド自体が invoke
メソッドから呼び出されているため、3-1節 や 3-2節 と同様に、invoke
メソッドの処理内で context.window_manager.modal_handler_add
関数を呼んでモーダルモードへ移行しても問題ありません。
タイマを登録すると、タイマの登録を解除するまでタイマイベントが送られてきます。 このため、タイマが不要になったら登録を解除する必要があります。
タイマの登録解除処理は、次に示す __handle_remove
メソッドで行っています。
def __handle_remove(self, context):
if self.__timer is not None:
# タイマの登録を解除
context.window_manager.event_timer_remove(self.__timer)
self.__timer = None
タイマは context.window_manager.event_timer_remove
関数を呼び出すことで登録解除できますが、引数には context.window_manager.modal_handler_add
関数の戻り値として返されたタイマのハンドラを渡す必要があります。 本節のサンプルでは、タイマのハンドラを保存したインスタンス変数 __timer
を引数に渡し、タイマの登録を解除します。
登録解除済のタイマのハンドラにアクセスすることによる不正な動作を避けるために、タイマのハンドラを保存するインスタンス変数 __timer
に None
を代入します。
タイマイベントが発生すると、modal
メソッドが呼ばれます。
3-1節 や 3-2節 と同様に modal
メソッドの最初で、[3Dビュー] エリアの画面更新と modal
メソッドの終了判定処理を行います。
# タイマイベント以外の場合は無視
if event.type != 'TIMER':
return {'PASS_THROUGH'}
3-1節 や 3-2節 で説明したように、modal
メソッドはキーボードやマウスのイベントが発生したときにも呼ばれます。 このためタイマイベントが発生したときのみオブジェクトを移動するようにしないと、キーボードやマウスの入力イベントが発生するたびにオブジェクトが移動してしまいます。 そこで発生したイベントがタイマイベントではないときに {'PASS_THROUGH'}
を返すことで、マウスやキーボードからのイベントが発生したときにオブジェクトが移動しないようにします。
# オブジェクトの移動を停止
if props.running is False:
self.__handle_remove(context)
# オブジェクトを初期の位置に移動する
for obj, loc in self.__orig_obj_loc.items():
obj.location = loc
return {'FINISHED'}
続いて、[終了] ボタンが押されたときにモーダルモードを終了する処理を実行します。 [終了] ボタンが押されると、invoke
メソッドの処理内で props.running
が False
に設定されます。 props.running
に False
が設定されていた場合は、__handle_remove
メソッドを呼び出してタイマを登録解除したあとにオブジェクトを初期位置に移動し、{'FINISHED'}
を返してモーダルモードを終了します。
オブジェクトを初期位置に移動するために、メンバ変数 __orig_obj_loc
に保存された初期位置を使っています。 メンバ変数 __orig_obj_loc
にオブジェクトの初期位置を保存する処理については、invoke
メソッドの処理で説明します。
最後に、__update_object_location
メソッドを呼び出してオブジェクトの位置を更新します。 __update_object_location
メソッド内で行なっている処理については、次に説明します。
タイマイベントが発生したときにオブジェクトの位置を更新する処理は、__update_object_location
メソッドで行います。
# オブジェクトの位置を更新
def __update_object_location(self, context):
self.__count = self.__count + 1
radius = 5.0 # 回転半径
angular_velocity = 3.0 # 角速度
angle = angular_velocity * self.__count * math.pi / 180
for obj, loc in self.__orig_obj_loc.items():
obj.location = loc + Vector(
(radius * math.sin(angle), radius * math.cos(angle), 0.0)
)
オブジェクトの初期位置はインスタンス変数 __orig_obj_loc
に保存されているため、オブジェクトの初期位置に移動先の位置を相対座標として加えることで、オブジェクトの位置を更新します。 オブジェクトの位置は obj.location
から参照・変更することができます。
更新するオブジェクトの位置(x, y, z)=(X, Y, Z)は、初期位置を(x, y, z)=(ix, iy, iz)、半径r、回転角度aとして次の計算式で求めます。
(X, Y, Z) = (ix + r * sin(a), iy + r * cos(a), iz)
本節のサンプルでは、タイマイベントが発生して __update_object_location
メソッドが呼び出されたときに、インスタンス変数 __count
をカウントアップします。 回転角a(ソースコード上の変数 angle
)がインスタンス変数 __count
の値が増えるに従って増加するため、初期位置を中心としてオブジェクトの位置が円を描くように回転するように移動します。
本節のサンプルでは、オブジェクトが初期位置を中心として半径(ソースコード上の変数 radius
)5.0
、角速度(ソースコード上の変数 angular_velocity
)3.0
で回転します。
# 開始ボタンが押された時の処理
if props.running is False:
self.__orig_obj_loc = {
obj: obj.location.copy()
for obj in bpy.data.objects
if obj.type == 'MESH' and obj.select
}
オブジェクトの初期位置は、invoke
メソッドの開始ボタンが押されたとき(props.running
が False
のとき)の処理の中で、インスタンス変数である __orig_obj_loc
にオブジェクトをキーとして保存します。
本節のサンプルでは、選択中のメッシュ型のオブジェクトを移動の対象としているため、移動対象のオブジェクトを選別した上でオブジェクトの初期位置を保存する必要があります。
オブジェクトが選択中であることは、obj.select
に True
が代入されていることで判断できます。 また、オブジェクトの型は変数 obj.type
で判断できます。 本節のサンプルでは、メッシュ型のオブジェクトであることを確認したいため、メッシュ型のオブジェクトであることを確認するためのコードは obj.type == 'MESH'
となります。 オブジェクトの型の一覧を次に示します。
型 | 意味 |
---|---|
MESH |
メッシュ |
CURVE |
カーブ |
SURFACE |
サーフェス |
META |
メタオブジェクト |
FONT |
テキストオブジェクト |
ARMATURE |
アーマチュア |
LATTICE |
ラティス |
EMPTY |
空のオブジェクト |
CAMERA |
カメラ |
LAMP |
ランプ |
SPEAKER |
スピーカー |
obj.location.copy
のように、copy
メソッドを用いて位置情報を示すVectorオブジェクトのコピーを作っています。 これは、Vector
オブジェクトのコピーを作らないと、__orig_obj_loc
はオブジェクトの位置情報の「参照」を持ち続けることになってしまうからです。 obj.location
への参照を持つということは、__update_object_location
メソッドで obj.location
を更新されたときに、更新された位置情報を持つことと同じことになるため、__orig_obj_loc
を利用するオブジェクトの位置更新処理が正しく動作しません。 このように、BlenderのAPIを変数に代入する場合は、参照コピーなのか実体コピーなのかを気をつけて実装する必要があります。
最後に、__update_object_location
メソッドを呼び出してオブジェクトの位置を更新します。
[3Dビュー] エリアのプロパティパネルに追加した項目 [一定間隔でオブジェクトを移動] は、2-8節 で説明した poll
クラスメソッドで、項目を表示する条件を絞っています。 本節のサンプルでは、最低でも1つのメッシュ型のオブジェクトが選択され、かつオブジェクトモードの時に項目を表示します。 オブジェクトの型がメッシュ型かつ選択された状態であるかを判定する方法は先ほど説明した通り、obj.type
が MESH
かつ obj.select
が True
の場合です。 そして、現在のオブジェクトが [オブジェクトモード] と [エディットモード] のどちらの状態にあるのかは obj.mode
により取得することができることから、先ほどの項目の表示条件を満たしたことを判定するコードは次のようになります。
@classmethod
def poll(cls, context):
objs = [
obj
for obj in bpy.data.objects
if obj.type == 'MESH' and obj.select and obj.mode == 'OBJECT'
]
if len(objs) == 0:
return False
return True
項目を表示する条件を満たすオブジェクトが存在しない場合、poll
クラスメソッドは False
を返すことで項目を表示しないようにします。
なお、obj.mode
には次のような値が設定され、オブジェクトが現在どのようなモードであるかを確認するときに利用することができます。
値 | モード |
---|---|
OBJECT |
オブジェクトモード |
EDIT |
エディットモード |
SCULPT |
スカルプトモード |
VERTEX_PAINT |
頂点ペイント |
WEIGHT_PAINT |
ウェイトペイント |
TEXTURE_PAINT |
テクスチャペイント |
PARTICLE_EDIT |
パーティクル編集 |
POSE |
ポーズモード |
bpy.context.mode
を参照して 'OBJECT'
であることを確かめる方法もあります。 また、オブジェクトモード時のみプロパティパネルに項目を表示したい場合は、2-8節 で示したパネルクラスのクラス変数bl_contextにobjectmodeを指定することでも実現可能です。
本節では、タイマのイベントを扱う方法を説明しました。 タイマを使うと指定した間隔でイベントを発生させることができるため、定期的に処理を実行するような機能を実現することができます。
3-1節 から本節まで3節にわたってイベントを扱う処理を説明しました。 イベントを扱う場合は、いずれの場合においても modal
メソッドや invoke
メソッドを実装する必要があるという点では同じです。
context.window_manager.event_timer_add
関数で行い、不要になったタイマは context.window_manager.event_timer_remove
関数で登録を解除するcontext.window_manager.modal_handler_add
の引数に指定したインスタンスの modal
メソッドが呼び出され、引数 event
のメンバ変数 event.type
に TIMER
が設定される