チュートリアル / 読んで触ってよくわかる!Mayaを使いこなす為のAtoZ
第78回:Mayaで複雑なツールを作る時のコツ ― 関数で使うデータの取り扱い
- Maya
- ゲーム
- コラム
- スクリプト・API
- チュートリアル
- 中級者
さて、前回はツールの機能を「ステップとして関数に分けて実行する」話をしました。この関数、実行の仕方にもまた色々な方法があります。何かを実行するということは「データ」をもとに「結果」を生み出すということですので、このデータや結果の受け渡し方で方法が分かれてきます。これもまた前回同様、大きく2つの方法に分けてご紹介したいと思います。一つは「クラスのメンバ変数で受け渡す方法」、もう一つは「引数で受け渡す方法」です。
クラスのメンバ変数で受け渡す方法
MELでは出来ませんが、PythonやC++で可能な方法です。例えば何かを実行する時、次のように引数は渡さずクラス内のメンバ変数をデータとして実行し、結果を返します。
# -*- coding: utf-8 -*- class MyTool: data = () def __init__(self): pass def doIt(self): self.data = (1,2,3,4) result = self.funcA() print result def funcA(self): # 意味ないですが、なんとなく全部足します。 return sum(self.data) tool = MyTool() tool.doIt() # 10
funcA内で必要なデータが増えても、funcAに渡す引数が増えないため、手っ取り早く管理も簡単です。規模が小さいうちは。
問題が出てくるのはツールの規模が大きくなってきた時です。funcAの実行で何のデータが処理に必要か、funcA内のコードを読まないとわからないためメンテナンスが大変です。また、必要なデータをどこで、どのタイミングで、どうやって準備するべきか曖昧なため、後からメンテナンスをしづらくなります。初めてこのツールを見た人が、コードをすべて読んでいなければどう思うでしょうか?self.dataが何のためにあるか、どういう値を求めているか分かりにくいですし、self.dataに処理したいデータを入れると思わず、funcAの結果が来ると思ってしまうかもしれません。もしfuncBなど他に関数があれば、self.dataの値が他の関数で使われていないという保証もありません。色々なことが曖昧になっています。
こういう状況になると、後から拡張や修正する時にコードを読みきれず、結局自分独自の別の関数を作って、そちらで処理してしまったほうが安全、という事になったりします…。
funcAの結果について、今回は返り値で受け取っているのでマシですが、結果をクラスのメンバ変数に記録すると、規模が大きくなるに連れ何がどこに書き込まれているか把握できなくなってきます。例えば次のようなパターンです。doIt内は簡潔でいい感じなのですが、一度問題が起きると何が起きているか把握するために、全てのコードを読まなければならなくなります。
# -*- coding: utf-8 -*- class MyTool: data = () sumOfData = 0.0 def __init__(self): pass def doIt(self): self.data = (1,2,3,4) self.funcA() result = self.funcB() print result def funcA(self): # 意味ないですが、なんとなく全部足します。 self.sumOfData = sum(self.data) def funcB(self): # もっと意味ないですが、なんとなく二倍にします。 return self.sumOfData * 2 tool = MyTool() tool.doIt() # 20
全てを把握している時はコンパクトで良いのですが、sumOfDataがどこで設定されて、どこで利用されるか見分けるのが難しいですよね。funcBを先に実行してfuncAを実行しても安全なのか、意図したように動作しているか調べるのも大変です。ちょっと危ないニオイがします。
引数で受け渡す方法
別の方法としては、関数が必要とするものは全て引数で渡す、という方法があります。次のようなパターンです。
# -*- coding: utf-8 -*- class MyTool: def __init__(self): pass def doIt(self): data = (1,2,3,4) result = self.funcA(data) print result def funcA(self, data): # 意味ないですが、なんとなく全部足します。 return sum(data) tool = MyTool() tool.doIt() # 10
関数に何が必要か、アウトプットが何なのか明確です。ただ、必要なデータが増えるごとに引数が長くなるため、見た目が悪くなります。
あまりに引数に渡すものが多く、バラバラな時は、データを意味のある単位にまとめる時が来ているかもしれません。次の例では、オブジェクトの名前と位置をobjectDataというクラスにしてから実行すると、そのオブジェクトの位置を設定します。もし回転など情報を追加したいなら、objectDataに変数を増やすだけですので、setPositionクラス側の引数の変更は不要です。C++では構造体やクラスを作って渡すことになります。こうすれば引数でデータを渡してもコードがスッキリしますし、読みやすいため、デバッグもしやすくなります。
# -*- coding: utf-8 -*- import maya.cmds as cmds class objectData: def __init__(self, objectName, position=(0,0,0)): self.objectName = objectName self.position = position class setPosition: def __init__(self): pass def doIt(self, data): for item in data: cmds.xform(item.objectName, t=item.position ) cube1 = cmds.polyCube() cube2 = cmds.polyCube() data = [] data.append( objectData(cube1[0], (1,1,0) ) ) data.append( objectData(cube2[0], (0,-1,-2) ) ) setPosition().doIt(data)
メンバ変数と引数をバランスよく使う方法「ファンクションセット」
関数を実行する時、クラス内のメンバ変数を使う場合でも、引数でデータを渡す場合でも、それぞれに利点・欠点があります。そのどちらも合わせた方法があります。
MayaのAPIを見ると「MFn」という名前がついたものがズラズラあることに気づかれることでしょう。これはファンクションセットというものです。例えばMFnMeshというAPIがあります。これはメッシュのファンクションセット(Fn)です。MayaのAPIはファクトリーメソッドパターンでデザインされており、オブジェクトの作成はMFnのついた名前のクラスから行うようになっています。
細かいことはさておき、今回のテーマである「関数のデータ」の扱いを見てみましょう。次の例では、キューブを作ってフェースを押し出しています。
# -*- coding: utf-8 -*- import maya.api.OpenMaya as om import maya.cmds as cmds cube = cmds.polyCube() # メッシュの選択リストを得て、MFnMeshに設定。 sel = om.MSelectionList().add(cube[0]) dagPath = sel.getDagPath(0) dagPath.extendToShape() meshFn = om.MFnMesh(dagPath) # メッシュに何か実行します。 # ここではフェースを押し出します。 faceList = [0,1] meshFn.extrudeFaces(faceList, thickness=0.5)
MFnMeshであるmeshFnは、DAGパスと共に作成されます。この時点でMFnMesh内にはDAGパスのデータがメンバ変数に保存されます。
次に、meshFn.extrudeFacesを実行する時に、フェースのリストと厚さを引数として渡して実行しています。この後続けてメッシュをスプリットしたり、削除をしたりなど色々なことを続けて実行できます。
概念としては、Pythonで書くと次のように値を取り回しています。メンバ変数と引数が適度に両立されています。
import maya.api.OpenMaya as om class MFnMesh(InternalMeshImpl): dagPath = om.MDagPath() def __init__(self, dagPath): self.dagPath = dagPath def extrudeFaces(self, faceList, faceTogether=True, offset=0.0, thickness=0.0): super().extrudeFaces(self.dagPath, faceList, faceTogether, offset, thickness) ...
ファンクションセットは、対応するデータを加工するツールとして機能します。上の例で分かる通り、ツールに対象物(ここではメッシュ)を設定すると、その情報はずっと使い続けるためメンバ変数に保存します。以後、なにか実行する時はパラメーターを引数として渡します。実行する機能のパラメーターを内部に保存しておかず、引数で指定します。というわけで、メンバ変数と引数をちょうどよい具合で使い分けているのです。
まとめ
関数の実行方法にも色々な方法があり、それぞれ一長一短ですからどれが良いということはありません。コードの量や、行っている内容によって適切な方法が変わりますし。どういう作り方があるか、自分がどれを行っているか知っておくことで、困った時に別の対応方法を見つけられるようにしておくことが大切です。
次回はもう少し先に進めて、独自のコンポーネントを作成する方法を見ていきたいと思います。