前回は関数を実装するところまでしか説明していなかったのですが、シンプルな機能の関数を実装したら、その都度、その動作を確認しておきましょう。今回はその確認の作業から始めたいと思います。
改めて、重複になりますが前回実装した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 の中身を出力してみましょう。

次に、コピーする個数を用意します。個数は何個でも良いですが、最初は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関数を実装すればよさそうだ、ということも確認できたと思います。
この部分は以前の記事と重複しますが、次のような動作をするツールを考えます。

このように、あるオブジェクトを、X軸、Y軸、Z軸方向にそれぞれ指定した個数、間隔でコピーするものを考えます。以前のチュートリアルでは単純にコピーしただけでしたが、今回のツールではコピー、インスタンス、参照を選べるようにしてみましょう。
まず、ユーザに入力させる情報をどうするか考えます。必要な情報をリストアップしてみます。
これらの情報をユーザから受け取り、実行すると結果が得られる、という動作が想定されます。情報を受け取る部分はUI(ユーザインターフェース)が受け持ちますが、一旦UIは後回しにし、まずは機能の部分だけで実装を進めていきましょう。あとでUIから操作できるようにする必要があるので、それを見越した設計をしていきます。
動作として想定されるのは、必要な情報を入力した上で実行ボタンを押すと結果が得られる、というものです。大掛かりなツールであれば実行部分もクラスとして設計したほうが良いのですが、今回は実行する内容が非常にシンプルなので、関数群(モジュール)として実装していこうと思います。
今回はクラスの詳しい説明は省略しますが、クラスというのは簡単に言うと「何らかのデータとそのデータを操作する関数をセットにしたもの」なので、クラスにするか関数群にするかは、その関数によって変更されるデータを保持しておく必要があるかどうか、というあたりで判断すると良いと思います。今回のツールはシーンの状態を操作するだけで、結果の状態を保持しておく必要はないのでクラスにする必要はないという判断です。(チュートリアルの後半で説明しますが、機能とUIを合体させるところで最終的にはクラス化します)
それほど大規模なツールを作るわけではないので複雑なフォルダ構造は必要ありませんが、このツールに関連するファイルだけは一つにまとまっていたほうが開発しやすいので、ツール名のフォルダを作ってその中にファイルを置くことにしましょう。サブフォルダが必要になったら適宜追加するとして、まずはツール名のフォルダを作ります。ここではツールの動作から「GridCopyTool」という名前にし、その中にGridCopy.py というファイルを置くことにします。

Pythonがインストールされていると、拡張子.py はPython のアイコンになります。このGridCopy.py に関数を書いていきましょう。
ここから実際の関数を考えますが、まず最初の方針として、必要なデータはすべて引数として受け取るようにしましょう。クラスのメンバ関数の場合は、メンバ変数をメンバ関数内で使用するといった実装も行いますが、単体の関数として設計するのであれば必要なものはすべて引数として受け取り、関数の外にある変数などを使わないような設計にしておくのが良いです。そうすることでこの関数の再利用性が高まり、コードの可読性(読みやすさ)も高まります。
ではさっそく、今回の実行の本体になる関数を実装していきましょう。まずは関数の枠を書きます。その前に、ファイルの先頭にライブラリのインポートを書きましょう。必要なものが出てきたら都度ファイルの先頭に書き足していきます。ここではまず、3dsmax のツールを作るときに欠かせないものを書いておきましょう。
from pymxs import runtime実は最近の3dsmax では起動時にすでにpymxs はインポートされた状態になっているのですが、コードとしては書いておかないとコード単体で読むときにわからなくなるので、明示的にインポートしておきます。pymxs全体をインポートしても良いのですが、必要なものを限定して書いたほうが何をインポートしているのか意識するようになるので、個別にインポートする習慣をつけておくのが良いと思います。
まず関数が受け取るべき引数を考えます。必要なのは
このような感じです。けっこうたくさんの引数が必要ですね。全部で8個になります。この関数は結果を返すのではなく、シーン自体を操作するものなので、一旦戻り値はなしで書いていきます。(成否を返す関数にすることもできますが、それは追々考えることにして、一旦は戻り値なしで書いていきましょう)
ポイントとして、関数を考えるときはまず受け取る引数と関数名を考え、なにも中身のない関数として枠だけ作る、というのがお勧めです。
def gridCopy(src_node, x_count, x_interval, y_count, y_interval, z_count, z_interval, copy_type):
pass関数を作るときは、まずこのような枠だけ作り、何を渡して何が返るのかを考えると良いです。
このgridCopy という関数は枠を見るとわかるように、かなり多くの引数を受け取ってけっこう複雑な処理をすることになります。ちょっと複雑なので、ここで最初にやった手動の操作を振り返ってみましょう。重複になりますがもう一度さっきのgif を見てみます。

この動作を言葉で説明してみましょう。
という動作です。最初に選択されたものをコピーし、コピーされた結果のものを選択してまたコピー、という動作をx、y、zそれぞれの軸に対して行っています。このgifでは最初にy軸方向へコピーしていますが、x軸方向を先に行っても同じなので、ツールではx→y→zの順に行いましょう。「元になるものを等間隔に複数コピーする」という動作が共通なので、ある軸方向にこれを行う動作をサブ関数として実装します。
このような複雑な一連の操作を行いたい場合、その操作を解きほぐして、シンプルな動作の組み合わせにできないかを考えましょう。そのうえで、一つ一つは単機能になる関数を作り、それを組み合わせて複雑な処理を実現するという発想で考えるのが良いでしょう。
ここでは、「元のオブジェクト」「コピーする個数」「コピーする間隔」の三つの情報を受け取って1方向にコピーするlineCopy という関数を考えましょう。これをそれぞれの軸方向に3回実行することでgridCopy を実現します。lineCopy も枠だけ書いてみましょう。
def lineCopy(src_nodes, count, offset, copy_type):
pass枠はこんな感じです。元オブジェクトのリスト、コピー個数、コピー間隔、コピー種別の4つを受け取ってコピー操作を実行します。元オブジェクトをリストにするのは、gridCopy における2回目以降の処理に対応するためです。つまり1回目は元オブジェクト1つだけが入ったリストを受け取る、という想定です。
これだとコピーする軸方向が指定できないような気がしますが、offset(移動距離)をPoint3というベクトルのデータで受け取ることでコピー方向を指定する想定です。どの引数がどんな型(タイプ)なのかわかるように、コメントに書いておきましょう。
def lineCopy(sec_nodes, count, offset, copy_type):
'''
元オブジェクトを指定した個数、間隔で線形にコピーして並べる
args:
list: 元オブジェクトのリスト
int: コピー個数
Point3: コピーの移動距離(ベクトル)
name: コピーの種類(コピー、参照、インスタンス)
'''
pass※ 最近のPython は引数や戻り値の型を指定して書く方法もあるのですが、そこを説明すると少し難しくなるので、このチュートリアルでは旧来のPythonの書き方で、型は自動識別を使用するように書いていきます。
※ ここで書いたPoint3 やname というのはPythonに用意されている型ではなく、pymxs のruntime モジュール内で定義されている型です。そのため、pymxs.runtime がインポートされている状態でないと使えません。
ここで、このlineCopy という関数は最終的にはgridCopy という関数の中で呼ばれる、ということを想定しておきます。使われ方として、まず元のオブジェクトを選択し、x軸方向にlineCopy を行います。次に、x軸方向にコピーされたものたちを元オブジェクトとしてy軸方向にlineCopy を行います。つまり、lineCopy した結果できたもののリストが次のlineCopy の引数になるということなので、コピーしてできたオブジェクトのリストをlineCopy 関数から返す必要があるわけです。
では関数を書いていきましょう。
def lineCopy(src_nodes, count, offset, copy_type):
# 戻り値として返すための配列を用意し、元オブジェクトとして渡されてきたものをコピーしておく
return_nodes = copy(src_nodes)まず、戻り値を返すためにreturn_nodes というリストを用意します。これは次のlineCopy の元オブジェクトになるリストなので、今回元オブジェクトとして渡されてきたものも含まれている必要があります。
ここで、copy という関数を使っていますが、この関数はリストオブジェクトのコピーを生成して返すという関数です。Pythonでは、通常の代入でlist を扱うとコピーはされず、同じものを参照するような動作になります。このため、物理的に別物として扱いたい場合は「同じ要素を持つリストをもう一個生成する」という動作を行う必要があります。copy 関数はそれを行うためのもので、copy というモジュールに入っています。そのためインポートセクションに以下を追加してください。
from copy import copy次に、渡された元オブジェクトのリストに含まれているものを、指定された移動間隔で配置しながらコピーする処理を書きます。
# 元オブジェクトリストに含まれているオブジェクトそれぞれに対して実行
for n in src_nodes:
# 今対象になっているオブジェクトの配置先(移動距離)をcur_offset とする
cur_offset = offset
# 指定個数分のコピーを行う
for i in range(count - 1):
# オブジェクトをコピーする処理複数のオブジェクトを複数個コピーするので、二重のfor ループになっています。もっと効率の良い方法があるだろうという話はありますが、ここではコードのわかりやすさを重視してこのまま二重ループで進めていきます。
最初なので、少々冗長ではありますが、ここでの「オブジェクトをコピーする処理」の部分をどうやって実装するのか、リファレンスの読み方も含めて説明していきます。
3dsmax 2023 のPython 開発で主流なのはpymxs というモジュールを使う方法です。このモジュールはpython からMaxScript の機能を使用するものなので、MaxScript の知識が必要です。目的の機能を実装するのに調べるべきは、MaxScript のヘルプなのです。
3dsmax のメインメニューからスクリプト→MAXScript ヘルプ を開きます。最近のヘルプはオンラインヘルプなのでWebブラウザが起動して情報が表示されます。このMAXScript ヘルプを使って目的の機能を探します。
今回使うのは、maxOps というインタフェースの中にあるcloneNodes という関数です。該当部分のリファレンスは以下のようになっています。

MaxScript のリファレンスは非常にわかりやすく丁寧な説明がされているので便利です。渡すべき引数とその内容が細かく説明されています。これを見ると、cloneNodes に渡さなければならない引数は最初のnode (元オブジェクト)だけで、それ以外は全部オプション(渡さなかった場合はデフォルト値が使われる)だということがわかります。元オブジェクトだけ渡して実行すると、元のオブジェクトと同じ位置にコピーが生成される、という動作になります。
基本的にはこうやって調べた関数をpymxs のruntime モジュールから使えば良い、という話なのですが、この関数は参照渡しの引数があるので少々ややこしいです。今回はその説明を詳しくしていきます。
長い小見出しになってしまいましたが、おそらく最初ここで躓く人が多いのではないかと思うのでちょっと冗長な説明をしてみたいと思います。
参照渡しというのはMaxScript で”&”をつけて渡す引数のことです。ここでは、cloneNodes 関数によって生成された新しいオブジェクトを受け取るために、キーワード引数のnewNodes に配列を渡す必要があります。はじめに、MaxScript で書くとどうなるのかやってみましょう。
-- MAXScript
src = Box()
new = #()
maxOps.cloneNodes src newNodes: &new
newこのコードは
という動作をします。実行してみましょう。

このように、Boxオブジェクトを生成し、それをコピーしたうえで新たに生成されたものを受け取る、という動作ができます。新たに生成されたものは戻り値としてではなく、参照引数で渡した変数の内容が更新されるという形で受け取ったことに注目してください。
ちなみに、このcloneNodes はさらに移動距離を渡すoffset という引数にも対応しているので、offset と newNodes を渡してこれを実行すれば今回欲しいlineCopy の機能は実現できそうです。
ここで問題になるのが、参照引数の渡し方です。ちょっと直感的にはわかりにくい書き方になるのでじっくりついてきてください。
pymxs モジュールには、MaxScript の参照引数を使うためのbyref という型が用意されています。これをインポートして使うのですが、ちょっととっつきにくい書き方になります。上記のMaxScript をそのままPython で書き直してみましょう。
from pymxs import runtime, byref
src = runtime.box()
# 参照渡しの関数を使用すると、関数の実行結果と、参照引数として受け取る内容のものが返ってきます。
# 戻り値が複数になるため、複数の変数で受け取ります。
# ここれは関数の戻り値をr で受け、newNodes に渡して得られるはずの参照引数の中身をnew で受け取っています。
r, new = runtime.maxOps.cloneNodes(src, newNodes=byref(None))
print(r)
print(new)
結果はこのようになります。
byref を使う関数を実行すると、MAXScript で実行したときの戻り値のほか、参照引数として受け取れるはずのものが戻り値として返ってきます。Python での書き方は、受け取る変数を用意しておき、その参照引数を渡す箇所にbyref(None) を渡す、という感じになります。
これを踏まえてlineCopy のコピー部分を考えましょう
# 元オブジェクトリストに含まれているオブジェクトそれぞれに対して実行
for n in src_nodes:
# 今対象になっているオブジェクトの配置先(移動距離)をcur_offset とする
cur_offset = offset
# 指定個数分のコピーを行う
for i in range(count - 1):
# オブジェクトをコピーする処理
# 受け取るけれど使用する予定のない戻り値は_ で受けるのが慣例
# コピー種別も渡すようにしておく
_, new_nodes = runtime.maxOps.CloneNodes(n, offset=cur_offset, cloneType=copy_type, newNodes=byref(None))
# new_nodes で受け取ったものを最終的に返すreturn_nodes に追加する
return_nodes.extend(new_nodes)
# cur_offset に offset 1つ分を足す
cur_offset += offsetこのような形になります。これを入れてlineCopy を完成させましょう。
def lineCopy(src_nodes, count, offset, copy_type):
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
関数の最後で出来上がったreturn_nodes を返して終わります。
かなり長くなったのでこの辺でいったん中断しましょう。
3dsmax 2023にはPython 3.9.7 が同梱されています。この、3dsmax に標準でついているPython を使っても開発はできるのですが、UIを作る際に便利なツールが含まれていないため、別途独立したPython の環境を用意したほうが良いでしょう。
まずpython の公式サイトであるpython.org にアクセスします。なにかとお世話になるサイトで、URLもシンプルなので覚えてしまいましょう。

メインメニューからDownloads→Windows と進みます。

この画像の時点では最新の安定板は3.11.3のようですね。

インストールするPythonは3.9.7以降であればなんでも構いませんが、あまり詳しくない人は3.9系にしておくと無難です。この項の執筆時点で3.9系の最新は3.9.16ですが、Windows用のインストールバイナリは3.9.13までしか出ていないようなのでここでは3.9.13を入れてみます。もちろん3dsmax 2023と同じ3.9.7を入れても問題ありません。
表示されるリストの中から3.9系で「Windows installer(64-bit)」が用意されているものを探します。基本的にページ上部にあるものほど新しいですが、Windows用のインストーラが用意されていないバージョンもあるため、インストーラのリンクが存在しているバージョンを探しましょう。この時点では3.9系でインストーラができている最も新しいバージョンは3.9.13でした。
ダウンロードしたインストーラを実行してインストールします。

インストーラの最初の画面で、まず下の方にある「Add Python3.9 to PATH」という項目にチェックを入れます。これは環境変数にPythonのパスを追加するという意味ですが、その意味が分からない人はチェックを入れておきましょう。わかる人は任意でチェックを入れるか判断してください。Pythonの複数バージョンを共存させている場合等、PATHに追加しないほうが良いケースもあります。
準備ができたら上の「Install Now」というところを押すとインストールが始まります。インストール先がPython2系のときとは変更されています。Windows10のUAC(ユーザ・アカウント・コントロール)に対応するため、インストール先がユーザディレクトリになっています。古くからPythonを使っている人はこれを嫌って以前のようなパス(C:¥Python 等)に変更している人もいますが、Win10以降ではこの運用が標準なのでこちらに慣れたほうが良いと思います。
以降はインストーラに従ってインストールすればOKです。特に気を付けるべきところはありません。
インストールが終わったら確認してみましょう。スタートメニューでcmd と打って検索し、コマンドプロンプトを開きます。この操作もしょっちゅう使うので覚えておきましょう。Winキー+Rでcmd と打って起動する方法でも構いません。ツール開発者たるもの、コマンドプロンプトは息をするように起動して使えるようにしておきましょう。

起動したらコマンドプロンプトに以下のコマンドを入力して実行してみてください。
python -V
このように、今インストールしたPython のバージョンが表示されれば成功です。
3dsmax のツール開発でUIを開発するのに使用するPySide2 というライブラリパッケージがあります。これをインストールしましょう。PySide2 は3dsmax にも入っているのですが、普通にインストールすると一緒に入るはずのツール群が入っていません。そこでこのように別途インストールしてそのツール群を取得しようというわけです。
Python には追加のライブラリパッケージを管理するパッケージマネージャとして、pip というツールが用意されています。現在のPythonでは標準でインストールされているため、Python本体を入れるだけで使える状態になっています。Pythonをインストールしたらまずpipをアップグレードする、というのはセットにして覚えておきましょう。
コマンドプロンプトに以下のコマンドを入力します。
python -m pip install --upgrade pip
これはpip というパッケージマネージャを使用して、pip 自身を更新する、というコマンドになります。pip が最新になっていない場合、pip を使って何かをインストールするたびに「pipが最新じゃないよ」というメッセージが出るので、Python をインストールしたらまずpip をアップグレードしてしまうと良いでしょう。
pip がアップグレードできたらPySide2を入れましょう。
pytohn -m pip install PySide2余談ですがここで使用している-m というオプションは、python を起動し、-m の後ろに書いてあるモジュールをロードする、という意味です。モジュールをロードしてそのモジュールのコマンドを使用する場合にこういう書き方をします。
上のコマンドを実行すると次のようになります。

pip でのインストールは「Successfully installed 〇〇」と表示されれば成功しています。ここではPySide2をインストールしましたが、インストールされたのはPySide2の5.15.2.1 というバージョンで、一緒に依存パッケージであるshiboken2 もインストールされたことがわかります。
ここまででPySide2がインストールできたわけですが、これをインストールすることによって一緒にインストールされるツールの中に、「Qt Designer」というPySide2を使ってUIを作るためのGUIツールがあります。3dsmax2023 にはPySide2は入っていますがこのQt Designer が含まれていないので、このように独立した環境でインストールしたわけです。
ではこのQt Designer がインストールされている場所を確認し、スタートメニューやタスクバーにPinしたり、ショートカットを作成したり、各自起動しやすい方法で登録しておきましょう。
Python3 系はデフォルトでは%LOCALAPPDATA%\Programs\Python 内にインストールされます。pip でインストールしたPySide2 はその中の以下のパスにあります。
%LOCALAPPDATA%\Programs\Python\Python39\Lib\site-packages\PySide2
このdesigner.exe というのがQt Designer というツールです。これを右クリックし、スタートメニューかタスクバーにPinするか、ショートカットとしてデスクトップに置いておくと良いでしょう。
まいどご無沙汰しております。おかげさまで3dsmax のツール開発に関するチュートリアルが細く長く読んでいただけているのですが、内容的にもはや古くて使えない(3dsmax2022以降では使えない)ため、最新の状況にアップデートして、3dsmax におけるPython を用いたツール開発のチュートリアルとして新たに書こうと思います。
これを書いているのは2023年5月、私の所属しているプロダクションでは今のところ3dsmax 2022が主力、2023の検証が進んでいて、今後2023を使うプロジェクトも増えてくるかな、といった状況です。
今回新たに初歩的なチュートリアルを書いてみるにあたり、この記事はその第0回として、2023年5月現在の3dsmax の事情について書いてみます。
個人的に大きな問題だったのは、以前のチュートリアルでも使用していたMaxPlus というクラスが丸ごと廃止になったことです。厳密には廃止というか、Python2.7用として用意されていたMaxPlusについてPython3 では提供しない、という決定がされたという形でした。これのおかげで既存社内ツールの大幅な書き直しが発生して「超」大変でした。
この話はあまり初心者向けではないので読み飛ばしていただいて構いませんが、MaxPlus の廃止は個人的にかなりショッキングな出来事だったので少し書いておきます。
MaxPlus というのは3dsmax に用意されているC++API(SDKとして提供されているもの)をPython でラップしたラッパーのようなパッケージで、これを使うことで、C++API を使わないとできなかったようなことがかなりの部分でPythonのみで書けるようになりました。私はこれに大喜びし、積極的にMaxPlus を使って3dsmax 2020までのツール開発を行っていました。
ところが、3dsmax が2020から2021になるとき、Python 環境は3のほうがメインになり、MaxPlus はno longer available という状態になりました。Python3への移行は必至だし、そうなれば過去のツールが何一つ使えないという状態が見えてきました。
MaxPlus はものすごく強力なパッケージだったのですが、内容がC++APIのラッパーに過ぎなかったこともあり、MaxScriptからPython に手を出していった多くの開発者にとってとっつきにくいものだったのだと思います。そのためユーザが増えず、Autodesk としては、「これいらないんじゃね?」という話になったのではないかと邪推しています。たしかに作ってる側からすればコスパ悪い状態だったのでしょうね。
うちの社内では、3dsmax2021はスキップし、主力は2020→2022へと移行していきました。支援ツールについても、3dsmax 2022をターゲットにし、Python2から3へのリライト、MaxPlus からpymxs へのリライトなどを進めました。一部MaxPlus でないと使えない機能については、C++ を使ってMaxScript を拡張し、それをpymxsから呼ぶ、といった回りくどいやり方で実現したものもあります。このコストをどうしてくれるんだAutodeskさん…
さらに、3dsmax 2022用に開発したツールが、2023では動かない問題もありました。これはPythonのマイナーバージョンが違うことにより、.py ファイルをコンパイルして生成されるバイトコード.pyc ファイルの中身が異なる、という現象に起因したものでした。
配布用のインストーラにはpyc ファイルを同梱するようにしていたため、2022用のpycバイナリが2023では動かないという問題はけっこう大きく、2022用と2023用を別ツールとして用意しなければならない、という状況になりました。3dsmax 2021、2022は3.7系、2023は3.9系であることがこの問題の原因です。今後2024以降でもPythonのマイナーバージョンが更新されることは十分想定できるので、今後3dsmax のバージョンごとにインストールバイナリを用意しなければならない、という事態になりそうです。
以上のような状況を踏まえて、2023年時点での3dsmax のPython開発入門という位置づけで、UIを持ったかんたんな支援ツールの開発をどういう風に始めれば良いのか、という部分について、詳しく解説したチュートリアルを書いていこうと思っています。このチュートリアルについては過去のチュートリアルと同様、全く初めての人がこれから始めるという状態を想定して、かなり冗長に説明していくつもりです。
一方で、ある程度書いている人向けの話というのも発信していくと良いかなと思うので、チュートリアルとは別にそういう記事も書いていこうかなと思っています。ひそかに期待してもらえると嬉しいです。
けっこうこのブログ全般、ほとんど反応はないけれど実は見られているらしい、という状態で、書いている本人としてはなかなかモチベーションが上がらないので、もし役に立ってましたらSNSでのシェアとかしていただけますとアガります。それによって更新頻度が上がるかは不明ですが、要望などもあれば寄せていただけると記事を書く参考になります。どうぞお気軽にご活用くださいませ。
The post [2023版]3dsmax Python開発事情|3dsmax python入門 #00 first appeared on Taguma CG Lab.]]>さて、そんな具合に管理方面の仕事が増えてきて自分がコードを書くことはだいぶ減っているのですが、過去に作ったもののメンテやら何やらを中心に一応実作業もしてます。そんな中で、3dsmax2023で、2022まで普通に動いていたマクロスクリプトをそのまま持ってくるとエラーになるケースがあり、どうもブロックコメントがNGであるということがわかったので紹介します。
※ 追記あり。コメントで指摘を頂き、ブロックコメントが全部NGなのではなく、マクロ宣言と本体の間に書くのがNGだということがわかりました。(2022以前ではOKだったのでやはり2023からの変更ではありますが)
まず前提知識として、MaxScriptにおけるコメントの記述に関しておさらいしておきます。そんなこと知ってるという人は読み飛ばしてください。
多くのプログラム言語には実行コードとは関係ないコメントを記入する書式が用意されていて、これまた多くの言語で、ブロックコメントとインラインコメントの二つが用意されています。MaxScriptの場合は以下の二種類です。
-- 1行コメント
-- 「--」 と記入するとそこから後ろがコメントになる
a = 0 -- aを0で初期化 といった具合に行の途中からコメントにすることもできる
/*
ブロックコメント
/と* を並べて書くととそこからブロックコメントが始まり、
改行を含んで複数行のコメントを書くことができる。*と/を並べて書いたところで終わる。
*/ようやく本題。今回、3dsmax2022 以前のバージョンに向けて作成、配布していたツールを2023 でも使えるように用意していたところ、3dsmax を起動した際にマクロがロードされない、という事態が発生しました。スクリプトリスナーを見ると下記のようになっていました。

ここで検証したコードを載せておきます。
わずかこれだけ。これを、Usermacroフォルダに格納し、3dsmax2023 を起動すると上記のようにリスナーにエラー(というか黒文字なのでエラー扱いではない?)が出て、このマクロは評価されていない状態になります。
ここでやっかいなのは、おかしいな、と思ってこのマクロスクリプトをスクリプトエディタで開き、そこで評価(Ctrl+E)すると、なんの問題も起きずに評価され、このマクロが使えるようになるんですね。起動時の読み込みでのみ、エラーになります。なんでだよ。
解決策は簡単で、ブロックコメントを使わずに全部一行ずつコメントにすればOKです。
ブロックコメントでマクロ宣言よりも前に書く、のが良いようです。(詳細後述)
こうすると起動時のエラーは出なくなり、正常にロードされます。
ブロックコメント使うのやめましょう、という話です。他の言語でもドキュメント自動生成ツールみたいなものを使う際に、ブロックコメントよりも通常の一行コメントの方が推奨されていたりするケースがありますし、なるべくコメントは一行コメントで書いた方が良さそうだと思っています。
kotenpa さんからコメントで、ブロックコメントが全部NGなのではなく、マクロ宣言と本体の間に書くのがNGだというご指摘をいただきました。検証してみたところ以下のようなことがわかりました。
/*
マクロ宣言より前に書くのはOK。
3dsmax 本体に最初から搭載されているマクロはここにブロックコメントで説明が書いてある。
*/
MacroScript TestMacro
ButtonText:"TEST"
Category:"TG_Tools"
/*
マクロ宣言の後、本体のブロックが始まる前の部分に書かれたブロックコメントはNG
僕は2022までここにマクロの説明を書いていたのでことごとくエラーに…
*/
(
/*
マクロ本体の中ならブロックコメントを書いてもOK
*/
print "test done."
)
マクロ宣言を書いた後、マクロの本体が始まる前(マクロ本体の()の前)にブロックコメントを書くとNG(一行コメントならOK)で、マクロ本体の中であればブロックコメントもOK、という挙動のようです。この挙動は3dsmax2022 には無かったのでやはり2023から何か変化したのだと思います。
どうするのがベストプラクティス?と思って、3dsmax に標準搭載されているマクロを見てみたところ、マクロ宣言よりも前にブロックコメントで説明が書かれていました。ちなみにサードパーティのV-Rayのマクロは先頭がマクロ宣言で、本体内に一行コメントでコメントが記載されているのみでした。
僕は個人的に、マクロ宣言の後にそのマクロの説明をブロックコメントで書いておいてから本体を書く、という感じにしていたのでこれが3dsmax2023でエラーになった、という事態でした。いったん一行コメントに直すという方法で対処したのですが、公式のマクロに習い、マクロ宣言よりも前に出す、という形式にしようと思います。
The post 3dsmax2023 でマクロにブロックコメントを使うと起動時にエラーになる first appeared on Taguma CG Lab.]]>PySide2 を使ってアプリケーションを作るとき、重めの処理を行うとその間UIが「応答なし」になってしまう。自分しか使わないツールならば別にこれでも良いのだけれど、やはり他の人が使う場合に、ソフトウェアが停止したのか処理中なのかわからない、というのはよろしくない。
そこで、重い処理をしている間プログレスバーを表示したい。どのぐらい時間がかかるかわからない処理ではあるものの、処理中であることを明確にして、少なくともソフトウェアが停止したわけではないと見てわかるようにしたい。
処理中にプログレスバーを持ったダイアログを別窓として開き、終了時に自動的に閉じるようにしたい。そのためモードレスのダイアログを用意してそれを開く。最初に書いたコード(抜粋)がこちら。
# 仮の何か時間のかかる処理
def heavyProcess(self):
time.sleep(10)
# 実行ボタンのハンドラ
def on_btn_clicked(self):
# 時間のかかる処理を別スレッドで実行
th = threading.Thread(target=self.heavyProcess, daemon=True)
th.start()
# 処理を開始したあとにメインスレッドでプログレスバーのダイアログを表示
pg = ProgressDialog(self)
pg.open()
# Thread.join() で別スレッドの終了を待って
th.join()
# プログレスバーの窓を閉じる
pg.accept()threading モジュールを使って別スレッドを立て、時間のかかる処理を走らせる。その間にメインスレッドでダイアログを表示し、別スレッドの終了を待ってダイアログを閉じる。
なんとなく、これで行けるような気がしたのだがうまくいかない。

こんな感じで、ダイアログ自体は開くものの、中身が表示されないまま「応答なし」になる。別スレッドの処理中はこの状態が続き、スレッドの終了とともにダイアログが閉じられて以降は正常動作に戻る。
ためしにこのダイアログをモーダルなダイアログとして表示してみる。
# 処理を開始したあとにメインスレッドでプログレスバーのダイアログを表示
pg = ProgressDialog(self)
# exec_() で開くとモーダルダイアログになる
pg.exec_()
# Thread.join() で別スレッドの終了を待つ
th.join()これは一見うまくいく。

ただ、これはモーダルなダイアログ、つまりこのダイアログを閉じるまでメインスレッドが止まったまま、という状態なので別スレッドの処理が終わってもダイアログは表示され続けてしまう。これだと別スレッドがどうなっているか全然わからないためプログレスバーの意味がない。
あれこれ模索して結局このモードレスなUIが応答なしになる現象から逃れられなかった私はTwitter に救いを求めた。
するとCG関係のエンジニアリング界隈で有名なchiyamaさん(@chiyama) が教えてくださった。
これがビンゴ。
# ダイアログはモードレスで開く
pg.open()
# スレッドが生きている間、定期的にQApplicatiln.processEvents() を呼ぶ
while th.is_alive():
QtWidgets.QApplication.processEvents()
# スレッドが終了するとis_alive() がFalse になってwhile を抜ける
pg.accept()
なお、QApplication はPySide ではQtGui に含まれていたけれど、PySide2 になってQtWidgets というモジュールに移されたのでこのような呼び方になる。場合によってwhile 節の中でtime.sleep() などで呼び出しサイクルを設定しても良いかもしれない。
なお、教えてくれたchiyama さんはCGWorld の連載「痴山紘史の日本CG見聞録」などで有名な方です。助かりました。
さて、一部で好評をいただいている3dsmax のスクリプティング初心者向けチュートリアルですが、記事が埋もれていて探しにくい状態が続いていたので、固定ページにまとめました。
3dsmax のMaxScript の入門チュートリアルと、Pythonを使用するツール作成のチュートリアルです。Python の方のものはMax2020対応版で、3dsmax 2021のPython2 モードでは動きますが、Python3モード、および3dsmax 2022以降では動きません。そのため、これの3dsmax2022 対応版も今後追加しようと考えております。
このチュートリアルはなかなか他では説明されていない部分に触れた内容になっているため、これから3dsmax でのスクリプティングを始めようとする人はもちろん、既にある程度やっている方でも発見があるかもしれません。ぜひご活用いただければと思います。
The post チュートリアルのまとめページを作りました first appeared on Taguma CG Lab.]]>原因を調査したところ、いくつかのプラグインに含まれているJavaScript でエラーが発生していました。WordPress の最近のバージョンアップでプラグイン周りの大きな変更があったようで、更新頻度の低いプラグインはこれに対応できておらず、正常に動作しないことが判明しました。
使用していたテーマも一部機能が正しく動かない状態になっていました。
という対策を行いました。表示されていなかった記事について、正しく表示される状態になったことを確認しました。
ご迷惑をおかけしました。正しく表示されていない記事などありましたら、Twitter @tagdev1 までお知らせいただけるとありがたいです。
The post 表示の不具合について first appeared on Taguma CG Lab.]]>3dsmax ではオブジェクトを選択してビューポート上で個別にシースルー表示する、というのが簡単にできますが、Maya ではどうやるのかな、と疑問に。
どうやらMel でできるぞってことで書いてみました。
{
int $state[];
int $val;
$sel = `ls -sl`;
for($n in $sel){
$state = `displaySurface -query -x $n`;
print $state;
$val = $state[0] ? 0 : 1;
displaySurface -x $val $n;
}
}
シェルフに置いておくと超便利!
The post Mayaで選択オブジェクトの透過表示を切り替えるMel first appeared on Taguma CG Lab.]]>このエントリではスクリプトを使用しない方法を紹介します。追ってスクリプトから操作する方法も紹介したいと思います。
3dsmax ではシーン内のオブジェクト(厳密にはノード)に対してカスタムアトリビュートを持たせることができます。
カスタムアトリビュートというのは文字通り、カスタムなアトリビュート、つまり、ユーザが自由に追加できるアトリビュート、パラメータという意味です。カスタムアトリビュートはさまざまなところに保存できますが、今回質問は「モディファイヤみたいなもの」ということだったのでモディファイヤを使用する方法を紹介してみようと思います。
シーン内の任意のオブジェクトを選択し、「アトリビュートホルダー」というモディファイヤを適用します。

アトリビュートホルダはその名の通り、アトリビュートを保持するという目的だけのためのモディファイヤです。これ自身にはパラメータは一つもありません。
メインメニューバーから、アニメーション→パラメータ編集とたどります。

若干UIが不親切ですが、「パラメータ編集」機能を使って、カスタムアトリビュートの操作ができます。3dsmax において、アトリビュートとパラメータという用語はほぼ同じものとして使われています。
パラメータ編集画面では、追加するカスタムアトリビュートの詳細や、ユーザインタフェースの設定などができます。
今回はオブジェクトに対して「メモ」を追加したいので、文字列(String)タイプのパラメータを追加しましょう。
まずは追加する場所を設定します。
「タイプを追加」のプルダウンから、追加先を選択できます。
今回はモディファイヤを選択しましょう。オブジェクトを選択して修正パネルを開き、先ほど追加した「アトリビュートホルダ―」を選択しておいてください。

続いてパラメータタイプを設定します。これは追加するアトリビュートの型(タイプ)を設定する部分です。プルダウンを開いてみると、実に様々なタイプのアトリビュートを追加できることがわかります。今回は「String」を選びます。

パラメータタイプを設定するとそれと連動してUIタイプが変化します。UIタイプはEditTextで良いのでそのままにしておき、名前を設定しましょう。今回はメモを入力する想定なのでmemoとしました。

これで準備ができたので、少し上へ戻って「追加」ボタンを押します。「追加」を押すと、選択していたアトリビュートホルダにカスタムアトリビュートが追加されます。

追加されたアトリビュートを見ると、テキスト編集部分が狭いですね…。入力したいメモが一言だけなのであればこれでも良いですが、これだと狭い場合はこのUIを修正しましょう。

すでに作成されているアトリビュートを編集する際は、「パラメータ編集」画面の「編集/削除」ボタンを押します。

「編集/削除」ボタンを押すと「アトリビュート/パラメータを編集」という窓が開き、カスタムアトリビュートが列挙されるので、編集したい項目を選択します。この辺もUIがわかりにくいのですが、選択した時点で「パラメータ編集」窓の中身は選択したアトリビュートの内容に更新されます。
この状態で内容を変更します。今回は「ストリングUIオプション」のところで、「サイズ」の「高さ」を拡大しましょう。

ここでは適当に50にしてみました。初期値で確認できるように、一行分でこの高さは17でした。それを参考に、必要な大きさに拡大してください。
数値を変更したら「アトリビュート/パラメータを編集」の画面に戻り、左下の「変更を適用」を押します。
手順をおさらいするとこのようになります。

これで、ティーポットオブジェクトにメモを入力するためのアトリビュートが追加できました。さっそく使ってみましょう。

メモが書けるようになりました。もちろんこのメモはシーンデータに保存されます。モデルの更新情報や、リグの注意点などを書いておくと便利だったりします。
カスタムアトリビュートは非常に便利で、アイデア次第でかなり発展的な使い方もできるのですが、UIのデザインが良くないせいでとっつきにくいものになっていると感じます。分かりにくさの原因のひとつは用語が統一されていないことです。
カスタムアトリビュートはわかりにくいですが、難しくはないのでぜひ使ってみてください。
なお、カスタムアトリビュートはスクリプトからもかなり柔軟に操作でき、GUIから操作すると面倒なこともより簡単にできたりします。が、こちらもGUIと同じかそれ以上にわかりにくいので、そのうち解説エントリを書きたいと思います。応用するとなんでもかんでもシーンデータに保存できるようになって恐ろしく便利です。
The post 3dsmax でテキストデータをオブジェクトに保存する方法 first appeared on Taguma CG Lab.]]>