Python + MaxScriptのツール開発入門(Max2020版)その14

2021年9月3日

 前回までで当初予定していたものは完成しました。Python を使った3dsmax の開発では、やはり実行部分はMaxScript で作り、UIをPython で作るというのがとっかかりとしては楽だと思います。

 しかし、これPython で開発していると言えるのだろうか、と思いますよね。なにしろPython なのはUI だけですから。そこで、今回はチュートリアルとしては番外編として、これまで作ってきたGridCopy を全部Pythonで、MaxScript を一切使わずに実装してみましょう。

実行部分をPython で実装する

 実際の動作部分をPython で作るわけですが、今回はすでに求める機能をMaxScript で実装済ですので、そのコードを参考にしましょう。gridCopy.ms を見ると、実行はlineCopy という関数と、gridCopy という関数に分かれています。

 動きを確認すると、lineCopy という関数は元になるノードのリストを受け取り、それを一方向へコピーして並べる、という動作です。そしてでき上った状態をリストにして返しています。

 ここで、MaxScript では複数のオフジェクトを格納するデータ型は配列(Array) しかなかったので特に何も考える必要はありませんでしたが、3dsmax のPythonAPI にはこういう場合に使える型がいくつかあります。何を使うのが良いかはケースバイケースで、Python のリストも使えるのですが、ここではせっかくなので3dsmax のPythonAPI のINodeTab を使いましょう。

def lineCopy(self, src_nodes, count, offset):
    u''' 対象のノード(複数可)を直線状にコピーする
    param:
        GridCopy: self
        INodeTab: 元ノードのリスト
        int: 個数
        float: 間隔
    '''
    # 戻り値を格納するINodeTab を初期化する
    return_nodes = MaxPlus.INodeTab()
    # 受け取った元ノードをループ
    for n in src_nodes:
        # cur_offset をoffset(間隔1つ分)で初期化
        cur_offset = offset
        # 指定個数 - 1 回コピーする
        for i in range(count - 1):
            # 元ノードをコピー
            new_node = n.CreateCopy()
            # コピーしたものを cur_offset 分移動
            new_node.Move(cur_offset)
            # 新たなノードを戻り値に格納
            return_nodes.Append(new_node)
            # cur_offset を offset 分増やす
            cur_offset += offset
    # 戻り値に元ノードを追加
    for n in src_nodes:
        return_nodes.Insert(n, 0)
    return return_node

 一気に書いてしまいましたがこんな感じです。MaxScriptのCloneNodes みたいにオフセット地を入れて実行したり、生成されたノードを回収したりということを一発でやるものは用意されていません。そのため、それぞれの処理は自前で実装する必要があります。

 あとはこれをx、y、zそれぞれの方向に実行するgridCopy を実装するだけです。

    def gridCopy(self, src_node, x_count, y_count, z_count, x_interval, y_interval, z_interval):
        # x
        # 大元の1個 をlineCopy に渡せるようにINodeTab に格納する
        src_nodes = MaxPlus.INodeTab()
        src_nodes.Append(src_node)
        # x間隔 からx方向の移動用Point3 を生成する
        offset_x = MaxPlus.Point3()
        offset_x.SetX(x_interval)
        # lineCopy を実行し、戻り値をnext_src に格納する
        next_src = self.lineCopy(src_nodes, x_count, offset_x)
        # y も同様
        offset_y = MaxPlus.Point3()
        offset_y.SetY(y_interval)
        next_src = self.lineCopy(next_src, y_count, offset_y)
        # z も同様
        offset_z = MaxPlus.Point3()
        offset_z.SetZ(z_interval)
        next_src = self.lineCopy(next_src, z_count, offset_z)
        # 最後に一応全部終わったあとの状態を返しておく
        return next_src

 このような感じですね。あとはこのgridCopy を実行ボタンのシグナルに接続したスロットの中で呼ぶようにします。ここではdoGridCopy がそれに当たるので、その中身を書き換えましょう。

def doGridCopy(self):
    # 選択されたものが1個かどうかを確認する(ここはMaxScript 版と同じ)
    sel_count = MaxPlus.SelectionManager.GetCount()
    if sel_count != 1:
        mb = QtWidgets.QMessageBox()
        mb.setText(u"コピー元オブジェクトを1つ選択してください。")
        mb.exec_()
        return
    # 自分自身が持っているgridCopy というメソッドを呼ぶ
    self.gridCopy(MaxPlus.SelectionManager.GetNodes()[0], self.spn_x_count.value(), self.spn_y_count.value(), self.spn_z_count.value(), self.dspn_x_interval.value(), self.dspn_y_interval.value(), self.dspn_z_interval.value())
    # ビューポートを更新
    MaxPlus.ViewportManager.ForceCompleteRedraw()

 ポイントは最後のビューの更新で、Python から操作を行うと、画面更新がかかるまでビューポートに反映されない(シーン内は変更されているのに画面が変わらない)という現象が起きます。そのため、画面更新の必要な処理(ここでは存在しなかったオブジェクトが増えるという操作)をした場合は、操作完了時にビューポートの再描画処理を入れましょう。

ついでに

 ここで完成したものをテストしていたら、入力欄のタブストップは横方向、つまりX個数→X間隔、Y個数→Y間隔、となっているほうが便利な気がしてきたので、そのように変更しました。

images/ss28.png

完成!

 ついに完成しました。これで初めてのPython による3dsmax のツール開発がまがりなりにも完成しました。お付き合いいただいた皆様、ありがとうございました。

 最後にGridCopy クラスの全体を置いておきますね。

# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, division
import os
import MaxPlus
import pymxs
from PySide2 import QtWidgets
from PySide2 import QtCore
from MyDialog import MyDialog
import gridCopy_ui as UI
class GridCopy(MyDialog, UI.Ui_Dialog):
    def __init__(self, parent, f_path):
        super(GridCopy, self).__init__(parent)
        self.setupUi(self)
        # ウィンドウタイトルを変更
        self.setWindowTitle("GridCopy")
        # シグナルとスロットのコネクト
        self.pb_exec.clicked.connect(self.doGridCopy)
        self.pb_clear.clicked.connect(self.clearInputs)
    def lineCopy(self, src_nodes, count, offset):
        return_nodes = MaxPlus.INodeTab()
        for n in src_nodes:
            cur_offset = offset + MaxPlus.Point3()
            for i in range(count - 1):
                new_node = n.CreateCopy()
                new_node.Move(cur_offset)
                return_nodes.Append(new_node)
                cur_offset += offset
        for n in src_nodes:
            return_nodes.Insert(n, 0)
        return return_nodes
    def gridCopy(self, src_node, x_count, y_count, z_count, x_interval, y_interval, z_interval):
        # x
        src_nodes = MaxPlus.INodeTab()
        src_nodes.Append(src_node)
        offset_x = MaxPlus.Point3()
        offset_x.SetX(x_interval)
        next_src = self.lineCopy(src_nodes, x_count, offset_x)
        # y
        offset_y = MaxPlus.Point3()
        offset_y.SetY(y_interval)
        next_src = self.lineCopy(next_src, y_count, offset_y)
        # z
        offset_z = MaxPlus.Point3()
        offset_z.SetZ(z_interval)
        next_src = self.lineCopy(next_src, z_count, offset_z)
        return next_src
    def doGridCopy(self):
        sel_count = MaxPlus.SelectionManager.GetCount()
        if sel_count != 1:
            mb = QtWidgets.QMessageBox()
            mb.setText(u"コピー元オブジェクトを1つ選択してください。")
            mb.exec_()
            return
        self.gridCopy(MaxPlus.SelectionManager.GetNodes()[0], self.spn_x_count.value(), self.spn_y_count.value(), self.spn_z_count.value(), self.dspn_x_interval.value(), self.dspn_y_interval.value(), self.dspn_z_interval.value())
        MaxPlus.ViewportManager.ForceCompleteRedraw()
    def clearInputs(self):
        self.spn_x_count.setValue(0)
        self.spn_y_count.setValue(0)
        self.spn_z_count.setValue(0)
        self.dspn_x_interval.setValue(0)
        self.dspn_y_interval.setValue(0)
        self.dspn_z_interval.setValue(0)
images/gridcopy11.gif

 このチュートリアルは本当に初めて3dsmax でPython を使ったツール開発をしてみたいという人に向けて書いたものなので、これでなんでもできる! というようなことはありません。でも入口部分で躓きやすいところはなるべく解説したつもりです。

 一旦これでチュートリアルは終了となりますが、3dsmax のPython 周りについても継続して記事を書いていこうと思っています。なにか要望などもあればぜひ聞かせてください。

まとめ

  • 3dsmax のPythonAPI には複数のノードを格納するデータ型がいくつもある
  • 3dsmax でPython からビューポートの変化を伴い処理をしたら明示的に再描画した方が良い
  • 3dsmax でPython を使ったツール開発をするのは楽しい!(これが一番重要!)