タイマのイベントを扱う
3-1節と 3-2節では、マウスやキーボードといった、ユーザからの入力イベントを扱う方法を説明しました。イベントを発生させる手段としては、ある時間が経過したときにイベントを発生させるタイマを設定する方法があります。一定間隔で処理を実行するアドオンを作るためには、本節で説明する、タイマを使ったイベント処理を理解する必要があります。
作成するアドオンの仕様
タイマのイベントを扱う方法を理解するため、定期的にイベントが発生することを利用した次の機能を持つアドオンを作成します。
- 3Dビュー エリアのプロパティパネルの項目 一定間隔でオブジェクトを移動 に、オブジェクトを一定間隔で移動するモードを開始または終了するためのボタンを配置する
- プロパティパネルの項目 一定間隔でオブジェクトを移動 は、オブジェクトモードかつ選択中のオブジェクトがメッシュ型の場合のみ表示する
- オブジェクトを一定間隔で移動するモードでは、開始ボタンを押した時に選択していたオブジェクトの位置を中心として、オブジェクトが一定間隔で円を描くように移動する
アドオンを作成する
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」が有効化されました。
プロパティパネルを表示し、項目 一定間隔でオブジェクトを移動 が追加されていることを確認します。 | ![]() |
---|---|
アドオンの機能を使用する
有効化したアドオンの機能を使い、動作を確認します。
Work
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メソッド
タイマイベントが発生すると、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
で回転します。
invokeメソッド
# 開始ボタンが押された時の処理
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
が設定される