MaxScriptを書こう ~その7

2021年9月3日

関数はどうあるべきか

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

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

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

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

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

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

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

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

単機能にする

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

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

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

function adjustPos = (
    for sel in selection do (
        sel.pos.x = floor sel.pos.x
        sel.pos.y = floor sel.pos.y
        sel.pos.z = floor sel.pos.z
    )
)

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

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

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

function adjustPos obj = (
   obj.pos.x = floor obj.pos.x
   obj.pos.y = floor obj.pos.y
   obj.pos.z = floor obj.pos.z
)

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

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

adjustPos $

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

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

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

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

for sel in selection do (
    adjustPos sel
)

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

function adjustPos obj = (
    obj.pos.x = floor obj.pos.x
    obj.pos.y = floor obj.pos.y
    obj.pos.z = floor obj.pos.z
)

for sel in selection do (
    adjustPos sel
)

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

for obj in geometry do (
    adjustPos obj
)

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

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

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

今回のまとめ

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

前:MaxScriptを書こう ~その6

次:MaxScriptを書こう ~その8