5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-04-21

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

hogefoo の子ですが、シーン中に 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のMFnAttributeaddAttrコマンド に追加されています。
つまり、プラグインで作るノードに追加するアトリビュートだけでなく、シーン中で好きなように追加するアトリビュートでも非ユニーク名を使えるわけです。

また、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.

もちろん、怒られます。だって、元々備わっている txenforcingUniqueNameTrue ですからね。
実は、API を使って、元々備わっている txenforcingUniqueName を無理やり 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

トップレベルの .hogetransform1..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 を得るのではなく、直接 OPlug を得られます。

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 というよく分からないノードタイプがでていますが、名前的にちゃんとしたノードではなさそう(笑)なので、それ以外でいうと weightGeometryFilterblendShape で、アトリビュートが1つずつ検出されています。

ノードタイプ名の横のカッコは親のノードタイプをルートに向けて列挙したものです。つまり、weightGeometryFiltergeometryFilter を継承していて、さらにそれは node(全てのノードタイプのルートの抽象型)を継承しています。
そして、blendShapeweightGeometryFilter を継承しています。
weightGeometryFilter とは一般デフォーマーの抽象型で、全てのデフォーマーはこれを継承しています。しかし、実は、Maya 2024 までは blendShape の親タイプは geometryFilter でした。おそらく、blendShape にはショート名 w のアトリビュートがあり、weightGeometryFilter の持つそれと名前が衝突してしまって、一般デフォーマー扱いにできなかったのでしょう。
そして、今回、晴れて非ユニーク名が認められるようになったので、wenforcingUniqueName=False にして、 blendShape の親タイプを修正できたということなのでしょう。長い黒歴史でしたね。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?