Maya 2025 に non-unique attribute names というなかなかえぐい仕様が追加されたので、調べたり cymel で対応したことを書きます。
どういうこと?
Maya のアトリビュートには compound
という型があり、子のアトリビュートを束ねることができるので、アトリビュートの階層構造が作られます。
たとえば transform
ノードの translate (t)
というアトリビュートは、正確には double3
という型ですが compound
型の一種です。ご存知の通り translateX (tx)
、translateY (ty)
、translateZ (tz)
という子が束ねられています(カッコ内はショートネーム)。
translate (t)
├ translateX (tx)
├ translateY (ty)
└ translateZ (tz)
こんな感じのものとして、当たり前に思い出すのが transform
などのDAGノードです。
DAGノードは、階層上の位置が違えば(親が異なれば)、ノード名の重複が許されています。
たとえば、次のような階層があるとします。
foo
└ hoge
hoge
は foo
の子ですが、シーン中に hoge
という名前のノードが1つしかなければ、それは単に hoge
だけで明示できます。
しかし次のように hoge
が複数あるとどうでしょう。
foo
└ hoge
bar
└ hoge
hoge
hoge
というノードが3個ありますが、名前が重複しているので、それらを1つ1を明示するには、次のように|
で区切ったパス表記をしますね。
foo|hoge
bar|hoge
|hoge
これと同じことが、1つのノード内のアトリビュート階層でも可能になる仕様が Maya 2025 で追加されました。
アトリビュートは compound
によって階層が作られますが、これまでは、そのノードの中でアトリビュート名の重複は許されませんでした。それが許されるようになります。
たとえば、 persp
ノードの tx
を明示する場合 persp.t.tx
が正式なパスになりますが tx
は1つしかないので、アトリビュートのパス表記を省略して persp.tx
と表記して問題なかったわけです。今後はそうとは限らなくなります。
この「.t.tx
でなく、いきなり tx
と表記できる」ことは、そのアトリビュート名が重複していない限りは、今後も変わりませんが、重複が許されるようになるので、重複していたらパス表記を省略できなくなるわけです。この辺りの感覚はDAGノードのパスと一緒です(実は、アトリビュートの場合は、途中の明白なパスはすっ飛ばせるという違いがあります。後ほどcymelのところに例があります)。
enforcingUniqueName フラグ
アトリビュート名の重複が可能になった(非ユニークなアトリビュート名が使えるようになった)とはいえ、これまでの互換性を重視してか、アトリビュートに enforcingUniqueName
というフラグが追加されました。
APIの説明を見ると
Returns true if this attribute enforces that it has a unique name in the attribute tree.
と書かれています。
ユニーク名に強制する??? なんのこっちゃ…という感じですが、「このアトリビュート名の重複を許すかどうか」と言い換えると分かりやすいのではないでしょうか。
そして、デフォルトは True
なので、ほとんどのアトリビュートは「重複を許さない」ままです。
このフラグは、 APIのMFnAttribute や addAttrコマンド に追加されています。
つまり、プラグインで作るノードに追加するアトリビュートだけでなく、シーン中で好きなように追加するアトリビュートでも非ユニーク名を使えるわけです。
また、MFnAttribute には、アトリビュートのパス名を得ることができる pathName
というメソッドも追加されています。
コマンドで試してみる
では、試しに transform
ノードに translateX
を追加してみましょう。
cmds.createNode('transform')
# Result: transform1
cmds.addAttr(ln='translateX')
# Warning: Name '' of new attribute clashes with an existing attribute of node 'transform1'.
# Error: RuntimeError: file <maya console> line 1: Found no valid items to add the attribute to.
もちろん、怒られます。だって、元々備わっている tx
は enforcingUniqueName
が True
ですからね。
実は、API を使って、元々備わっている tx
の enforcingUniqueName
を無理やり False
に変えてしまえば可能なのですが、そんな超危険なことは絶対やってはいけません。
# よいこはまねしちゃだめだよ
import maya.api.OpenMaya as api
api.MFnAttribute(api.MNodeClass('transform').attribute('tx')).enforcingUniqueName = False
cmds.addAttr(ln='translateX', eun=False)
気をとりなおして、addAttr
コマンドでさっきの hoge
を作ってみましょう。
ところが、Maya 2025.0 現在、アトリビュートエディターが、非ユニーク名にあまり対応していないっぽくて、エラーが出てしまうので、アトリビュートエディターを閉じた状態でやります。
Maya 2025.1 では、アトリビュートエディターがエラーを吐く問題は修正されているようです。
ただし、トップレベルの非ユニーク名アトリビュートは、コンパウンド下に同名のものがあると表示されないという問題があるようです(このテストでは .hoge が表示されません)。
さっき作った transform1
を選択している状態で、次のようにします。
cmds.addAttr(ln='foo', at='compound', nc=1)
cmds.addAttr(ln='hoge', p='foo', eun=False)
cmds.addAttr(ln='bar', at='compound', nc=1)
cmds.addAttr(ln='hoge', p='bar', eun=False)
cmds.addAttr(ln='hoge', eun=False)
ちなみに、上記コマンドは undo できません。子を1つ(nc=1)と指定したcompound下にcompoundやdouble3などではないアトリビュートを作成する操作が問題になるようです。これは、非ユニークアトリビュート名とは一切関係なく古くからあるMayaの問題です。
追加されたアトリビュートのリストを得てみると、次のようになります。
cmds.listAttr(ud=True)
# Result: ['foo', 'foo.hoge', 'bar', 'bar.hoge', '.hoge']
トップレベルの hoge
は先頭に .
が付くんですね。これはDAGパスでいう partial path name(なるべく短くて済むパス名)ですよ。区切りが |
でなく .
になっている以外は同じ感覚です。
では、setAttr
で値をセットします。ちゃんとパス表記する必要があります。
cmds.setAttr('.foo.hoge', 1)
cmds.setAttr('.bar.hoge', 2)
cmds.setAttr('.hoge', 3)
そして、getAttr
で値を確認します。さっきはノード名を省略しましたが、付けて表記してみます。
cmds.getAttr('transform1.foo.hoge')
# Result: 1.0
cmds.getAttr('transform1.bar.hoge')
# Result: 2.0
cmds.getAttr('transform1.hoge')
# Result: 3.0
トップレベルの .hoge
は transform1..hoge
にはならないのですね。
ノード名とアトリビュート名の区切りも .
なので、ちょっとややこしくなる部分ですね。
cymel も対応
今回、cymel も、非ユニークアトリビュート名やパス表記に対応させました。
さきほどの hoge
を作ってみます。さっきはデフォルト型の double
型でしたが、cymelだと楽なので double3
型にしてみます。
from cymel.all import *
cmds.file(f=True, new=True)
# Result: 'untitled'
node = cm.nt.Transform()
node.addAttr('foo', 'compound', nc=1)
node.addAttr('hoge', 'double3', p='foo', eun=False, cb=True)
node.addAttr('bar', 'compound', nc=1)
node.addAttr('hoge', 'double3', p='bar', eun=False, cb=True)
node.addAttr('hoge', 'double3', eun=False, cb=True)
続いて、値のセット。
node.foo.hoge.set((1, 2, 3))
node.bar.hoge.set((4, 5, 6))
node.hoge.set((7, 8, 9))
値のゲットも同様に書けますが、plug
メソッドにパスを指定してみましょう(従来からそうですが、 plug
には配列のインデックスまでも含む長いパスを指定することもできます)。
node.plug('foo.hoge').get()
# Result: [1.0, 2.0, 3.0]
node.plug('bar.hoge').get()
# Result: [4.0, 5.0, 6.0]
node.plug('.hoge').get() # 厳密な指定
# Result: [7.0, 8.0, 9.0]
node.plug('hoge').get() # 頭のドットは省略可
# Result: [7.0, 8.0, 9.0]
トップレベルは先頭に .
を付けるのが厳密な指定ですが、省略することができます。ユニークでない場合、APIではそう指定するとエラーになりますが、cymel ではコマンドのように使えるように先頭 .
無し指定を認めています。
また、Node
オブジェクトから Plug
を得るのではなく、直接 O
で Plug
を得られます。
cm.O('transform1.foo.hoge').get()
# Result: [1.0, 2.0, 3.0]
cm.O('transform1.bar.hoge').get()
# Result: [4.0, 5.0, 6.0]
cm.O('transform1..hoge').get() # 厳密な指定
# Result: [7.0, 8.0, 9.0]
cm.O('transform1.hoge').get() # 頭のドットは省略可
# Result: [7.0, 8.0, 9.0]
transform1
が選択状態なら、コマンドと同様にノード名指定を省略できます。
cm.O('.foo.hoge').get()
# Result: [1.0, 2.0, 3.0]
cm.O('.bar.hoge').get()
# Result: [4.0, 5.0, 6.0]
cm.O('..hoge').get() # 厳密な指定
# Result: [7.0, 8.0, 9.0]
cm.O('.hoge').get() # 頭のドットは省略可
# Result: [7.0, 8.0, 9.0]
ここで、ちょっと興味深いことをしてみましょう。
末尾の hogeX
に次のようにアクセスします。
node.foo.hogeX
# Result: Plug('transform1.foo.hoge.hogeX')
途中の hoge
をすっ飛ばしてアクセスできました。
そうなんです。 アトリビュートパスは、途中の明白な箇所は省略できるのです。 感覚的には、ここだだけがDAGパスと異なる点かと思います。
cymel でないコマンドでも、もちろん同じです。
cmds.getAttr('.foo.hogeX')
# Result: 1.0
cmds.getAttr('.foo.hoge.hogeX')
# Result: 1.0
ところで、アトリビュートのパス表記によって非ユニーク名が可能になったのは 2025 からですが、cymel ではどのバージョンでも同じ挙動になるように、過去バージョンでも .
から始まる表記を認めたり、アトリビュート階層をある程度厳密に評価するように変更しました。要は、enforcingUniqueName=False
指定によって重複が可能になるのは 2025 からですが、それ以外の挙動はどのバージョンも同じになるようにしたのです。
それは、過去バージョンで動いたものが、2025でパス対応で解釈が厳密化されてエラーになったりすることがないようにするためですが、逆に、 これまでいい加減な書き方で動いてしまっていたものが、今回の更新によって動かなくなることもあり得るので注意してください。
たとえば、以下は過去の cymel でも今の cymel でも、さすがにエラーになります。
node.t.rx
# Error: AttributeError: no inferior attribute exists: transform1.t.rx
もちろん、t
の子に rx
は無いからですが、次のコードは動いてしまっていました。
node.plug('t.rx')
# Result: Plug('transform1.rx')
それは、過去の Maya ではアトリビュート名はユニークに決まっていたので、末尾の rx
が得られたら、その上位はマルチ(配列)アトリビュートのインデックスを解決する以外のことでは無視していたため、正しくないパス表記でも動いてしまっていたのです。
また、以下のようにあり得ない名前を指定しても動いてしまっていました。
node.plug('mechakucha.rx')
# Result: Plug('transform1.rx')
今度から、そういったものはエラーになってしまいます。
node.plug('t.rx')
# Error: AttributeError: no attribute exists: transform1.t.rx
ただし、次のような表記は認められ、エラーにはなりません。
node.plug('.rx')
# Result: Plug('transform1.rx')
厳密には rx
はトップレベルには無いため、頭の .
は余計なのでエラーにもなり得るのですが、ユニーク名である限りこの程度のパスの誤りなら認められます。選択されているノードの名前をコマンドでは省略できるように、頭を .
から始めたアトリビュート名表記というのはよく使われていたので、それを通すようにするためです。あからさまにパスが間違っているのはエラーですが、冗長な .
は認めるという具合です。
しかし、strict=True
オプションを付けると、それさえもエラーになります。
node.plug('.rx', strict=True)
# Error: AttributeError: no attribute exists: transform1..rx
また、APIの MFnAttribute に追加された機能を Plug オブジェクトにも追加しました。
さきほどの hoge
で試すと、次のようになります。
node.foo.hoge.isEnforcingUniqueName()
# Result: False
node.foo.hoge.pathName()
# Result: 'foo.hoge'
ユニーク名の場合でも、オプション指定でパスを見ることができます。
node.rx.pathName(useLongName=False, useCompression=False)
# Result: 'r.rx'
これらの機能は 2024 以前の cymel でも使うことができます。
2024 以前では、非ユニーク名の機能は無いため isEnforcingUniqueName
は常に True となり、 pathName
は 2025 と同様に動作します。
標準ノードはどうなっているのか
enforcingUniqueName フラグの説明で、ほとんどのアトリビュートは True のままなので、重複を許さないと書きましたが、実際のところはどうでしょう。
次のコードで調べてみました。
from cymel.all import *
emptyDict = {}
typeDict = {}
for nodetype in cm.iterNodetypeTree():
c = api.MNodeClass(nodetype)
try:
attrs = c.getAttributes()
except:
continue
inheritedDict = emptyDict
parents = cm.getInheritedNodeTypes(nodetype)[1:]
for p in parents:
inheritedDict = typeDict.get(p)
if inheritedDict is not None:
break
attrDict = {}
for attr in attrs:
mfn = api.MFnAttribute(attr)
p = mfn.pathName()
if p in inheritedDict:
continue
if not mfn.enforcingUniqueName:
attrDict[p] = mfn.shortName
if attrDict:
print('%s (%s)' % (nodetype, ', '.join(parents)))
for ps in attrDict.items():
print(' %s (%s)' % ps)
attrDict.update(inheritedDict)
typeDict[nodetype] = attrDict
else:
typeDict[nodetype] = inheritedDict
Mayaの全ノードタイプを調べ、enforcingUniqueName が False となっているアトリビュートを print します。
結果は次のようになりました。
hierarchyTestNode4 (node)
.envelope (en)
.pnts (pt)
.pnts.px (x)
.pnts.py (y)
.pnts.pz (z)
kitA.envelope (env)
kitA.pnts (pt)
kitA.pnts.px (x)
kitA.pnts.py (y)
kitA.pnts.pz (z)
kitB.envelope (env)
kitB.pnts (pt)
kitB.pnts.px (x)
kitB.pnts.py (y)
kitB.pnts.pz (z)
weightGeometryFilter (geometryFilter, node)
weightList.weights (w)
blendShape (weightGeometryFilter, geometryFilter, node)
.weight (w)
hierarchyTestNode4
というよく分からないノードタイプがでていますが、名前的にちゃんとしたノードではなさそう(笑)なので、それ以外でいうと weightGeometryFilter
と blendShape
で、アトリビュートが1つずつ検出されています。
ノードタイプ名の横のカッコは親のノードタイプをルートに向けて列挙したものです。つまり、weightGeometryFilter
は geometryFilter
を継承していて、さらにそれは node
(全てのノードタイプのルートの抽象型)を継承しています。
そして、blendShape
は weightGeometryFilter
を継承しています。
weightGeometryFilter
とは一般デフォーマーの抽象型で、全てのデフォーマーはこれを継承しています。しかし、実は、Maya 2024 までは blendShape
の親タイプは geometryFilter
でした。おそらく、blendShape
にはショート名 w
のアトリビュートがあり、weightGeometryFilter
の持つそれと名前が衝突してしまって、一般デフォーマー扱いにできなかったのでしょう。
そして、今回、晴れて非ユニーク名が認められるようになったので、w
を enforcingUniqueName=False
にして、 blendShape
の親タイプを修正できたということなのでしょう。長い黒歴史でしたね。