cymel入門

インポート

cymel の機能は以下の3つを選択してインポートできます。

以下のようにインポートするのがおすすめです。

import cymel.main as cm
import cymel.ui as cmu
from cymel.constants import *

cymel.constantscymel.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 です。

CyObject には、別名 O でもアクセスできます。

>>> 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')

ただし、互換性のないクラスを指定するとエラーになります。 たとえば、 jointtransform でもありますが 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 ではなく、以下のように isTypehasFn メソッドを利用してください。

>>> cm.nt.Node('initialShadingGroup').isType('objectSet')
True
>>> cm.nt.Node('initialShadingGroup').hasFn(api.MFn.kSet)
True

とはいえ、純粋にノードタイプを判別したいという用途ではなく、文字通り、派生クラスのインスタンスかどうかを判別したいのならば isinstance は有用です。 たとえば、 カスタムノードクラス ではノードタイプ以外の条件も加味してクラスを決定できるため、そういった条件込みで判別したい場合などには有用です。

アトリビュートのラッパークラス

プラグへのアクセス方法

ノードをラッパーオブジェクトとして扱うと、プラグ(アトリビュート)へのアクセスも便利になります。

以下のように、ノードオブジェクトの属性として、 Plug クラスのオブジェクトを得られます。 PlugNode と同様に、基底クラス 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 クラスにも様々なメソッドがありますが、 たとえば setget メソッドでは値のセットやゲットができます。

>>> 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]

ここで、ひとつ重要な注意点があります。 それは、単位付きタイプの場合、 setget では「内部単位」で扱われるという点です。

単位付きタイプには「距離」(doubleLinear)、「角度」(doubleAngle)、「時間」(time) がありますが、 内部単位は、それぞれ Centimeter、Radians、Second となっています。

たとえば、rotate では、通常の人が慣れた Degrees ではなく、以下のように Radians で扱う必要があります。

>>> cm.sel.rx.set(PI * .5)
>>> cm.sel.rx.get()
1.5707963267948966

一見面倒に見えるかもしれませんが、これは「シーン設定(単位)に依存しないプログラミングをすべき」という思想に基づいています。 もし、どうしても「UI設定単位」で扱いたい場合、 setugetu を用いることもできます。

>>> cm.sel.rx.getu()
90.0
>>> cm.sel.rx.setu(180)
>>> cm.sel.rx.get()
3.141592653589793

ただし、 setugetu を用いるのは、 スクリプトエディターでちょっとタイプして結果を得るようなインスタントなスクリプトに留めるのが無難です。

コネクション編集

>><<connect メソッドで、プラグの接続ができます。

また、接続を調べるには NodeinputsoutputsPluginputsoutputs メソッドが利用できます。

>>> 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 メソッドを利用した方が統一感があります。

ワールドスペースプラグ

アトリビュートには、ワールドスペースの値を出力するマルチアトリビュートがあります。 それは PlugisWorldSpace が 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コマンドの引数にそのまま渡すことができます。

また、コマンドの返す結果を OOs で受ければ、すぐに NodePlug として扱えます。

CyObject には、同じものを示す API オブジェクトを得るメソッドがあるので、API を併用する場合に便利です。 Node.mnode では API2 の MObjectNode.mpath では API2 の MDagPathPlug.mplug では API2 の MPlug が得られます。 また、 Node.mnode1 では API1 の MObjectNode.mpath1 では API1 の MDagPathPlug.mplug1 では API1 の MPlug が得られます。

さらに、 CyObject のオブジェクトを得る際には、名前だけでなく、 API2 の MObjectMDagPathMPlug を指定することもできます (API1 のそれらはサポートされていません)。

データタイプクラス

クラスの種類

cymelは以下の数学クラスを提供します。カッコ内は別名です。

それらの中には Plug の値として直接セットしたり、直接ゲットしたりすることができるものもあります。

また、異なる型同士の変換操作もサポートされています。

BoundigBox

BoundingBox (BB) はバウンディングボックスクラスで、Maya API の MBoundingBox に相当します。

DagNodeboundingBox メソッドで取得できます。

BoundingBox の保持する位置情報には Vector が利用されています。

Vector

Vector (V) は3次元ベクトルクラスで、 Maya API の MPointMVector に相当します。 API では、位置を表すか方向を表すかで2種類を使いわける必要がありますが、cymelでは Vector のみに統一されています。

VectorMPoint と同じく同次座標表現が可能な 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型アトリビュートのゲットやセットや DagNodegetMsetM で直接サポートされます。

以下は、ローカルマトリックスを取得する例です。 プラグから得ることでも 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)))

VectorMatrix を乗じるか 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 を他の型に変換する操作もサポートされています。

平行移動値を取り出す asTMasT 、 回転を取り出す asRMasQasEasD 、 スケールやシアーを取り出す asSMasSasSh 、 全部まとめて分解( 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 とは、その asQQuaternionasM とで相互に変換ができます。 また、 EulerRotation とは、その asQQuaternionasE とで相互に変換ができます。 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 とは、その asEEulerRotationasM とで相互に変換ができます。 また、 Quaternion とは、その asEEulerRotationasQ とで相互に変換ができます。 asD では、オイラー角回転を Degrees で得られます。 さらに、 asX では Transformation 型に変換できます。

Transformation

Transformation (X) はトランスフォーメーション情報クラスで、 Maya API の MTransformationMatrix に似ていますが、もっと洗練されています。

Mayaのmatrix型アトリビュートは、単なる「マトリックス」か「トランスフォーメーション情報」かの2種類の形式で情報を持てるようになっています。 cymelのクラスでいうと MatrixTransformation です。

そして、 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 が持っている全ての属性値を、 transformjoint ノードのプラグに、そのままセットすることに相当します。

たとえば、以下に示すように 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 が変更できない問題がありましたが修正されました)。

以下の例では、これまでと同じ Transformationjoint ノードにセットしています。 ピボットは変更せずに維持しされつつ、マトリックスが一致するように 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 メソッドを使用することと同じです (割愛しますが、属性名を指定せずに EulerRotationQuaternion をそのまま指定することも同様に可能です)。

以下の例では、 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 などのアトリビュートは出力専用アトリビュートでしたので、 ダイナミックアトリビュートを使って、もう少し試してみましょう。

NodeaddAttr メソッドは 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 クラスを明示してインスタンスを得る必要があります。 selselected でカレントセレクションから得たり、親や子の 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 の代わりに dagNodenode などを指定して、広範囲のノードタイプに対応させることもできます(その場合は、継承するクラスも DagNodeNode にすべきですし、この例で実装した機能は 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 に在るのが自動生成されたクラスです。上記の例では JointObjectSet がそれにあたります。それらは、ただそのノードタイプに対応したクラスがあるだけで、特別な機能は何も持っていません。 もちろん、プラグインで追加したノードタイプに対応するクラスも自動生成されますが、当然、何も特別な機能は持ちません。

それらを自分で実装してしまっても良いでしょう。

ノードタイプに一対一で対応させる場合は、検査メソッド _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()