メッシュ・ポリゴン
3ds Max Python チートシート の章一覧
頂点や面を直接扱う操作です。編集可能ポリゴンは rt.polyop、編集可能メッシュ(三角形)は getVert などの関数群(および rt.meshop)で扱います。
頂点番号・面番号はすべて 1 始まり です(MaxScript の関数に渡す番号だからです)。
編集可能ポリゴンにする
b = rt.getNodeByName("Box001")
rt.convertToPoly(b) # MXS: convertToPoly b
rt.classOf(b.baseObject) == rt.Editable_Poly # True
nv = rt.polyop.getNumVerts(b) # 頂点数(MXS: polyop.getNumVerts b) → 8
nf = rt.polyop.getNumFaces(b) # 面数 → 6
- Box のパラメータ(
lengthなど)は失われる変換です。取り消せるようにしたいときはwith pymxs.undo(True):で囲むか、先にrt.copy(b)します。
頂点を読む / 書く
b = rt.getNodeByName("Box001")
rt.convertToPoly(b)
p = rt.polyop.getVert(b, 1) # 1 番の頂点(ワールド座標)
rt.polyop.setVert(b, [1], p + rt.Point3(0, 0, -5)) # 位置を指定(MXS: polyop.setVert b #{1} (p + [0,0,-5]))
rt.polyop.moveVert(b, [2, 3], rt.Point3(0, 0, 10)) # 相対移動
all_pos = rt.polyop.getVerts(b, list(range(1, rt.polyop.getNumVerts(b) + 1))) # まとめて取得
zs = [v.z for v in all_pos]
- ノードを渡すと座標は現在の参照座標系のコンテキストになります(pymxs には
in coordsysが無いので)。3ds Max の参照座標系の設定に影響されることがあるので、確実にワールド座標で欲しいときは先にrt.setRefCoordSys(rt.Name("world"))を呼びます。 - 頂点のリスト(
<vertlist>)には、Python のlistをそのまま渡せます。中身は 1 始まりの番号 です。動きますが、公式ヘルプは「<vertlist>は配列ではなく BitArray で渡すこと。配列で渡すとパフォーマンスに重大な影響がある」としています。頂点数が多いときはrt.execute("#{1..%d}" % n)のように BitArray で渡します(→「大量の頂点を速く処理する」節)。 setVert/moveVertの 3 つ目の引数にPoint3のlistを渡すと、頂点ごとに違う値を与えられます。個数は頂点リストと同じにします。
面を読む
b = rt.getNodeByName("Box001")
rt.convertToPoly(b)
verts = list(rt.polyop.getFaceVerts(b, 1)) # 面 1 を作る頂点番号(中身も 1 始まり)
center = rt.polyop.getFaceCenter(b, 1) # 面の中心(ワールド)
normal = rt.polyop.getFaceNormal(b, 1) # 面の法線(ワールド)
area = rt.polyop.getFaceArea(b, 1) # 面積
mat_id = rt.polyop.getFaceMatID(b, 1) # マテリアル ID
- ⚠️
getFaceVertsが返すのは MaxScript の配列です。入れ物の添字は 0 始まり、中身の頂点番号は 1 始まり なので、verts[0]は「1 つ目の頂点の番号(1 始まり)」です。list()しなくても、pymxs では返ってきた配列にそのままarr[0]のように 0 始まりで添字を付けられます。
サブオブジェクトの選択
b = rt.getNodeByName("Box001")
rt.convertToPoly(b)
rt.execute("fn pyToArray v = (v as array)") # BitArray → 配列の変換関数
rt.polyop.setVertSelection(b, [1, 2, 3]) # 頂点を選ぶ(MXS: polyop.setVertSelection b #{1..3})
rt.polyop.setFaceSelection(b, [1]) # 面を選ぶ
sel_verts = list(rt.pyToArray(rt.polyop.getVertSelection(b))) # [1, 2, 3]
up_faces = [f for f in range(1, rt.polyop.getNumFaces(b) + 1)
if rt.polyop.getFaceNormal(b, f).z > 0.9] # 上向きの面
rt.polyop.setFaceSelection(b, up_faces)
- 選択は MaxScript の BitArray で返ってきます。Python の
listにするには、as arrayをする小さな関数を使います(→「pymxs の基本」章)。 - 選択を設定するときは Python の
listを渡せます。
面を消す / オブジェクトを結合する
b = rt.getNodeByName("Box001")
rt.convertToPoly(b)
rt.polyop.deleteFaces(b, [1, 2]) # 面 1 と 2 を削除(面が使わなくなった頂点も一緒に消える。delIsoVerts=False で残せる)
s = rt.getNodeByName("Sphere001")
rt.polyop.attach(b, s) # s を b に結合(s のノードは消える)
- ⚠️ 面や頂点を消すと 番号が詰められます。番号のリストを使って順番に消すときは、まとめて 1 回で渡すか、大きい番号から消します。
編集可能メッシュ(三角形)を読む
t = rt.getNodeByName("Teapot001")
m = rt.snapshotAsMesh(t) # モディファイヤ・位置・回転も反映済みの形を TriMesh として取り出す(ワールド座標)
n = rt.getNumVerts(m) # MXS: getNumVerts m
p = rt.getVert(m, 1) # 1 番の頂点
f = rt.getFace(m, 1) # 面 1 の 3 頂点の番号(Point3。値は 1 始まり)
nrm = rt.getFaceNormal(m, 1) # 面の法線
snapshotAsMeshはノードを変えずに「今の見た目の三角形メッシュ」を取り出せます。モディファイヤが載ったままのオブジェクトを調べるのに便利です。- シーンに実体のコピーを作りたいときは
rt.snapshot(t)です(新しいノードができる)。
UV を読む
b = rt.getNodeByName("Box001")
b.mapcoords = True # プリミティブは「マッピング座標を生成」が必要
rt.convertToPoly(b)
ch = 1 # マップチャネル 1(通常の UV。0 は頂点カラー)
n_uv = rt.polyop.getNumMapVerts(b, ch) # UV 頂点の数
uv = rt.polyop.getMapVert(b, ch, 1) # 1 番の UV 頂点(Point3。x=U, y=V)
face_uv = list(rt.polyop.getMapFace(b, ch, 1)) # 面 1 が使う UV 頂点の番号
- Box などのプリミティブは、「マッピング座標を生成」プロパティ(
mapcoords)がオフ(既定)だと UV を持ちません。convertToPolyの前にb.mapcoords = Trueにしておきます。 - ⚠️ UV 頂点と幾何の頂点は別の番号 です。面ごとに
getMapFaceで対応を取ります。 - 使っていないチャネルを読むと失敗することがあります。先に
rt.polyop.getMapSupport(b, ch)で確かめます。
メッシュを一から作る
verts = [rt.Point3(0, 0, 0), rt.Point3(100, 0, 0), rt.Point3(100, 100, 0), rt.Point3(0, 100, 0)]
faces = [rt.Point3(1, 2, 3), rt.Point3(1, 3, 4)] # 三角形ごとに頂点番号(1 始まり)
quad = rt.mesh(vertices=verts, faces=faces, name="MyQuad") # MXS: mesh vertices:verts faces:faces
rt.convertToPoly(quad)
- 面は三角形の 3 頂点を
Point3で指定します。この例は三角形 2 枚でできる四角形です。1 枚の四角形として扱いたいときは、ポリゴンに変換してから対角線のエッジを消します。
大量の頂点を速く処理する
import time
s = rt.getNodeByName("Sphere001")
rt.convertToPoly(s)
n = rt.polyop.getNumVerts(s)
all_v = rt.execute("#{1..%d}" % n) # 頂点番号を BitArray で作る
t0 = time.perf_counter()
rt.setRefCoordSys(rt.Name("world"))
positions = rt.polyop.getVerts(s, all_v) # ① 1 回でまとめて取る(BitArray で渡す)
c = s.pos # オブジェクトの中心(ワールド)
new_positions = [c + (p - c) * 1.1 for p in positions] # ② Python で計算する(中心基準に拡大)
with pymxs.redraw(False): # ビューポートの再描画を止める(何度も呼ぶときに効く)
rt.polyop.setVert(s, all_v, new_positions) # ③ 1 回でまとめて戻す
print("{:.3f} 秒".format(time.perf_counter() - t0))
- pymxs の呼び出しは 1 回ごとに Python と MaxScript の間を行き来する ので、頂点ごとに
getVert/setVertを呼ぶと遅くなります。まとめて取って、Python で計算して、まとめて戻す のが基本です。 - 頂点リストは配列ではなく BitArray で渡します。
rt.execute("#{1..%d}" % n)のように MaxScript 側で作るのが手軽です。 - それでも遅いときは、ループごと MaxScript で書いて
rt.executeで 1 回だけ呼びます(→「MaxScript との連携」章)。
落とし穴まとめ
- 頂点・面・UV の番号は 1 始まり。返ってきた配列の添字は 0 始まり、中身の番号は 1 始まり。
- 面や頂点を消すと番号が詰められる。
- UV 頂点と幾何の頂点は別の番号。
- 1 頂点ずつ呼ぶと遅い。まとめて取得・設定する。頂点リスト(
<vertlist>)は配列ではなく BitArray で渡す(配列だとパフォーマンスへの影響が大きい)。
関連
- ポリゴンへの変換と集約: 「モディファイヤ」章
- MaxScript 側でループを書く: 「MaxScript との連携」章
コメント