Maya の拡張アトリビュートの話


アドカレも qiita に投稿するのも初めてですが、Maya の拡張アトリビュートについて書いてみます。


## Maya 2017 の mtoa をディスる

2018.5 もリリースされ、無事に [いやなバグ]
(https://forums.autodesk.com/t5/maya-programming/array-attr-inside-a-compound-are-not-evaluated-maya-2018-update/td-p/8233305)も治った Maya さんですが、2017 で Arnold 化した時にはこんな問題がありました。

まず、Maya 2017 で普通にキューブを１個作ってエクスポートします。

``` py
import maya.cmds as cmds
cmds.file(f=True, new=True)
cmds.polyCube()

import os.path
import tempfile
fname = os.path.join(tempfile.gettempdir(), 'tmp.ma')
cmds.file(fname, f=True, es=True, typ='mayaAscii')
```

これを mayapy で読み込んでみます。

``` py
import maya.standalone
maya.standalone.initialize(name='python')
 
import maya.cmds as cmds
import tempfile
import os.path
fname = os.path.join(tempfile.gettempdir(), 'tmp.ma')
cmds.file(fname, f=True, o=True)
```

すると、こんな感じのエラーが出ます。実際はシェイプの数だけ大量に出ます。

```
RuntimeError: file: c:/users/xxxx/appdata/local/temp/tmp.ma line 24: The mesh 'pCubeShape1' has no '.ai_translator' attribute.
file: c:/users/xxxx/appdata/local/temp/tmp.ma line 24: setAttr: No object matches name: .ai_translator
Error reading file.
Error reading file.
...
```

ちなみに、2018 ではこの問題は修正されており、2018 からエクスポートしたファイルではこのようなエラーは出ません（mtoa 絡みの警告メッセージはいろいろ出ますが…）。

これは何かというと…、
mtoa プラグインがシェイプノードに ai_translator という拡張アトリビュートを追加しながらも、プラグインがそのアトリビュートに自身を関連付けることを怠っていたっぽいという問題です。おそらく。


## スタティックアトリビュートとダイナミックアトリビュート

Maya では、ノードタイプに最初から備わっているアトリビュートをスタティックアトリビュート、ノードの実体ごとに追加するアトリビュートをダイナミックアトリビュートといいます。ダイナミックアトリビュートは [addAttr](http://help.autodesk.com/cloudhelp/2017/JPN/Maya-Tech-Docs/CommandsPython/addAttr.html) コマンドで追加します。

以下のコードはスタティックアトリビュートとダイナミックアトリビュートの違いの例です。

``` py
import maya.cmds as cmds
a = cmds.createNode('transform', n='a')
b = cmds.createNode('transform', n='b')
cmds.addAttr(a, ln='foo', k=True)
cmds.setAttr(a + '.translateX', 1.)
cmds.setAttr(a + '.foo', 2.)
cmds.setAttr(b + '.translateX', 1.)
#cmds.setAttr(b + '.foo', 2.)  # これだけエラーになる。
```

ノード a と b を作り、a にダイナミックアトリビュート foo を追加しています。
translateX は transform というノードタイプが持つスタティックアトリビュートなので a と b 双方に存在しますが、foo はノード a の持つダイナミックアトリビュートなので b には存在しません。

そして、ダイナミックアトリビュートを追加した場合、その情報（addAttrコマンド実行記録）は mayaAscii や mayaBinary などのシーンファイルにも保存されますので、シーンを保存して開き直しても a.foo は健在です。当たり前ですが。

一方、スタティックアトリビュートは transform というノードタイプが元々持っているアトリビュートなので addAttr コマンドのような情報はシーンファイルには保存されません。スタティックアトリビュートは Maya のバージョンが上がると増えたりもします。減ることは殆どありません。プラグインでノードタイプを追加した場合も、こっそりとスタティックアトリビュートを追加することが後から出来ます。


## 拡張アトリビュート

さて、Maya 2012 から、さらに拡張アトリビュートというものが追加されました。

これは、いわば、既存のノードタイプにスタティックアトリビュートを追加することが出来る機能です。
追加するには [addExtension](http://help.autodesk.com/cloudhelp/2017/JPN/Maya-Tech-Docs/CommandsPython/addExtension.html) コマンドを使います。文法は [addAttr](http://help.autodesk.com/cloudhelp/2017/JPN/Maya-Tech-Docs/CommandsPython/addAttr.html) とほとんど同じです。
ダイナミックアトリビュートはノードの実体に対して追加を行うのに対し、拡張アトリビュートはスタティックアトリビュートの一種ですのでノードタイプに対して追加をする点が違います。

先ほどのノード a と b のコードに続いて、以下のコードを実行してみます。

``` py
cmds.addExtension(nt='transform', ln='bar', k=True)
cmds.setAttr(a + '.bar', 3.)
cmds.setAttr(b + '.bar', 3.)
```

ノードタイプ transform に bar という拡張アトリビュートを追加しています。
addExtension コマンド一発によって、全ての tansform ノードはそのアトリビュートを持つことになります。
強力ですね。
ですが、拡張アトリビュートは、いわば環境をいじる機能といえますので、使用するには細心の注意が必要です。
どういうことか見てみましょう。

いったんシーンを保存します。

``` py
import os.path
import tempfile
fname = os.path.join(tempfile.gettempdir(), 'tmp.ma')
cmds.file(fname, f=True, es=True, typ='mayaAscii')
```

そして Maya を再起動してから、シーンを開き直します。

``` py
import maya.cmds as cmds
import tempfile
import os.path
fname = os.path.join(tempfile.gettempdir(), 'tmp.ma')
cmds.file(fname, f=True, o=True)
```

すると、ノード a と b にアトリビュート bar が無いというエラーが出るはずです。

```
// Error: file: c:/users/xxxx/appdata/local/temp/tmp.ma line 81: The transform 'a' has no '.bar' attribute. // 
// Error: file: c:/users/xxxx/appdata/local/temp/tmp.ma line 81: setAttr: No object matches name: .bar // 
// Error: file: c:/users/xxxx/appdata/local/temp/tmp.ma line 86: The transform 'b' has no '.bar' attribute. // 
// Error: file: c:/users/xxxx/appdata/local/temp/tmp.ma line 86: setAttr: No object matches name: .bar // 
```

mayaAscii をテキストエディタで開いて読んでみるとわかりますが、addAttr コマンドはシーンに保存されますが、addExtension は保存されないのです。
つまり、このシーンファイルを正常に開くためには、以下のように addExtension を事前に実行する必要があります。

``` py
import maya.cmds as cmds
cmds.addExtension(nt='transform', ln='bar', k=True)

import tempfile
import os.path
fname = os.path.join(tempfile.gettempdir(), 'tmp.ma')
cmds.file(fname, f=True, o=True)
```

これじゃあ他の人にシーンファイルを渡すとき困りますよね。

拡張アトリビュートは環境で保証しなければならないわけです。
しっかり管理されたプロダクション環境上じゃないと大変そうです。


## API における拡張アトリビュート

mtoa さんの問題が少し見えてきたところで、API も見てみましょう。

拡張アトリビュートの追加は、先ほどは [addExtension](http://help.autodesk.com/cloudhelp/2017/JPN/Maya-Tech-Docs/CommandsPython/addExtension.html) コマンドを使って行いましたが、API を使用することも出来ます。
API では [MDGModifier](http://help.autodesk.com/view/MAYAUL/2017/ENU/?guid=__cpp_ref_class_m_d_g_modifier_html) の addExtensionAttribute メソッドを用います。

とはいえ、API を使って拡張アトリビュートを追加したとしても、コマンドの場合と同様に「環境でどのように保証するか」が問題になりそうです。
ところが、もうちょっとメソッドを漁ってみると、 linkExtensionAttributeToPlugin というメソッドがあることに気づきます。
それを使うと、拡張アトリビュートの追加がプラグインによって行われたことを示して、シーンにプラグインを紐付けることができます（シーンファイルにプラグインの [requires](http://help.autodesk.com/cloudhelp/2017/JPN/Maya-Tech-Docs/CommandsPython/requires.html) コマンドが埋め込まれるようになる）。

つまり、mtoa プラグインはロードされた時にシェイプタイプに拡張アトリビュート ai_translator を追加していると思われますが、自身を linkExtensionAttributeToPlugin することを怠っていたのではないかと想像出来るのです。
簡単なプラグインで実験してみましょう。

``` py:testExtAttr.py
# -*- coding: utf-8 -*-
u"""
プラグインによる拡張アトリビュート追加のテスト。
"""
__author__ = 'ryusas'
__version__ = '1.0.0'

maya_useNewAPI = True

import maya.api.OpenMaya as api

attrPool = []


def initializePlugin(mobj):
    u"""
    プラグインのロード時に拡張アトリビュートを追加。
    """
    api.MFnPlugin(mobj, __author__, __version__, 'Any')

    nodecls = api.MNodeClass('transform')
    mod = api.MDGModifier()

    fnNumeric = api.MFnNumericAttribute()
    attr = fnNumeric.create('bar', 'bar', api.MFnNumericData.kDouble, 0.)
    fnNumeric.keyable = True
    mod.addExtensionAttribute(nodecls, attr)
    mod.linkExtensionAttributeToPlugin(mobj, attr)  # これが重要!!
    attrPool.append((nodecls, attr))

    #mod.doIt()  # これは不要そう。


def uninitializePlugin(mobj):
    u"""
    プラグインのアンロード時に拡張アトリビュートを削除。
    """
    nodecls = api.MNodeClass('transform')
    mod = api.MDGModifier()

    while attrPool:
        mod.removeExtensionAttribute(*attrPool.pop())  # こちらで問題なさそう。
        #mod.removeExtensionAttributeIfUnset(*attrPool.pop())

    #mod.doIt()  # これは不要そう。

```

このプラグイン testExtAttr.py を環境変数 MAYA_PLUG_IN_PATH が通ったパスに置いて、以下のコードを実行してみます。

``` py
import maya.cmds as cmds
cmds.loadPlugin('testExtAttr')

a = cmds.createNode('transform', n='a')
b = cmds.createNode('transform', n='b')
cmds.setAttr(a + '.bar', 1.)
cmds.setAttr(b + '.bar', 2.)

cmds.select([a, b])
import os.path
import tempfile
fname = os.path.join(tempfile.gettempdir(), 'tmp.ma')
cmds.file(fname, f=True, es=True, typ='mayaAscii')
```

プラグインをロードし、追加されている拡張アトリビュート bar に対して値をセットしてからノードをファイルにエクスポートしています。

次に、そのまま以下のコードを実行します。

``` py
cmds.file(f=True, new=True)
cmds.unloadPlugin('testExtAttr')

cmds.file(fname, f=True, o=True)
```

まず、New Scene した状態でプラグインをアンロードしています。
それで拡張アトリビュートが削除されていますが、シーンファイルにはプラグインの要求が埋め込まれているので、シーンを読み込む際にプラグインが自動的にロードされ、拡張アトリビュートが解決された上で a.bar と b.bar への setAttr が成功します。

また、この状態でプラグインをアンロードしようとすると以下のエラーとなり、アンロード出来ません。

``` py
# Error: Plug-in, "testExtAttr", cannot be unloaded because it is still in use
```

これは、拡張アトリビュート bar が編集されているノード a や b が存在しているため、アンロードが禁止されていることが分かります。良いですね！
ちなみに、アンロード時には [MDGModifier](http://help.autodesk.com/view/MAYAUL/2017/ENU/?guid=__cpp_ref_class_m_d_g_modifier_html) の removeExtensionAttribute メソッドを利用しています。もう一つ removeExtensionAttributeIfUnset というメソッドも在ったのですが、この挙動を見る限り removeExtensionAttribute で問題なさそうです。


## おまけ

これまで検証したように、プラグインで拡張アトリビュートを追加した場合、正しくプラグインを紐づければ問題ないことが分かりました。
そうすることで、その拡張アトリビュートを利用しているシーンをセーブすると、正しく [requires](http://help.autodesk.com/cloudhelp/2017/JPN/Maya-Tech-Docs/CommandsPython/requires.html) が埋め込まれます。
また、その拡張アトリビュートの利用中はプラグインをアンロードできなくなるという安全性も担保されますので、プラグインのアンロード時は拡張アトリビュートを削除出来るというわけでした。

Maya 2017 の mtoa はこの紐付けを正しくやっていないと思われることから、シーンをセーブしても mtoa の requires が保存されないというわけです。
そこで、mayaAscii を編集して mtoa の requires を手書きで追加したら mayapy でも ai_translator アトリビュートも無事に読めるだろうと、試してみましたところ・・・、クラッシュしました。
でも Maya 2018 では修正されているので我慢です。