リファレンス / 3ds Max / 3ds Max Python チートシート

コールバック・イベント

  • 3dsMax
  • Python
  • pymxs
  • チートシート
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 を使う。

関連

← 3ds Max Python チートシート へ