こんにちは。はじめまして。
KLabのテクニカルアーティストグループに所属している、こかろまと申します。
去年の KLab Creative Advent Calender 2018 以来のブログ執筆となります。
今回の内容はMaya+Pythonでツールを書き始めた方を想定しています。
ユーザーにとって「扱いやすいツール」とはどういった要素があげられるでしょうか。
作る方によって意見は変わってくるかと思いますが、私の場合は特に「ツールがユーザーにとって直観的であるかどうか(=ツールの直観性)」が重要だと考えています。
ここでいう「ツールの直観性」とは、使用するユーザーが普段利用しているツールの挙動に合わせることであったり、アプリケーションに最低限備わっているユーザーインターフェイスにできる限り合わせることとして位置づけています。
ユーザーインターフェイスの部品の一例としては、普段オペレーティングシステムで
使っている「ダイアログ」や...
アプリケーションによって異なりますが、「Ctrl+Z」のundo(元に戻す)・
「Ctrl+Y」のredo(やり直し)などのショートカットであったり...
そこで、今回は社内のツール実装方針として共有する目的も兼ねて、
Maya+Pythonにおける「undo(元に戻す)」「redo(やり直し)」についてお話ししたいと思います。
Maya+Pythonでツールを実装するにあたって、undo・redoで躓く点は
大きく分けて下記の二つになると思います。
それぞれの対処方法についてお話ししていきます。
Mayaでツールを実装する際に考慮していないと陥りやすいケースです。
「シーン上にあるpCube1オブジェクトを選択する」だけのツールを例に挙げてみます。分かりやすいようにバリデータなども一切組んでいません。
# select_tool.py from __future__ import absolute_import from __future__ import division from __future__ import print_function import maya.cmds as cmds class SelectTool(object): def __init__(self, select_object_name=''): self.select_object = select_object_name def execute(self): cmds.select(self.select_object, r=True) return def main(): _select_tool = SelectTool('pCube1') _select_tool.execute()
Maya+PythonにおいてMayaのdagノードを触るようなツールを実装する際には
方針によって様々ですが、基本以下のいずれかを利用するかと思います。
このうち、「maya.cmds」「maya.mel」は各コマンドにundo・redo機能が実装されており、「一つのコマンドごとに」mayaのコマンド履歴の待ち行列に溜まっていきます。
「pymel」も内部的に「maya.cmds」「maya.mel」を利用しているため、同様の挙動となります。
つまり、上記のような「シーン上にあるpCube1オブジェクトを選択する」だけのツールの場合、特にツール側に考慮は必要なくundo(pCube1選択前に戻る)・redo(pCube1選択後に戻る)することができます。
ですが、ツールは往往にして複雑になっていくものです。
たとえば、選択しているオブジェクトすべてを「Object_」というPrefixを付け、
後ろに数字を3桁の連番ごとにリネームするようなツールが必要になったとします。
その場合、大雑把ですが下記のような実装になると思います。
# rename_tool.py
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import maya.cmds as cmds
class RenameTool(object):
def __init__(self):
pass
@staticmethod
def validate():
if not cmds.lockNode(q=True, l=True):
return False
return True
def execute(self):
_selection_dag_nodes = cmds.ls(sl=True, sn=True, fl=True, dag=True, tr=True)
for _index, _value in enumerate(_selection_dag_nodes ):
if self.validate() is False:
continue
_rename = 'object_{0:03d}'.format(_index)
cmds.select(_value, r=True)
cmds.rename(_rename)
return
ツール自体は問題なく、シーン上の選択可能なdagノードがリネームされます。
また、undo・redoも行うことができます。
ただし、一点このツールには問題があります。
選択しているノードが大量にある場合のundo・redoの挙動です。
たとえば、50個くらいリネームすることができるdagノードを選択している場合に
上記のツールを実行します。
その後「あ、間違えてツール実行しちゃった!戻さなきゃ」とユーザーがundoを行うとします。
発行したコマンド回数分undoを行わないとツール実行前に戻れません。
今回の例では1つのdagノードあたりの実行順にすると「cmds.lockNode」「cmds.ls」「cmds.select」「cmds.rename」の4回分コマンドを発行しているので、ツール実行前の状態に戻す場合、1つのdagノードあたり合計4回undoを行う必要がありますが、mayaで設定されている待ち行列以上にundoすることはできません。
※正確にはコマンド単位ではなく、用意されている関数によって異なります。
Autodesk社のMayaコマンドリファレンスサイトに『「元に戻す」が可能』と記載があるものが待ち行列追加対象のものとなります。下記はlsコマンドのリファレンスです。
余談ですが、mayaの待ち行列の回数はMayaの『プリファレンス』⇒『元に戻す』項目より設定することができます。
インストール時点ではデフォルトで50に設定されています。
ツールによってコマンドが発行される回数次第で、ツール実行前の履歴を消してしまうこともありえます。
こういったツールは、「ユーザーにとって扱いやすいツール」とはいえません。
昨今のmayaでは待ち行列に登録する範囲を設定するチャンクが用意されています。
maya.cmds.undoInfoのopenChunk, closeChunkがそれにあたります。
openChunk関数でチャンクを開き、closeChunkでチャンクを閉じる間に処理を書くことで、その間の処理を1回の待ち行列として定義することができます。
今回のツールでは、シーン上のオブジェクトを回すイテレータを実行している「execute関数」に上記のチャンク範囲を適用することで、シーン上のオブジェクト数分のコマンド発行から「ツールを実行する」単位となり、undo・redoの回数を1回に抑えることができます。
# rename_tool.py from __future__ import absolute_import from __future__ import division from __future__ import print_function import maya.cmds as cmds class RenameTool(object): def __init__(self): pass @staticmethod def validate(): if not cmds.lockNode(q=True, l=True): return False return True def execute(self): cmds.undoInfo(openChunk=True) _selection_dag_nodes = cmds.ls(sl=True, sn=True, fl=True, dag=True, tr=True) for _index, _value in enumerate(_selection_dag_nodes ): if self.validate() is False: continue _rename = 'object_{0:03d}'.format(_index) cmds.select(_value, r=True) cmds.rename(_rename) cmds.undoInfo(closeChunk=True) return
ツールを実行し、リネームが確認できた後、Ctrl+Zでundoしてみます。
今度は1回ですべて戻す(=ツール実行前に戻る)ことができました!
上記でもチャンクを一つにまとめることはできますが、せっかくPythonを使っているので、本処理をデコレータとして定義しておき、共通ライブラリとして扱えるようにしておくとツールや関数ごとに定義する必要がないので便利です。
# undo_wrapper.py from __future__ import absolute_import from __future__ import division from __future__ import print_function import maya.cmds as cmds def undo_chunk(function): def wrapper(*args, **kwargs): cmds.undoInfo(ock=True) function(*args, **kwargs) cmds.undoInfo(cck=True) return wrapper
# rename_tool.py from __future__ import absolute_import from __future__ import division from __future__ import print_function import maya.cmds as cmds from . import undo_wrapper class RenameTool(object): def __init__(self): pass @staticmethod def validate(): if cmds.lockNode(q=True, l=True): return False return True @undo_wrapper.undo_chunk def execute(self): _selection_dag_nodes = cmds.ls(sl=True, sn=True, fl=True, dag=True, tr=True) for _index, _value in enumerate(_selection_dag_nodes ): if self.validate() is False: continue _rename = 'object_{0:03d}'.format(_index) cmds.select(_value, r=True) cmds.rename(_rename) return
頂点すべてに処理を加える、UVを展開する、スキンウェイトを調整するなど、
処理自体が重いツールを実装する場合、maya.cmdsやpymelでは実行速度に期待ができないのもあって、弊社では基本的にopenmaya for Python APIを利用しています。
openmaya for Python API では、undo・redo機能は提供されていません。
そもそも処理したことが待ち行列にスタックされないため、undoを行っても
openmaya for Python APIを実行する前の履歴が実行され、処理したことはそのまま残り続けます。
そのため、undo・redoが必須な場合はすべて自前で実装する必要があります。
openmayaでundo・redoを考慮に入れる場合、MPxCommandのプロキシクラスを継承したクラス単位で処理を実装する必要があります。
また、一種のコマンドとして実装する必要があるため、プラグイン化する必要があります。
プラグイン化については今回は省きますが、Autodesk社公式リファレンスが非常に参考になります。
選択しているオブジェクトの頂点カラーをすべて赤(1.0, 0.0, 0.0, 1.0)で
塗りつぶす簡単なプラグインを例に挙げます。
今回は簡単に説明するためにdagノードの名前単位で頂点カラー情報を保持していますが、
厳密にアンドゥ・リドゥを実装する際にはMTypeID等で制御するべきです。
# vertex_color_fill_plugin.py # coding: utf-8 from __future__ import absolute_import from __future__ import division from __future__ import print_function import re import _ctypes import maya.cmds as cmds import maya.api.OpenMaya as OpenMaya class VertexColorFillTool(OpenMaya.MPxCommand): kPluginCmdName = 'VertexColorFillTool' def __init__(self): OpenMaya.MPxCommand.__init__(self) self.before_calculate_vertex_color_dict = {} self.selection_list = [] @staticmethod def initialize(): return VertexColorFillTool() @staticmethod def validate(): if not cmds.ls(sl=True): print('error : no selection') return False _color_set_list = cmds.polyColorSet(q=True, acs=True) if not _color_set_list: print('error : color set no exists') return False return True def get_active_vtx_selection_list(self): _vertex_list = self.get_select_vtx_list() if not _vertex_list : cmds.error(u'not selected dag object or vtx') return cmds.select(_vertex_list , r=True) return OpenMaya.MGlobal.getActiveSelectionList() @staticmethod def get_select_vtx_list(): _vtx_list = cmds.polyListComponentConversion(tv=True) _vtx_list = cmds.ls(_vtx_list, fl=True) return _vtx_list def doIt(self, args): """ コマンド実行時(関数実行) 関数 :param args: :return: """ if not self.validate(): return self.selection_list = self.get_active_vtx_selection_list() for _selection_object in xrange(self.selection_list.length()): _vertex_colors = [] _dag_path, _ = self.selection_list.getComponent(_selection_object) _target_mesh = OpenMaya.MFnMesh(_dag_path) # 頂点カラーの実行前の状態を保持 self.before_calculate_vertex_color_dict[_selection_object] = _target_mesh.getVertexColors() _vertex_list = cmds.ls(self.selection_list.getSelectionStrings(_selection_object), fl=True) _vertex_colors = [(1.0, 0.0, 0.0, 1.0) for i in _vertex_list] _target_mesh.setVertexColors(_vertex_colors, xrange(_target_mesh.numVertices)) return def redoIt(self): """ 待ち行列の実態(リドゥ) 基本処理を再度実行すればOK :return: """ self.doIt() return def undoIt(self): """ 待ち行列の実態(アンドゥ) 実行前の値を書き込むことで元に戻す(ように見せる) :return: """ for _selection_object in xrange(self.selection_list.length()): _dag_path, _ = self.selection_list.getComponent(_selection_object) target_mesh = OpenMaya.MFnMesh(_dag_path) target_mesh.setVertexColors(self.before_calculate_vertex_color_dict[_selection_object], xrange(target_mesh.numVertices)) return def isUndoable(self): return True def maya_useNewAPI(): """ プラグインに渡されるオブジェクトの型を示すための定義。openmaya for Python API 2.0以降のみ必須。 :return: """ pass def initializePlugin(mobject): _open_maya_plugin = OpenMaya.MFnPlugin(mobject) try: _open_maya_plugin.registerCommand(VertexColorFillTool.kPluginCmdName, VertexColorFillTool.initialize) except TypeError: print('error initiliaze plugin') def uninitializePlugin(mobject): _open_maya_plugin = OpenMaya.MFnPlugin(mobject) try: _open_maya_plugin .deregisterCommand(VertexColorFillTool.kPluginCmdName) except: print('error uninitiliaze plugin')
# 実行側のコード(スクリプトエディタなど) import maya.cmds as cmds cmds.loadPlugin('vertex_color_fill_plugin') cmds.VertexColorFillTools()
プラグインとしてロードし、実行した結果が下記のとおりです。
各dagノードが元々持っていた頂点カラーに戻せていることが確認できます。
あくまで上記のクラスを継承した際にはundoやredo等、ユーザ操作に対応した
継承元のコールバック関数が呼び出されるだけなので、
上記3つの実装方針に基づいてundo・redoを実装することになります。
そのため、処理次第ではundo・redoではない処理を実装することもできてしまいます。
細心の注意を払って実装しないと、うっかりデータを破壊するような処理になりかねません。
また、プラグインの都合上Maya上でunInitialize(プラグインマネージャでプラグインのロードを外す等)されてしまうと当然そのツールで保持していた待ち行列は破棄されてしまうため、注意が必要です。
ツールやユーザーのニーズによってundo・redoの範囲の要件は変わります。
また、undo・redoがそもそも必要ないケースもあるかと思います。
openmaya for Python API を扱う必要があるツールにundo・redoが必須要件としてツール完成後に加わる場合、『単なるPythonのパッケージ群のツール設計』から『Mayaでのプラグイン設計が必須のツール設計』と、実装方式や再設計に係る工数が大きく変わることもあるため、必ず初期のツール設計段階の要件の一つとして洗い出しを行うことを強く推奨します。
今回は「ユーザーにとって扱いやすいツール」の一例として、undo・redoについて
お話しさせていただきました。
本記事がMayaでツールを作る人の手助けになれば幸いです。
KLabのクリエイターがゲームを制作・運営で培った技術やノウハウを発信します。
合わせて読みたい
KLabのクリエイターがゲームを制作・運営で培った技術やノウハウを発信します。