変形・配置
3ds Max Python チートシート の章一覧
オブジェクトを動かす操作です。プロパティに代入する(絶対指定) 方法と、move / rotate / scale で相対的に動かす 方法の 2 系統があります。
pymxs には in coordsys が無いので、ローカル軸で動かすときは 行列で計算する のが Python 流です。
位置・回転・スケールを読む / 書く
b = rt.getNodeByName("Box001")
b.pos # MXS: b.pos -- Point3
b.rotation # MXS: b.rotation -- Quat
b.scale # MXS: b.scale -- Point3
b.transform # MXS: b.transform -- Matrix3。位置+回転+スケール
b.pos = rt.Point3(10, 20, 0) # MXS: b.pos = [10,20,0]
b.scale = rt.Point3(2, 2, 2) # MXS: b.scale = [2,2,2]
- ⚠️
b.pos.x = 10は効きません(b.posはコピーが返るため)。成分だけ変えるときは取り出して戻します。
b = rt.getNodeByName("Box001")
p = b.pos
p.z += 30 # MXS: b.pos.z += 30
b.pos = p
相対的に動かす(move / rotate / scale)
b = rt.getNodeByName("Box001")
rt.move(b, rt.Point3(10, 0, 0)) # MXS: move b [10,0,0]
rt.rotate(b, rt.EulerAngles(0, 0, 45)) # MXS: rotate b (eulerAngles 0 0 45)
rt.rotate(b, rt.Quat(90, rt.Point3(1, 0, 0))) # MXS: rotate b (quat 90 [1,0,0]) -- 軸と角度で回す
rt.scale(b, rt.Point3(2, 2, 2)) # MXS: scale b [2,2,2]
rt.move(rt.objects, rt.Point3(0, 0, 5)) # MXS: move objects [0,0,5]
move/rotate/scaleは 今の状態からの相対変形 です。絶対値で指定したいときはプロパティに代入します。move/rotate/scaleは mapped 関数なので、rt.objectsやrt.selection、配列をそのまま渡すと中のオブジェクト全部に一括で適用されます(上の例は全オブジェクトを Z に 5 動かしています)。- 軸はワールド(
in coordsysを付けない MaxScript の既定と同じ)、回転・スケールの中心は各オブジェクトのピボットです。
回転を角度(オイラー角)で読む
b = rt.getNodeByName("Box001")
rt.rotate(b, rt.EulerAngles(0, 0, 30))
z = rt.getProperty(b, "rotation.z_rotation") # UI の [回転] Z と同じ値(度) → 30.0
# MXS: b.rotation.z_rotation
x = rt.getProperty(b, "rotation.x_rotation") # X / Y も同じ書き方
rotation.x_rotation/y_rotation/z_rotationは、既定の回転コントローラ(オイラー XYZ)の値で、UI の [回転] 欄と同じ度数です。b.rotation.z_rotationとは書けないので(→「pymxs の基本」章のネストしたプロパティ)、rt.getPropertyにパスを文字列で渡します。- ⚠️
b.rotationは Quat(四元数)なので、そこから角度を計算すると、UI の数字と符号や表し方が一致しないことがあります。画面と同じ値が欲しいときは上の書き方で読みます。
行列で配置する(Matrix3)
b = rt.getNodeByName("Box001")
tm = rt.rotateZMatrix(90) * rt.transMatrix(rt.Point3(0, 50, 0)) # Z 90 度回して (0,50,0) へ
b.transform = tm # 位置・回転・スケールがすべてこの行列の値で置き換わる
x_axis = b.transform.row1 # ローカル X 軸の向き(ワールド)
pos = b.transform.row4 # 位置(= b.pos)
- MaxScript の行列は 「左から順に適用」 です。
A * Bは「A をしてから B をする」という意味になります(行ベクトルの流儀)。たとえばT * b.transformは「先に T、次に今のb.transform」という意味で、MaxScript のpreTranslate(左から掛ける)に対応します(右から掛けるtranslateはその逆です)。 rt.transMatrix/rt.rotateXMatrix/rt.rotateYMatrix/rt.rotateZMatrix/rt.scaleMatrixで部品を作って掛け合わせます。row1〜row3がローカル X / Y / Z 軸、row4が位置です。
ローカル軸で動かす(in coordsys local の代わり)
b = rt.getNodeByName("Box001")
rt.rotate(b, rt.EulerAngles(0, 0, 90)) # 準備: Z 90 度回しておく
b.transform = rt.transMatrix(rt.Point3(10, 0, 0)) * b.transform # MXS: in coordsys local move b [10,0,0] -- ローカル X に 10
b.transform = rt.rotateXMatrix(30) * b.transform # ローカル X 軸まわりに 30 度
- 「移動や回転の行列」を左から掛けるとローカル軸、右から掛けるとワールド軸での変形になります(
preTranslate/preRotateが左から、translate/rotate(行列版)が右からの掛け算に対応します)。 - 回転していない Box ならローカル X とワールド X は同じ向きなので結果も同じですが、回したあとはローカル X がその向きに沿って動きます。この例では先に Z 90 度回転しているため、ローカル X に 10 動かすとワールドでは Y に 10 動きます。
MaxScript の書き方をそのまま使いたいときは、rt.execute にノードをグローバル変数で渡します。
rt.myToolNode = rt.getNodeByName("Box001")
rt.execute("in coordsys local move myToolNode [10,0,0]")
rt.execute("about [0,0,0] rotate myToolNode (eulerAngles 0 0 90)") # 原点を中心に回す
- 使い終わったら
rt.myToolNode = Noneにして、グローバル変数を残さないようにします。
別オブジェクトに合わせる(位置合わせ)
a = rt.getNodeByName("Box001")
t = rt.getNodeByName("Sphere001")
a.pos = t.pos # MXS: a.pos = t.pos -- 位置だけ合わせる
a.rotation = t.rotation # 向きだけ合わせる
a.transform = t.transform # 位置・向き・スケールを丸ごと一致させる
バウンディングボックスと接地
s = rt.getNodeByName("Sphere001")
s.min, s.max, s.center # 値を見る(min / max は読み取り専用) -- ワールド空間の最小 / 最大 / 中心
size = s.max - s.min # 大きさ
p = s.pos
p.z -= s.min.z # MXS: s.pos.z -= s.min.z -- 一番下を Z=0 に合わせる
s.pos = p
bb_min, bb_max = rt.nodeLocalBoundingBox(s) # ローカル空間のバウンディングボックス
# bb = rt.nodeLocalBoundingBox(s, asBox3=True) # 3ds Max 2022 以降は Box3 値としても受け取れる
min/max/centerは ワールド空間 の値です。回転したオブジェクトの「自分の向きでの大きさ」はnodeLocalBoundingBoxで取ります。- 配列を返す関数は、Python のアンパック(
a, b = ...)で受け取れます。
ピボット(基点)を操作する
b = rt.getNodeByName("Box001")
b.pivot = b.center # MXS: b.pivot = b.center -- ピボットをバウンディングボックス中心へ
rt.CenterPivot(b) # [中心オブジェクト] と同じ(結果は b.pivot = b.center と同じ)
rt.WorldAlignPivot(b) # [ワールド座標に位置合わせ] と同じ
rt.ResetXForm(b) # [X フォームをリセット](XForm モディファイヤが載る)
pivotを変えても メッシュは動きません(基点だけが動く)。UI の [基点にのみ影響] と同じです。ResetXFormのあとはスタックに XForm が載るので、確定させたいときはrt.collapseStack(b)します(→「モディファイヤ」章)。
よく使う形
# 選択を X 方向に 30 ずつ並べる
for i, o in enumerate(rt.selection):
o.pos = rt.Point3(i * 30, 0, 0)
# 位置を 10 単位のグリッドに丸める
for o in rt.objects:
p = o.pos
o.pos = rt.Point3(round(p.x / 10) * 10, round(p.y / 10) * 10, round(p.z / 10) * 10)
# ランダムに散らす
import random
for o in rt.objects:
o.pos = rt.Point3(random.uniform(-100, 100), random.uniform(-100, 100), 0)
- Python の
enumerate/round/random(標準ライブラリ)がそのまま使えます。 - ⚠️ Python 3 の
round()は偶数への丸め(銀行丸め)です。round(0.5)は1ではなく0になります。
落とし穴まとめ
b.pos.x = ...は効かない。取り出して、変えて、戻す。move/rotate/scaleは相対、プロパティ代入は絶対。in coordsys/aboutは pymxs に無い。行列を左から掛けるか、rt.executeで MaxScript を使う。min/max/centerはワールド空間。pivotの変更はメッシュを動かさない。
コメント