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

2021年9月3日

実行関数を磨く

 前回のラストで選択オブジェクトをX方向に1つ複製、移動するところまでの実装ができました。

 ここからこれを目的の動作に向けてブラッシュアップしていきます。

X方向の複製動作を実装する

 まずは、X方向に「指定した個数の複製を作る」という動作を実現しましょう。

 単純に考えて、同じ動作を繰り返すのでforループになりそうです。maxOps.CloneNodesの動作をよく思い出して、「何を」コピーするのかを確認しておきます。

 すると、実行したときに新たに生成されたものを元にして次の実行を行えばよい、ということがわかります。

for i=1 to x_count do (
    maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
    src_nodes = new_list
)

 maxOps.CloneNodesの実行部分をこのように、ループに書き換えます。1つ複製するごとに、その新しいオブジェクトをさらに複製する、という動作です。これを関数に渡したx_count分繰り返します。

 実行してみると以下のようになります。

 複製動作を3回繰り返したので、結果として4つのオブジェクトが並びました。

 ここで、この結果はこのツールの動作としてふさわしいのか、という視点で見直してみます。今、X方向の「個数」に3を渡しました。この3を使用して複製を「3回」行ったため、シーンには4つのオブジェクトが並んでいます。これはツールの仕様としてまずいですね。実行結果として3個並んでいる状態が望ましいわけです。

 そこでループの回数を渡された引数‐1にします。

function gridCopy src_node x_count y_count z_count x_interval y_interval z_interval = (
    --------------------------------------
    -- 選択したノードをグリッド状にコピーする
    --  param
    --          node: 元のノード
    --          int: x個数
    --          int: y個数
    --          int: z個数
    --          int: x間隔距離
    --          int: y間隔距離
    --          int: z間隔距離
    --------------------------------------
    local src_nodes = #(src_node)
    local src_list = #()
    local new_list = #()
    local offset = [x_interval, 0, 0]
    for i=1 to x_count - 1 do (
        maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
        src_nodes = new_list
    )
)

 ここまでの結果、gridCopy関数は上記のようになりました。

Y方向の複製を実装する

 同様にしてY方向の複製を考えましょう。

 Y方向は、X方向の複製が終わったあと、その最後の状態を元にして複製を行う必要があります。

 ここで、gridCopy関数の、X方向の複製が終わった時点での各変数に何が入っているかを確認しましょう。

 src_nodesとsrc_listには最後から2番目のオブジェクトが格納されています。new_listには最後の1つが格納されています。

 つまり、X方向の複製が終わった時全てのオブジェクトを格納している変数はありません。Y方向の複製を開始する前に、その元オブジェクトのリストとなる変数を作っておく必要があるわけです。

local src_nodes = #(src_node)
local src_list = #()
local new_list = #()
local offset = [x_interval, 0, 0]
local next_src = src_nodes
-- X方向の複製を実行する
for i=1 to x_count - 1 do (
    maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
    src_nodes = new_list
    join next_src new_list
)
-- Y方向の複製を実行する
src_nodes = next_src
offset = [0, y_interval, 0]
for i=1 to y_count - 1 do (
    maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
    src_nodes = new_list
    join next_src new_list
)

 next_srcという変数を設け、元のオブジェクトと、複製されて新たに作られたオブジェクトを格納していきます。これを、Y方向の複製を始めるときのsrc_nodesとして使用します。

 また、Y方向を実行するときにはoffsetの値もY用のものに変更する必要があります。

 これを実行すると以下のようになります。

 意図通りの結果が得られていますね。

Z方向の複製を実装する

 あとはまったく同様です。Y方向の実行結果をもとにしてZ方向の複製を実装します。全く同じことを繰り返せば良いだけです。

function gridCopy src_node x_count y_count z_count x_interval y_interval z_interval = (
    --------------------------------------
    -- 選択したノードをグリッド状にコピーする
    --  param
    --          node: 元のノード
    --          int: x個数
    --          int: y個数
    --          int: z個数
    --          int: x間隔距離
    --          int: y間隔距離
    --          int: z間隔距離
    --------------------------------------
    local src_nodes = #(src_node)
    local src_list = #()
    local new_list = #()
    local offset = [x_interval, 0, 0]
    local next_src = src_nodes
    -- X方向の複製を実行する
    for i=1 to x_count - 1 do (
        maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
        src_nodes = new_list
        join next_src new_list
    )
    -- Y方向の複製を実行する
    src_nodes = next_src
    offset = [0, y_interval, 0]
    for i=1 to y_count - 1 do (
        maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
        src_nodes = new_list
        join next_src new_list
    )
    -- Z方向の複製を実行する
    src_nodes = next_src
    offset = [0, 0, z_interval]
    for i=1 to z_count - 1 do (
        maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
        src_nodes = new_list
        join next_src new_list
    )
)

 コードはこのようになり、実行結果は以下のようになります。

 ばっちりですね。

 これでこの関数を完成としても良いのですが、やはりX方向の実行、Y方向の実行、Z方向の実行の中身が非常に似通っていて、これをなんとかしたいところです。

似たようなコードをサブ関数に分離する

 では早速、似たようなことを繰り返し書いている部分に注目し、これをサブ関数に分離することを考えます。

 X、Y、Zそれぞれの軸方向へ複製動作を行っていますが、見たところfor文のところが丸ごと分離できそうです。

 変化しているのは、

  • 繰り返しの回数
  • offsetの値

の2つです。また、実行するたびに次の実行時にコピー元となるもののリスト(next_src)が更新されなければなりません。そこで、変化するものは引数で受け取り、next_srcは参照引数で受け取って関数の中で更新する、という設計にしてみます。

function lineCopy src_nodes count offset &next_src = (
    --------------------------------------
    -- 選択した直線状にコピーする
    --  param
    --          node: 元のノードのリスト
    --          int: 個数
    --          Point3:オフセット
    --          &Array: 実行結果のリスト
    --------------------------------------
    local src_list = #()
    local new_list = #()
    for i=1 to count - 1 do (
        maxOps.CloneNodes &src_nodes offset:offset expandHierarchy:false cloneType:#copy actualNodeList:&src_list newNodes:&new_list
        src_nodes = new_list
        join next_src new_list
    )
)

 サブ関数はlineCopyという名前にしてみました。直線上に複製を並べるからです。

 呼び出し側は以下のように変更します。

function gridCopy src_node x_count y_count z_count x_interval y_interval z_interval = (
    --------------------------------------
    -- 選択したノードをグリッド状にコピーする
    --  param
    --          node: 元のノード
    --          int: x個数
    --          int: y個数
    --          int: z個数
    --          int: x間隔距離
    --          int: y間隔距離
    --          int: z間隔距離
    --------------------------------------
    local src_nodes = #(src_node)
    local src_list = #()
    local new_list = #()
    local offset = [x_interval, 0, 0]
    local next_src = src_nodes
    -- X方向の複製を実行する
    lineCopy src_nodes x_count offset &next_src
    -- Y方向の複製を実行する
    src_nodes = next_src
    offset = [0, y_interval, 0]
    lineCopy src_nodes y_count offset &next_src
    -- Z方向の複製を実行する
    src_nodes = next_src
    offset = [0, 0, z_interval]
    lineCopy src_nodes z_count offset &next_src
)

 これで実行すると以下のようになります。

 当然ですがサブ関数を分離する前と同じ結果が得られています。

呼び出し用の関数を用意する

 最後に、実行関数を呼び出す呼び出し用の関数を用意します。今回、この実行関数はPythonスクリプトから呼び出して使いたいのですが、Pythonスクリプトからシーン内のオブジェクトをMaxScriptに渡すのは少々難しいです。単なる文字列や数値は簡単に渡せますが、それ以外の値は何らかの変換処理が必要だったり、あるいは持っていく手段が無かったりすることもあります。

 そこで、コピー元のオブジェクトを取得する部分はMaxScript側でやってしまう、という発想で呼び出し用の関数を用意します。

 今回、gridCopy関数の7つの引数のうち、「コピー元オブジェクト」以外の6つは全部数値です。数値はPythonと容易にやり取りできるのでPythonから受け取るようにします。コピー元オブジェクトだけは面倒なことになるので、呼び出し関数で取得して渡すようにしてみます。

function doGridCopy x_count y_count z_count x_interval y_interval z_interval = (
    undo on (
        gridCopy selection[1] x_count y_count z_count x_interval y_interval z_interval
    )
)

 ついでにUndoも入れておきました。

 doGridCopyでは数値の引数を6つ受け取り、選択されているオブジェクトとともにgridCopyに渡しています。

※この実装はスマートとは言い難く、まったくBest Practiceではありません。今回はPythonツールからMaxScriptの実行関数を呼ぶという趣旨で進めているため、あまり良い実装ではありませんが、このような形で進めます。

まとめ

  • 実行部分はシンプルな状態で実装してから、求める動作になるようにブラッシュアップしていく
  • 出来上がったコードを見て、同じようなことを何度も書いている部分がある場合、それをサブ関数に分離することを考えてみる