cymel入門¶
インポート¶
cymel の機能は以下の3つを選択してインポートできます。
UI以外の全機能
cymel.main
UIに関する機能
cymel.ui
任意でグローバルに展開すると便利なごく僅かな定数
cymel.constants
以下のようにインポートするのがおすすめです。
import cymel.main as cm
import cymel.ui as cmu
from cymel.constants import *
cymel.constants
は cymel.main
内にも展開されているため、グローバルスコープを汚すことを嫌う場合はインポートしなくても構いません。
また、以下のように cymel.all
をインポートするだけで、すべてのインポートを一度に行うこともできます。
上記といくつかの推奨モジュールがインポートされます。
from cymel.all import *
スタンドアロン Python (mayapy) で cymel をインポートすると、
cymel.initmaya
モジュールの働きによって Maya が初期化されます。
注釈
本来の maya.standalone モジュールの initialize では userSetup.py は呼ばれますが userSetup.mel は呼ばれません。 cymel による初期化ではそれを補い、 userSetup.mel も呼ばれるようにしています。
ちなみに、pymel.core をインポートすると Maya UI を起動したのに近い、さらに多くのことがされます(たとえば、プラグインの自動ロードなど)。 cymel では、あえてそこまではやらず、ごく最低限のことに留めています。
ノードのラッパークラス¶
ノードオブジェクトの取得¶
cymel は、プラグインも含まれる全てのノードタイプのラッパークラスを提供します。
全てのノードクラスは CyObject
を基底クラスとし、ノードタイプのツリーに沿って継承されています。
ノードをラップしたオブジェクトを得るには CyObject
コンストラクタを利用するのが簡単です。
たとえば、以下のようにノード名を指定します。
>>> cm.CyObject('persp')
Transform('persp')
pymel をご存知なら PyNode に相当するのが CyObject
です。
>>> cm.O('persp')
Transform('persp')
選択されているノードの場合はもっと簡単です。
選択ノードを1つ得るには sel
を使います(複数選択されていても最初の1つとなります)。
>>> cmds.select(['persp', 'side'])
>>> cm.sel
Transform('persp')
または selobj
関数ではインデックスを指定できます。
>>> cm.selobj(1)
Transform('side')
選択されているものをすべて得るには selection
か、または pymel 風に selected
関数を使います。
>>> cm.selection
[Transform('persp'), Transform('side')]
>>> cm.selected()
[Transform('persp'), Transform('side')]
ノードクラスによる操作¶
全てのノードクラスは NodeTypes
のインスタンスである cm.nt
からアクセスできます
(ごく一部の代表的なノードクラスは cm
からでもアクセスできます)。
クラス名は、ノードタイプ名の先頭を大文字にした名前となります。
>>> cm.nt.Joint
<class 'cymel.core.typeregistry.Joint'>
ノードクラスに既存ノードの名前を指定しないと、新規にノードが生成されます。
>>> cm.nt.Joint()
Joint('joint1')
これには createNode コマンドのオプション引数を指定できます。
>>> cm.nt.Joint(n='foo#')
Joint('foo1')
また、ノードクラスは ls コマンドのラッパーとしても機能します。
>>> cm.nt.Joint.ls()
[Joint('foo1'), Joint('joint1')]
ls コマンドに -type オプションが自動的に指定された結果を得られますが、その他のオプション引数は自由に指定できます。
>>> cm.nt.Joint.ls('foo*')
[Joint('foo1')]
ノードクラスを明示したオブジェクト取得¶
既存のノード名を指定してラッパーオブジェクトを得るとき CyObject
ではないノードクラスを直接指定することもできます。
>>> cm.nt.Joint('foo1')
Joint('foo1')
このとき、互換性のある(継承している)クラスなら全て指定できます(上位になるほど抽象的になり、サポートされる機能が少なくなります)。
>>> cm.nt.Transform('foo1')
Transform('foo1')
ただし、互換性のないクラスを指定するとエラーになります。
たとえば、 joint は transform でもありますが shape ではないので、 Shape
クラスを指定するとエラーになります。
やはり、通常は、クラスを明示するよりも CyObject
を指定するのが簡単で確実です。
クラスの明示は、 カスタムノードクラス を作り未登録のまま使う場合や、あえて抽象的な振る舞いをさせたいような場合に使用します。
たとえば、 DagNode
派生クラスは DAGパスを含んでいるため、同一ノードのインスタンスでもパスが異なれば違うものとして扱われます。
しかし、より抽象的な Node
インスタンスとして扱えば、DAGパスは含まれないため、同じものになります。
>>> cmds.file(f=True, new=True)
u'untitled'
>>> cmds.polyCube()
[u'pCube1', u'polyCube1']
>>> cmds.instance()
[u'pCube2']
>>> cm.O('pCube1|pCubeShape1') == cm.O('pCube2|pCubeShape1')
False
>>> cm.Node('pCube1|pCubeShape1') == cm.Node('pCube2|pCubeShape1')
True
あるノードがあるノードタイプの派生タイプかどうかを調べたい場合、以下のように Python の insinstance が利用できると思われるかもしれません。
>>> isinstance(cm.O('initialShadingGroup'), cm.nt.ObjectSet)
True
しかし、先に説明したように、抽象的なノードクラスを明示してそのインスタンスを取得できるということは、 以下のように isinstance ではノードタイプを厳密に判別できないことにもなります。
>>> isinstance(cm.nt.Node('initialShadingGroup'), cm.nt.ObjectSet)
False
この弱点は設計段階から把握された上で、あえてそのようになっています。
何故かというと、 カスタムノードクラス を自由に作れるという仕組みによって、 isinstance でタイプ判別ができるという前提は既に崩れているからです。 pymel も然りです。
そこで、確実にノードタイプを判別するには、
isinstance ではなく、以下のように isType
か hasFn
メソッドを利用してください。
>>> cm.nt.Node('initialShadingGroup').isType('objectSet')
True
>>> cm.nt.Node('initialShadingGroup').hasFn(api.MFn.kSet)
True
とはいえ、純粋にノードタイプを判別したいという用途ではなく、文字通り、派生クラスのインスタンスかどうかを判別したいのならば isinstance は有用です。 たとえば、 カスタムノードクラス ではノードタイプ以外の条件も加味してクラスを決定できるため、そういった条件込みで判別したい場合などには有用です。
アトリビュートのラッパークラス¶
プラグへのアクセス方法¶
ノードをラッパーオブジェクトとして扱うと、プラグ(アトリビュート)へのアクセスも便利になります。
以下のように、ノードオブジェクトの属性として、 Plug
クラスのオブジェクトを得られます。
Plug
は Node
と同様に、基底クラス CyObject
の派生型です。
ショート名でもロング名でも同じものが得られます。
>>> cmds.file(f=True, new=True)
u'untitled'
>>> cm.nt.Transform()
>>> cm.sel.t
Plug('transform1.t')
>>> cm.sel.translate
Plug('transform1.t')
また、MELコマンドの場合と同様に、 transform から shape のアトリビュートに直接アクセスもできます。
>>> cm.O('persp').focalLength
Plug('perspShape.fl')
アトリビュート名は、まれに Pythonのキーワードや、ノードオブジェクトのメソッド名などと衝突する場合もあります。
そういった場合のために plug
メソッドでもアクセスできます。
>>> cm.sel.plug('t')
Plug('transform1.t')
コンパウンドアトリビュートから子アトリビュートを得ることもできますが、 ノードから直接得ることもできます。
>>> cm.sel.t.tx
Plug('transform1.tx')
>>> cm.sel.tx
Plug('transform1.tx')
しかし、コンパウンドのマルチの場合、いきなり子プラグを得るとインデックスが未解決となってしまいます。
>>> cmds.file(f=True, new=True)
u'untitled'
>>> cmds.polyCube()
[u'pCube1', u'polyCube1']
>>> cmds.select(cm.sel.shape())
>>> cm.sel.gcl
Plug('pCubeShape1.iog[-1].og[-1].gcl')
そういった複雑なケースでは、マルチ要素を解決しながらコンパウンドを下っていけます。
>>> cm.sel.iog[0].og[0].gcl
Plug('pCubeShape1.iog[0].og[0].gcl')
他にも様々な方法でアクセスできます。
>>> cm.sel.plug('iog[0].og[0].gcl')
Plug('pCubeShape1.iog[0].og[0].gcl')
>>> cm.O('pCubeShape1.iog[0].og[0].gcl')
Plug('pCubeShape1.iog[0].og[0].gcl')
>>> cm.O('.iog[0].og[0].gcl')
Plug('pCubeShape1.iog[0].og[0].gcl')
値のセットとゲット¶
Plug
クラスにも様々なメソッドがありますが、
たとえば set
や get
メソッドでは値のセットやゲットができます。
>>> cm.sel.t.get()
[0.0, 0.0, 0.0]
>>> cm.sel.t.set([1, 2, 3])
>>> cm.sel.t.get()
[1.0, 2.0, 3.0]
ここで、ひとつ重要な注意点があります。
それは、単位付きタイプの場合、 set
や get
では「内部単位」で扱われるという点です。
単位付きタイプには「距離」(doubleLinear)、「角度」(doubleAngle)、「時間」(time) がありますが、 内部単位は、それぞれ Centimeter、Radians、Second となっています。
たとえば、rotate では、通常の人が慣れた Degrees ではなく、以下のように Radians で扱う必要があります。
>>> cm.sel.rx.set(PI * .5)
>>> cm.sel.rx.get()
1.5707963267948966
一見面倒に見えるかもしれませんが、これは「シーン設定(単位)に依存しないプログラミングをすべき」という思想に基づいています。
もし、どうしても「UI設定単位」で扱いたい場合、 setu
や getu
を用いることもできます。
>>> cm.sel.rx.getu()
90.0
>>> cm.sel.rx.setu(180)
>>> cm.sel.rx.get()
3.141592653589793
ただし、 setu
や getu
を用いるのは、
スクリプトエディターでちょっとタイプして結果を得るようなインスタントなスクリプトに留めるのが無難です。
コネクション編集¶
>>
や <<
や connect
メソッドで、プラグの接続ができます。
また、接続を調べるには Node
の inputs
や outputs
、
Plug
の inputs
や outputs
メソッドが利用できます。
>>> cmds.file(f=True, new=True)
>>> a = cm.nt.Transform(n='a')
>>> b = cm.nt.Transform(n='b')
>>> a.t >> b.t
>>> a.t.isSource(), a.t.isDestination()
(True, False)
>>> b.t.isSource(), b.t.isDestination()
(False, True)
>>> b.inputs(asPair=True)
[(Plug('b.t'), Plug('a.t'))]
connect
メソッドは pymel と指定順序が逆なので注意してください。
これは disconnect
メソッドと指定順を統一するためです。
そのため、演算子は >>
よりも <<
の利用を推奨します。
>>> b.r.connect(a.r) # b.r << a.r
>>> b.r.inputs()
[Plug('a.r')]
>>> b.s << a.s
>>> b.s.inputs()
[Plug('a.s')]
切断は //
や disconnect
メソッドで行えます。
>>> a.t // b.t
>>> b.r.disconnect(a.s)
>>> b.s.disconnect() # 入力プラグは省略可能
//
は pymel と同じく左から右への接続の切断なので disconnect
メソッドを利用した方が統一感があります。
ワールドスペースプラグ¶
アトリビュートには、ワールドスペースの値を出力するマルチアトリビュートがあります。
それは Plug
の isWorldSpace
が True を返すものです。
たとえば dagNode の worldMatrix (wm) や locator の worldPosition (wp) など、様々なものがあります。
ワールドスペースプラグのインデックスは、DAGノードインスタンスの番号に依存して決められる必要があります。 インスタンス番号は、DAGノードインスタンスの削除時に自動で欠番が詰められるなど動的に変化するため、 ワールドスペースプラグのインデックスも動的に変化します。 そのため、MELコマンドでは、ワールドスペースプラグをインデックス指定した要素で直接扱うことは推奨されず、 DAGパスと矛盾のないインデックスが Maya によって自動補完されるようになっています。 cymel でもその仕様を踏襲し、ワールドスペースプラグは要素にしないで扱うことを推奨します。
以下は使用例で、ロケータをインスタンスコピーし、その worldPosition をインデックス指定せずに参照しています。
>>> cmds.file(f=True, new=True)
>>> a = cm.nt.Locator(n='a').transform()
>>> b = cm.O(cmds.instance(a)[0])
>>> a.t.set([1, 2, 3])
>>> b.t.set([4, 5, 6])
>>> a.wp.get()
[1.0, 2.0, 3.0]
>>> b.wp.get()
[4.0, 5.0, 6.0]
以下のようにインデックス指定することが、本来のプラグへのアクセスになるのですが、ワールドスペースプラグではそれは推奨されません。
>>> a.wp[0].get()
[1.0, 2.0, 3.0]
>>> b.wp[1].get()
[4.0, 5.0, 6.0]
コマンドやAPIの併用¶
cymelは、pymelのように全てのMayaコマンドのラッパーを提供しません。 また、全てのノードタイプのクラスを提供するものの、APIやコマンドを完全に置き換えるほどの機能は提供しません。 頂点やポリゴンなどのコンポーネントもラップしません。
しかし、ノードやプラグを扱う上での主要な機能は整っているので、それで足りない部分はコマンドやAPIを併用してください。
CyObject
を文字列として評価するとその名前になるので、Mayaコマンドの引数にそのまま渡すことができます。
また、コマンドの返す結果を O
や Os
で受ければ、すぐに Node
や Plug
として扱えます。
CyObject
には、同じものを示す API オブジェクトを得るメソッドがあるので、API を併用する場合に便利です。
Node.mnode
では API2 の MObject 、
Node.mpath
では API2 の MDagPath 、
Plug.mplug
では API2 の MPlug が得られます。
また、
Node.mnode1
では API1 の MObject 、
Node.mpath1
では API1 の MDagPath 、
Plug.mplug1
では API1 の MPlug が得られます。
さらに、
CyObject
のオブジェクトを得る際には、名前だけでなく、
API2 の MObject 、 MDagPath 、 MPlug を指定することもできます
(API1 のそれらはサポートされていません)。
データタイプクラス¶
クラスの種類¶
cymelは以下の数学クラスを提供します。カッコ内は別名です。
BoundigBox (
BB
) ... バウンディングボックスQuaternion (
Q
) ... クォータニオンEulerRotation (
E
) ... オイラー角回転Transformation (
X
) ... トランスフォーメーション情報
それらの中には Plug
の値として直接セットしたり、直接ゲットしたりすることができるものもあります。
また、異なる型同士の変換操作もサポートされています。
BoundigBox¶
BoundingBox
(BB
) はバウンディングボックスクラスで、Maya API の MBoundingBox に相当します。
DagNode
の boundingBox
メソッドで取得できます。
BoundingBox
の保持する位置情報には Vector
が利用されています。
Vector¶
Vector
(V
) は3次元ベクトルクラスで、
Maya API の MPoint と MVector に相当します。
API では、位置を表すか方向を表すかで2種類を使いわける必要がありますが、cymelでは Vector
のみに統一されています。
Vector
は MPoint と同じく同次座標表現が可能な w を持っていますが、
デフォルトの 1.0 である限りは隠蔽され、ほとんど意識する必要はありません。
また、方向ベクトルとして扱う場合も 0.0 にする必要はなく、メソッドの種類に応じて適切に扱われます。
たとえば、
*
演算子か dot
メソッドで、3次元ベクトルの内積を計算しますが、
Vector.dot4
メソッドは4次元ベクトルの内積です。
また、 dot4r
メソッドは、ベクトルが4x1行列と1x4行列であるものとして、行列の積を計算します。
>>> cm.V(1, 2, 3) * cm.V(4, 5, 6)
32.0
>>> cm.V(1, 2, 3).dot(cm.V(4, 5, 6))
32.0
>>> cm.V(1, 2, 3).dot4(cm.V(4, 5, 6))
33.0
>>> cm.V(1, 2, 3).dot4r(cm.V(4, 5, 6))
Matrix(((4, 5, 6, 1), (8, 10, 12, 2), (12, 15, 18, 3), (4, 5, 6, 1)))
また、 ^
演算子か cross
メソッドでは、3次元ベクトルの外積を計算します。
>>> cm.V(1, 2, 3) ^ cm.V(4, 5, 6)
Vector(-3.000000, 6.000000, -3.000000)
>>> cm.V(1, 2, 3).cross(cm.V(4, 5, 6))
Vector(-3.000000, 6.000000, -3.000000)
他にも様々なメソッドがありますので、ドキュメントを参照してください。
Vector
は w を持っていますが、それがデフォルトの 1.0 である限り、長さ 3 のシーケンスとして振る舞います。
よって、4次元ベクトル値としては扱いにくいですが、3次元ベクトル値としては扱いやすいものになっています。
たとえば、double3型アトリビュートの値に直接セットすることができます。
ゲットで得られるのは list
ですが、そこからすぐに Vector
にすることもできます。
>>> v = cm.V(1, 2, 3)
>>> cm.nt.Transform()
>>> cm.sel.t.set(v)
>>> v + cm.V(cm.sel.t.get())
Vector(2.000000, 4.000000, 6.000000)
Matrix¶
Matrix
(M
) は4x4行列クラスで、Maya API の MMatrix に相当します。
matrix型アトリビュートのゲットやセットや DagNode
の getM
や setM
で直接サポートされます。
以下は、ローカルマトリックスを取得する例です。
プラグから得ることでも getM
を使用することでも、同じものが得られます。
>>> cmds.file(f=True, new=True)
>>> a = cm.nt.Transform(n='a')
>>> a.t.set((1, 2, 3))
>>> a.r.setu((10, 20, 30))
>>> a.s.set((1.2, 1.4, 1.6))
>>> a.m.get()
Matrix(((0.976557, 0.563816, -0.410424, 0), (-0.617357, 1.23559, 0.228446, 0), (0.605636, 0.0288453, 1.48067, 0), (1, 2, 3, 1)))
>>> a.getM()
Matrix(((0.976557, 0.563816, -0.410424, 0), (-0.617357, 1.23559, 0.228446, 0), (0.605636, 0.0288453, 1.48067, 0), (1, 2, 3, 1)))
以下は、ワールドマトリックスを取得する例です。 既に説明済み ですが、 wm にはインデックスを指定しないことが推奨されます。
>>> b = cm.nt.Transform(n='b', p=a)
>>> b.t.set((4, 5, 6))
>>> b.r.set((-10, -20, -30))
>>> b.wm.get()
Matrix(((0.365467, 0.560012, 1.41804, 0), (1.25209, -0.335616, -0.121765, 0), (0.0174783, 1.19128, -0.622367, 0), (5.45326, 10.6063, 11.3845, 1)))
>>> b.getM(ws=True)
Matrix(((0.365467, 0.560012, 1.41804, 0), (1.25209, -0.335616, -0.121765, 0), (0.0174783, 1.19128, -0.622367, 0), (5.45326, 10.6063, 11.3845, 1)))
以下は setM
の使用例です。
>>> c = cm.nt.Transform(n='c')
>>> c.setM(b.getM(ws=True))
>>> c.m.get()
Matrix(((0.365467, 0.560012, 1.41804, 0), (1.25209, -0.335616, -0.121765, 0), (0.0174783, 1.19128, -0.622367, 0), (5.45326, 10.6063, 11.3845, 1)))
*
演算子で Matrix
同士の積を計算できます。
>>> b.m.get() * a.m.get()
Matrix(((0.365467, 0.560012, 1.41804, 0), (1.25209, -0.335616, -0.121765, 0), (0.0174783, 1.19128, -0.622367, 0), (5.45326, 10.6063, 11.3845, 1)))
Vector
に Matrix
を乗じるか xform4
メソッドで、位置座標を変換できます。
>>> m = c.getM()
>>> cm.V(1, 2, 3) * m
Vector(8.375328, 14.068906, 10.691944)
>>> cm.V(1, 2, 3).xform4(m)
Vector(8.375328, 14.068906, 10.691944)
また、方向ベクトルを変換するには xform3
メソッドを使用します。
それは w が 0.0 の場合に似ていますが、 xform3
を用いれば w はデフォルトの 1.0 のままです。
>>> cm.V(1, 2, 3, 0) * m
Vector(2.922072, 3.462623, -0.692589, 0.000000)
>>> cm.V(1, 2, 3).xform3(m)
Vector(2.922072, 3.462623, -0.692589)
Matrix
を他の型に変換する操作もサポートされています。
平行移動値を取り出す asTM
や asT
、
回転を取り出す asRM
や asQ
や asE
や asD
、
スケールやシアーを取り出す asSM
や asS
や asSh
、
全部まとめて分解( Transformation
を得る)する asX
などがあります。
Quaternion¶
Quaternion
(Q
) はクォータニオンクラスで、Maya API の MQuaternion に相当します。
長さ 4 のシーケンスとしても振る舞います。
ノードの getQ
メソッドで、ノードの回転値を Quaternion
で得ることができます。
以下のコードは Matrix で説明した例の続きで、
getQ
の使用例です。
>>> a.getQ()
Quaternion(0.0381346, 0.189308, 0.239298, 0.951549)
>>> a.getQ(ws=True)
Quaternion(0.0381346, 0.189308, 0.239298, 0.951549)
>>> b.getQ()
Quaternion(-0.711601, -0.405992, -0.551087, 0.158423)
>>> b.getQ(ws=True)
Quaternion(-0.691413, -0.473257, -0.399343, 0.372158)
getQ
は、デフォルトでは rotateAxis を含まない回転を得られます(jointOrient や ws=True による上位の変換は含まれます)。
getJOQ
では、rotate を含まない回転(jointOrient まで)を得られます。
さきほどの例は、 transform ノードなので jointOrient を持たないため、親で getQ
することと等しくなります。
>>> b.getJOQ(ws=True)
Quaternion(0.0381346, 0.189308, 0.239298, 0.951549)
*
演算子でクォータニオン同士の積を計算できます。
>>> b.getQ() * a.getQ()
Quaternion(-0.678253, -0.5056, -0.367246, 0.386616)
上記で b と a のローカルクォータニオンの積が b のワールドクォータニオンと等しくならないのは、a が非一様 scale を持っているからです。 a の scale を初期化すれば等しくなります。
>>> a.s.set((1, 1, 1))
>>> b.getQ(ws=True)
Quaternion(-0.678253, -0.5056, -0.367246, 0.386616)
回転情報を扱う他の型との変換操作もサポートされています。
Matrix
とは、その asQ
と Quaternion
の asM
とで相互に変換ができます。
また、
EulerRotation
とは、その asQ
と Quaternion
の asE
とで相互に変換ができます。
asD
では、オイラー角回転を Degrees で得られます。
さらに、 asX
では Transformation
型に変換できます。
EulerRotation¶
EulerRotation
(E
) はオイラー角回転クラスで、Maya API の MEulerRotation に相当します。
rotateOrder も持っていますが、単なる長さ 3 のシーケンスとしても振る舞いますので、 オイラー角回転値を持つ rotate 、 rotateAxis 、 jointOrient などのアトリビュートのセットやゲットに便利です。
>>> cm.E(a.r.get(), a.ro.get())
EulerRotation(0.174533, 0.349066, 0.523599, XYZ)
>>> a.getQ(jo=False).asE()
EulerRotation(0.174533, 0.349066, 0.523599, XYZ)
degrot
関数によって、Degrees 単位の値から取得することもできます。
>>> cm.degrot(10, 20, 30)
EulerRotation(0.174533, 0.349066, 0.523599, XYZ)
回転情報を扱う他の型との変換操作もサポートされています。
Matrix
とは、その asE
と EulerRotation
の asM
とで相互に変換ができます。
また、
Quaternion
とは、その asE
と EulerRotation
の asQ
とで相互に変換ができます。
asD
では、オイラー角回転を Degrees で得られます。
さらに、 asX
では Transformation
型に変換できます。
Transformation¶
Transformation
(X
) はトランスフォーメーション情報クラスで、
Maya API の MTransformationMatrix に似ていますが、もっと洗練されています。
Mayaのmatrix型アトリビュートは、単なる「マトリックス」か「トランスフォーメーション情報」かの2種類の形式で情報を持てるようになっています。
cymelのクラスでいうと Matrix
か Transformation
です。
そして、 Transformation
は、
transform ノードと joint ノードのローカルマトリックスに影響を与えるアトリビュートを
オブジェクト属性として扱えるようにしつつ、Matrix
の合成・分解操作をサポートします。
トランスフォーメーションを Matrix
として扱うと、元のアトリビュート値は維持されませんが
(translate、rotate、scale、shearには分解できますが、ピボットや複数の回転アトリビュートなどの元の状態の完全な復元はできません)、
Transformation
として扱えば、アトリビュートの状態を完全に保持できます。
なお、2020から追加された offsetParentMatrix はローカルマトリックスには含まれず parentMatrix に含まれる扱いとなるため、
Transformation
のオブジェクト属性としてはサポートされません。
では、その働きを見るために、まず、ノードを1つ作り、アトリビュートを細かく設定します。
>>> cmds.file(f=True, new=True)
>>> a = cm.nt.Transform(n='a')
>>> a.t.set((1, 2, 3))
>>> a.rp.set((2, 3, 4))
>>> a.r.setu((10, 20, 30))
>>> a.ro.set(YXZ)
>>> a.ra.setu((3, 6, 9))
>>> a.sp.set((5, 6, 7))
>>> a.s.set((1.2, 1.4, 1.6))
translate、rotate、scale だけでなく rotateOrder や rotateAxis 、ピボットなども設定しました。
そして、アトリビュート m と xm をゲットした結果を比べてみます。
>>> a.m.get()
Matrix(((0.784932, 0.76995, -0.480686, 0), (-0.818564, 1.07082, 0.378546, 0), (0.767799, 0.091752, 1.40074, 0), (0.260018, -1.52541, -0.437171, 1)))
>>> a.xm.get()
Transformation(rp=Vector(2.000000, 3.000000, 4.000000), sp=Vector(5.000000, 6.000000, 7.000000), sh=Vector(0.000000, 0.000000, 0.000000), s=Vector(1.200000, 1.400000, 1.600000), r=EulerRotation(0.185486, 0.343542, 0.586718, XYZ), ra=Quaternion(0.0219557, 0.0542077, 0.0769589, 0.995317), t=Vector(1.000000, 2.000000, 3.000000))
m も xm も同じmatrix型アトリビュートですが、m には単なるマトリックスが出力され、xm にはトランスフォーメーション情報が出力されています。 そして、cymel はそれらをそのまま取得できます。
トランスフォーメーション情報を持っているアトリビュートでも単なるマトリックスとして評価することもできます( getAttr コマンドではそうなります)。
その場合は、明示的に getM
メソッドを使うか、得られた Transformation
の属性 m
を参照します。
>>> a.xm.getM()
Matrix(((0.784932, 0.76995, -0.480686, 0), (-0.818564, 1.07082, 0.378546, 0), (0.767799, 0.091752, 1.40074, 0), (0.260018, -1.52541, -0.437171, 1)))
>>> a.xm.get().m
Matrix(((0.784932, 0.76995, -0.480686, 0), (-0.818564, 1.07082, 0.378546, 0), (0.767799, 0.091752, 1.40074, 0), (0.260018, -1.52541, -0.437171, 1)))
ノードから Transformation
を得るには getX
メソッドも利用できます。
xm アトリビュートからゲットすることと等しいですが、 getX
ではワールドスペースの値を得ることもできます。
>>> b = cm.nt.Transform(n='b', p=a)
>>> b.t.set((4, 5, 6))
>>> b.r.set((-10, -20, -30))
>>> b.xm.get()
Transformation(s=Vector(1.000000, 1.000000, 1.000000), sh=Vector(0.000000, 0.000000, 0.000000), r=EulerRotation(-10, -20, -30, XYZ), t=Vector(4.000000, 5.000000, 6.000000))
>>> b.getX()
Transformation(s=Vector(1.000000, 1.000000, 1.000000), sh=Vector(0.000000, 0.000000, 0.000000), r=EulerRotation(-10, -20, -30, XYZ), t=Vector(4.000000, 5.000000, 6.000000))
>>> b.getX(ws=True)
Transformation(q=Quaternion(-0.651708, -0.507318, -0.329514, 0.457521), s=Vector(1.567808, 1.300522, 1.318314), sh=Vector(0.047563, -0.101130, -0.171420), t=Vector(3.913720, 7.459003, 7.937241))
一方、Transformation
情報をセットするには setX
メソッドが便利です。
それは Transformation
が持っている全ての属性値を、
transform や joint ノードのプラグに、そのままセットすることに相当します。
たとえば、以下に示すように setM
メソッドではマトリックスを完全に一致させることができますが、個々のプラグ値までは一致しません。
>>> c = cm.nt.Transform(n='c')
>>> c.setM(a.getM())
>>> c.m.get().isEquivalent(a.m.get())
True
>>> cm.V(c.t.get()).isEquivalent(cm.V(a.t.get()))
False
>>> cm.V(c.rp.get()).isEquivalent(cm.V(a.rp.get()))
False
>>> cm.V(c.r.get()).isEquivalent(cm.V(a.r.get()))
False
>>> c.ro.get() == a.ro.get()
False
>>> cm.E(c.ra.get()).asQ().isEquivalent(cm.E(a.ra.get()).asQ())
False
>>> cm.V(c.sp.get()).isEquivalent(cm.V(a.sp.get()))
False
>>> cm.V(c.s.get()).isEquivalent(cm.V(a.s.get()))
True
しかし、 setX
では、プラグ値を全て一致させることができます。
プラグ値の完全なコピーができるので、個々の値に誤差もないため、この例では単純に == で比較しています。
>>> c.setX(a.getX())
>>> c.m.get() == a.m.get()
True
>>> c.t.get() == a.t.get()
True
>>> c.rp.get() == a.rp.get()
True
>>> c.r.get() == a.r.get()
True
>>> c.ro.get() == a.ro.get()
True
>>> c.ra.get() == a.ra.get()
True
>>> c.sp.get() == a.sp.get()
True
>>> c.s.get() == a.s.get()
True
joint ノードと transform ノードのように、使用できるアトリビュートが異なるノード間でも Transformation
をコピーできます。
joint には jointOrient や inverseScale などの transform には無いアトリビュートが追加されている一方、ピボットは変更できません。
shear は隠されていますが変更可能です(Maya 2019 から 2020.0 まで shear が変更できない問題がありましたが修正されました)。
以下の例では、これまでと同じ Transformation
を joint ノードにセットしています。
ピボットは変更せずに維持しされつつ、マトリックスが一致するように translate 値が調整されているのを確認できます。
>>> d = cm.nt.Joint(n='d')
>>> d.setX(a.getX())
>>> d.m.get().isEquivalent(a.m.get())
True
>>> cm.V(d.t.get()).isEquivalent(cm.V(a.t.get()))
False
>>> cm.V(d.rp.get()).isEquivalent(cm.V(a.rp.get()))
False
>>> cm.V(d.r.get()).isEquivalent(cm.V(a.r.get()))
True
>>> d.ro.get() == a.ro.get()
True
>>> cm.E(d.ra.get()).asQ().isEquivalent(cm.E(a.ra.get()).asQ())
True
>>> cm.V(d.sp.get()).isEquivalent(cm.V(a.sp.get()))
False
>>> cm.V(d.s.get()).isEquivalent(cm.V(a.s.get()))
True
Transformation を利用した Matrix の分解¶
これまでの例では Transformation
をノードから取得しましたが、もちろん、単なる値として生成することもできます。
たとえば、以下のようにコンストラクタに属性値を指定して生成できます。
>>> cm.X(r=cm.degrot(10, 20, 30, YXZ), t=(1, 2, 3))
Transformation(r=EulerRotation(0.174533, 0.349066, 0.523599, YXZ), t=Vector(1.000000, 2.000000, 3.000000))
回転情報は EulerRotation
ではなく Quaternion
で指定することもできます。
以下は最初に内部的に設定される値が Quaternion
になっていますが、結局同じ Transformation
を生成していることになります。
>>> cm.X(q=cm.degrot(10, 20, 30, YXZ).asQ(), ro=YXZ, t=(1, 2, 3))
Transformation(q=Quaternion(0.0381346, 0.189308, 0.268536, 0.943714), ro=4, t=Vector(1.000000, 2.000000, 3.000000))
また、コンストラクタには Matrix
をそのまま渡せます。
それは asX
メソッドを使用することと同じです
(割愛しますが、属性名を指定せずに EulerRotation
や Quaternion
をそのまま指定することも同様に可能です)。
以下の例では、 Matrix
から Transformation
を得るとともに、その属性値を参照しています。
>>> r = cm.degrot(10, 20, 30, YXZ)
>>> m = r.asM() * cm.M.makeT((1, 2, 3))
>>> cm.X(m)
Transformation(q=Quaternion(0.0381346, 0.189308, 0.268536, 0.943714), s=Vector(1.000000, 1.000000, 1.000000), sh=Vector(0.000000, 0.000000, 0.000000), t=Vector(1.000000, 2.000000, 3.000000))
>>> m.asX()
Transformation(q=Quaternion(0.0381346, 0.189308, 0.268536, 0.943714), s=Vector(1.000000, 1.000000, 1.000000), sh=Vector(0.000000, 0.000000, 0.000000), t=Vector(1.000000, 2.000000, 3.000000))
>>> x = m.asX()
>>> x.t
Vector(1.000000, 2.000000, 3.000000)
>>> x.r
EulerRotation(0.185486, 0.343542, 0.586718, XYZ)
得られた Transformation
から q や r や s や sh や t などの値を得ることができますので、
この操作はマトリックスをトランスフォーメーション要素に分解することと等しいわけです。
さらに、進んだ操作として、ピボットなどの補助属性を条件として設定した上で、マトリックスを分解することもできます。
>>> x = cm.X()
>>> x.rp = cm.V(2, 4, 6)
>>> x.jo = cm.degrot(5, 10, 15).asQ()
>>> x.ro = ZYX
>>> x.sp = cm.V(1, 2, 3)
>>> x.m = m
>>> x.t
Vector(0.865305, 2.632209, 2.573444)
>>> x.r
EulerRotation(-0.0165951, 0.207732, 0.291455, ZYX)
>>> x.q
Quaternion(0.00688977, 0.103775, 0.143573, 0.984159)
>>> x.s
Vector(1.000000, 1.000000, 1.000000)
>>> x.sh
Vector(0.000000, 0.000000, 0.000000)
上記の例では、最初にピボットや jointOrient などを設定し(コンストラクタの引数で指定することもできます)、最後に matrix を代入しています。 そして、補助属性とマトリックスから逆算される t と r (または q ) と s と sh が分解されているのです。
アトリビュートとデータタイプのさらなる使用例¶
Transformation
の例で使用した m や xm などのアトリビュートは出力専用アトリビュートでしたので、
ダイナミックアトリビュートを使って、もう少し試してみましょう。
Node
の addAttr
メソッドは addAttr コマンドを簡単に使用できるようにしたラッパーです。
たとえば、double3 型アトリビュートの追加も、以下のように簡単です。
>>> cmds.file(f=True, new=True)
>>> a = cm.nt.Transform(n='a')
>>> a.addAttr('testrot', 'double3', 'doubleAngle', cb=True)
それでは、matrix型アトリビュートを追加してみます。
>>> a.addAttr('foo', 'matrix')
>>> a.foo.get()
追加したアトリビュートの初期値は値を返しません(None)。 データ型アトリビュートの初期値は null だからです。
以下のように、 Matrix
をセットすれば、その値を返すようになります。
>>> a.foo.set(cm.M())
>>> a.foo.get()
Matrix(((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1)))
または、 Transformation
をセットしても同様です。
>>> a.foo.set(cm.X())
>>> a.foo.get()
Transformation(s=Vector(1.000000, 1.000000, 1.000000), sh=Vector(0.000000, 0.000000, 0.000000), r=EulerRotation(0, 0, 0, XYZ), t=Vector(0.000000, 0.000000, 0.000000))
matrix型は、どちらの形式でも値を保持できます。
Plug
は本来のコマンドでは不可能な reset
メソッドも持っています(もちろん undo も可能です)。
初期値は null でしたので、リセットすると null に戻ります(Python では None)。
>>> a.foo.reset()
>>> a.foo.get()
そして、実は、 addAttr
の際にデフォルト値を指定することもできます。
本来のコマンドでは、デフォルト値の指定は数値型のアトリビュートでしかサポートされていませんが、cymel なら可能です(もちろん undo も可能です)。
以下の例では、デフォルト値に Transformation
を指定したアトリビュートを追加しています。
>>> a.addAttr('bar', 'matrix', dv=cm.X())
>>> a.bar.get()
Transformation(s=Vector(1.000000, 1.000000, 1.000000), sh=Vector(0.000000, 0.000000, 0.000000), r=EulerRotation(0, 0, 0, XYZ), t=Vector(0.000000, 0.000000, 0.000000))
>>> a.bar.set(cm.M())
>>> a.bar.get()
Matrix(((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1)))
>>> a.bar.reset()
>>> a.bar.get()
Transformation(s=Vector(1.000000, 1.000000, 1.000000), sh=Vector(0.000000, 0.000000, 0.000000), r=EulerRotation(0, 0, 0, XYZ), t=Vector(0.000000, 0.000000, 0.000000))
カスタムクラス¶
cymel では、標準で備わっているノードやプラグのクラスを継承した独自のクラスを使用することができます。
カスタムノードクラス¶
独自のノードクラスを使用する最も簡単な方法は、そのノードタイプに対応する標準のクラスを継承したクラスを実装し、使用する際はただそれを明示することです。
次のコードでは、標準の Transform
クラスを継承した MyTransform
クラスを作っています。
class MyTransform(cm.nt.Transform):
def clearRestPose(self):
self.mfn().clearRestPosition()
def saveRestPose(self):
self.mfn().setRestPosition(self.mfn().transformation())
def gotoRestPose(self):
mfn = self.mfn()
r = mfn.restPosition()
u = mfn.transformation()
setx = mfn.setTransformation
cm.docmd(lambda: setx(r), lambda: setx(u))
Maya API の MFnTransformation クラスの持つ Rest Position 機能を利用して、現在のポーズを一時的に保存したり、その状態に戻ったりするメソッドを実装しています。APIのこの機能は、Maya内部では使用されず、シーンファイルにも保存されない、APIレベルの一時的なキャッシュです。
APIでの操作となると通常はアンドゥはできませんが、この実装では cymel の docmd
を使用してアンドゥにも対応させています。
以下はこのクラスの使用例です。
>>> cmds.polyCube()
>>> obj = MyTransform(cmds.ls(sl=True)[0])
>>> obj.t.set((1, 2, 3))
>>> obj.r.setu((10, 20, 30))
>>> obj.s.set((2, 4, 6))
>>> obj.saveRestPose()
>>> obj.t.reset()
>>> obj.r.reset()
>>> obj.s.reset()
>>> obj.gotoRestPose()
>>> cmds.undo()
# Undo: obj.gotoRestPose() #
>>> cmds.redo()
# Redo: obj.gotoRestPose() #
この例では、実装した MyTransform
クラスを明示してインスタンスを得る必要があります。 sel
や selected
でカレントセレクションから得たり、親や子の transform やコネクションを辿って得られるオブジェジェクトでは通常の Transform
クラスが使用されてしまい MyTransform
クラスが使用されることはありません。
検査メソッド付きノードクラスの登録¶
カスタムクラスを明示せずにそのインスタンスを得られるようにするには、そのクラスを NodeTypes
(別名: cm.nt
) に登録する必要があります。登録するには NodeTypes.registerNodeClass
を使用します。
早速 MyTransform
クラスを登録してみましょう、といきたいところですが、このクラスは transform ノードタイプに対応する標準の Transform
クラスを継承したものなので、そのまま登録しようとすると transform に対応するクラスが2つになってしまい、その使い分けをどうするかが問題になります。
この問題を解決するには、同じ transform タイプでも MyTransform
を利用すべきかどうでないかを判別するためのタグ情報をノードに実際に追加するようにします。どのようなタグにするかは完全に自由ですが、シーンファイルに保存されることが望ましいので、カスタムアトリビュートを使用することが一般的です。
クラスでタグを識別する仕組みと、ノードを新規に作成した際にタグを追加する仕組みは、cymel のクラスでサポートされているので、さきほどの MyTransform
クラスに、次の2つのメソッドを追加実装します(これらのメソッドは cyeml のクラスタシステムで決められているルールです)。
class MyTransform(cm.nt.Transform):
@staticmethod
def _verifyNode(mfn, name):
return mfn.hasAttribute('myNodeTag')
@classmethod
def createNode(cls, **kwargs):
nodename = super(MyTransform, cls).createNode(**kwargs)
cmds.addAttr(nodename, ln='myNodeTag', at='message', h=True)
return nodename
# (実装済みのメソッドが続きます)
cm.nt.registerNodeClass(MyTransform, 'transform')
絶対に必要なのは検査メソッド _verifyNode
のみで、生成メソッド createNode
は実装が推奨されているくらいの位置付けです。
最後に registerNodeClass
を呼び出して、クラスを cymel に登録しています。第二引数には、紐付けるノードタイプ名を指定します。
もし、検査メソッド _verifyNode
を実装していない MyTransform
クラスを登録しようとすると、ノードタイプの継承関係と完全に一致しないという理由でエラーになります(Maya の transform の親タイプは dagNode なので、クラスでも DagNode
を直接継承しなければならないため)。
一方、検査メソッド付きのクラスでは、ノードタイプとの関連付けは厳格でなくても良いので、紐付けるノードタイプは、矛盾さえなければ割と自由に指定できます。たとえば、 transform の代わりに dagNode や node などを指定して、広範囲のノードタイプに対応させることもできます(その場合は、継承するクラスも DagNode
や Node
にすべきですし、この例で実装した機能は MFnTransform の機能を利用したものなので無理がありますが)。
また、 registerNodeClass
を複数回呼び出して、継承関係の無い複数のノードタイプに紐付けることさえできます。
cymel の通常の使用方法として、ノードクラスのインスタンスを得る際に既存の名前を指定しなければ createNode
が発動します。
次のように使用できます。
>>> MyTransform()
# Result: MyTransform('myTransform1') #
>>> MyTransform(n='foo')
# Result: MyTransform('foo') #
このように作成したノードは、識別タグ(カスタムアトリビュート)が設定されているので、 sel
などで普通に得ることができます。
>>> cm.sel
# Result: MyTransform('foo') #
しかし、先ほどの例のように作成済みのキューブなどには利用できないので、既存の transform にもタグのアトリビュートを追加するメソッドを追加すれば良いです。そのやり方は完全に自由で、システムでも何もサポートされていません。ここでは、次のように addClassTag
クラスメソッドを追加し、先ほどの createNode
メソッドでもそれを呼ぶように変更します。 _verifyNode
やその他のメソッドはそのままです。
class MyTransform(cm.nt.Transform):
@classmethod
def createNode(cls, **kwargs):
nodename = super(MyTransform, cls).createNode(**kwargs)
cls.addClassTag(nodename)
return nodename
@classmethod
def addClassTag(cls, nodename):
cmds.addAttr(nodename, ln='myNodeTag', at='message', h=True)
# (実装済みのメソッドが続きます)
これで以下のようにして既存 transform にもタグを追加することで、 MyTransform
が使用されるようにできます。
>>> MyTransform.addClassTag(cmds.polyCube()[0])
>>> cm.sel
# Result: MyTransform('pCube1') #
ベーシックノードクラスの登録¶
先ほどの MyTransform
は、標準のクラス Transform
が在った上で、それに機能追加したクラスを実装していました。
しかし、標準のクラスを完全に乗っ取ってしまいたいこともあります。
実際は、標準のクラスを乗っ取るというより、標準だと何の機能も実装されていないノードタイプのクラスを自前で実装したいことがあります。
cymel では全てのノードタイプに対応したクラスが提供されますが、本当に機能が実装されたクラスはごくわずかで、ほとんどのものは自動生成されるクラスでノードタイプ階層をクラス階層にマップする意義くらいしかありません。
全てのノードタイプクラスは cm.nt
で提供されますが、標準で機能が実装されていないクラスは最初にアクセスした際に自動生成されます。それは、クラスの出自を確認することで判別できます。
>>> cm.nt.DagNode
# Result: <class 'cymel.core.cyobjects.dagnode.DagNode'> #
>>> cm.nt.Transform
# Result: <class 'cymel.core.cyobjects.transform.Transform'> #
>>> cm.nt.Shape
# Result: <class 'cymel.core.cyobjects.shape.Shape'> #
>>> cm.nt.Joint
# Result: <class 'cymel.core.typeregistry.Joint'> #
>>> cm.nt.ObjectSet
# Result: <class 'cymel.core.typeregistry.ObjectSet'> #
cymel.core.typeregistry
に在るのが自動生成されたクラスです。上記の例では Joint
と ObjectSet
がそれにあたります。それらは、ただそのノードタイプに対応したクラスがあるだけで、特別な機能は何も持っていません。
もちろん、プラグインで追加したノードタイプに対応するクラスも自動生成されますが、当然、何も特別な機能は持ちません。
それらを自分で実装してしまっても良いでしょう。
ノードタイプに一対一で対応させる場合は、検査メソッド _verifyNode
や生成メソッド createNode
は不要です。ただ registerNodeClass
するだけです。
ただし、クラス階層が実際のノードタイプ階層と完全に一致している必要があるので、クラスの継承は正確に指定する必要があります。自動で解決されるように記述するには parentBasicNodeClass
メソッドの使用が便利です。
次の例は objectSet タイプに対応するクラスの実装例です(あくまでも簡易的な実装で、あまり深くは考えていません)。
class ObjectSet(cm.nt.parentBasicNodeClass('objectSet')):
def __contains__(self, item):
return cmds.sets(item, im=self.name())
def __len__(self):
return cmds.sets(self.name(), q=True, s=True)
def __getitem__(self, i):
return cm.O(cmds.sets(self.name(), q=True, no=True)[i])
def add(self, *items):
cmds.sets(*items, add=self.name())
def remove(self, *items):
cmds.sets(*items, rm=self.name())
cm.nt.registerNodeClass(ObjectSet, 'objectSet')
もし、そのノードタイプに対してクラスが既に生成されていたら次のような警告が出力された上で上書き登録されます。
# Warning: node class deregistered: <class 'cymel.core.typeregistry.ObjectSet'> #
そのクラスを継承しているクラスが存在するならいったん全て登録抹消されます。それらのノードタイプのクラスも次に評価された際に再生成されますが、生成済みのインスタンスは登録抹消されたクラスのままとなるので気をつけてください。このようなことから、クラスの登録は Maya 起動後の早い段階でやるのが望ましいといえます。
カスタムプラグクラス¶
(工事中)
ユーティリティ¶
(工事中)
UIコントロールクラス¶
cymel は、MELの全てのUIコントロールをラップしたクラスを提供します。
各クラス名は、MELコマンド名の先頭を大文字にした名前になります。
たとえば window なら Window
という具合です。
pymel にとてもよく似ていて、大きな進化はしていません(たとえば with が少し使いやすくなっていたりしますが)。 使い方もほとんど同じです。
以下に簡単な使用例を示します。
import cymel.ui as cmu
with cmu.Window() as wnd:
with cmu.AutoLayout():
cmu.Button(l='foo')
cmu.Button(l='bar')
cmu.Button(l='baz')
wnd.show()