Maya 2025 非ユニークアトリビュート名の話

Maya 2025 に non-unique attribute names というなかなかえぐい仕様が追加されたので、調べたり [cymel](https://ryusas.github.io/cymel/ja/index.html) で対応したことを書きます。


# どういうこと？
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](https://help.autodesk.com/view/MAYADEV/2025/ENU/?guid=MAYA_API_REF_cpp_ref_class_m_fn_attribute_html) や [addAttrコマンド](https://help.autodesk.com/cloudhelp/2025/ENU/Maya-Tech-Docs/CommandsPython/addAttr.html) に追加されています。  
つまり、プラグインで作るノードに追加するアトリビュートだけでなく、シーン中で好きなように追加するアトリビュートでも非ユニーク名を使えるわけです。  

また、[MFnAttribute](https://help.autodesk.com/view/MAYADEV/2025/ENU/?guid=MAYA_API_REF_cpp_ref_class_m_fn_attribute_html) には、アトリビュートのパス名を得ることができる `pathName` というメソッドも追加されています。  


# コマンドで試してみる
では、試しに `transform` ノードに `translateX` を追加してみましょう。  

``` py
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` に変えてしまえば可能なのですが、そんな超危険なことは絶対やってはいけません。  

``` py
# よいこはまねしちゃだめだよ
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` を選択している状態で、次のようにします。  

``` py
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の問題です。  

追加されたアトリビュートのリストを得てみると、次のようになります。  

``` py
cmds.listAttr(ud=True)
# Result: ['foo', 'foo.hoge', 'bar', 'bar.hoge', '.hoge']
```

トップレベルの `hoge` は先頭に `.` が付くんですね。これはDAGパスでいう partial path name（なるべく短くて済むパス名）ですよ。区切りが `|` でなく `.` になっている以外は同じ感覚です。  

では、`setAttr` で値をセットします。ちゃんとパス表記する必要があります。  

``` py
cmds.setAttr('.foo.hoge', 1)
cmds.setAttr('.bar.hoge', 2)
cmds.setAttr('.hoge', 3)
```

そして、`getAttr` で値を確認します。さっきはノード名を省略しましたが、付けて表記してみます。  

``` py
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](https://ryusas.github.io/cymel/ja/index.html) も、非ユニークアトリビュート名やパス表記に対応させました。  

https://qiita.com/ryusas/items/eba6ab1b5af0700799e6

さきほどの `hoge` を作ってみます。さっきはデフォルト型の `double` 型でしたが、cymelだと楽なので `double3` 型にしてみます。  

``` py
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)
```

続いて、値のセット。  

``` py
node.foo.hoge.set((1, 2, 3))
node.bar.hoge.set((4, 5, 6))
node.hoge.set((7, 8, 9))
```

値のゲットも同様に書けますが、`plug` メソッドにパスを指定してみましょう（従来からそうですが、 `plug` には配列のインデックスまでも含む長いパスを指定することもできます）。  

``` py
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` を得られます。  

``` py
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` が選択状態なら、コマンドと同様にノード名指定を省略できます。  

``` py
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` に次のようにアクセスします。  

``` py
node.foo.hogeX
# Result: Plug('transform1.foo.hoge.hogeX')
```

途中の `hoge` をすっ飛ばしてアクセスできました。  
そうなんです。 **アトリビュートパスは、途中の明白な箇所は省略できるのです。** 感覚的には、ここだだけがDAGパスと異なる点かと思います。  
cymel でないコマンドでも、もちろん同じです。  

``` py
cmds.getAttr('.foo.hogeX')
# Result: 1.0
cmds.getAttr('.foo.hoge.hogeX')
# Result: 1.0
```

ところで、アトリビュートのパス表記によって非ユニーク名が可能になったのは 2025 からですが、cymel ではどのバージョンでも同じ挙動になるように、過去バージョンでも `.` から始まる表記を認めたり、アトリビュート階層をある程度厳密に評価するように変更しました。要は、`enforcingUniqueName=False` 指定によって重複が可能になるのは 2025 からですが、それ以外の挙動はどのバージョンも同じになるようにしたのです。  
それは、過去バージョンで動いたものが、2025でパス対応で解釈が厳密化されてエラーになったりすることがないようにするためですが、逆に、 **これまでいい加減な書き方で動いてしまっていたものが、今回の更新によって動かなくなることもあり得るので注意してください。**

たとえば、以下は過去の cymel でも今の cymel でも、さすがにエラーになります。  

``` py
node.t.rx
# Error: AttributeError: no inferior attribute exists: transform1.t.rx
```

もちろん、`t` の子に `rx` は無いからですが、次のコードは動いてしまっていました。  

``` py
node.plug('t.rx')
# Result: Plug('transform1.rx')
```

それは、過去の Maya ではアトリビュート名はユニークに決まっていたので、末尾の `rx` が得られたら、その上位はマルチ（配列）アトリビュートのインデックスを解決する以外のことでは無視していたため、正しくないパス表記でも動いてしまっていたのです。  

また、以下のようにあり得ない名前を指定しても動いてしまっていました。  

``` py
node.plug('mechakucha.rx')
# Result: Plug('transform1.rx')
```

今度から、そういったものはエラーになってしまいます。  

``` py
node.plug('t.rx')
# Error: AttributeError: no attribute exists: transform1.t.rx
```

ただし、次のような表記は認められ、エラーにはなりません。  

``` py
node.plug('.rx')
# Result: Plug('transform1.rx')
```

厳密には `rx` はトップレベルには無いため、頭の `.` は余計なのでエラーにもなり得るのですが、ユニーク名である限りこの程度のパスの誤りなら認められます。選択されているノードの名前をコマンドでは省略できるように、頭を `.` から始めたアトリビュート名表記というのはよく使われていたので、それを通すようにするためです。あからさまにパスが間違っているのはエラーですが、冗長な `.` は認めるという具合です。  
しかし、`strict=True` オプションを付けると、それさえもエラーになります。  

``` py
node.plug('.rx', strict=True)
# Error: AttributeError: no attribute exists: transform1..rx
```

また、APIの [MFnAttribute](https://help.autodesk.com/view/MAYADEV/2025/ENU/?guid=MAYA_API_REF_cpp_ref_class_m_fn_attribute_html) に追加された機能を Plug オブジェクトにも追加しました。  
さきほどの `hoge` で試すと、次のようになります。  

``` py
node.foo.hoge.isEnforcingUniqueName()
# Result: False
node.foo.hoge.pathName()
# Result: 'foo.hoge'
```

ユニーク名の場合でも、オプション指定でパスを見ることができます。  

``` py
node.rx.pathName(useLongName=False, useCompression=False)
# Result: 'r.rx'
```

これらの機能は 2024 以前の cymel でも使うことができます。  
2024 以前では、非ユニーク名の機能は無いため `isEnforcingUniqueName` は常に True となり、 `pathName` は 2025 と同様に動作します。  


# 標準ノードはどうなっているのか
enforcingUniqueName フラグの説明で、ほとんどのアトリビュートは True のままなので、重複を許さないと書きましたが、実際のところはどうでしょう。  
次のコードで調べてみました。  

``` py
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` の親タイプを修正できたということなのでしょう。長い黒歴史でしたね。