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

MaxScriptを書こう ~その8

UIをつけてツール化しよう

UIというのはユーザインタフェースのことです。ここではグラフィカルなものを指しているので厳密にはGUI(グラフィカルユーザインタフェース)と呼んだほうが良いのですが、3dsmaxでは「ユーザインタフェースのカスタマイズ」という具合に使われているのでそれに習うことにします。

※余談ですが私は「ユーザインタフェイス」と表記する方が好きです

さて、MaxScriptでグラフィカルなUI、つまりボタンやスピナー、スライダやリストなど、視覚的に操作できるインタフェースを作る方法は大きく分けて2つあります。ロールアウトと呼ばれる要素を作ってそれをダイアログとして表示する方法と、複数のロールアウトを組み合わせてロールアウトフロータと呼ばれる小窓を生成する方法です。

まずはロールアウトダイアログを作る方法でやってみましょう。

ロールアウトを作る

ロールアウトというのは3dsmaxのパネル上でも使用されているものです。百聞は一見にしかず、とにかく作ってみましょう。

ロールアウトはrolloutというキーワードを使って次のように定義します。

これを踏まえると今定義したロールアウトは、ro_adjust_posという名前の変数に格納され、タイトルとして「位置の値を丸めるツール」という文字列が表示されますよ、ということになります。

中身の部分は何もないので空っぽのものができます。このロールアウトを生成するには、次のようにcreateDialog()というビルトイン関数を使用します。createDialogには引数としてロールアウトオブジェクトを渡します。ここでロールアウトの宣言に使用した変数を使います。

実行してみてください。rolloutの定義をエディタに書いて評価し、その上でcreateDialogをリスナーから実行しても構いません。エディタのrollout定義の下にcreateDialogを書いても構いません。rollout定義がcreateDialogより先に評価されていれば問題なくロールアウトが表示されます。

ボタンを作る

空のロールアウトを生成できたので、次はこれにボタンを乗せてみましょう。ボタンは次のようにして定義します。

これに習って実行ボタンを作りましょう。

createDialogまでエディタに書いてしまってまとめて実行されるようにしました。書けたら評価(Ctrl+E)してみてください。

ボタンができました。もちろん、押しても何も起こりません。ボタンを押した時どうなるか、という処理をまだ実装していないからです。その「ボタンが押されたときに行う処理」のことをイベントハンドラと呼びます。「ボタンが押された」というような状況のことをイベントと呼び、他にも「テキストが入力された」「ラジオボタンが押された」「チェックボックスにチェックがつけられた」「スライダが動かされた」などの操作のほか、「ロールアウトがドラッグされた」「サイズが変更された」などもイベントです。それらのイベントが検知されたときに呼ばれる処理がイベントハンドラです。

あまり難しく考えず、「ボタンが押されたときに実行される処理」ぐらいに思っておけば大丈夫です。

ではその処理を書いていきましょう。ボタンが押された、というイベントのハンドラはpressedというキーワードで定義します。まずは仮で、リスナーに文字列を出力してみましょう。

スクリプトを評価してUIの実行ボタンを押してみてください。

リスナーに「ボタンが押されたよ」と表示されれば成功です。

あとはこのイベントハンドラの中に、ボタンを押した時に実行したい内容を書けば良いだけです。今回は「選択されているオブジェクトすべてに対してadjustPos()を実行する」という処理を実装してみましょう。

評価してシーン内のオブジェクトを適当に選択し、実行ボタンを押してみましょう。

※このロールアウトを評価する前にadjustPos()の関数定義を評価しておかないとエラーになります。先にadjustPos()を評価しておいてからロールアウトを評価してください。

選択したオブジェクトに対して「位置の値を丸める」という操作を実行するツールができました。

次回はこのツールを少し改良しましょう。

今回のまとめ

  • ロールアウトを定義し、createDialogすることでユーザインタフェースを表示することができる
  • ボタンが押されたときに何か処理をするにはイベントハンドラを定義する必要がある
  • ツール内で使用する関数は使用する前に評価されている必要がある

前:MaxScriptを書こう ~その7

次:MaxScriptを書こう ~その9

MaxScriptを書こう ~その7

関数はどうあるべきか

いきなり大上段に構えましたが、「こうあらねばならぬ!」というような固いものではなく、どんな風になっていると使いやすいか、というようなことを考えてみよう、という程度の話です。

最近では「関数型」と呼ばれる構造のプログラムも出てきており、関数はどうあるべきか、という問題はあちこちで議論されています。とても厳格にルールを定める領域もあれば、割と緩いものもあります。

3DCGのスクリプトというレベルで考えれば、言ってしまえば「どうだっていい」ということになるような気がします。ただ、関数を作る時の指針を自分なりに設けておくと、作ったものが後で再利用しやすい、という実利があるので考えてみる価値はあるでしょう。

その関数がどういう場面で使われるものかによっても変わってきます。さまざまなところで何度も使いまわすようなライブラリ関数的なもの、特定のデータを扱うクラスメソッドのようなもの(クラスについてはのちのちPythonの話題で触れると思います)など用途によって考え方は異なりますし、また柔軟に考えて良いと私は思っています。

今回は汎用性のあるライブラリ関数的なものを作る、という方向で関数のあり方を考えてみましょう。こうした関数を集めて自分のライブラリを作っておくと、ツールを作るのがどんどん楽になっていきます。

ライブラリ関数の指針として私が気をつけているのは以下のようなことです。

  • なるべく単機能にする
  • 操作するデータは引数で受け取る(関数の中で作らない)
  • パラメータを引数で受け取るようにして柔軟性を持たせる

これらを踏まえて、私たちのadjustPos()を汎用関数にし、それを使うツールを作ってみましょう。

単機能にする

まずは単機能にするというところですが、adjustPos()は既に「位置の値を丸める」という単機能の関数と言えますので問題ありません。例えば仮に、位置もスケールも回転も丸める、というような物を作る場合は、位置を丸める関数、スケールを丸める関数、回転を丸める関数、という3つに分けて作ると良い、というような意味だと考えてください。

操作するデータを引数で受け取る

操作するデータというのはこの場合「選択されているオブジェクト」ということになりますね。これを引数で受け取るようにしよう、ということです。まずは元の状態のコードを確認してみましょう。

「操作するデータ」という点に注目すると、関数の中でselectionをループしているところに問題がある、ということになりそうです。そこでこれを引数で受け取るようにしよう、と考えます。

ここで考えるべきこととして、引数としてコレクションを受け取って関数内でループするのか、それとも個々のオブジェクトを受け取って1つだけを処理するような関数にするのか、という問題が出てきます。

今回はライブラリ関数として使えるようなものを目指すので、関数自体はなるべくシンプルな方が良いでしょう。そこで1つだけを処理するという方向で調整してみましょう。

引数としてobj(Objectの略のつもり)という変数を用意し、そのobjに対して操作を行うような関数になりました。(この引数の名前もなんでも構いませんが、見てわかりやすい名前が良いでしょう)

書き換えたらこの関数を評価(Ctrl+E)して実行してみましょう。今回は引数が必要になったので、シーン内でオブジェクトを1つ選択して次のように実行します。

引数のある関数は関数名のあとに半角スペースを空けて、引数として渡したい変数(ここでは$)を書きます。

無事に実行できましたが、複数選択をして引数に$を渡すとエラーになってしまいます。せっかく複数選択に対応していたのに、関数を直したら後退してしまったような気がしますね。

しかしこれにより、adjustPosはより汎用性の高い、さまざまなシチュエーションで役に立つものになったのです。今はまだ実感がわかないかもしれませんが、単機能であるがゆえに使い回しが利くのです。では選択したもの全部を処理したい場合はどうすんだよ、ということになりますが、selectionをループする処理は関数の外で行えば良いということになります。

adjustPos()を定義した(関数を書くことを「定義する」と言います)部分の下に、以下のように追記してください。

追記したのでエディタのウィンドウ全体では以下のようになります。

上で関数adjustPos()を定義し、下のfor文でそれを使用しています。なんだか手間が増えただけのような気がしますが、関数は単にオブジェクトを受け取るだけなので、今回の場合、使用する部分を次のように書き換えるとシーン内の全ジオメトリオブジェクトを対象にして実行することもできます。

今度はselectionの代わりにgeometryを使っています。geometryにはシーン内に存在する全てのジオメトリオブジェクトが格納されています。

このように、関数をシンプルな状態にしておくことで、その関数は汎用的に使いやすいものになります。どうすれば使い回しがしやすいか、ということを念頭に置いて関数を設計すると良いでしょう。

次回はこの機能をツール化すべく、ユーザインターフェイスをつけてみましょう。

今回のまとめ

  • 関数はなるべく単機能にする
  • 関数の中で操作するデータは引数として渡すようにする
  • シンプルな状態にしておいたほうが汎用的に使いやすい

前:MaxScriptを書こう ~その6

次:MaxScriptを書こう ~その8

MaxScriptを書こう ~その6

for文を使ってselectionをループする

いきなりわけのわからない小見出しでひるみそうになったかもしれませんが、このページを読み終えるころには完全にわかるようになるのでそのまま進んでください。

前回説明したように、「選択されているもの」を対象にして何かを行うときには、$ではなくselectionを使うのが良いです。selectionには現在選択されているオブジェクトが全部入っていますので、そこから1つずつ取り出しては操作を行う、というような処理を行うことになります。

私たちのadjustPosは1つのオブジェクトに対して処理を行うようになっているので、選択されているすべてのオブジェクト(selectionのなかに入っているもの全部)から1つずつ取り出して処理を実行してやれば良い、ということになりますね。

まずはadjustPosは横へ置いておいて、selectionを使って中のオブジェクト1つ1つに対して何かする、というもののシンプルな例をやってみましょう。

スクリプトエディタで「ファイル→新規」として新しいエディタを開き、次のコードを入力してください。

シーン内のオブジェクトを選択しておいてこれを評価(Ctrl+E)すると、選択されているオブジェクトの名前がずらずらっとリスナーに出力されるはずです。

ここで使用した「for」というのはよく「for文」と呼ばれるもので、MaxScript以外のほかのプログラム言語にもたいてい存在しています。for文は一般的に何らかの処理を繰り返すときに使用され、「ループ構文」などと呼ばれるものの一つです。

MaxScriptのfor文には大きく分けて2種類の書き方がありますが、ここで使用したのはそのうちの1つです。コレクションオブジェクトに対して使用する場合に使う書き方ですので覚えてしまいましょう。

ここで使っている「1つ1つを格納するための変数名」はなんでも構いません。わかりやすいもので良いでしょう。ここではselectionのなかの1つなのでsel(selected object の略のつもり)という変数名をつけました。

変数名は3dsmax内で既に使われている名前でない限りは何を使っても構いません。逆に、例えばビルトイン関数の名前であるfloorのようなものに他の何かを代入することはできないので、floorは変数名に使えません。

この書き方と上のコードを比べてみると、selという変数にselectionから取り出したオブジェクトを代入し、print sel.nameとすることでその名前をリスナーに出力している、ということになります。

関数を書き換えてみる

ではこのfor文を使ってadjustPosを書き換えてみましょう。もとのadjustPosで$を使っていた部分に、この「1つ1つを格納するための変数名」を使えば良いことになります。

関数を書き直したら改めて評価(Ctrl+E)します。

さあ、シーン内のオブジェクトを複数選択してadjustPos()を実行してみましょう。

めでたく複数のオブジェクトに対して実行することができました。

次回は関数のあり方について少し学び、さらに汎用性の高いものに改善していきましょう。

今回のまとめ

  1. 繰り返しの処理には「ループ構文」の一種である「for文」を使う
  2. for文を使うことでコレクションオブジェクトの中の個々のオブジェクトを操作できる

前:MaxScriptを書こう ~その5

次:MaxScriptを書こう ~その7