モジュール管理・デバッグ
3ds Max Python チートシート の章一覧
ツールが大きくなってきたときの ファイルの分け方・読み込み方 と、知らない API を調べる方法・エラーの扱い方 です。
自作モジュールを import する(sys.path)
import os
import sys
tool_dir = os.path.join(rt.getDir(rt.Name("temp")), "my_tool_pkg") # 例: ツールを置くフォルダ
os.makedirs(tool_dir, exist_ok=True)
with open(os.path.join(tool_dir, "my_tool_util.py"), "w", encoding="utf-8") as f:
f.write("def hello(name):\n return 'Hello, ' + name\n") # 例のモジュールを作る
if tool_dir not in sys.path:
sys.path.append(tool_dir) # import の検索先に足す(二重に足さない)
import my_tool_util
my_tool_util.hello("pymxs") # 'Hello, pymxs'
- 実際には、ツールのフォルダ(例:
C:\tools\my_tool)をsys.pathに足して、importで読み込みます。 - 1 つのファイルが大きくなったら、UI(Qt)と処理(
rtを呼ぶ部分)を別のモジュールに分けると、処理だけを使い回しやすくなります。
書き換えたモジュールを読み直す(reload)
import importlib
import my_tool_util
importlib.reload(my_tool_util) # 3ds Max を再起動せずに変更を反映する
- ⚠️ Python は一度
importしたモジュールを覚えているので、ファイルを書き換えても もう一度importするだけでは反映されません。開発中はimportlib.reloadします。 reloadは指定したモジュールだけを読み直します。そのモジュールが別の自作モジュールを使っている場合は、依存先から順に reload します。
起動時に Python を実行する(スタートアップ)
3ds Max 2022 では、スタートアップフォルダに置いた .py は実行されません。MaxScript のスタートアップファイルから Python を呼びます。
ユーザのスタートアップフォルダ(rt.getDir(rt.Name("userStartupScripts")) で確認できる場所)に、次の .ms を置きます。
-- my_tool_startup.ms
python.ExecuteFile @"C:\tools\my_tool\startup.py"
起動のたびに実行したい Python(startup.py)の例:
import sys
TOOL_DIR = r"C:\tools\my_tool"
if TOOL_DIR not in sys.path:
sys.path.append(TOOL_DIR)
# ここでマクロスクリプトの登録やコールバックの登録を行う
- ツールバーやメニューに割り当てられるマクロスクリプトの登録は、起動のたびに行う処理としてここにまとめます(→「Qt UI」章)。
外部パッケージを入れる(pip)
3ds Max に同梱の Python([3ds Max のインストール先]\Python37\python.exe)に対して pip を使います。コマンドプロンプトで実行します。
cd "C:\Program Files\Autodesk\3ds Max 2022\Python37"
python.exe -m ensurepip --upgrade --user
python.exe -m pip install --user pillow
ensurepipは、pip が入っていない・古いときだけ実行すれば十分です(pip は 3ds Max 2021.1 Update 以降の Python に同梱されています)。- ⚠️ 3ds Max 同梱の
python.exeを使う ことが大切です。PATH にある別の Python に入れても 3ds Max からは読めません。 --userを付けると、3ds Max のインストールフォルダではなくユーザのフォルダに入ります(管理者権限が要らず、3ds Max の更新でも消えません)。- 入ったか確かめるときは、3ds Max の中で
import PILを実行します。 - プロジェクトごとにパッケージを分けたいときは仮想環境(
venv)も使えます。環境を有効にしたコマンドプロンプトから3dsmax.exeを起動します(公式ヘルプ「Virtual Environments and 3ds Max Python」)。
環境を確かめる
import sys
sys.version # Python のバージョン(3ds Max 2022 は 3.7 系)
sys.executable # 実行中の Python
list(rt.maxVersion()) # 3ds Max のバージョン情報(先頭は内部番号。2022 は 24000)
rt.getDir(rt.Name("userScripts"))
sys.executableは、埋め込みの Python では3dsmax.exeのパスになることがあります。Python 本体は[3ds Max のインストール先]\Python37\python.exeです。list(rt.maxVersion())の 8 番目の値(Python の添字では[7])は製品のリリース年です(2022 なら2022)。3ds Max 2019.2 Update 以降で取得できます。
知らない API を調べる
b = rt.getNodeByName("Box001")
rt.execute("fn pyToString v = (v as string)") # StringStream → 文字列に変換する MaxScript 関数を定義
props = [str(n) for n in rt.getPropNames(b)] # パラメータ名の一覧
# MXS: getPropNames b
rt.hasProperty(b, "height") # そのプロパティがあるか
rt.classOf(b), rt.superClassOf(b) # クラスと大分類
ss = rt.StringStream("")
rt.showProperties(b, to=ss) # 型付きの一覧(to= に出力先が必要)
# MXS: showProperties b to:ss
props_text = rt.pyToString(ss)
ss = rt.StringStream("")
rt.apropos("getVert", to=ss) # 名前で関数・グローバルを探す(部分一致)
found = rt.pyToString(ss)
ss = rt.StringStream("")
rt.showInterfaces(rt.getNodeByName("Box001"), to=ss) # そのオブジェクトが持つインタフェースと関数
- ⚠️
showProperties/showClass/apropos/showInterfacesは、pymxs から呼ぶと リスナーには何も出ません。to=にrt.StringStream("")を渡して受け取ります(公式ヘルプの書き方)。 getPropNamesは Name の配列を返すので、str()で Python の文字列にします。pyToStringはexecuteで定義した MaxScript 側の関数で、StringStreamの中身を文字列にします。Python のstr(ss)ではStringStream:'...'という形になってしまうため、こうして中身だけを取り出します。aproposは既定でパターンの前後に暗黙のワイルドカードが付く部分一致です。構造体の関数(polyop.getVertなど)も見つかりますが、パターンにはpolyop.を付けず、名前の部分("getVert")だけを書きます("polyop.getVert"と書くと何も見つかりません)。implicitWild=Falseを指定するとこの暗黙のワイルドカードを外せます(パターンの末尾に*を付ければ先頭一致にできます)。- MaxScript のリスナー(MaxScript モード)で同じ関数を実行すると、そのまま表示されるので手早く調べられます。
例外を扱う
b = rt.getNodeByName("Box001")
try:
b.noSuchProperty # 存在しないプロパティ
except AttributeError as e:
prop_err = str(e)
try:
rt.execute("myToolNoSuchFunction_xyz()") # MaxScript 側のエラー
except RuntimeError as e:
mxs_err = str(e) # MaxScript のエラーメッセージが入る
- 存在しないプロパティを読むと
AttributeError、MaxScript の実行中に起きたエラーはRuntimeErrorとして Python に届きます。 - ⚠️
with pymxs.undo(True):の中で起きた例外は、そこまでの変更を取り消したうえで 外に伝わりません(→「pymxs の基本」章)。エラーを知りたいときはundoの内側でtryします。
ログを出す
import logging
import os
import traceback
log_path = os.path.join(rt.getDir(rt.Name("temp")), "my_tool.log")
logger = logging.getLogger("my_tool")
if not logger.handlers: # 何度実行してもハンドラを増やさない
handler = logging.FileHandler(log_path, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info("start")
try:
1 / 0
except Exception:
logger.error(traceback.format_exc()) # スタックトレースごと残す
pymxs.print_("my_tool: エラーが起きました。ログを見てください\n", True) # リスナーに赤字で出す
print()はリスナーに出ます。pymxs.print_(文字列, True)はエラー扱い(赤字)で出せます。- ユーザの手元で起きた問題を調べるには、ファイルに残すログの方が役に立ちます。
loggingは Python 標準です。
処理時間を測る
import time
t0 = time.perf_counter()
with pymxs.redraw(False):
for i in range(100):
rt.Box(pos=rt.Point3(i * 30, 0, 0))
# MXS: with redraw off (for i = 0 to 99 do Box pos:[i*30,0,0])
elapsed = time.perf_counter() - t0
print("{:.3f} 秒".format(elapsed))
- 遅い処理は、まず時間を測ってから直します。pymxs では「
rtの呼び出し回数を減らす」「再描画を止める」の 2 つが効くことが多いです(→「メッシュ・ポリゴン」章)。
落とし穴まとめ
sys.pathに同じフォルダを何度も足さない。- 書き換えたモジュールは
importlib.reloadしないと反映されない。 - スタートアップフォルダの
.pyは実行されない。.msからpython.ExecuteFileで呼ぶ。 - pip は 3ds Max 同梱の
python.exeで、--userを付けて使う。 showPropertiesなどはto=に StringStream を渡さないと何も出ない。pymxs.undo(True)の中の例外は外に伝わらない。
関連
- MaxScript の関数を Python から呼ぶ: 「MaxScript との連携」章
- 起動時にツールバーやメニューに割り当てられるマクロスクリプトを登録する: 「Qt UI」章
コメント