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

pymxs の基本

  • 3dsMax
  • Python
  • pymxs
  • チートシート
3ds Max Python チートシート の章一覧

pymxs は MaxScript の世界を Python から触るための 薄いラッパー です。 MaxScript の関数・クラス・グローバル変数は、すべて rt.(= pymxs.runtime)の下にそのままの名前で並んでいます。 この章では、以降の章を読むのに必要な「翻訳ルール」をまとめます。

rt の基本

b = rt.Box(length=20, width=20, height=20)    # MXS: b = box length:20 width:20 height:20
rt.classOf(b)                                  # MXS: classOf b          → Box
rt.superClassOf(b)                             # MXS: superClassOf b     → GeometryClass
rt.redrawViews()                               # MXS: redrawViews()
  • MaxScript のクラス・関数・グローバルは rt.名前 で呼べます。キーワード引数 は 名前=値 で渡します(MaxScript の 名前:値)。
  • rt の名前は 大文字小文字を区別しません(rt.converttomesh も rt.convertToMesh も同じ)。Python としては不思議ですが、中身が MaxScript だからです。本書では MaxScript のヘルプと同じ表記にそろえます。

$ の代わり(ノードを名前で取る)

b = rt.getNodeByName("Box001")                       # MXS: $Box001
sel = rt.selection[0]                                # MXS: selection[1](1 つだけ選んでいるときは $ でも可)
boxes = list(rt.execute("$Box* as array"))           # MXS: $Box* as array(ワイルドカード)
  • Python には $ 構文がありません。名前で 1 つ取るなら getNodeByName、ワイルドカードやパス指定を使いたいときは rt.execute で MaxScript の式として評価します。rt.readValue(rt.StringStream("$Box*")) のように readValue でストリームを読ませる書き方もヘルプで紹介されています。
  • 見つからないとき getNodeByName は None を返します(MaxScript の undefined)。

Name 値(#world など)

world = rt.Name("world")               # MXS: #world
rt.toolMode.coordsys(world)            # MXS: toolMode.coordsys #world(ツールバーの参照座標系を実際に切り替える)

n = rt.Name("MyToolOption")
str(n)                                 # 'MyToolOption'(# は付かない)
n == rt.Name("mytooloption")           # True(大文字小文字を区別しない)
  • 関数の「決まった選択肢」を表す MaxScript の Name 値(#xxx)は rt.Name("xxx") で作ります。Name は大文字小文字を区別せず 1 つに統合されるので、既に使われている綴りの名前は 最初に登録されたときの大文字小文字で表示されます。
  • Python の文字列を渡すと、型エラーになるか無視されることが多いので、Name を求める引数には必ず rt.Name を使います。
  • よく使うものは定数にしておくと読みやすくなります(NO_PROMPT = rt.Name("noPrompt") など)。

値の型(Point3 / Color / 行列など)

p = rt.Point3(10, 0, 5)                # MXS: [10,0,5]
c = rt.Color(255, 128, 0)              # MXS: color 255 128 0
q = rt.EulerAngles(0, 0, 45)           # MXS: eulerAngles 0 0 45
m = rt.Matrix3(1)                      # MXS: matrix3 1(単位行列)
r = rt.Interval(0, 100)                # MXS: interval 0 100
p2 = rt.Point2(640, 480)               # MXS: [640,480]

p.x, p.y, p.z                          # 成分を読む
p + rt.Point3(1, 1, 1)                 # 演算子もそのまま使える
rt.length(p)                           # MXS: length p
  • 数値・文字列・真偽値は Python の int / float / str / bool に自動で変換されます。
  • Point3 などの MaxScript の値はラッパー(MXSWrapperBase)のまま渡ってきます。p.x のように成分を読むことも、+ や * で計算することもできます。
  • MaxScript の undefined は Python の None になります。

⚠️ ネストしたプロパティへの代入は効かない

b = rt.getNodeByName("Box001")
b.pos.x = 10          # ✗ 効かない。b.pos は「コピー」が返るので、コピーの x が変わるだけ
print(b.pos.x)        # 0.0 のまま

# 正しい書き方 1: 取り出して、変えて、戻す
p = b.pos
p.x = 10
b.pos = p             # MXS: b.pos.x = 10

# 正しい書き方 2: setmxsprop / getmxsprop でパスを文字列で渡す
b.setmxsprop("pos.y", 20)
b.getmxsprop("pos.y")   # 20.0
rt.getProperty(b, "pos.y")  # 20.0(MaxScript の getProperty でも読める)
  • pymxs で一番ハマる所 です。MaxScript の b.pos.x = 10 は「位置の x を書き換える」特別扱いがありますが、Python では b.pos を取り出した時点でノードとのつながりが切れます。
  • ネストしたプロパティの読み書きには 3 通りの書き方があります。setmxsprop / getmxsprop は pymxs のラッパー(MXSWrapperBase)自身が持つメソッドで、rt.setProperty / rt.getProperty は MaxScript の関数です。どちらもパスを文字列で渡せば同じように動きます。

配列とインデックス(0 始まりと 1 始まり)

arr = rt.Array(10, 20, 30)            # MXS: #(10, 20, 30)
arr[0]                                # 10(Python から添字で読むと 0 始まり)
len(arr)                              # 3
rt.append(arr, 40)                    # MXS: append arr 40
py_list = list(arr)                   # Python の list に変換 → [10, 20, 30, 40]

rt.meditMaterials[0]                  # マテリアルエディタの 1 番目のスロット(添字は 0 始まり)
rt.setMeditMaterial(1, rt.PhysicalMaterial())   # 関数の「番号」引数は 1 始まりのまま
  • Python から添字でアクセスすると 0 始まり、MaxScript の関数に渡す番号(頂点番号・スロット番号など)は 1 始まり です。
  • 関数が返す配列の 中身の値 が番号の場合も 1 始まりです(例: polyop.getFaceVerts が返す頂点番号)。「入れ物の添字」と「中身の番号」を区別してください。
  • Python の list を MaxScript の関数に 引数として 渡すと、配列に自動で変換されます。ただし rt.名前 = リスト でグローバル変数に入れたときは変換されないので、rt.Array(*リスト) にしてから入れます(→「MaxScript との連携」章)。

== で比べる(is は使わない)

a = rt.getNodeByName("Box001")
b = rt.selection[0]
a == b          # True(同じノード)
a is b          # False(ラッパーは毎回別の Python オブジェクト)
  • pymxs のオブジェクトは取り出すたびに新しいラッパーに包まれます。同じノードかどうかは == で比べます。
  • ノードは Python の dict のキーや set の要素にもできます(3ds Max 2019 Update 1 以降)。

コンテキスト(undo / animate / at time / redraw)

b = rt.getNodeByName("Box001")

with pymxs.undo(True, "箱を動かす"):          # MXS: undo "箱を動かす" on ( ... )
    b.pos = rt.Point3(0, 0, 30)

with pymxs.animate(True):                     # MXS: with animate on ( ... )
    with pymxs.attime(0):                     # MXS: at time 0 b.pos = [0,0,0]
        b.pos = rt.Point3(0, 0, 0)
    with pymxs.attime(50):
        b.pos = rt.Point3(100, 0, 0)

with pymxs.redraw(False):                     # MXS: with redraw off ( ... )
    for i in range(10):
        rt.Sphere(radius=5, pos=rt.Point3(i * 15, 50, 0))
  • MaxScript のコンテキスト式は、pymxs の関数を Python の with 文で使います。使えるのは animate / attime / atlevel / quiet / redraw / undo です。
  • ⚠️ in coordsys と about は pymxs にありません。rt.setRefCoordSys(rt.Name("local")) のように参照座標系を切り替える方法もありますが、これはツールバーの参照座標系ドロップダウンを実際に切り替える UI 操作です。UI の設定まで変わるので、本書ではこの方法は使いません。代わりの書き方は「変形・配置」章でも扱います。
  • ⚠️ pymxs の操作を 1 つの undo 項目にまとめ、元に戻せるようにするには pymxs.undo(True, "ラベル") で囲みます(Python のヘルプは、囲まない操作は undo に記録されないと説明しています。MaxScript 自体のヘルプは「既定で undo 可能」と記述しており、両者の説明は食い違うため、ここでは断定しません)。
  • ⚠️ pymxs.undo(True) の中で例外が起きると、それまでの変更が取り消され、例外は外に伝わりません(MaxScript とは違う動き)。

グローバル変数と Dictionary

rt.myToolCount = 3                      # MXS: global myToolCount = 3
rt.execute("myToolCount + 1")           # 4(MaxScript からも見える)

d = rt.Dictionary(("one", 1), ("two", 2))   # MXS: Dictionary #("one", 1) #("two", 2)
d["three"] = 3
py_dict = {str(k): d[k] for k in d.keys}    # Python の dict に変換
  • rt.名前 = 値 で MaxScript のグローバル変数を作れます。MaxScript のコードや rt.execute と値をやり取りするときに使います。
  • 3ds Max 2021 以降 は、このように rt.名前 = 値 と書くだけで新しいグローバル変数を暗黙に作成できます。2020 以前 では rt.execute('global myToolCount = 3') のように MaxScript 側で global 宣言してから値を代入します。
  • グローバルはセッション中ずっと残るので、名前には myTool... のような接頭辞を付けて衝突を避けます。

BitArray と as 変換

rt.execute("fn pyToArray v = (v as array)")      # MaxScript で変換関数を 1 度だけ定義
ba = rt.execute("#{1..3, 7}")                    # MXS: #{1..3, 7}
list(rt.pyToArray(ba))                           # [1, 2, 3, 7]
  • MaxScript の as 変換(x as array / x as string など)は Python に同じ構文がありません。小さな MaxScript 関数を rt.execute で定義して呼ぶ のが定番です。
  • 頂点や面の選択(BitArray)を Python の list にするときによく使います。

時間の値

rt.sliderTime = 10                   # MXS: sliderTime = 10(整数を渡すとフレーム数として扱われる)
rt.currentTime                       # 現在の時間(Time 値)
rt.currentTime.frame                 # 10.0(フレーム数。Python では float)
rt.animationRange = rt.Interval(0, 200)   # MXS: animationRange = interval 0 200
rt.animationRange.end.frame          # 200.0
  • MaxScript の時間リテラル(10f / 1s など)は Python にありません。整数を渡すと、基本的にフレーム数として扱われます。
  • .frame は Python では float で返りますが、MaxScript のリスナーでは整数のように表示されます。

落とし穴まとめ

  • b.pos.x = 10 のようなネストした代入は効かない。取り出して戻すか setmxsprop を使う。
  • 添字は 0 始まり、関数に渡す番号は 1 始まり。
  • 同じノードかどうかは is ではなく == で比べる。
  • Name を求める引数には文字列ではなく rt.Name("...") を渡す。
  • in coordsys / about は無い。pymxs の操作を 1 つの undo 項目にまとめ、元に戻せるようにするには pymxs.undo(True, "ラベル") で囲む(Python のヘルプは、囲まない操作は undo に記録されないと説明している)。
  • print / assert は pymxs からは呼べない。Python の print() / assert 文、または pymxs.print_() を使う(→「モジュール管理・デバッグ」章)。

関連

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