前言

  前段时间去有了个小需求,需要批量统计之前做好的动画文件的文件帧数和相应的骨骼数。
  之前大部分的批处理都是在前台做的,也没有什么大问题,就是 Maya 一直在加载文件, UI 各种更新确实影响到了执行效率。
  最近总算是闲了一点,于是打算好好研究一下通过 standalone 批处理的方法。

  其实早在去年,我就用过 standalone 模式,一直没有写文章进行深刻的总结,不过这个模式也不复杂。
  其实就是实现无 UI 界面使用 Maya,通过 Python 命令可以实现大部分的 Maya 操作,这样的好处上面也提到了, Maya 不需要更新 UI ,文件加载效率更高。
  关于 standlone 模式, DT 有一套老教程有提到,可惜教程之前被爆破了,我最近上传了补档教程,希望对大家有帮助。 链接
  由于教程很少,我之前是通过官方文档研究的。 链接
  Stack Overflow 也有不少有用的参考回答 链接


  我之前就一直很好奇, Maya 是如何执行 userSetup 的
  为什么使用 standalone 模式也会自动加载到 userSetup 导致开启速度大降
  结合着上述的问题,我开始了研究。

Maya 初始化探索

  我之前写了一篇文章,关于如何提取出 mayapy.exe 并且可以正常输出 Maya 文件的文章 链接
  mayapy.exe 其实就是重新编译的 python.exe
  完全兼容 python2 并且对 maya 内置 dll 进行了底层对接。
  我之前的研究发现 maya 的 bin 目录下有 commandList 文件,里面记录了所有的 mel 命令对应读取的 dll 文件。
  这些发现其实都和 Maya 启动初始化的读取相关的。

  我们可以直接双击启动 mayapy.exe , 打开之后的操作和双击启动的 python.exe 没有任何区别
  最大的特点是这里定位的 python 包里面已经自带了 maya 包
  可以通过 from maya import cmds 来导入 cmds 库
  但是当你 print dir(cmds) 你会发现 cmds 库里面的 mel 命令是空空如也的。
  根据官方的说法,你需要使用 maya 的 standalone 模式初始化,才可以正常使用 cmds 库。
  pymel文档也有提到 链接

  今天就针对上面所列的代码,探讨 initialize 函数执行的背后到底发生了什么。
  直觉告诉我,这个东西绝对和 python 的环境相关。
  于是我简单粗暴的将 os.environ 的环境赋值为空。
  就是想看看 mayapy 会怎么花式报错。

1
2
3
4
5
import os
os.environ = {}

from maya import standalone
standalone.initialize()

  执行上面的代码,会有如下的代码报错提示 (测试使用的是 Maya2017 的 mayapy.exe)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Python 2.7.11 (default, Dec 21 2015, 22:48:54) [MSC v.1700 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.environ = {}
>>>
>>> from maya import standalone
>>> standalone.initialize()
Error: local variable 'commandListPath' referenced before assignment
# Traceback (most recent call last):
# File "C:\Program Files\Autodesk\Maya2017\Python\lib\site-packages\maya\app\startup\batch.py", line 5, in <module>
# import maya.app.startup.basic
# File "C:\Program Files\Autodesk\Maya2017\Python\lib\site-packages\maya\app\startup\basic.py", line 76, in <module>
# maya.app.commands.processCommandList()
# File "C:\Program Files\Autodesk\Maya2017\Python\lib\site-packages\maya\app\commands.py", line 43, in processCommandList
# sys.stderr.write("Unable to process commandList %s" % commandListPath)
# UnboundLocalError: local variable 'commandListPath' referenced before assignment

  这个脚本路径,一看就知道大概是怎么回事了,原来 maya.app 里面还有 startup 的库。
  可以断定 startup 肯定和启动初始化有着千丝万缕的关系了。
  于是我又将 startup 这个名字稍稍改掉
  然后再执行一遍。

1
2
3
4
5
6
7
8
9

Python 2.7.11 (default, Dec 21 2015, 22:48:54) [MSC v.1700 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from maya import standalone
>>> standalone.initialize()
Error: No module named startup.batch
# ImportError: No module named startup.batch
>>>

  盲生发现了华点了,可以断定 initialize 函数导入了 startup.batch 脚本。
  于是追踪 startup.batch 的脚本可以找到下面的代码

1
2
3
4
5
6
7
8
9
"""
This module is imported during the startup of Maya in batch mode.
"""

import maya.app.startup.basic

# Run the user's userSetup.py if it exists
maya.app.startup.basic.executeUserSetup()

  这里导入了 basic 库,然后进行初始化。
  于是又可以去追踪 basic 的脚本

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""
This module is always imported during Maya's startup. It is imported from
both the maya.app.startup.batch and maya.app.startup.gui scripts
"""
import atexit
import os.path
import sys
import traceback
import maya
import maya.app
import maya.app.commands
from maya import cmds, utils

def setupScriptPaths():
"""
Add Maya-specific directories to sys.path
"""
# Extra libraries
#
try:
# Tkinter libraries are included in the zip, add that subfolder
p = [p for p in sys.path if p.endswith('.zip')][0]
sys.path.append( os.path.join(p,'lib-tk') )
except:
pass

# Per-version prefs scripts dir (eg .../maya8.5/prefs/scripts)
#
prefsDir = cmds.internalVar( userPrefDir=True )
sys.path.append( os.path.join( prefsDir, 'scripts' ) )

# Per-version scripts dir (eg .../maya8.5/scripts)
#
scriptDir = cmds.internalVar( userScriptDir=True )
sys.path.append( os.path.dirname(scriptDir) )

# User application dir (eg .../maya/scripts)
#
appDir = cmds.internalVar( userAppDir=True )
sys.path.append( os.path.join( appDir, 'scripts' ) )

def executeUserSetup():
"""
Look for userSetup.py in the search path and execute it in the "__main__"
namespace
"""
if not os.environ.has_key('MAYA_SKIP_USERSETUP_PY'):
try:
for path in sys.path[:]:
scriptPath = os.path.join( path, 'userSetup.py' )
if os.path.isfile( scriptPath ):
import __main__
execfile( scriptPath, __main__.__dict__ )
except Exception, err:
# err contains the stack of everything leading to execfile,
# while sys.exc_info returns the stack of everything after execfile
try:
# extract the stack trace for the current exception
etype, value, tb = sys.exc_info()
tbStack = traceback.extract_tb(tb)
finally:
del tb # see warning in sys.exc_type docs for why this is deleted here
sys.stderr.write("Failed to execute userSetup.py\n")
sys.stderr.write("Traceback (most recent call last):\n")
# format the traceback, excluding our current level
result = traceback.format_list( tbStack[1:] ) + traceback.format_exception_only(etype, value)
sys.stderr.write(''.join(result))

# Set up sys.path to include Maya-specific user script directories.
setupScriptPaths()

# Set up string table instance for application
maya.stringTable = utils.StringTable()

# Set up auto-load stubs for Maya commands implemented in libraries which are not yet loaded
maya.app.commands.processCommandList()

# Set up the maya logger before userSetup.py runs, so that any custom scripts that
# use the logger will have it available
utils.shellLogHandler()

# Register code to be run on exit
atexit.register( maya.app.finalize )

  自动桌的脚本注释还挺全的。
  从注释可以知道这个脚本每次启动都会自动加载。

  executeUserSetup 通过代码可以知道如果环境变量存在 MAYA_SKIP_USERSETUP_PY 这个,就不会被执行。
  后续也是自动执行 userSetup.py 这个脚本, 其实都是 python 脚本,甚至可以修改这里 userSetup 命名,让 Maya 启动其他名称的脚本 (如果真的这么干的话 TD或TA 估计会疯掉的)
  所以代码查到这里,我还额外发现了 MAYA_SKIP_USERSETUP_PY 这个不错的环境变量来加速 Mayapy 的启动。
  使用方法也很简单。

1
2
3
4
5
import os
# NOTE 注意环境变量只能赋值字符串
os.environ["MAYA_SKIP_USERSETUP_PY"] = ""
from maya import standalone
standalone.initialize()

  下面的代码可以再逐个分析。
  setupScriptPaths 函数会自动将 Maya 一些默认的 scripts 目录路径添加到 sys.path 里面方便导入。
  stringTable 就是个字典类, 主要为了实现类似 mel的 res 脚本实现多语言匹配的效果,maya 包里有其他的脚本有用到,可以对照参考。
  shellLogHandler 可以追查源码,和 logging 库相关
  最后一句则是让 Maya 正常退出的时候执行退出操作。

  processCommandList 可以去找 commands 的 python 脚本

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

import maya.cmds
import sys, os.path

# Locations of commandList file by OS type as returned by maya.cmds.about( os=True )
commandListLocations = {
'nt' : 'bin',
'win64' : 'bin',
'mac' : 'Resources',
'linux' : 'lib',
'linux64' : 'lib'
}

def __makeStubFunc( command, library ):
def stubFunc( *args, **keywords ):
""" Dynamic library stub function """
maya.cmds.dynamicLoad( library )
# call the real function which has replaced us
return maya.cmds.__dict__[command]( *args, **keywords )
return stubFunc

def processCommandList():
"""
Process the "commandList" file that contains the mappings between command names and the
libraries in which they are found. This function will install stub functions in maya.cmds
for all commands that are not yet loaded. The stub functions will load the required library
and then execute the command.
"""

try:
# Assume that maya.cmds.about and maya.cmds.internalVar are already registered
#
commandListPath = os.path.realpath( os.environ[ 'MAYA_LOCATION' ] )
platform = maya.cmds.about( os=True )
commandListPath = os.path.join( commandListPath, commandListLocations[platform], 'commandList' )

file = open( commandListPath, 'r' )
for line in file:
commandName, library = line.split()
if not commandName in maya.cmds.__dict__:
maya.cmds.__dict__[commandName] = __makeStubFunc( commandName, library )
except:
sys.stderr.write("Unable to process commandList %s" % commandListPath)
raise

  这里的操作主要是读取 commandList 文件的 dll 对应关系。
  如果前面的 standalone 初始化有些命令没有顺利加载,可以通过 mel 命令引入的 dynamicLoad 方法
  重新加载命令。

总结

  Maya 已经和 Python 深度结合了,启动的时候会执行下面的操作

  1. 首先加载 dll 添加 cmds 库的 mel 命令
  2. 加载 maya.app.startup.batch 库
  3. 通过 mel 命令获取 我的文档的 maya 目录一些默认的 scripts 路径 | 添加到 sys.path 里面
  4. 初始化 StringTable 调用
  5. 通过 processCommandList , 避免一些 mel 命令漏网之鱼没有加载
  6. 初始化信息打印
  7. 注册关闭 Maya 事件
  8. executeUserSetup 执行 sys.path 下所有的 userSetup.py 脚本>