UIとロジックを分離する

最近、ツールの設計ということに目が向くようになってきました。Pythonが主流になったことによると思います。

Pythonでのツール開発の場合、やはりUIとロジックは厳格に分離した方が良いです。なぜかと言えば、UIはQtDesignerを使って感覚的に作り、ロジックはエディタでゴリゴリと書きたいからです。途中でUIの仕様が変更になったりした際に、UIを書いているPythonファイルに動作のロジックも書いてしまうと都度書きなおしになるため、QtDesignerでのUIカスタマイズができなくなってしまいます。

そのため、UIのクラスを継承してツールのクラスを作る、というようなことから始めました。

次に、汎用性のあるツールの開発で、全く同じ機能を持つツールを3dsmaxとMotionbuilderの両方で使いたい、という要望が出てきました。当然の要望と言えます。

そこで、UIは完全に共通にし、それを継承したクラスとして共通の処理をもつ抽象クラスを作り、さらにそれを継承して3dsmax用とMotionbuilder用を作る、というような構造にしました。これが非常に便利です。

そんなもの当たり前だという話なんですが、これまで「継承」という発想のないMaxScriptが開発の主軸だったため、こういうオブジェクト指向ならではの効率化にまったく目が向いていなかったのです。

Pythonでこれをやりだしてから、MaxScriptオンリーでツールを書く際にもUIとロジックの分離ということを意識するようにしました。

MaxScriptにはクラスというものはありませんが、マクロスクリプトツールは使用するデータとメソッドがパッケージされているという意味でクラス的です。(継承はできないのでいわゆるオブジェクト指向のクラスとは違いますが)

ロールアウトの中にそこで使うデータ変数を置き、イベントハンドラやハンドラから呼ばれる関数を定義する、というような書き方をするのが一般的だと思いますが、これだとUIとロジックはがっぷり四つ状態でがんじがらめです。

そこでこれを分離したいわけですが、オススメは構造体を使うことです。メソッドとデータをまとめた構造体を定義し、ロールアウト側でその構造体をインスタンス化して使います。

このような設計でツールを作っておくと非常に多くのメリットが得られます。構造体にしてあるとコマンドラインから呼び出して処理を実行することができます。つまり、別のツールから機能を再利用したりすることができるわけです。

実は今回、この設計にしておいたことで非常に助かった例がありました。

構造体を用いてUIとロジックを分離して作ってあったMaxScriptのツールで、ユーザからUIに関しての要望が出されました。その要望はMaxScriptのUIでは実現できないもので、逆にPySideを使えば楽勝、という話でした。

そこで、PySideでUIを作り、それを継承してツールのクラスを作り、そこからMaxScriptの構造体を呼んでメソッドを実行する、という仕組みで接続しました。

これにより、100%MaxScriptで書かれていたツールのUIだけをPySideに置き換える、といったことが実現できたのです。

ちなみに3dsMax 2017から実装されたpymxsというモジュールを使うとかなり柔軟にPythonからMaxScriptを実行できます。これを使えばこのような非常に柔軟にPythonとMaxScriptをまたいだツール開発ができてしまうのです。

ただ、pymxsがあるのとないのとではまるで開発効率が違うので、このようにPythonとMaxScriptをまたぐものをやりたい場合は3dsmaxは2017以降を使うのが良いでしょう。

この辺の話はいずれサンプルコードを用いて具体的に解説してみたいと思っています。

【MaxScript】シーン内の○○を回収する

MaxScriptで何かツールっぽいものを作ろうとしたとき、割と頻繁に必要になるのが「シーン内の○○を回収する」という操作です。例えば「シーン内のカメラを回収する」とか、「シーン内のBipedを回収する」といった操作です。

これをうまい具合にやる方法を考えてみます。

シーン全体を対象にループする

objectsを使う

まず「シーン全体から」という部分ですが、最もシンプルなのはobjectsというコレクションオブジェクトを使用する方法です。

簡単なシーン(適当にオブジェクトをいくつか(いろんな種類のもの)置いて、次のコードをエディタに書いて評価(Ctrl+E)します。

実行するとシーン内のすべてのオブジェクトの名前がリスナーに出力されます。オブジェクトの種類を問わず、全オブジェクトを対象にしたい場合はこのobjectsというコレクションを使います。

geometryを使う

回収する対象がジオメトリオブジェクト(ごくザツに言うとレンダリングできるオブジェクト)である場合は、geometryというコレクションが使えます。同様に実行してみてください。今度はシーン内にカメラやライト、ヘルパなども適当に置いてみます。

対象をgeometryにした以外は前のコードとまったく同じです。objectsで実行するとカメラやライト、ヘルパなどもリストアップされ、geometryにするとそれらはリストされなくなります。

このように、ジオメトリオブジェクトに限定できる場合にはgeometryを使ったほうが、検索対象の母数が減るので実行速度が若干速くなります。

特定の種類のものを探す

漠然と「種類」と言っていますが、Max上でオブジェクトの種類を識別したい場合、そのオブジェクトのクラスを確認することになります。シーン内の適当なオブジェクトを1つ選択して以下のコードをリスナーで実行すると、選択したオブジェクトがどのクラスのオブジェクトかがわかります。

種類を識別する最もシンプルなのはこのclassofというビルトイン関数を使用する方法です。

しかし、classofだと細かく分かれすぎていて面倒くさい、というケースがあります。例えばシーン内のライトを全部回収したい場合などです。

ライトはオムニ、スポット、ターゲットスポット、指向性など、ライトの種類によってすべて異なるクラスに属しています。これを、ライトの種類にはよらずすべてのライトを回収しようとすると、classofの結果が○○、または□□、または△△、といった具合にひたすら条件をたくさん書く必要が生じます。

このような場合にはクラスのさらに上のスーパークラスで識別するのが良いです。スーパークラスは以下のコードで取得できます。

これを使うと、ライトはその種類のよらず全部lightというスーパークラスに属していることがわかります。

※厳密には、例えば指向性ライトはlightクラスを継承したDirectionallightというクラスのオブジェクトです。sperclassofというビルトイン関数を使うと、そのオブジェクトのクラスが継承している継承先のクラスを知ることができます。

全てのライトオブジェクトはlightクラスを継承しているため、superclassofでlightが返ってくるものを回収すると、シーン内の全てのライトを回収することができます。

コードは次のようになります。

上のコードを実行すると変数arrにシーン内のライトが全部格納されるはずです。

コレクションから特定の条件のものを回収するときの書き方

このような、コレクションオブジェクトから特定の条件を満たすものを回収するという操作はさらにシンプルに書くことができます。

こんな風に一行で書くことができます。for文を回しながら、whereの条件を満たすようなoをcollectする、というような意味です。このように書くとシンプルに書けるだけでなく、検索する母数(この場合はObjectsが持っているオブジェクトの個数)が大きくなるとこの書き方のほうが速いという実利もあります。

※少数を対象にしている場合はほとんど差は体感できません。

Biped Rootを回収する

同様にしてBipedのルートオブジェクトを回収する方法を考えます。Bipedのルートを回収したいケースにはBipedに絡むツールを作ると割りと頻繁に遭遇しますが、これが一筋縄ではいきません。やってみましょう。

Bipedルートを選択してクラス、スーパークラスをそれぞれ確認してみます。

このようにBipedのクラスはBiped_Objectで、これはルートもそれ以外も全部共通です。さらにスーパークラスはGeometryObjectで、これは他のあらゆるジオメトリオブジェクトと共通です。そのため、クラスを使ってもスーパークラスを使ってもルートだけを回収することはできません。

Biped_Objectから辿って見つける

一つ目として、クラスがBiepd_Objectであるものを見つけ、そこからそのオブジェクトが属しているBipedのルートを探す、という方法があります。

実行するとこんな感じです。

先に言ってしまうと、この方法はお勧めしません。

ここで使っているappendIfUniqueというビルトイン関数は、配列に対して、今から追加しようとしているものがまだ配列の中に存在しなければ追加し、既にある場合は追加しない、という動作をする関数です。

これは何かと便利なのですが、如何せん処理が遅いという問題があります。かといって、上記のコードではこれを使わないと同じルートオブジェクトが大量に回収されてしまい、実用的ではありません。

コントローラからダイレクトに見つける

Bipedを回収する際に定番なのは、コントローラの種類から見つける、というものです。Bipedのルートとそれ以外のパーツは、持っているコントローラのクラスが違うのです。

このように、Bipedのルートだけが、Vertical_Horizontal_Turnという特殊なコントローラを持っており、これによりシーン内からダイレクトにBipedルートだけを回収することができます。

これだと直接Bipedルートを回収できます。もちろんBiped_Objectから辿る方法よりもはるかに速く動作します。

外部参照にも対応できる

3dsmaxの厄介な点として、外部参照でシーンに持ってきたオブジェクトは、基本的にすべてXRefObjectというクラスになってしまいます。classof を実行するとすべてXRefObjectが返ってきて、superclassofのほうは本来のsuperclassが返ってきます。

ただ、Bipedの場合前述のようにスーパークラスはGeometryObjectなので役に立ちません。

このcontrollerのクラスを見る方法であればたとえ外部参照になっていても問題なく識別できるため、Bipedを識別する場合にはcontrollerのクラスを見るのが一般的です。

MaxScriptを書こう ~その15

マクロスクリプトとは

MaxScriptで作ったツールは、メニューの「スクリプトを起動」から起動するか、またはスクリプトエディタに読み込んで評価(Ctrl+E)することで起動できます。が、これは普段使うにはあまり気軽な方法とは言えません。ボタンを押せば起動するとか、ショートカットで起動するとかしたいものです。

そのような、ボタンやメニュー、ショートカットなどから起動できるようにするのは、マクロスクリプトという形で記述する必要があります。「え?今まで作ってきたあれは書き直さないといけないの?」と思われるかもしれませんが、もちろんそんなことはありません。

マクロスクリプトというのは簡単に言ってしまうと、スクリプトとして書いたものにラベルをつけたもの、というような印象です。全体をブロックとして囲ってしまい、その外側にマクロスクリプトとして必要なものを書き足すだけでできてしまいます。

マズはマクロスクリプトの定型を見てみましょう。

このような感じです。非常に簡単ですね。「スクリプトの中身」というところに、これまで作ってきた例のスクリプトを丸ごと書けばOKです。

マクロスクリプトは3dsmaxに認識させるためにまず一度評価する必要があります。評価するとそのときのユーザのユーザマクロフォルダ(#usermacros)に内容が.mcr形式のファイルとして保存されます。同じ名前、カテゴリのマクロスクリプトを評価するたびに、ユーザマクロフォルダ内の.mcrファイルが更新されていきます。

やってみましょう。

スクリプトの中身を書くところにはFileInでスクリプトファイルを実行するような処理を書いてみました。

これを実行するには、前回まで作ってきたツールをro_adjustPos.msというファイル名でユーザスクリプトフォルダに保存しておく必要があります。もちろんro_adjustPos.ms内でincludeしているcommonLib.msも同じくユーザスクリプトフォルダに入れておきます。

ここまでできたら上記のマクロスクリプトを評価(Ctrl+E)してみましょう。マクロスクリプトにエラーがなく、評価が成功した場合はリスナーに5桁の数字が表示されます。

数値自体は環境によって異なるので画像と違ってもなんら問題ありません。

評価が成功すると、このマクロスクリプトは3dsmaxからいつでも呼び出せるようになります。3dsmaxを終了して改めて起動しても継続して使うことができるのです。

マクロスクリプトの使い方

マクロスクリプトは一般に、何らかのメニューに登録して使います。メニューの「カスタマイズ→ユーザインタフェースのカスタマイズ」を開きます。

上部にタブがあり、さまざまなメニューにマクロを割り当てることができます。どのタブでも基本は同様で、「カテゴリ」でマクロスクリプト内に書いたカテゴリを探して指定します。するとそのカテゴリに属しているマクロだけがリストに残ります。

あとはそれにショートカットキーを割り当てたり、クアッドメニューやメインメニューに載せたり、ツールバーにボタンを置いたり、好きなように設定して使うことができます。

ここではためしにクアッドメニューに追加してみました。クアッドメニュー上での表記は、マクロスクリプト内で「ButtonText」として設定したものになります。このメニューを押すと私たちのadjustPosツールが起動します。

このように、スクリプトをマクロスクリプト化すると大変便利に使えるようになります。何かツールを作ったらどんどんマクロ化して登録していきましょう。

ちなみに、マクロスクリプトはボタンやメニューなどから呼び出されたときに中身が評価されるので、今回のように中の処理を全部ファイルにまとめてある場合、FileInで書いてもincludeで書いても同じ結果になります。

今回のまとめ

  • スクリプトをメニューやボタンから起動するためにはマクロスクリプトにする必要がある
  • マクロスクリプトとして一度評価するとユーザマクロフォルダに保存されて次回からも継続して使える(同じユーザのみ)

おわりに

以上でadjustPosツールは一旦完成です。

「MaxScriptを書こう」と題して15回に渡って簡単なツールを作ってみましたが、いかがでしたでしょうか。

このチュートリアルがスクリプトに親しむきっかけになることを願っています。

前:MaxScriptを書こう ~その14

 

 

MaxScriptを書こう ~その14

桁数指定をUIから行えるようにする

前回round関数を作成し、数値を丸める際に桁数を指定できるようになりました。これを私たちのadjustPosツールからも指定できるようにしましょう。

ユーザインタフェース上から桁数にあたる数値を渡したいのですが、これを実現する方法はけっこうたくさんあります。今回の用途で最も使いやすいのはスピナーでしょう。

スピナーは3dsmaxのインタフェース上でも数値を操作する部分に良く使われています。スピナーの良いところは、数値以外のものは入力できない、という点です。このため、スピナーから送られてくるデータは必ず数値だ、という前提で処理してしまって構いません。これがテキスト入力などの場合、入力されたものが数値かどうか(厳密には数値に変換できるかどうか)を確認して処理を分岐する必要が生じます。

スピナーを追加する

ではスピナーを搭載してみましょう。

スピナーもボタンと似たような方法で定義します。違うのはtypeの指定です。typeは:を使って指定しているので、デフォルト値を指定された引数だということになります。(デフォルトは#floatで、浮動小数点数という意味です)

今回、桁数は整数なのでtypeを#integer(整数の意)に指定します。

実行すると次のようになります。

桁数を指定するスピナーができました。もちろんまだこの値をどうこうする処理を何も書いていないのでこのスピナーは何も意味を持っていません。ただ、動作は確認できるので触ってみましょう。

ちょっと触ってみるとわかりますが、このスピナーは0より下に行きません。0の状態から下向きの矢印を押しても変化しませんし、直接負数を入力しても0に戻されてしまいます。

負数を指定したら10の位、100の位などで丸めてほしいのでこのスピナーの状態はあまりよろしくありません。そこで値の範囲も指定することにします。

値の範囲はrangeというキーワード引数で指定します。スピナーのところだけ抜き出してみます。

範囲はこのように書き、[最小値、最大値、初期値]という形で指定します。つまりrange:[-10. 10. 0]というのは「スピナーの範囲はー10から10までで、初期値は0ですよ」という意味になります。

-10から10までにしたのは、桁数の丸めについてだいたい10桁ぐらいあれば用が足りるだろう、という目算によりますが、余裕を見て-100~100ぐらいに設定しても良いでしょう。

スピナーから値を受け取って処理に使用する

あとはこのスピナーから値を得て処理に使えば良いだけです。

が。やろうと思うと問題があることに気づきます。ツールから使用している関数はadjustPos()ですが、この関数には引数は1つしかありません。対象のオブジェクトを受け取るだけです。adjustPosの中でroundを呼んでいて、そこに桁指定があります。

困ったことになりました。スピナーから取得した桁数をroundの実行に使いたいのですが、窓口のadjustPosは桁数を受け取らないのです。

これをどのようにするかは難儀な問題なのですが、簡単に解決するならadjustPosにも引数を追加して、受け取った引数をそのままroundに渡せば良い、ということになります。

こんな感じでしょうか。adjustPos関数に2番目の引数を追加しました。これにデフォルト引数を設定していないのは、二重にデフォルト引数を指定するとわかりにくくなってしまうからです。

ためしにテストしてみるとこんな感じです。

※関数型パラダイムなどではこのような「受け取った引数をそのまま別の関数に渡す」という処理はやらないほうが良いとされますが、ここではこれで用が足りるので良いことにしてしまいます。

これで受け取る準備ができたので、スピナーの値を受け取って実行するようにしましょう。

adjustPosを呼び出している部分を次のように書き換えます。

スピナーオブジェクトを格納している変数spn_digitsを使用し、そのvalueプロパティを取得してadjustPosの2番目の引数に渡しています。

実行してみると以下のようになり、ちゃんとスピナーで指定した桁数に丸められていることが確認できます。

これでほぼ動作が意図通りになりました。これでスクリプトツールとしては完成でも良いのですが、このままだとこのツールを使うには毎回「スクリプト→スクリプトを起動」というメニューを辿ってこのmsファイルを指定する、という作業が発生します。

次回はこれをマクロ化し、ショートカットキーを割り当てたり、ツールバーから実行したりできるようにしましょう。

今回のまとめ

  • 数値を受け取るインタフェースとしてスピナーが便利
  • スピナーの値の型(タイプ)はtypeキーワードで指定する
  • スピナーの値の範囲はrangeキーワードで指定する

前:MaxScriptを書こう ~その13

次:MaxScriptを書こう ~その15

MaxScriptを書こう ~その13

四捨五入する

ここまで私たちのadjustPosツールを通じてスクリプトのいろいろなことを学んできましたが、肝心の処理そのものについては目を瞑ってきました。今回はその処理を見直してみましょう。

第二回のときに、元の座標が3.99999という値でも、実行すると3.0になってしまう、という点を問題にしましたが、そのまま素通りしてきました。やはりこれは少々問題がありそうです。問題は数値を丸める際に切り捨てを使っているところにあります。これを四捨五入にしましょう。

そう考えてMaxScriptのリファレンスを探すと、なんと、MaxScriptには四捨五入の関数は用意されていないようです。切り捨て(floor)と切り上げ(ceil)はありますが、四捨五入(一般にroundという関数で用意されていることが多い)は存在しません。

そこで、四捨五入を行う関数roundを定義しましょう。

せっかくなので、これも前回作成したcommonLib.msに書いてしまいます。既にcommonLib.msに記述済みの関数adjustPosでも新たに作るroundという関数を使うことになるので、書く位置としてはadjustPosよりも前(上)になるようにしてください。

※四捨五入関数はどのようにしたら作れるか、先を読む前に考えてみることをオススメします。

私はこんな感じにしました。四捨五入したい数値をnumという変数で引数として受け取り、その値に0.5を足してからfloorで切り捨て、出来上がった値をreturnで返しています。

これでfloorやceilと同じように動作する四捨五入関数ができましたので、adjustPosの中でfloorを使っているところをこのroundに書き換えましょう。

関数にはどんな処理を行う関数なのか、コメントを書いておくとあとで再利用するときに便利です。

四捨五入する桁を指定できるようにする

少し改善するとさらに改善したくなるのが世の常(?)というもの。ビルトイン関数のfloorやceil、さらにそのfloorを使って作ったroundという関数はすべて、小数第一位を処理して整数に丸める、という処理になっていますが、これを小数第一位まで求める、小数第二位まで求める、など指定できるようにしたいですよね。さすがに小数点以下が5桁もあるのはやりすぎにしても、じゃぁ整数で、というのではあまりに乱暴です。適度に必要に応じて処理できるようにならないものでしょうか。

そこで次はこの桁指定をできるようにしてみましょう。

まず、関数の設計を考えます。桁指定をするということは、その指定を関数に知らせる必要があるので、引数をもう一つ渡す必要が生じます。もちろん指定する桁数を渡せば良さそうです。問題はそれを受け取ってどういう処理をすれば目的の結果を得られるか、という点です。

これを元に中身を考えましょう。

例えば桁数として1を渡したとき、小数第二位を四捨五入して小数第一位までの答えを返す、という仕様にしましょう。つまり2番目の引数では、最終的に得られる数の小数点以下の桁数を指定する、ということになります。0を渡した場合は整数を返す、という風になります。

ここで、中で使うことになるfloor関数が整数に丸める操作しかできないことに注目し、この処理で求める結果を得られるように工夫しましょう。

こんな風にしてみました。

まず、受け取った桁数(最終的に欲しい桁数)分だけ小数点の位置を移動させます。10進数で小数点の位置を移動させるということは、10のべき乗を行えば良い、ということになります。

べき乗計算はビルトイン関数のpowで行うことができます。pow a bと2つの数値を渡すことで、aのb乗を得ることができます。

必要な分だけ小数点を移動させた上で0.5を足し、その数をfloorにかけることで四捨五入した結果を得ます。これは小数点を移動した上で整数を得た状態になっているので、元の位置まで小数点を戻す(10のべき乗で割る)という操作を行い、得られた数をreturnしています。

引数にデフォルト値を与える

round関数に新機能を足したのは良いのですが、このままだとadjustPos()関数がエラーを吐くことになります。なぜなら、round()関数が引数を2つ要するようになったのに、adjustPos側はこの関数の呼び出しで1つしか値を渡していないからです。引数が不足している、というエラーになってしまうのですね。

もちろんadjustPosからの呼び出しを書き直せば良いのですが、ここではround関数の2番目の引数を渡さなかった場合は0が渡されたものとする、という処理にしてみましょう。

この変更は実は非常に簡単です。

このように、デフォルト値を設定したい引数に:(コロン)を付け、その後ろにデフォルト値を書くだけです。

※引数が複数ある場合、デフォルト値を設定する引数は設定しない引数より後に書く必要があります。今回の例の場合、デフォルト値を設定するdigitsを設定しないnumより前(左)に書くことはできません。

これで呼び出し側を変更しなくてもエラーは出なくなりました。

桁数がマイナスの場合の挙動

2番目の引数に負の数(マイナスの数値)を渡した際はどうなるでしょうか。二番目の引数は小数点を右に移動させる(数値を10倍ずつ大きくする)のに使っているので、これがマイナスだと逆に左へ移動することになります。つまり結果として「10の位まで求める」「100の位まで求める」といったような処理になるわけです。

試してみましょう。

ここで、デフォルト引数を指定した引数は、呼び出し時にその引数名を用いて「引数名:値」という書き方で渡す必要があります。この点に注意しましょう。

今回は2番目の引数が負数でも問題なく整合性のある結果が得られるのでこのままで構いませんが、渡す値によっておかしな結果になってしまう場合は、受け取った引数の内容に応じて処理を分けるような実装にする必要があるかもしれません。

次回はこの桁指定をツール上から設定できるようにしましょう。

今回のまとめ

  • 既存の関数をカスタムしてより使いやすい関数を作ることができる
  • 引数にデフォルト値を設定することができる
  • デフォルト値を設定した引数は呼び出し時に「引数名:値」という形で渡す

前:MaxScriptを書こう ~その12

次:MaxScriptを書こう ~その14

MaxScriptを書こう ~その12

スクリプトファイルはどこに保存すればよいか

さて、今回はツール内で使う汎用関数を別のファイルに保存しておいて呼び出して使う、ということをやろうと思うわけですが、そもそもスクリプトファイルはどこに保存するのが良いのでしょうか。

3dsmaxでスクリプトファイルを保存する場所としては以下のような候補があります。

  • ユーザスクリプトフォルダ
  • スクリプトフォルダ
  • ユーザスタートアップフォルダ
  • スタートアップフォルダ
  • その他任意の場所

3dsmaxで推奨されているのは上の4つです。要約すれば、スクリプトフォルダかスタートアップフォルダで、全ユーザを対象とするか、自分だけを対象とするか、という違いになります。

3dsmaxではPCのログインユーザごとにユーザフォルダが作成されます。このユーザフォルダの中にスクリプトフォルダやスタートアップフォルダがあり、そのユーザでログインしている時に有効になります。一般にはオリジナルのスクリプトはここに保存するのが良いです。

ユーザ固有ではないスクリプトフォルダやスタートアップフォルダは3dsmaxのインストールフォルダ内にあります。こちらに入れると、そのPCを使う全てのユーザが共通で使用できることになります。

スタートアップフォルダ

スタートアップフォルダに入れたスクリプトファイルは、3dsmaxの起動時に自動的に読み込まれ、評価されます。つまり、関数を記述したファイルをスタートアップフォルダに入れておけば、起動後、その関数はいつでも使える状態になっている、ということになります。

インストールフォルダにあるスタートアップフォルダであれば、そのPCの全てのユーザに関して、起動時に実行されます。

ユーザスタートアップフォルダの方は、そのユーザでログインしている時のみ起動時に実行されます。

※そのPCを使用するユーザが1人しかいない場合はどちらでもほとんど差はありません。

スクリプトフォルダ

スクリプトフォルダに入れたものは、特に自動的に実行されたりはしません。汎用関数のようなもの以外の一般的なスクリプトファイルは、通常必要な時に実行すれば良いため、スタートアップフォルダではなくスクリプトフォルダの方に保存します。

これもユーザスクリプトフォルダであれば当該ユーザでログインしているときだけアクセスできるようになります。

スクリプト関連フォルダへのアクセス

スクリプトフォルダ、スタートアップフォルダなどへは3dsmax上から簡単にアクセスすることができます。それぞれ以下のような名前でアクセスすることになります。

  • スクリプトフォルダ: #scripts
  • スタートアップフォルダ: #startup
  • ユーザスクリプトフォルダ: #userscripts
  • ユーザスタートアップフォルダ: #userstartup

これらの名前をビルトイン関数getDirの引数に渡すと、そのフォルダへのフルパスが返ってきます。やってみましょう。以下を1行ずつリスナーで実行してみてください。

結果は以下のようになります。

スクリプトファイルは他の任意の場所に保存しても構わないのですが、3dsmaxが推奨している場所に保存すると、このようにスクリプト上から簡単にアクセスすることができるので重宝なのです。

スクリプトからスクリプトファイルを読み込む

スクリプトからスクリプトファイルを読み込む方法には2種類あります。

FileInを使う

1つはFileInというビルトイン関数を使う方法です。FileIn関数は引数としてスクリプトファイルのパスを受け取り、そのスクリプトファイルの中身を評価します。

では例のadjustPos()をユーザスクリプトフォルダにcommonLib.ms(共有ライブラリ、ぐらいの意味です)という名前で保存し、以下を実行してみてください。

getDirに#userScriptsを渡すことで、ユーザスクリプトフォルダへのフルパスが文字列として返ってきます。これにファイル名の部分を足したものをFileIn関数に渡しています。

ここで、ファイルパスの区切りは\ですが、\には特別な意味があるため、文字列内に\を書きたいときは\\と2つ並べて書く必要があることに注意してください。

※\はフォントによってバックスラッシュになったり円マークになったりしますが同じものを意味しています。

実行するとリスナーにはadjustPos()と表示され、関数の評価が実行されたと思います。

includeを使う

もう一つはincludeを使う方法です。こちらはFileInとは違い、ファイルの中身は評価されません。評価されるのではなく、中身がそのまま読み込まれます。やってみましょう。

またいきなり初めて見る書き方を使ってしまいました。ここではgetDirでフルパスを取得するのではなく、文字列の中に$userScriptsと書いています。このような表記方法も使うことができます。

もちろん、getDirでフルパスを取得してそれをincludeに渡しても問題ありません。

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

includeに渡したファイルの中身が展開され、表示されました。

FileInとincludeの使い分けの指針

外部のスクリプトファイルを読み込む方法としてFileInとincludeがあることを紹介しましたが、どちらを使えば良いのでしょうか。

これはまさにケース・バイ・ケースです。この2つは内容が異なるため、それぞれの用途に合わせて使用することになります。

まず大きな違いとして、FileInは実行した時にすぐファイルの中身が評価されます。このとき、評価される内容はファイルの内容そのままなので、今回のように関数だけが書いてある場合、その関数はグローバルスペースに宣言されることになります。

対してincludeの方はその場所にファイルの中身が展開される、ということなので、展開した後それを評価するまで評価は行われません。

この場合、例えばブロックの中にincludeを書けば、ファイル内の関数はそのブロック内で宣言されることになり、スコープを制限することができます。このため、グローバルスペースで宣言する必要がない、あるいは宣言したくないものについては、includeを使用して読み込むのが良いでしょう。

このツールとしては、includeでrollout内にadjustPos()を宣言する方を採用しましょう。

これでめでたく、adjustPosツールが形になりました。

次回は、位置の値を丸める処理そのものをもう少し見直してみましょう。

今回のまとめ

  • スクリプトからスクリプトファイルを読み込む方法にはFileInとincludeがある
  • FileInでは書いた場所でスクリプトファイルが評価され、includeではその場所に展開される
  • FileInで読み込んだスクリプトはグローバルスコープで評価したのと同じ結果になる
  • includeで読み込んだスクリプトはその時点では評価されず、全体として評価されるのでincludeした場所に応じてスコープが適用される

前:MaxScriptを書こう ~その11

次:MaxScriptを書こう ~その13

MaxScriptを書こう ~その11

ツール内で使う関数はどこで定義すべきか

またまた大上段に振りかぶりました。○○すべき、という言葉を使うとどうもたいそうなことを言っている感じになってしまいますが、これも厳格な話ではなく、いくつかの方法を紹介して、その都度目的に合った場所に書きましょう、というような方向で説明してみたいと思います。

同じスクリプトファイルに書く

同じファイル内のロールアウト定義より前に書くことができます。

このような感じです。これを評価すると上から順に評価されるため、関数の評価→ロールアウトの評価→createDialogという順番で評価され、問題なく実行できます。

ここで注目すべきところは、function adjustPosと書いてあるところのadjustPosは変数の一種である、ということです。これは関数の名前なのですが、関数というのはある一定の処理に対して名前をつけたものです。名前をつけたものというのはつまり、関数自体が変数の中に入っている、と考えることができます。

実際にはadjustPosという変数に格納されているのは関数の本体へのポインタのようなものになりますが、ここでは関数そのものを格納していると考えて差し支えありません。(本当はこの発想は相当強引ですが、MaxScriptの範囲でスクリプトを書いている限りにおいてはほとんど問題にならないので大丈夫です)

※ポインタについてはC++を扱う記事のところで紹介する機会があると思います。

関数宣言のスコープ

だいぶ脱線しましたが、adjustPosも変数だという話でした。これが変数だということは、これにもスコープがあるということになります。ここが重要です。

重要なのでもう一度書きます。function キーワードで定義した関数の名前は変数と考えることができるので、これにはスコープ、つまりアクセスできる範囲がある、ということです。

functionで定義する関数名にはグローバルとローカルの区別はありません。常に、その定義を書いた場所でのブロックスコープが適用されます。どのブロックにも属していない場所に書くとグローバルスコープが適用されます。

※厳密には関数を「宣言」した場所によってスコープが決まります。MaxScriptでは特殊なことをしない限り、関数の宣言と定義が同じ場所にあるため、まずは定義している場所で同時に宣言もされている、と考えてください。

※関数の定義と宣言を分離して使用する例は別の機会に説明します。

ではやってみましょう。

testfunc()はロールアウトブロックで宣言&定義されているので、このロールアウト内のどこからでもアクセスできます。全てのイベントハンドラから共通で使うことができる、ということになります。

testfunc2はifブロックの中で宣言&定義されたので、ifブロック内からしかアクセスできません。

そのため、ifブロックを抜けた後に書いてある呼び出し部分(15行目)でエラーが発生します。

だいぶ遠回りしましたが元のツールに戻りましょう。

今、同じスクリプトファイルのロールアウト定義よりも前に関数定義を書くと、この場所はどのブロックにも属していない、ということになり、グローバルスコープで関数が宣言されます。

つまり、このようなスクリプトファイルを評価すると、以降その関数はリスナー上から直接実行することができます。

ロールアウトブロックに書く

ロールアウトブロックに書くと以下のようになります。

この場合、adjustPos関数はロールアウトブロックのローカル関数となり、このツール内では自由に使うことができますが、ツールの外からはアクセスできません。

※今これを評価してリスナーからadjustPosを実行してもエラーにならないかもしれません。それは以前グローバルスコープで評価したものが残っているからです。一度3dsmax本体を再起動し、改めて評価して試してみてください。

次回は、この関数を汎用関数として別のファイルに書いておき、ツールのファイルからそのファイルを読み込んで使用する、という例をやってみましょう。

今回のまとめ

  • 関数は書く場所によってスコープ(有効な範囲)が決まる
  • あるブロック内に書いた関数はそのブロックより下の階層からしかアクセスできない
  • MaxScriptでは特別な書き方をしない限り、関数は宣言と同時に定義されていることになる

前:MaxScriptを書こう ~その10

次:MaxScriptを書こう ~その12

MaxScriptを書こう ~その10

メッセージを改良する

前回、条件によってメッセージを表示するという機能を追加しました。正常に終了したときには「完了しました」というメッセージを出すようになっていましたが、これを、オブジェクトをいくつ処理したのかを表示するメッセージに変更しましょう。

具体的には、処理終了時に「○個のオブジェクトを処理しました。」というようなメッセージを表示させる、ということです。

実は今回の目的だけを実現するのであれば、このツールは「選択されているオブジェクトに対して処理を行う」ということがわかっているので、処理されるオブジェクトの数は最初からselection.countであることがわかっています。

ただ、それでは面白くないので、処理を実行するたびに数を数えて、最終的にいくつ処理したのかを表示させるようなものを作ってみたいと思います。

変数を知ろう

まず、処理したものの数を数えたいので、その数を格納する変数を作ります。

実はこれまで、変数はいろいろなところに登場していたのですが、それと意識して宣言したことはまだありませんでした。たとえばrollout ro_adjust_posのro_adjust_posは変数ですし、for sel in selection のselも変数です。(厳密にはselectionも特別な変数です)

変数のは何らかのデータに名前をつけて保持するためのもので、よく入れ物にたとえられます。例えば、

とやれば、aという変数に10を代入したことになります。以降、aは10として振舞います。a + 2 とやれば12が返ってきます。

基本はこれだけです。変数にはなんでも入れることができます。例えば1つオブジェクトを選択して次を実行してみてください。

これで変数aに現在選択されているオブジェクトが格納されます。そのため、以下のようなことが可能です。

ここで、$はあくまでも「現在選択されているオブジェクト」ですが、aの方は代入された時点で$が指していたオブジェクトを格納しているため、代入を実行したあとで選択を外してしまっても、代入したオブジェクトはそのまま保持されています。上記を実行したあと選択を解除していろいろ試してみてください。何も選択していない状態だと$.posはエラーになりますが、a.posは問題なく実行できます。

変数のスコープを意識する

変数にはその変数が有効な範囲があります。有効な範囲というのは、その変数にアクセスできる範囲、という意味です。その有効範囲のことを「スコープ」と呼びます。「変数のスコープ」という呼称はよく出てくるので覚えておきましょう。

変数には大きく分けてグローバル変数とローカル変数の2種類があります。グローバル変数はどこからでもアクセス可能なもので、ローカル変数は限られた範囲でだけアクセスできるものです。

グローバル変数はどこからでもアクセスできるから便利のような気がしますが、どこからでも改変されてしまう可能性があるという危険をはらんでいます。

そのため、ツールを作成する際には、変数は可能な限りローカル変数として宣言するようにしましょう。どうしてもグローバル変数にする必要があるときだけグローバル変数を使い、その場合は簡単にはバッティングしないような変数名をつけるようにしましょう。

今回は全てローカル変数で問題ないため、全部ローカル変数として宣言していきます。

では、処理したオブジェクトの数を数える変数を宣言してみましょう。

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

9行目でlocalというキーワードを使い、ローカル変数を宣言しています。何もつけずに変数を宣言するとMaxScriptではグローバル変数になります。そのため常にlocalというキーワードをつけて変数を宣言する習慣をつけましょう。

ローカル変数のスコープ

変数は大きく分けてグローバルとローカルという説明をしましたが、実はローカル変数にもスコープ(有効な範囲)があります。

MaxScriptのローカル変数はブロックスコープです。ブロックスコープというのは、宣言したブロックでのみ有効、という意味です。

ブロックというのは、ものすごくザツに言うと()の中、というようなイメージです。

例えばfor文は次のようになります。

私たちのツールはブロックがさらに階層になっています。

外側のブロックをスコープとする変数はその内側からも見えますが、内側のブロックの変数は外からは見えません。

例えばロールアウトのブロックにlocal宣言した変数はボタンハンドラの中からもアクセスできますし、さらにその中のifやforのブロックからもアクセスできます。

ですが、ifブロックの中でlocal宣言した変数はボタンハンドラブロックからはアクセスできませんし、ボタンハンドラブロックでlocal宣言した変数はロールアウトブロックからはアクセスできません。

元のコードに戻ってみると、local宣言したcnt(countの略のつもり)という変数はボタンハンドラのブロックに宣言されています。そのためそれより内側にあるforブロックの中からアクセスできるわけです。

ここでcnt += 1という表記が出てきますが、これはcntに1を足したものを改めてcntに代入する、という意味になります。つまりcnt = cnt + 1と同じことを意味しています。

最後にそのカウントした値をメッセージに表示するわけですが、表示するためには文字列でなければなりません。cntは数値のデータですが、これを数「字」にする必要があります。

MaxScriptでは変数のタイプ(型と呼びます)を変更するにはas○○という表現を使います。

※変数の型については別のところで詳しく説明する予定です。今回は「数値を文字列と連結するにはas stringする必要がある、ということだけ覚えておいてください。

次回はあらかじめ評価しておく必要のあるadjustPos()の関数定義をどこに書くべきか、ということを考えてみます。

今回のまとめ

  • 変数にはグローバル変数とローカル変数があり、それぞれスコープ(有効な範囲)が異なる
  • ローカル変数は宣言する場所によってスコープが決まり、そのスコープの外からはアクセスできない
  • 数値と文字列を連結するには数値を文字列に変換する必要があり、その変換にはas stringを使う

前:MaxScriptを書こう ~その9

次:MaxScriptを書こう ~その11

MaxScriptを書こう ~その9

ツールを改良する

前回までに作ったadjustPosツールを改良しましょう。一口に改良と言っても改良の余地はたくさんあるので、どこから手をつけたものか迷いますね。何度か使ってみて、どこが使いにくいか、どこが物足りないか、といったことを洗い出し、それぞれどういう方向で改善するかを考えましょう。

まず私が気になったのは、このツールは実行したときにどうなったのかよくわからない、という点です。例えば何も選択せずに実行ボタンを押すと、何も実行されずに終わります。厳密にはselectionの中身が無いため、for文の中に入ることなく処理が終了します。

そこで、何も選択されていない場合はその旨をメッセージで知らせ、実行した場合は実行しましたよというメッセージを表示させるようにしてみましょう。

メッセージを表示する

メッセージを表示するにはmessageBoxというビルトイン関数を使います。この関数は以下のように実行することで、メッセージを表示した小窓を表示します。

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

これを使ってメッセージを表示しましょう。

※ロールアウトを評価する前に関数adjustPosを評価しておいてください。

実にあっさりと新しい要素を使ってしまいました。それは5行目にあるifです。ifはif文と呼ばれ、for文と同様、ほとんどのプログラム言語にあります。条件分岐構文と呼ばれるものの一つで、その名のとおり、ある条件を満たした場合の処理と、満たさない場合の処理を分岐することができます。

if文を使う(条件分岐構文)

if文の書き方は以下のようになります。

これを踏まえて上のコードの中でif文のところだけを取り出してみましょう。

これは、もしselection.count(選択されているオブジェクトの数)が0に等しい(==は等しいという意味の記号です)場合、messageBoxというビルトイン関数に”操作するオブジェクトを選択してください”という文字列を引数として渡して実行し、falseを返す、という意味になります。

return というのはMaxScriptでは関数から何かを返し、処理を抜ける際に使うキーワードです。ここで返しているfalseというのは真偽値(ブール値)と呼ばれる値の1つで、処理に失敗した場合などに返される値です。この辺は追々開設していくので、今のところは関数の途中で何らかの値をreturnするとそこで処理が終わる、と思ってください。

ちょっとまて、関数だったのか?と思われますよね。

今、このif文が書いてあった場所はボタンのイベントハンドラの中でした。イベントハンドラは関数なのでしょうか。完全にイコールかと言えば厳密には違いますが、広義ではイベントハンドラも関数の一種と考えて良いでしょう。イベントハンドラを途中で中断したい場合も、returnで何か値を返して抜ければよい、ということになります。

ちなみに、CやC++などの言語では単にreturnとして、何も値を返さずに処理を抜けることができますが、MaxScriptでは戻り値のないreturnは認められていません。途中で処理を終えたいときは必ず何らかの値を返す必要があります。

どんな値を返しても良いので例えばreturn 2とかいうことも可能ですが、わけがわからなくなるので無意味な値は返さないようにしましょう。一般には真偽値(trueまたはfalse)や(OK)などを返すことが多いです。今回の場合、selection.countが0の場合は処理が行われないので、OKなどを返すと誤解を招くのでfalseを返すことにしました。

条件式が満たされない場合、そのブロックは実行されません。今回のケースではselection.countが0でない場合(つまり選択されているオブジェクトがある場合)はif文の中身は実行されず、処理は次のfor文へ移行します。

逆に条件式を満たした場合(つまり選択されているオブジェクトが無い場合)はif文の中が実行され、そこにreturnがあるのでそこで処理が終了し、その後の部分は実行されない、ということになります。

もしここにreturnが無かったら、if文の部分を実行したあとに次の行へ進行し、移行の処理が実行されることになります。

これで、選択されているものがなければメッセージを表示し、選択されているものがある場合は処理を行って「完了しました」というメッセージを表示するという機能が実装できました。

次回はこのメッセージにもう一工夫してみましょう。

今回のまとめ

  • 条件によって処理をわけたい場合はif文を使う
  • メッセージを表示するにはmessageBoxを使う
  • 関数(やそれに類するもの)を途中で抜けるにはreturnを使う

前:MaxScriptを書こう ~その8

次:MaxScriptを書こう ~その10