コールバック・イベント
3ds Max Python チートシート の章一覧
「ファイルを開いたら」「ノードが増えたら」「時間が変わったら」のように、3ds Max の出来事に合わせて処理を走らせる 仕組みです。
登録関数の多くは Python の関数をそのまま受け取れます(callbacks.addScript は 3ds Max 2021 以降です)。どの仕組みでも 登録したら必ず解除できるようにしておく のが鉄則です。
一般コールバック(ファイル・ノード作成・レンダリングなど)
MY_ID = rt.Name("myTool") # 自分のコールバックの目印(id)
created = []
def on_node_created():
node = rt.callbacks.notificationParam() # 作られたノード(MXS: callbacks.notificationParam())
created.append(node)
rt.callbacks.removeScripts(id=MY_ID) # 二重登録しないよう、先に同じ id を消す
rt.callbacks.addScript(rt.Name("nodeCreated"), on_node_created, id=MY_ID)
# MXS: callbacks.addScript #nodeCreated onNodeCreated id:#myTool
rt.Box(name="NewBox") # → on_node_created が呼ばれる
callbacks.addScriptに Python の関数を渡せるのは 3ds Max 2021 以降 です。それより前(2020 以前)は、まずrt.execute("global myToolOnCreated")で MaxScript 側にグローバルを宣言してからrt.myToolOnCreated = on_node_createdで関数を入れ、rt.callbacks.addScript(rt.Name("nodeCreated"), "myToolOnCreated()", id=MY_ID)のように文字列で登録します。- 何が起きたかの詳細(作られたノード、開いたファイル名など)は、コールバックの中で
rt.callbacks.notificationParam()を呼ぶと取れます。中身はイベントごとに違います。 - ⚠️ id を付けずに登録すると、あとで個別に消せません。 必ず id を付け、登録の前に
removeScripts(id=...)で消してから登録します(スクリプトを何度実行しても 1 つだけになる)。
よく使うイベント
イベント(rt.Name で渡す) |
いつ呼ばれるか |
|---|---|
filePostOpen / filePreSave / filePostSave |
シーンを開いた後 / 保存の前 / 保存の後 |
systemPostReset / systemPostNew |
リセット・新規作成の後 |
nodeCreated / nodePreDelete |
ノードが作られた / 削除される直前 |
selectionSetChanged |
選択が変わった |
preRender / postRender |
レンダリングの前 / 後 |
postSystemStartup |
3ds Max の起動が終わった |
登録を確かめる / 解除する
MY_ID = rt.Name("myTool")
def on_open():
print("opened:", rt.maxFileName)
rt.callbacks.removeScripts(id=MY_ID) # 二重登録しないよう、先に同じ id を消す
rt.callbacks.addScript(rt.Name("filePostOpen"), on_open, id=MY_ID)
registered = rt.callbacks.show(id=MY_ID, asArray=True) # 登録内容を配列で受け取る
rt.callbacks.removeScripts(id=MY_ID) # id が同じものをすべて解除
# 上と同じことを、イベントも指定してやりたい場合は次のようにします(どちらか一方でよい)
rt.callbacks.removeScripts(rt.Name("filePostOpen"), id=MY_ID)
callbacks.showはふつうリスナー(標準出力)に書き出しますが、この出力は pymxs では無効になっています。asArray=Trueで配列として受け取るか、to=にStringStreamを渡して出力を捕まえます。
ノードイベントコールバック(変更をまとめて受け取る)
def on_changed(event, handles):
nodes = [rt.getAnimByHandle(h) for h in handles] # ハンドル → ノード
print(str(event), [n.name for n in nodes if n is not None])
# 出力例: #nameChanged [#Box001]
my_node_events = rt.NodeEventCallback(
mouseUp=True, # マウスを離すまで待ってから通知する
delay=500, # 最後の変更から 500 ミリ秒待ってまとめて通知する
nameChanged=on_changed,
selectionChanged=on_changed,
)
# MXS: myNodeEvents = NodeEventCallback mouseUp:true delay:500 nameChanged:onChanged selectionChanged:onChanged
my_node_events = None # 解除: 参照を捨てて
rt.gc(light=True) # ガベージコレクションを走らせる(MXS: gc light:true)
NodeEventCallbackは、ノードの変更(名前・選択・形状・マテリアル・表示など)を まとめて(バッチで) 受け取る仕組みです。一般コールバックの上に作られた仕組みで、ジオメトリやトポロジの変更、Undo/Redo による変化まで拾えるので、UI の表示をノードの状態に追従させるような用途に向いています。- 関数には「イベント名」(
rt.Nameの値、例:#nameChanged)と「ノードのハンドル(整数の AnimHandle)の配列」が渡されます。ノードに戻すにはrt.getAnimByHandleを使います。 - ⚠️ 参照を持ち続けないと、ガベージコレクションのタイミングで勝手に解除されて止まってしまいます。グローバル変数やクラスの属性に持っておき、確実に解除したいときは変数を
Noneにしてからrt.gc(light=True)を呼びます。登録済みの一覧はrt.getNodeEventCallbacks()(3ds Max 2019.3 以降)で確認できます。 - 一時的に止めたいだけなら、作成時に
enabled=Falseを付けると止まった状態で作れます。
時間(タイムスライダ)が変わったとき
frames = []
def on_time_changed():
frames.append(rt.currentTime.frame) # 引数は無い。今の時間は currentTime で読む
rt.registerTimeCallback(on_time_changed) # MXS: registerTimeCallback onTimeChanged
rt.sliderTime = 10
rt.sliderTime = 20
rt.unRegisterTimeCallback(on_time_changed) # 解除には同じ関数を渡す
- 登録した関数は、タイムスライダを動かすたび・再生中は毎フレーム呼ばれます。重い処理を入れると再生がカクつく ので注意します。
- ⚠️ コールバックの実行中にエラーが起きると、そのコールバックは永久にオフになります。レンダリング中は呼ばれません。シーンをリセットしても登録は残ります。
- 同じように、ビューポートの再描画のたびに呼ぶ
rt.registerRedrawViewsCallback(fn)/rt.unRegisterRedrawViewsCallback(fn)もあります。
ノードの変化を見張る(when 構文)
MaxScript の when 構文は pymxs に対応するものが無いので、rt.execute で登録します。
rt.myToolWatched = rt.getNodeByName("Box001")
moved = []
rt.myToolOnMoved = lambda: moved.append(1) # Python の関数を MaxScript のグローバルに
rt.execute("when transform myToolWatched changes id:#myToolWhen do myToolOnMoved()")
rt.myToolWatched.pos = rt.Point3(10, 0, 0) # → myToolOnMoved が呼ばれる
rt.deleteAllChangeHandlers(id=rt.Name("myToolWhen")) # MXS: deleteAllChangeHandlers id:#myToolWhen
whenは特定のノードのtransform/geometry/name/deletedなどの変化を見張れます。id:を付けておくと、deleteAllChangeHandlers(id=...)でまとめて解除できます。- ⚠️ ハンドラの実行中にエラーが起きると、そのハンドラは永久に使えなくなります。見張っているオブジェクトが全部削除されると、ハンドラも自動的に消えます。シーンには保存されず、アニメーションの再生による変化では発火しません(ユーザの操作による変化のときだけ発火します)。
一定間隔で処理を走らせる(Qt のタイマー)
from PySide2 import QtCore
class MyTicker(object):
def __init__(self):
self.count = 0
self.timer = QtCore.QTimer()
self.timer.setInterval(1000) # 1000 ミリ秒ごと
self.timer.timeout.connect(self.tick)
def tick(self):
self.count += 1
rt.displayTempPrompt("tick {}".format(self.count), 1000) # ステータスバーに一時表示
my_ticker = MyTicker() # 参照を持っておく(消えるとタイマーも止まる)
my_ticker.timer.start()
# ... あとで止める
my_ticker.timer.stop()
- 3ds Max の UI は Qt で動いています。
QTimerのtimeoutシグナルはメインスレッドで呼ばれるので、tickの中でrtを安全に呼べます。 - ⚠️ Python の
threadingで作った別スレッドからrtを直接呼ぶことはできません(例外になります)。重い処理を別スレッドで行い、結果の反映はタイマーやメインスレッドで行います。別スレッドからどうしてもrtにアクセスしたい場合のためにpymxs.mxstoken()という関数もありますが、本書では扱いません。
落とし穴まとめ
- 一般コールバックは必ず id を付け、登録前に
removeScripts(id=...)する。 NodeEventCallbackは変数をNoneにしてrt.gc(light=True)で解除。参照を失くさない。- 時間コールバックに重い処理を入れない。
rtは別スレッドから呼べない。タイマーはメインスレッドで動くQTimerを使う。
関連
- MaxScript のグローバルに Python の関数を入れる: 「MaxScript との連携」章
- UI をイベントに追従させる: 「Qt UI」章
コメント