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

2021年9月3日

 前回はツールクラスを呼び出すmain()の準備をし、ファイルが直接実行された場合にmain()が呼び出される仕組みを作りました。

 今回はこのmain()を完成させます。

ツール本体のクラスを作る 3

ツールクラスの呼び出し関数を完成させる

 ではツール本体を呼び出すmain()関数を整えていきましょう。

def main(f_path):
    win = GridCopy(MaxPlus.GetQMaxMainWindow(),  f_path)
    win.show()

 この呼び出しについては、3dsmaxの少し前のバージョンではもっと面倒な書き方が必要でした。しかし、3dsmax2020ではこれだけでOKです。

 まず、winという変数を用意し、そこにGridCopyクラスのインスタンスを取得しています。クラス名を関数のようにして呼び出すと、そのクラスのコンストラクタが呼ばれ、クラスインスタンスが得られます。

 そのため、ここでは引数として、GridCopyクラスのコンストラクタが要求するparent(生成するウィジェットの親になるウィジェット)と、このファイルが置いてあるディレクトリのパスを渡しています。

 3dsmaxのウィンドウは、MaxPlusモジュールのGetQMaxMainWindow()という関数で取得することができます。

※この方法も3dsmaxのバージョンによって異なります。バージョンアップされるたびにPythonAPIの見直しが行われ、この辺の関数名や呼び出し方などが変更されているので、継続的にツール開発をする場合には、公式リリースのWhat’s Newをよく読むようにしましょう。

 winはGridCopyクラスのインスタンスで、GridCopyクラスはQDialogクラスのサブクラスなので、winはQDialogウィジェットとして振舞います。

 次の行でwin.show()を呼び出し、ダイアログの表示を実行しています。

 ここまでを実行してみましょう。

 実行するとUIが表示され、「実行」ボタンを押すとMaxScriptリスナー上にdoGridCopyという文字が表示されます。

 ここまで確認できれば、あとはdoGridCopy()の中身を作り込んでいくだけです。

実行メソッドを実装する

 実行メソッド内で行うことをまず整理しましょう。

 動作の流れとしては、gridCopy.msファイルに書いてある呼び出し用の関数doGridCopy()に対し、UIから取得した6つの数値(XYZそれぞれの個数と間隔)を渡し、doGridCopy()は選択されたオブジェクトと受け取った6つの数値をgridCopy()に渡す、ということになります。

 まず、gridCopy.msというファイルに書いてあるMaxScriptの関数を実行するために、これを一度評価する必要があります。一度評価されてからでないと自分で作った関数は呼び出すことができないからです。

 また、doGridCopy()を何も選択されていない状態で実行したらどうなるでしょうか。gridCopy()は内部でmaxOps.CloneNodesに受け取ったオブジェクトを渡します。このとき、何も選択されていないとmaxOps.CloneNodesにundefinedが渡されてエラーになります。

※ちなみにmaxOps.CloneNodesにundefinedを渡すとACCESS_VIOLATION(アクセス違反)という致命的な例外が投げられます。

 そこで、doGridCopy()を呼ぶ前にシーンオブジェクトがちゃんと選択されているかどうかを確認するようにしましょう。また、複数のオブジェクトが選択されていた場合の処理についても考慮していないので、今回は選択されているものが1つだけかどうかを確認するようにします。

選択状態を確認する処理を実装する

def doGridCopy(self):
    sel_count = MaxPlus.SelectionManager.GetCount()
    if sel_count != 1:
        mb = QtWidgets.QMessageBox()
        mb.setText(u"コピー元オブジェクトを1つ選択してください。")
        mb.exec_()
        return

 ではPythonツールクラス内のdoGridCopyメソッドを書き換えていきましょう。仮で書いておいたprint()を削除し、上記のように書き直します。

 まず、sel_countという変数を用意し、MaxPlusモジュールのSelectionManagerクラスのGetCountメソッドにより、現在シーンで選択されているオブジェクト(厳密にはノード)の数を取得します。

 続いて、このカウント値が1かどうかを確認し、1でない場合にはメッセージを表示して処理を終了します。

 メッセージの表示にはQMessageBoxというクラスを使用します。このような実装が一般的です。

 setTextメソッドの引数に文字列リテラルを渡していますが、その先頭にあるuはこの文字列リテラルがUnicode文字列であることを示しています。

 exec_()メソッドでメッセージボックスを開き、メッセージボックスが開いている間処理は待機状態となります。閉じられたら次の行から実行が再開されます。ここではreturnを実行してメソッドを終了しています。

MaxScriptファイルの評価を実装する

 続いてmsファイルの評価を実装しましょう。msファイルの評価にはMaxScriptのfileIn()関数を使います。ここで、読み込みたいmsファイルのフルパスが必要になるのですが、Pythonファイルの__file__変数はGridCopyクラスの外にあるため、GridCopyクラスのインスタンスからは見えません。ここが重要なポイントです。

 このため、Pythonファイルの__file__が持っている情報からディレクトリのパスを取得し、それをGridCopyクラスに渡す必要があるのです。この目的のためにGridCopyクラスのコンストラクタでf_pathという引数を受け取るようにしておいたということです。

 インスタンス生成時にコンストラクタが受け取ったディレクトリパスは、self.f_pathというプロパティに格納されています。これを使ってgridCopy.msを評価しましょう。

script_path = os.path.join(self.f_path,  "gridCopy.ms")
pymxs.runtime.fileIn(script_path)

 こんな感じになります。まずos.path.joinを使用し、ディレクトリパスにファイル名をつないでファイルのフルパスを得ます。

 これをMaxScriptのfileIn()に渡して評価しています。

 PythonからMaxScriptを実行するのに、pymxsモジュールのruntimeクラスを使います。

※fileIn()で評価した内容は、MaxScriptのグローバルスペースに保持されます。つまり、msファイル内で定義した関数はそれぞれグローバルスペースに呼び出され、以後グローバルスペース内で使用できるようになります。

 これでgridCopy.ms内で定義したMaxScript関数が使えるようになりました。あとは実際に呼び出すだけです。

pymxs.runtime.doGridCopy(self.spn_x_count.value(), self.spn_y_count.value(), self.spn_z_count.value(), self.spn_x_interval.value(), self.spn_y_interval.value(), self.spn_z_interval.value())

 長いですが実行は一行だけです。msファイルを評価した後であれば、そのファイル内で定義されている関数はpymxs.runtimeから呼び出せます。(評価する前に呼び出そうとするとNameErrorが発生します)

 引数にはUIのSpin Boxで設定されている値を取得して渡しています。ここでもSpin Boxのオブジェクト名が必要になってくるので、このオブジェクト名はわかりやすい名前にしておいた方が便利です。

 Spin Boxの現在の値はvalue()メソッドで取得できます。

処理終了後にビューポートを更新する

 最後に、実行後の状態をビューポートに描画します。PythonAPIを使用するケースで、処理後にビューポートが自動更新されないことがよくあります。そのため、ビューポート内が変化するような処理を行った場合には、強制的にビューポートを再描画しておくと良いです。

 ここまでを書き終え、GridCopyクラスのdoGridCopy()メソッドは以下のようになりました。

def doGridCopy(self):
    sel_count = MaxPlus.SelectionManager.GetCount()
    if sel_count != 1:
        mb = QtWidgets.QMessageBox()
        mb.setText(u"コピー元オブジェクトを1つ選択してください。")
        mb.exec_()
        return
    script_path = os.path.join(self.f_path,  "gridCopy.ms")
    pymxs.runtime.fileIn(script_path)
    pymxs.runtime.doGridCopy(self.spn_x_count.value(), self.spn_y_count.value(), self.spn_z_count.value(), self.spn_x_interval.value(), self.spn_y_interval.value(), self.spn_z_interval.value())
    MaxPlus.ViewportManager.ForceCompleteRedraw()

 無事に、PySide2のUIから情報を取得してMaxScriptの関数を実行するツールができました。

 これで一通りの実装はできましたが、次回は各部の改善を考えてみましょう。

まとめ

  • 呼び出し関数でツール本体のクラスインスタンスを生成し、show()メソッドを呼ぶことで3dsmax上に表示する
  • 必要なMaxScript側の関数はmsファイルにまとめ、fileInで評価することで使えるようにする
  • PythonからMaxScript関数を使用するにはpymxs.runtimeを使う