動作を実装する(2) | 3dsmax Python入門 #03

 前回、作ろうとしているツールの動作を考え、それを分解してシンプルな操作を行う関数に分ける、という考え方を解説しました。

 前回は関数を実装するところまでしか説明していなかったのですが、シンプルな機能の関数を実装したら、その都度、その動作を確認しておきましょう。今回はその確認の作業から始めたいと思います。

関数の動作を確認する

 改めて、重複になりますが前回実装したlineCopy という関数を確認します。

def lineCopy(src_nodes, count, offset, copy_type):
    '''
        元オブジェクトを指定した個数、間隔で線形にコピーして並べる
        args:
            list: 元オブジェクトのリスト
            int: コピー個数
            Point3: コピーの移動距離(ベクトル)
            name: コピーの種類(コピー、参照、インスタンス)
        return:
            list: 元オブジェクトを含む、生成されたもののリスト
    '''
    return_nodes = copy(src_nodes)
    for n in src_nodes:
        cur_offset = offset
        for i in range(count - 1):
            _, new_nodes = runtime.maxOps.CloneNodes(n, offset=cur_offset, cloneType=copy_type, newNodes=byref(None))
            return_nodes.extend(new_nodes)
            cur_offset += offset
    return return_nodes

 この関数をGridCopy.py の中に書き、3dsmax でテストしてみます。

定義した関数を評価して使える状態にする

 3dsmax 2023を起動し、スクリプトエディタを開いてGridCopy.py を開いた状態にします。開いた状態で3dsmax を終了すれば、次回起動時にまた開いた状態で起動してくれます。開いたら、GridCopy.py を開いたスクリプトエディタの窓の中をマウスでクリックしてアクティブにし、そこでCtrl+E を実行します。スクリプトエディタ上でのCtrl+E はEvaluate、つまりスクリプト内容の「評価」という動作になります。MAXScript で書いたものを評価するとスクリプトリスナーに何らかの出力があるのですが、Python のものだとコード内に出力が明示されていない限り何も表示されません。表示は何も出ませんが、Ctrl+E をした後、リスナーのPython モードで関数名を入力してEnter すると、画像のように関数が定義されていることを確認できます。なお、コード内で定義されていない名前を入力した場合は図のようにエラーになります。

評価したスクリプト内で定義したものはリスナーから使えるようになる

 このように、GridCopy.py 内で定義したgridCopy とlineCopy はリスナーから使うことができます。試しに定義されていない「abcde」という文字列を実行してみると、「そんな名前は定義されていない」というエラーが返ってきます。

関数をテストする

 この関数は以下の4つの引数を要します。

  • コピー元オブジェクトのリスト
  • コピーする個数
  • コピーを配置する間隔
  • コピーの種類

 テストを行うためにはこれらの引数が必要になるので、これを事前に用意します。まずはコピー元オブジェクトのリストですが、これはオブジェクトとしてシーン内に存在する必要があるので、まずシーン内に適当にオブジェクトを作りましょう。

 引数で求められるのはオブジェクトの「リスト」ですが、ひとまずボックスを1つだけ作りました。このボックスを引数に渡すため、リストに入れておく必要があります。テスト用として、ここでは「選択されているオブジェクトをPythonのリストにして取得する」という方法をやってみましょう。

 テストのポイントとして、先ほどGridCopy.py を評価したので、ファイル内でインポートしておいたものはリスナーからすでに使える状態になっている、ということを覚えておきましょう。

 シーン内で、先ほど作ったボックスを選択した状態にします。その状態でPythonモードのリスナーに以下を入力して実行します。リスナーのPython モードでは、普通にエンターを押すと「改行」という扱いになるので、実行するときはShift+Enter を押します。これは複数行にわたるコードをリスナーで実行するための配慮なので、最初煩わしく感じるかもしれませんがぜひ慣れておきましょう。

sel = list(runtime.selection)

 このコードはMAXScript のグローバル変数selection を引数としてPython のlist 関数を実行する、という意味です。MAXSript のグローバル名前空間はpymxs.runtime に定義されているので、MAXScript におけるselection は、Pythonからだとpymxs.runtime.selection でアクセスすることができます。

 selection はMAXScript のコレクションオブジェクトというものなので、これをPython のリストに変換するため、上記のようにlist 関数を使用しています。実行したらsel の中身を出力してみましょう。

sel に「選択されているオブジェクトのリスト」が取得できた

 次に、コピーする個数を用意します。個数は何個でも良いですが、最初は2 が良いでしょう。個数分ループするというコードの性質上、最低でもループが2回回るようにしたいところです。これも一応変数に格納しておきます。

count = 2

 テストするだけなら別に変数に入れておかなくても良いのですが、テストコードをコピペして何回も実行するために変数に入れておくと便利です。これは後程どういう意味か実際にやってみます。

 三つ目はオフセット、コピーしたものを配置する場所を指定するベクトル情報です。これはMAXScript のPoint3 というデータで渡しますが、Point3データはX、Y、Z それぞれの移動距離を格納したリストのようなデータです。以下のようにして生成することができます。

offset = runtime.point3(30, 0, 0)

 前述のようにPoint3 はruntime 内で定義されているので、runtime.point3 として呼び出します。引数にfloat(浮動小数点数)を3つ渡すことでベクトルを生成することができます。ここでは結果がわかりやすいように、X軸方向にのみ移動するようなベクトルにしておきましょう、

 最後に、コピー種別を指定します。コピー種別はコピー、参照、インスタンスの三種で、それぞれMAXScript の名前オブジェクトで指定します。名前オブジェクトはnameという関数にPython の文字列(str)を渡すことで生成できます。

copy_type = runtime.name("copy")

 これでlineCopy に必要な引数がすべて、リスナーの名前空間内に用意できました。さっそくlineCopy を実行して、シーンがどのようになるか観察してみましょう。

lineCopy(sel, count, offset, copy_type)
テストの実行結果

 シーン内に実行した結果が反映され、さらに、リスナーには戻り値が出力されています。戻り値には元のオブジェクトも含まれていることに注目してください。また、lineCopy 関数はコピーを実行した結果いくつになるのか、という値を「個数」として扱っているため、2を渡した結果としてオブジェクトが2個になった、ということです。

 では次に、改めて今シーンにある2つのボックスを元に、今度はY軸方向に3個コピーしてみましょう。先ほどの実行結果を変数で受け取っていなかったので、改めてシーンのボックス2つを選択し、選択をリストで取得します。

sel = list(runtime.selection)

 これを実行すると、先ほど定義したsel という変数の中身が上書きされ、現在の選択状態がsel に格納されます。さらに、count、offset の値も変更しましょう。

count = 3
offset = runtime.point3(0, 40, 0)

 ここまで実行したうえで、先ほどlineCopy のテストをしたときのコマンドをコピぺしましょう。

lineCopy(sel, count, offset, copy_type)

 このように、テストに使用する引数を簡単なものでも変数に入れるようにしておくと、同じコードを引数を変えてテストするときにとても便利です。

 続いて、今度はここまでの操作でできた6個のボックスを元に、Z軸方向にも実行してみます。今回はcopy_type も変更してみましょう。まず、シーン内でここまでに作った6個のボックスを選択しておき、以下を実行します。

sel = list(runtime.selection)
count = 4
offset = runtime.point3(0, 0, 50)
copy_type = runtime.name("reference")

 このように必要な変数を用意したうえで、さっきと同じlineCopy の呼び出しを実行します。

lineCopy(sel, count, offset, copy_type)
参照コピーしたので、元のものを変更するとコピー先にも反映される

 このように、今回はコピー種別を「参照」にしたので、一番下のオブジェクト(元にしたもの)を編集するとそのコピーたちにも同じ編集結果が反映されているのがわかります。さらに、「コピー」でコピーした部分は変化していないことにも注目してください。

 これで、lineCopy 関数が意図通りに実装されていることが確認できました。さらに、今テストしたXYZそれぞれの軸方向への実行をまとめて行うようにgridCopy関数を実装すればよさそうだ、ということも確認できたと思います。

まとめ

  • シンプルな関数を実装し、それを個別にテストしておく
  • テストに必要な引数を変数に入れておいてテストすると良い
  • 引数をいろいろ変化させて正しく動くか確認する