前言

  去年的时候我有水过一篇文章 Maya 命令 自定义触发 & Mel 源码获取
  看标题就知道介绍了 Maya 一些基础的命令调用方式,并且详细介绍了几个好用的 Mel 命令来辅助解析 Maya 回显的命令。
  利用 whatIsrunTimeCommand 可以非常方便查询到背后的命令。

  但是使用 Mel 其实有更加 Python 的方法,那就是利用 pymel 来实现。

Pymel 调用 Mel

1
2
3
4
5
6
import pymel.core as pm
print(pm.mel.whatIs('copyAttrValues'))
# Mel procedure found in: C:/Program Files/Autodesk/Maya2018/scripts/others/copyAttrValues.mel
from maya import mel
print(mel.eval('whatIs copyAttrValues'))
# Mel procedure found in: C:/Program Files/Autodesk/Maya2018/scripts/others/copyAttrValues.mel

  直接 pymel.mel 就可以直接类似调用 Python 方法的方式调用 mel 函数。
  返回结果和 mel.eval 方法类似。

1
2
3
import pymel.core as pm
print(pm.mel)
# <pymel.core.language.Mel object at 0x0000024FDFA296A0>

  从上面可以看到 mel 是 pymel language.py 脚本中的 Mel 类实例。
  这个脚本当中还有 envmelGlobals 的实例
  env 的调用可以参考文档链接
  melGlobals 这个实例则可以获取到 mel 的全局变量。

1
2
3
4
5
6
import pymel.core as pm
print(pm.melGlobals['gMainWindow'])
# MayaWindow
from maya import mel
print(mel.eval('$temp = $gMainWindow'))
# MayaWindow

  利用上面的方法,可以完全用 Python 的调用方式来获取 Mel 才能调用的功能。

附注: Mel how to 文档有获取 Mel 所有全局变量的方法 链接

1
2
3
4
5
6
7
8
9
10
11
string $allGlobals[] = `env`;
$allGlobals = `sort $allGlobals`;
print $allGlobals;
// $AOVS
// $AttrPaintInstOn
// $AttrPaintPolyClr
// $AttrPaintPolyShapes
// $CAM_MENU_ADD
// $CAM_MENU_CAMERA
// $CAM_MENU_IGNORE
// ...

Pymel 运行 Mel 原理

  Mel 类重载了 __getattr__ 方法。

1
2
3
4
5
6
7
8
def __getattr__(self, command):
if command.startswith('__') and command.endswith('__'):
try:
return self.__dict__[command]
except KeyError:
raise AttributeError, "object has no attribute '%s'" % command

return MelCallable(head='', name=command)

  这个方法返回 MelCallable 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MelCallable(object):

""" Class for wrapping up callables created by Mel class' procedure calls.

The class is designed to support chained, "namespace-protected" MEL procedure
calls, like: Foo.bar.spam(). In this case, Foo, bar and spam would each be MelCallable
objects.
"""

def __init__(self, head, name):
if head:
self.full_name = '%s.%s' % (head, name)
else:
self.full_name = name

def __getattr__(self, command):
if command.startswith('__') and command.endswith('__'):
try:
return self.__dict__[command]
except KeyError:
raise AttributeError, "object has no attribute '%s'" % command

return MelCallable(head=self.full_name, name=command)

def __call__(self, *args, **kwargs):
cmd = pythonToMelCmd(self.full_name, *args, **kwargs)
return Mel._eval(cmd, self.full_name)

  调用的时候会调用 pythonToMelCmd 方法,将函数名和参数转换为 mel 语句。

1
2
3
import pymel.core as pm
print (pm.pythonToMelCmd('testCall',[1,2],["asd",1],True))
# testCall({1,2},{"asd","1"},true)

  最后调用 Mel 类的 _eval 的方法来执行 mel 语句。

alt

  从上面的代码可以看到 OpenMaya.MGlobal.executeCommand 利用 Maya C++ API 实现 mel 代码运行
  并通过上面的 返回值处理返回对应的数据。

附注: pm.mel 执行的返回都是字符串,并非是 PyNode 处理过的对象需要特别注意。

实用 Mel 命令汇总

  经过上面的操作可以很方便调用 Mel 内置的函数。
  因此我们可以了解一下 Mel 内置了哪些便利的函数,可以更加方便地调用,而不需要从零开始写代码。

  下面的代码可能默认没有加载到 Maya 里面,直接调用可能会有找不到函数的错误提示。
  这个时候可以利用 source 语句 source 脚本来解决问题

1
2
3
4
5
6
7
pm.mel.isOnlyNurbsSelected()
// Error: line 1: Cannot find procedure "isOnlyNurbsSelected". //
# Error: MelUnknownProcedureError: file C:\Program Files\Autodesk\Maya2018\Python\lib\site-packages\pymel\core\language.py line 985: Error during execution of MEL script: line 1: Cannot find procedure "isOnlyNurbsSelected". #

pm.mel.source('artisanValues')
print (pm.mel.isOnlyNurbsSelected())
0

  如上所示通过 source 方法就可以加载到相关的 Mel 函数了


alt

https://help.autodesk.com/cloudhelp/2018/ENU/Maya-Tech-Docs/Commands/index.html

  Mel 命令文档里面有不少函数是 M 后缀的,也就是 Mel 方法特有的,正常情况下无法通过 Python 调用。
  当然这些只是记录在文档里面的,还有一些藏在 {Maya安装目录}/scripts/others/*.mel 这个 scripts 路径下了。
  翻阅里面的 Mel 可以看到很多脚本都是 98 99 写的远古 Mel 代码。
  现在新工具新插件的代码都不用 Mel 而是 Python 了。

  并且 Mel 有很多函数是复刻 Python 的,比如 startsWith strip dirname basename
  除此之外还有很多非常 native 的用法,调用官方的写法更加省心。

copyAttrValues

alt

  copyAttrValues 全局函数选择两个物体,最后选择物体的通道选择上,会根据选择的通道同步属性值。
  用这个函数复制属性值非常便利,不需要再一个值一个值地进行传递。
  这个因为要选择对应的属性,更偏向于实际使用,而非代码

duplicateAttr

  duplicateAttr 和上面函数的用法类似,只是可以复制属性通道。

selectedChannelBoxAttributes

1
2
selectedChannelBoxAttributes;
// Result: tx ty tz rx ry //

  selectedChannelBoxAttributes 获取当前通道盒选中的属性值

selectedChannelBoxPlugs

1
2
selectedChannelBoxPlugs;
// Result: pSphere1.tx pSphere1.ty pSphere1.tz pSphere1.rx pSphere1.ry pSphere2.tx pSphere2.ty pSphere2.tz pSphere2.rx pSphere2.ry //

  selectedChannelBoxPlugs 获取当前通道盒选中的通道插槽

selectedChannelBoxObjects

1
2
selectedChannelBoxObjects;
// Result: pSphere1 pSphere2 //

  selectedChannelBoxObjects 注意必须是选中通道才可以获取到物体

raiseChannelBox & isChannelBoxRaised

  raiseChannelBox 将通道盒置顶
  isChannelBoxRaised 判断通道盒是否置顶

findRelatedDeformer

1
2
3
4
5
6
7
8
import pymel.core as pm
# NOTE 创建球体
pm.mel.CreatePolygonSphere()
# NOTE 创建Blendshape
pm.mel.CreateBlendShape()
# NOTE 获取创建的变形器
print (pm.mel.findRelatedDeformer(pm.selected()[0]))
# [u'blendShape1', u'tweak1']

  利用这个获取到 blendshape 等关联的变形器节点。
  蒙皮节点也属于变形器的一种,因此也可以获取。

findRelatedSkinCluster

1
2
3
4
5
6
7
import pymel.core as pm
jnt = pm.joint()
sphere, = pm.polySphere(ch=0)
pm.select(jnt,sphere)
pm.mel.SmoothBindSkin()
print (pm.mel.findRelatedSkinCluster(sphere))
# skinCluster1

  与上面的方法类似,只是针对获取蒙皮节点。
  利用这个方法获取蒙皮节点非常省事

getFaces & getEdges & getUVs & getVerts

alt

getFaces
getEdges
getUVs
getVerts

  上面四个函数可以在不同的选择的 component 下切换。

attributeExists

1
2
3
4
5
6
import pymel.core as pm
sphere, = pm.polySphere(ch=0)
print(pm.mel.attributeExists("tx",sphere))
# 1
print(pm.mel.attributeExists("ta",sphere))
# 0

  这个函数可以非常方便地判断属性是否存在。

rootOf & longNameOf & shortNameOf

1
2
3
4
5
6
7
8
9
import pymel.core as pm
sphere, = pm.polySphere(ch=0)
pm.mel.Group()
print (pm.mel.rootOf(sphere))
# |group1
print (pm.mel.longNameOf(sphere))
# |group1|pSphere1
print (pm.mel.shortNameOf(sphere))
# pSphere1

  这些功能其实 pymel 都有所覆盖
  还有 isSameObject isParentOf 就不再赘述了。

getCurrentCamera & getPanelCamera

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pymel.core as pm
camera = pm.mel.getCurrentCamera()
panel = pm.mel.getCameraPanel(camera)
print(panel)
# modelPanel4

# NOTE 传统方法
active_mp = None
for mp in pm.getPanel(type="modelPanel"):
if pm.modelEditor(mp, q=1, av=1):
active_mp = mp
break
print(active_mp)
# modelPanel4

  利用 mel 封装的方法可以非常方便获取到当前聚焦的 ModelPanel
  缺点就是脚本编辑器下聚焦不在 modelPanel 用 mel 运行获取不到具体的 modelPanel 需要做成 工具架按钮触发才可以。

getCurrentOutlinerPanel

1
2
3
import pymel.core as pm
print(pm.mel.getCurrentOutlinerPanel())
# outlinerPanel1

  获取当前聚焦的 Outliner
  同样需要注意在 脚本编辑器下执行聚焦点不在 outliner 没有返回值

getSceneName

1
2
3
import pymel.core as pm
print(pm.mel.getSceneName())
# untitled

  直接获取当前文件名(不带后缀),可以和 pm.sceneName() 方法获取文件路径相辅相成。

getAssignedMaterials & getConnectedShaders & createAndAssignShader

1
2
3
4
5
6
import pymel.core as pm
sphere, = pm.polySphere(ch=0)
pm.mel.source("cpvTexture")
materials = pm.mel.getAssignedMaterials(sphere.getShape()))
print(materials)
# [u'lambert1']

  getAssignedMaterials 可以直接通过 shape 节点获取到关联的材质。

1
2
whatIs getAssignedMaterials;
// Result: Mel procedure found in: C:/Program Files/Autodesk/Maya2018/scripts/unsupported/cpvTexture.mel //

  需要注意这个函数在 unsupported 目录下,默认不加载,需要 source 文件,也不排除哪天就没了(:з」∠)
  所以我推荐另一个自带的方法 getConnectedShaders

1
2
3
4
5
6
7
8
import pymel.core as pm
sphere, = pm.polySphere(ch=0)
sg_list = pm.mel.getConnectedShaders(sphere,0)
print(sg_list)
# [u'initialShadingGroup']
for sg in sg_list:
print (pm.PyNode(sg).listConnections(type="shadingDependNode"))
# [nt.Lambert(u'lambert1')]

  getConnectedShaders 可以快速获取模型的 shadingEngine 节点,通过这个节点可以找到关联的材质。 shadingDependNode 是所有渲染节点的基类,囊括了所有的材质以及一些不搭噶的渲染节点。

1
2
3
4
5
6
7
8
9
import pymel.core as pm
sphere, = pm.polySphere(ch=0)
faces = [sphere.f[i] for i in range(100)]
pm.select(faces)
# NOTE 创建并添加材质
pm.mel.createAndAssignShader("blinn","")
sg_list = pm.mel.getConnectedShaders(sphere,True)
print(sg_list)
# [u'initialShadingGroup', u' pSphere2.f[100:399]', u'blinn2SG', u' pSphere2.f[0:99]']

  getConnectedShaders 第二个参数为 True 时可以获取到详细关联 shadingEngine 和 component 的数据。

gotoBindPose

1
2
import pymel.core as pm
pm.mel.gotoBindPose()

  绑定下可以利用这个命令回到 bindpose 状态。

deleteIfNotReferenced

1
2
3
4
5
6
7
8
import pymel.core as pm
sphere, = pm.polySphere(ch=0)
print (pm.mel.deleteIfNotReferenced(sphere))
# 1
sphere, = pm.polySphere(ch=0)
sphere.lock() # 注: 锁定节点无法删除
print (pm.mel.deleteIfNotReferenced(sphere))
# 0

  更加安全的删除命令,无法删除的物体返回 0

replaceNode

https://ilmvfx.wordpress.com/2014/01/27/how-do-i-change-a-materials-type-from-lambert-to-blinn-for-example-using-mel-chs/
参考上面的链接 Mel How-to 系列

  这个命令就是实现材质替换的功能,不过理论上可以替换任何 Maya 节点。

1
2
3
4
import pymel.core as pm
sphere,sphere_node = pm.polySphere(ch=1)
cube,cube_node = pm.polyCube(ch=1)
pm.mel.replaceNode(sphere_node,cube_node)

  上面执行生成了两个 方块, 因为利用 replaceNode 将 球体生成节点 替换为了 方块生成节点。

总结

  以上就是关于 pymel 的用法探索,有机会还要加一篇文章分析 pymel 的实现原理,这个库简直是神器。
  虽然 pymel 存在一定的性能问题,但是它的背后其实都是调用 OpenMaya1.0 的API,如果在高性能情况下用 pymel 转换为 C++ 的方式调用,性能未必就差的,pymel 有内置的 api 转换方法,有机会也补充一下~