Maya Python OpenMaya 教学第一卷 Zurbrigg - Maya Python API (Volume1)_哔哩哔哩_bilibili
00_introduction_哔哩哔哩_bilibili
Maya Python OpenMaya 教学第三卷 Zurbrigg - Maya Python API (Volume3)_哔哩哔哩_bilibili

第一卷

Maya API的应用场景:

maya api 主要应用于扩展maya,例如它可以用来为maya添加新的:节点,命令,工具,发射器等
当需要遍历很多数据时,使用maya api 要比maya command要快很多
image.png

C++的利与弊与python的利与弊

C++

左边是好处,右边是坏处
image.png

python

左边是好处,右边是坏处
image.png

api2.0与api1.0的一些区别

api2.0:image.png
api1.0:image.png

api2.0自带MFnPlugin方法,而api1.0没有MFnPlugin方法,api1.0需要额外导入OpenMayaMPx模块来使用这个模块的MFnPlugin,MPxNode,MPxCommand等方法

脚本(scripts)与插件(plugin)的区别

image.png

一种简单的让maya识别到自定义插件的方式

在maya的版本文档下新建一个plug-ins文件夹,将插件放到这个文件夹下。
这种方式虽然简单,但是有个缺点是如果有多个版本的maya,那么需要在每个版本的maya文件夹下都要新建plug-ins文件夹。

image.png

写maya插件的模板

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
# coding: utf-8
import maya.api.OpenMaya as om
import maya.cmds as cmds

def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

om.MFnPlugin(plugin, vendor, version) # 定义插件

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
pass

if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
plugin_name = "empty_plugin.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

Hello World

HelloWorld命令

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
# coding: utf-8
import maya.api.OpenMaya as om
import maya.cmds as cmds


def maya_useNewAPI():
pass


class HelloWorldCmd(om.MPxCommand):
COMMAND_NAME = "HelloWorld"

def __init__(self):
super(HelloWorldCmd, self).__init__()

def doIt(self, args):
""" 执行的命令 """
print("Hello, World!")

@classmethod
def creator(cls):
""" 注册maya命令时使用的方法,用来得到类的实例 """
return HelloWorldCmd()


def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件

try:
# 向maya注册一个新命令,第一个参数是命令的名字,第二个参数是类的实例
plugin_fn.registerCommand(HelloWorldCmd.COMMAND_NAME, HelloWorldCmd.creator)
except:
om.MGlobal.displayError("Failed to register command: {0}".format(HelloWorldCmd.COMMAND_NAME)) # 注册失败时输出


def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数 """
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterCommand(HelloWorldCmd.COMMAND_NAME) # 取消注册新命令
except:
om.MGlobal.displayError("Failed to deregister command: {0}".format(HelloWorldCmd.COMMAND_NAME)) # 取消注册失败时输出


if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
plugin_name = "command_01_HelloWorld.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred(
'if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred(
'if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

HelloWorld节点

这里是创建一个helloworld节点,创建节点后会在maya界面上绘制字体
image.png

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# coding: utf-8
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import maya.api.OpenMayaRender as omr

import maya.cmds as cmds


def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass


class HelloWorldNode(omui.MPxLocatorNode):
TYPE_NAME = "helloworld"
TYPE_ID = om.MTypeId(0x0007f7f7)
DRAW_CLASSIFICATION = "drawdb/geometry/helloworld"
DRAW_REGISTRANT_ID = "HelloWorldNode"

def __init__(self):
super(HelloWorldNode, self).__init__()

@classmethod
def creator(cls):
return HelloWorldNode()

@classmethod
def initialize(cls):
pass


class HelloWorldDrawOverride(omr.MPxDrawOverride):
NAME = "HelloWorldDrawOverride"

def __init__(self, obj):
super(HelloWorldDrawOverride, self).__init__(obj, None, False)

def prepareForDraw(self, obj_path, camera_path, frame_context, old_data):
pass

def supportedDrawAPIs(self):
return omr.MRenderer.kAllDevices

def hasUIDrawables(self):
return True

def addUIDrawables(self, obj_path, draw_manager, frame_context, data):
draw_manager.beginDrawable()
draw_manager.text2d(om.MPoint(100, 100), "Hello World")
draw_manager.endDrawable()

@classmethod
def creator(cls, obj):
return HelloWorldDrawOverride(obj)


def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件
try:
plugin_fn.registerNode(HelloWorldNode.TYPE_NAME,
HelloWorldNode.TYPE_ID,
HelloWorldNode.creator,
HelloWorldNode.initialize,
om.MPxNode.kLocatorNode,
HelloWorldNode.DRAW_CLASSIFICATION)
except:
om.MGlobal.displayError("Failed to register node: {0}".format(HelloWorldNode.TYPE_NAME))

try:
omr.MDrawRegistry.registerDrawOverrideCreator(HelloWorldNode.DRAW_CLASSIFICATION,
HelloWorldNode.DRAW_REGISTRANT_ID,
HelloWorldDrawOverride.creator)
except:
om.MGlobal.displayError("Failed to register draw override: {0}".format(HelloWorldDrawOverride.NAME))


def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
plugin_fn = om.MFnPlugin(plugin)
try:
omr.MDrawRegistry.deregisterDrawOverrideCreator(HelloWorldNode.DRAW_CLASSIFICATION,
HelloWorldNode.DRAW_REGISTRANT_ID)
except:
om.MGlobal.displayError("Failed to deregister draw override: {0}".format(HelloWorldDrawOverride.NAME))

try:
plugin_fn.deregisterNode(HelloWorldNode.TYPE_ID)
except:
om.MGlobal.displayError("Failed to deregister node: {0}".format(HelloWorldNode.TYPE_NAME))


if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
plugin_name = "hello_world_node.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

cmds.evalDeferred('cmds.createNode("helloworld")')

maya官方给的maya python api示例

首先下载maya开发者工具包,下载压缩后在devkitBase\devkit\plug-ins\scripted目录下可以看到各种示例image.pngpy开头的是使用了maya api 2.0的,其他的是使用了maya api 1.0

maya api 基础

maya api 的四种对象类型:image.png

MObject

image.png
image.png

MFunction Sets

image.png
image.png
image.png

Wrappers

image.png

image.png
image.png
迭代器也被认为是wrappers
迭代器类的名字前缀为MIt

Proxies

image.png

image.png

举例

遍历当前maya选择的对象,根据对象是否拥有对应的函数集来输出调用函数集的功能所得到的值

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
import maya.api.OpenMaya as om
selection = om.MGlobal.getActiveSelectionList()
for i in range(selection.length()):
if i > 0:
print("--------")
obj = selection.getDependNode(i) # Return an MObject
print("API Type: {0}".format(obj.apiTypeStr))

if obj.hasFn(om.MFn.kDependencyNode): # 检查这个MObject是否能够使用这个函数集

depend_fn = om.MFnDependencyNode(obj) # 将这个函数集附加到obj上
print("Dependency Node: {0}".format(depend_fn.name()))

if obj.hasFn(om.MFn.kTransform):
transform_fn = om.MFnTransform(obj)
print("Translation: {0}".format(transform_fn.translation(om.MSpace.kTransform)))

elif obj.hasFn(om.MFn.kMesh):
mesh_fn = om.MFnMesh(obj)
print("Mesh Vertices: {0}".format(mesh_fn.getVertices()))

elif obj.hasFn(om.MFn.kCamera):
camera_fn = om.MFnCamera(obj)
print("Clipping Planes: {0}, {1}".format(camera_fn.nearClippingPlane, camera_fn.farClippingPlane))


如果选择一个方盒子和一个摄像机,输出的内容:
image.png

Dependence Graph(DG)

DG的介绍与特点

image.png
Dependency Graph(DG)是一个基于节点的体系架构,它为创建maya场景提供了基本构建块
它有以下四种特点
image.png

image.png
image.png
image.png

DG如何使用Push-Pull模型更新

image.png
image.png
https://www.bilibili.com/video/BV1MG4y1W7oH?t=388.5
1.例如这是一个DG结构,绿色代表这些节点都是干净的(clean),然后如果A发生改变,那么A节点就会变成脏的(dirty)状态,A节点变成了dirty状态,那么它会影响C和D都变成dirty状态
2.当maya要需要用到D时,发现D时dirty状态,那么它就从D出发然后追溯到A,然后更新A后,A变成了干净的状态就可以将C,D变成干净的状态了,都变成绿色后maya就可以刷新视口完成更新。
image.pngimage.png

总结

image.png
image.png

自定义节点

节点介绍

image.png

节点属性

image.png
image.png

计算方法

image.png
image.png

依赖属性

image.png
image.png

修改属性

修改属性有三种方法

使用plug修改属性

image.png
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import maya.api.OpenMaya as om
node_name = "pCube1"
attribute_name = "translateY"

selection_list = om.MSelectionList()
selection_list.add(node_name)

obj = selection_list.getDependNode(0)

if obj.hasFn(om.MFn.kTransform):
transform_fn = om.MFnTransform(obj)

plug = transform_fn.findPlug(attribute_name,False) # 获取pCube的translateY的plug对象,False的含义在以后的章节讲

attribute_value = plug.asDouble() # 得到plug对应的数据
print("{0}: {1}".format(plug, attribute_value)) # 这里输出plug虽然是pCube1.translateY,但是plug并不是字符串,plug覆盖了string方法,使其输出内容改变了。

plug.setDouble(2.0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import maya.api.OpenMaya as om
node_name = "pCube1"
attribute_name = "translate"

selection_list = om.MSelectionList()
selection_list.add(node_name)

obj = selection_list.getDependNode(0)

if obj.hasFn(om.MFn.kTransform):
transform_fn = om.MFnTransform(obj)

plug = transform_fn.findPlug(attribute_name,False)

if plug.isCompound: # 判断插头是否是复合类型
for i in range(plug.numChildren()): # 遍历插头的子项
child_plug = plug.child(i)

attribute_value = child_plug.asDouble()
print("{0}: {1}".format(child_plug, attribute_value))

利用函数集自带的修改属性的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import maya.api.OpenMaya as om
node_name = "pCube1"
attribute_name = "translate"

selection_list = om.MSelectionList()
selection_list.add(node_name)

obj = selection_list.getDependNode(0)

if obj.hasFn(om.MFn.kTransform):
transform_fn = om.MFnTransform(obj)

translation = transform_fn.translation(om.MSpace.kTransform)
translation[1] = 3.0
transform_fn.setTranslation(translation, om.MSpace.kTransform)

创建自定义节点所需要的基础

MPxNode

image.png
HelloWorld节点用到的MPxLocatorNode class是MPxNode派生的。

initializePlugin()

任何新的自定义节点都需要在maya中注册,然后才能使用。
注册是在initializePlugin() 函数内使用MPxPlugin.registerNode()
image.png

MPxPlugin.registerNode()

注册方法需要节点的名字,一个唯一的ID,创建的方法,初始化的方法。image.png

creator()

creator()用来返回一个类的新实例

initialize()

initialize()有三个作用:1.初始化所有的节点属性2.添加或修改节点的属性都在这个方法内3.只有当加载plugin时调用image.png

注:

creator()与initialize()方法可以用不同的函数名,creator与initialize是通用的容易表示的。只是一个命名约定

Compute()

image.png
image.png
image.png

postConstructor()

image.png
当创建节点后调用

connectionMade() 和 connectionBroken()

image.png
当节点连接状态发生改变时调用

举例:自定义一个简单的数学节点

节点功能:将两个数相乘并输出结果值
image.png

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# coding: utf-8
import maya.api.OpenMaya as om
import maya.cmds as cmds

def maya_useNewAPI():
""" 这个函数告诉了maya这个插件生成,并且生成的对象使用maya python api 2.0 """
pass

class MutiplyNode(om.MPxNode):
TYPE_NAME = "multiplynode"
TYPE_ID = om.MTypeId(0x0007F7F8)
# 提前声明节点的属性
multiplier_obj = None
multiplicand_obj = None
product_obj = None

def __init__(self):
super(MutiplyNode, self).__init__()

def compute(self, plug, data):
"""_summary_

Args:
plug (_type_): 当plug是dirty状态时,会传过来,要求更新
data (_type_): data提供了读取和写入节点属性值的方法
"""
if plug == MutiplyNode.product_obj:

multiplier = data.inputValue(MutiplyNode.multiplier_obj).asInt() # 获取multiplier_obj对象的输入的属性值
multiplicand = data.inputValue(MutiplyNode.multiplicand_obj).asDouble() # 获取multiplicand_obj对象的输入的属性值
product = multiplier * multiplicand

product_data_handle = data.outputValue(MutiplyNode.product_obj) # 获取product_obj对象的输出数据对象
product_data_handle.setDouble(product) # 设置product_obj对象的输出数据

data.setClean(plug) # 将plug设置为clean状态


@classmethod
def creator(cls):
return MutiplyNode()

@classmethod
def initialize(cls):
numeric_attr = om.MFnNumericAttribute() # 创建一个用来设置数字系列属性的对象

cls.multiplier_obj = numeric_attr.create("multiplier", "mul", om.MFnNumericData.kInt, 2) # 属性长名,属性短名,属性数字类型,属性初始值
numeric_attr.keyable = True # 设置属性可以key关键帧(这样属性就能出现在channel box中)
numeric_attr.readable = False # 设置属性没有输出引脚(其他属性不能读它)

cls.multiplicand_obj = numeric_attr.create("multiplicand", "mulc", om.MFnNumericData.kDouble, 0.0)
numeric_attr.keyable = True
numeric_attr.readable = False

cls.product_obj = numeric_attr.create("product", "prod", om.MFnNumericData.kDouble, 0.0)
numeric_attr.writable = False # 设置属性没有输入引脚(其他属性不能直接写入它)

# 添加属性
cls.addAttribute(cls.multiplier_obj)
cls.addAttribute(cls.multiplicand_obj)
cls.addAttribute(cls.product_obj)
# 设置属性影响
cls.attributeAffects(cls.multiplier_obj, cls.product_obj)
cls.attributeAffects(cls.multiplicand_obj, cls.product_obj)

def initializePlugin(plugin):
"""
加载插件时执行此函数
plugin: MObject用于使用MFnPlugin函数集注册插件
"""

vendor = "RuiChen"
version = "1.0.0"

plugin_fn = om.MFnPlugin(plugin, vendor, version)
try:
plugin_fn.registerNode(MutiplyNode.TYPE_NAME,
MutiplyNode.TYPE_ID,
MutiplyNode.creator,
MutiplyNode.initialize,
om.MPxNode.kDependNode)
except:
om.MGlobal.displayError("Failed to register node: {0}".format(MutiplyNode.TYPE_NAME))

def uninitializePlugin(plugin):
"""
取消加载插件时执行此函数
plugin: MObject用于使用MFnPlugin函数集取消注册插件
"""
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterNode(MutiplyNode.TYPE_ID)
except:
om.MGlobal.displayError("Failed to deregister node: {0}".format(MutiplyNode.TYPE_NAME))

if __name__ == "__main__":
"""
测试时使用
"""

cmds.file(new=True, force=True)

plugin_name = "multiply_node.py"

cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

cmds.evalDeferred('cmds.createNode("multiplynode")')

常用的调节属性的性质

image.png
image.png
image.png
image.png
image.png
image.png
channelBox不需要设置为True,因为设置keyable为True就完全不需要设置channelBox为True了。
image.png
image.png

举例:自定义一个车轮节点

数学原理:

image.png
image.png

代码

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# coding: utf-8
import maya.api.OpenMaya as om
import maya.cmds as cmds

def maya_useNewAPI():
""" 这个函数告诉了maya这个插件生成,并且生成的对象使用maya python api 2.0 """
pass

class RollingNode(om.MPxNode):
TYPE_NAME = "rollingnode"
TYPE_ID = om.MTypeId(0x0007F7F9)
# 提前声明节点的属性
distance_obj = None
radius_obj = None
rotation_obj = None

def __init__(self):
super(RollingNode, self).__init__()

def compute(self, plug, data):
"""_summary_

Args:
plug (_type_): 当plug是dirty状态时,会传过来,要求更新
data (_type_): data提供了读取和写入节点属性值的方法
"""
if plug == RollingNode.rotation_obj:

distance = data.inputValue(RollingNode.distance_obj).asDouble()
radius = data.inputValue(RollingNode.radius_obj).asDouble()
if radius==0:
rotation = 0
else:
rotation = distance / radius

rotation_data_handle = data.outputValue(RollingNode.rotation_obj)
rotation_data_handle.setDouble(rotation)

data.setClean(plug)

@classmethod
def creator(cls):
return RollingNode()

@classmethod
def initialize(cls):
numeric_attr = om.MFnNumericAttribute() # 创建一个针对数字属性的函数集

cls.distance_obj = numeric_attr.create("distance", "dis", om.MFnNumericData.kDouble, 0.0) # 属性长名,属性短名,属性数字类型,属性初始值
numeric_attr.keyable = True # 设置属性可以key关键帧(这样属性就能出现在channel box中)
numeric_attr.readable = False # 设置属性没有输出引脚(其他属性不能读它)

cls.radius_obj = numeric_attr.create("radius", "rad", om.MFnNumericData.kDouble, 0.0)
numeric_attr.keyable = True
numeric_attr.readable = False

unit_attr = om.MFnUnitAttribute() # 创建一个针对单位属性的函数集

cls.rotation_obj = unit_attr.create("rotation", "rot", om.MFnUnitAttribute.kAngle, 0.0)

# unit_attr.writable = False # 设置属性没有输入引脚(其他属性不能直接写入它)

# 添加属性
cls.addAttribute(cls.distance_obj)
cls.addAttribute(cls.radius_obj)
cls.addAttribute(cls.rotation_obj)
# 设置属性影响
cls.attributeAffects(cls.distance_obj, cls.rotation_obj)
cls.attributeAffects(cls.radius_obj, cls.rotation_obj)

def initializePlugin(plugin):
"""
加载插件时执行此函数
plugin: MObject用于使用MFnPlugin函数集注册插件
"""

vendor = "RuiChen"
version = "1.0.0"

plugin_fn = om.MFnPlugin(plugin, vendor, version)
try:
plugin_fn.registerNode(RollingNode.TYPE_NAME,
RollingNode.TYPE_ID,
RollingNode.creator,
RollingNode.initialize,
om.MPxNode.kDependNode)
except:
om.MGlobal.displayError("Failed to register node: {0}".format(RollingNode.TYPE_NAME))

def uninitializePlugin(plugin):
"""
取消加载插件时执行此函数
plugin: MObject用于使用MFnPlugin函数集取消注册插件
"""
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterNode(RollingNode.TYPE_ID)
except:
om.MGlobal.displayError("Failed to deregister node: {0}".format(RollingNode.TYPE_NAME))

if __name__ == "__main__":
"""
测试时使用
"""

cmds.file(new=True, force=True)

plugin_name = "rolling_node.py"

cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name)) # 取消加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name)) # 加载插件

cmds.evalDeferred('cmds.createNode("rollingnode")')

#cmds.evalDeferred(cmds.file("D:/ZhangRuiChen/zrctest/test.ma",o=True,f=True))

自定义命令

需要的函数

image.pngimage.png

image.pngimage.png
image.pngimage.png
image.pngimage.png

堆栈

image.pngimage.png

举例:

创建一个命令,这个命令用来设置选择的一个物体的位置

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# coding: utf-8
import maya.api.OpenMaya as om
import maya.cmds as cmds


def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass


class SimpleCmd(om.MPxCommand):
COMMAND_NAME = "SimpleCmd"
# 定义命令的标志
TRANSLATE_FLAG = ["-t", "-translate", (om.MSyntax.kDouble,om.MSyntax.kDouble,om.MSyntax.kDouble)]
VERSION_FLAG = ["-v", "-version"]

def __init__(self):
super(SimpleCmd, self).__init__()

self.undoable = False # 初始设置命令不能撤回

def doIt(self, arg_list):
"""
doIt 通常用来进行执行redoIt的初始设置以及检查
doIt 在使用命令时只调用一次
"""

try:
arg_db = om.MArgDatabase(self.syntax(), arg_list) # 创建对象解析语法与参数
except:
self.displayError("Error parsing arguments")
raise

selection_list = arg_db.getObjectList()

self.selected_obj = selection_list.getDependNode(0)
# om.MFn为所有API类型提供常量的静态类
# om.MSpace 提供坐标空间常量的静态类。
if self.selected_obj.apiType() != om.MFn.kTransform:
raise RuntimeError("This command requires a transform node")

self.edit = arg_db.isEdit
self.query = arg_db.isQuery

self.translate = arg_db.isFlagSet(SimpleCmd.TRANSLATE_FLAG[0]) # 判断语法中是否有这个flag
if self.translate:
transform_fn = om.MFnTransform(self.selected_obj)
self.orig_translation = transform_fn.translation(om.MSpace.kTransform)

if self.edit:
self.new_translation = [arg_db.flagArgumentDouble(SimpleCmd.TRANSLATE_FLAG[0],0),
arg_db.flagArgumentDouble(SimpleCmd.TRANSLATE_FLAG[0],1),
arg_db.flagArgumentDouble(SimpleCmd.TRANSLATE_FLAG[0],2)]
self.undoable = True

self.version = arg_db.isFlagSet(SimpleCmd.VERSION_FLAG[0])

self.redoIt()

def undoIt(self):
transform_fn = om.MFnTransform(self.selected_obj)
transform_fn.setTranslation(om.MVector(self.orig_translation),om.MSpace.kTransform)

def redoIt(self):
transform_fn = om.MFnTransform(self.selected_obj)

if self.query:
if self.translate:
self.setResult(self.orig_translation)
else:
raise RuntimeError("Flag does not support query")

elif self.edit:
if self.translate:
transform_fn.setTranslation(om.MVector(self.new_translation),om.MSpace.kTransform)
else:
raise RuntimeError("Flag does not support edit")

elif self.version:
self.setResult("1.0.0")
else:
self.setResult(transform_fn.name())

def isUndoable(self):
return self.undoable

@classmethod
def creator(cls):
""" 注册maya命令时使用的方法,用来得到类的实例 """
return SimpleCmd()

@classmethod
def create_syntax(cls):

syntax = om.MSyntax()

syntax.enableEdit = True
syntax.enableQuery = True

syntax.addFlag(*cls.TRANSLATE_FLAG) # 这里*的意思是解包,相当于将列表的中括号去掉
syntax.addFlag(*cls.VERSION_FLAG)

# 设置要传递给命令的对象的类型和数量
syntax.setObjectType(om.MSyntax.kSelectionList, 1, 1)
# 如果设置为True,那么当命令行上没有提供对象时,Maya将传递当前选择。默认为False。
syntax.useSelectionAsDefault(True)

return syntax


def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件

try:
# 向maya注册一个新命令,第一个参数是命令的名字,第二个参数是类的实例, 第三个参数是命令的语法
plugin_fn.registerCommand(SimpleCmd.COMMAND_NAME, SimpleCmd.creator, SimpleCmd.create_syntax)
except:
om.MGlobal.displayError("Failed to register command: {0}".format(SimpleCmd.COMMAND_NAME)) # 注册失败时输出


def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数 """
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterCommand(SimpleCmd.COMMAND_NAME) # 取消注册新命令
except:
om.MGlobal.displayError("Failed to deregister command: {0}".format(SimpleCmd.COMMAND_NAME)) # 取消注册失败时输出


if __name__ == '__main__':
cmds.file(new=True, force=True)

plugin_name = "simple_cmd.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred(
'if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred(
'if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

cmds.evalDeferred('cmds.polyCube()')

第二卷

变形器

变形器的介绍

image.pngimage.png
image.png

MPxDeformerNode

image.png
image.png
image.png
image.png
deform方法是由compute方法自动调用的

MPxDeformerNode Attributes

MPxDeformerNode 自带的属性:
image.png

提醒

image.png
image.png

创建变形器节点的三个举例

basicdeformernode

创建一个名字叫basicdeformernode的变形器(遍历顶点,每隔一个顶点,改变顶点的位置)
然后可以通过cmds.deformer(typ=“basicdeformernode”)为选择的物体添加变形节点
变形使用的函数:
image.png

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
# coding: utf-8
import maya.OpenMaya as om
import maya.OpenMayaMPx as ommpx
import maya.cmds as cmds

class BasicDeformerNode(ommpx.MPxDeformerNode):

TYPE_NAME = "basicdeformernode"
TYPE_ID = om.MTypeId(0x0007F7FC)

def __init__(self):
super(BasicDeformerNode, self).__init__()

def deform(self, data_block, geo_iter, matrix, multi_index):

envelope = data_block.inputValue(self.envelope).asFloat() # 总权重

if envelope == 0:
return

geo_iter.reset() # 重置迭代器
while not geo_iter.isDone():

if geo_iter.index() % 2 == 0:
pt = geo_iter.position()
# 局部空间
# pt.x += (0.2*envelope)

# 世界空间
pt = pt * matrix * 0.1

geo_iter.setPosition(pt)

geo_iter.next()

@classmethod
def creator(cls):
return BasicDeformerNode()

@classmethod
def initialize(cls):
pass

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = ommpx.MFnPlugin(plugin, vendor, version) # 定义插件

try:
plugin_fn.registerNode(BasicDeformerNode.TYPE_NAME,
BasicDeformerNode.TYPE_ID,
BasicDeformerNode.creator,
BasicDeformerNode.initialize,
ommpx.MPxNode.kDeformerNode)
except:
om.MGlobal.displayError("Failed to register node: {0}".format(BasicDeformerNode.TYPE_NAME))

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
plugin_fn = ommpx.MFnPlugin(plugin)
try:
plugin_fn.deregisterNode(BasicDeformerNode.TYPE_ID)
except:
om.MGlobal.displayError("Failed to deregister node: {0}".format(BasicDeformerNode.TYPE_NAME))

if __name__ == '__main__':
cmds.file(new=True,f=True)
plugin_name = "basic_deformer_node.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred(
'if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred(
'if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))
cmds.evalDeferred('cmds.file("C:/Users/Adiministrator/Destop/test.ma",o=True,f=True)')
cmds.evalDeferred('cmds.select("nurbsPlane1"); cmds.deformer(typ="basicdeformernode")')

blenddeformernode

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# coding: utf-8
import maya.OpenMaya as om
import maya.OpenMayaMPx as ommpx
import maya.cmds as cmds

class BlendDeformerNode(ommpx.MPxDeformerNode):

TYPE_NAME = "blenddeformernode"
TYPE_ID = om.MTypeId(0x0007F7FD)

def __init__(self):
super(BlendDeformerNode, self).__init__()

def deform(self, data_block, geo_iter, matrix, multi_index):
"""
变形的逻辑
Args:
data_block (_type_): 数据块
geo_iter (_type_): 针对geometry的顶点迭代器
matrix (_type_): 世界空间的矩阵
multi_index (_type_): _description_
"""
# envelope是MPxDeformerNode类自带的属性
envelope = data_block.inputValue(self.envelope).asFloat() # 获取控制整体权重值的值
if envelope == 0:
return

blend_weight = data_block.inputValue(self.blend_weight).asFloat() # 获取混合的权重值
if blend_weight == 0:
return

target_mesh = data_block.inputValue(self.blend_mesh).asMesh() # 获取目标mesh
if target_mesh.isNull():
return

target_points = om.MPointArray() # 定义一个接受目标mesh所有点的数组

target_mesh_fn = om.MFnMesh(target_mesh) # 定义一个目标mesh的函数集
target_mesh_fn.getPoints(target_points) # 使用函数集的方法将点放入到点数组中

global_weight = blend_weight * envelope # 得到总的权重值

geo_iter.reset() # 重置迭代器
while not geo_iter.isDone():

source_pt = geo_iter.position()
target_pt = target_points[geo_iter.index()]

source_weight = self.weightValue(data_block, multi_index, geo_iter.index()) # 获取绘制的权重值

final_pt = source_pt + ((target_pt - source_pt) * global_weight * source_weight)


geo_iter.setPosition(final_pt)

geo_iter.next()

@classmethod
def creator(cls):
return BlendDeformerNode()

@classmethod
def initialize(cls):

typed_attr = om.MFnTypedAttribute()
cls.blend_mesh = typed_attr.create("blendMesh", "bMesh", om.MFnData.kMesh)

numeric_attr = om.MFnNumericAttribute()
cls.blend_weight = numeric_attr.create("blendWeight", "bWeight", om.MFnNumericData.kFloat, 0.0)
numeric_attr.setKeyable(True)
numeric_attr.setMin(0.0)
numeric_attr.setMax(1.0)

cls.addAttribute(cls.blend_mesh)
cls.addAttribute(cls.blend_weight)

#变形器节点具有默认的outputGeom属性,因此我们没必要再创建一个输出的属性,我们可以直接利用这个默认的outputGemo属性
output_geom = ommpx.cvar.MPxGeometryFilter_outputGeom

cls.attributeAffects(cls.blend_mesh, output_geom)
cls.attributeAffects(cls.blend_weight,output_geom)

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = ommpx.MFnPlugin(plugin, vendor, version) # 定义插件

try:
plugin_fn.registerNode(BlendDeformerNode.TYPE_NAME,
BlendDeformerNode.TYPE_ID,
BlendDeformerNode.creator,
BlendDeformerNode.initialize,
ommpx.MPxNode.kDeformerNode)
except:
om.MGlobal.displayError("Failed to register node: {0}".format(BlendDeformerNode.TYPE_NAME))

cmds.makePaintable(BlendDeformerNode.TYPE_NAME, "weights", attrType="multiFloat", shapeMode = "deformer") # 使其能绘制权重

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
cmds.makePaintable(BlendDeformerNode.TYPE_NAME, "weights",remove=True) # 移除使其能绘制权重
plugin_fn = ommpx.MFnPlugin(plugin)
try:
plugin_fn.deregisterNode(BlendDeformerNode.TYPE_ID)
except:
om.MGlobal.displayError("Failed to deregister node: {0}".format(BlendDeformerNode.TYPE_NAME))

if __name__ == '__main__':
cmds.file(new=True,f=True)
plugin_name = "blend_deformer_node.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred(
'if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred(
'if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))
cmds.evalDeferred('cmds.file("D:/ZhangRuiChen/zrctest/blend_test.ma",o=True,f=True)')
cmds.evalDeferred('cmds.select("sourceSphere"); cmds.deformer(typ="blenddeformernode")')
cmds.evalDeferred('cmds.connectAttr("deformerTargetShape.outMesh", "blenddeformernode1.blendMesh", force=True)')

attractordeformernode

image.pngimage.png

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# coding: utf-8
import maya.OpenMaya as om
import maya.OpenMayaMPx as ommpx
import maya.cmds as cmds

class AttractorDeformerNode(ommpx.MPxDeformerNode):

TYPE_NAME = "attractordeformernode"
TYPE_ID = om.MTypeId(0x0007F7FE)

MAX_ANGLE = 0.5 * 3.14159265 # 90度

def __init__(self):
super(AttractorDeformerNode, self).__init__()

def deform(self, data_block, geo_iter, world_matrix, multi_index):
"""
变形的逻辑
Args:
data_block (_type_): 数据块
geo_iter (_type_): 针对geometry的顶点迭代器
matrix (_type_): 世界空间的矩阵
multi_index (_type_): geom_index
"""
# envelope是MPxDeformerNode类自带的属性
envelope = data_block.inputValue(self.envelope).asFloat() # 获取控制整体权重值的值
if envelope == 0:
return

max_distance = data_block.inputValue(AttractorDeformerNode.max_distance).asFloat()
if max_distance == 0:
return

target_position = data_block.inputValue(AttractorDeformerNode.target_position).asFloatVector()
target_position = om.MPoint(target_position) * world_matrix.inverse() # 将目标位置转换为局部空间下的数值
target_position = om.MFloatVector(target_position) # 获取目标位置在局部空间下的floatVector

input_handle = data_block.outputArrayValue(self.input) # 使用outputArray代替inputArray以避免重新计算(外网翻译)
input_handle.jumpToElement(multi_index)
input_element_handle = input_handle.outputValue()

input_geom = input_element_handle.child(self.inputGeom).asMesh()
mesh_fn = om.MFnMesh(input_geom)

normals = om.MFloatVectorArray() # 用来存取inputgeom的顶点的所有浮点法线
mesh_fn.getVertexNormals(False, normals) # False的作用是不要average normal


geo_iter.reset()
while not geo_iter.isDone():
# 顶点迭代器所获取的位置都是在局部空间下的位置
pt_local = geo_iter.position()

target_vector = target_position - om.MFloatVector(pt_local)

distance = target_vector.length()

if distance <= max_distance:

normal = normals[geo_iter.index()] # 局部空间下的顶点的法线浮点向量

angle = normal.angle(target_vector) # 顶点的法线与顶点与目标点的向量之间的角度
if angle <= AttractorDeformerNode.MAX_ANGLE:

offset = target_vector * ((max_distance-distance)/max_distance)

new_pt_local = pt_local + om.MVector(offset) # 局部空间下的新顶点位置

geo_iter.setPosition(new_pt_local)

geo_iter.next()

def accessoryAttribute(self):
""" 返回要辅助修改的属性 """
return AttractorDeformerNode.target_position

def accessoryNodeSetup(self, dag_modifier):
""" dag_modifier用于执行节点创建和连接操作 """
locator = dag_modifier.createNode("locator")

locator_fn = om.MFnDependencyNode(locator)
locator_translate_plug = locator_fn.findPlug("translate", False) # False意思是不需要networkplug,networkplug意思是在DG中建立连接的属性

target_position_plug = om.MPlug(self.thisMObject(), AttractorDeformerNode.target_position) # 获取变形器的target_position的plug
dag_modifier.connect(locator_translate_plug, target_position_plug)

# 将定位器得位置设置在output_geom的位置
# 这里的output_geom指的是shape节点
# parent指的是transform节点,因为只有transform节点才有xyz坐标
output_geom_plug = om.MPlug(self.thisMObject(), self.outputGeom)
mPlugArray2 = om.MPlugArray()
output_geom_plug[0].connectedTo(mPlugArray2,False,True)
output_geom_obj = mPlugArray2[0].node()
output_geom_fn = om.MFnDagNode(output_geom_obj)
parent_obj = output_geom_fn.parent(0)
parent_fn = om.MFnDependencyNode(parent_obj)
parent_translate_plug = parent_fn.findPlug("translate",False)
parent_translate_x_handle = parent_translate_plug.child(0).asFloat()
parent_translate_y_handle = parent_translate_plug.child(1).asFloat()
parent_translate_z_handle = parent_translate_plug.child(2).asFloat()

locator_translate_plug.child(0).setFloat(parent_translate_x_handle)
locator_translate_plug.child(1).setFloat(parent_translate_y_handle)
locator_translate_plug.child(2).setFloat(parent_translate_z_handle)





@classmethod
def creator(cls):
return AttractorDeformerNode()

@classmethod
def initialize(cls):

numeric_attr = om.MFnNumericAttribute()
cls.max_distance = numeric_attr.create("maximumDistance", "maxDist", om.MFnNumericData.kFloat, 1.0)
numeric_attr.setKeyable(True)
numeric_attr.setMin(0.0)
numeric_attr.setMax(2.0)

cls.target_position = numeric_attr.createPoint("targetPosition", "targetPos")
numeric_attr.setKeyable(True)

cls.addAttribute(cls.max_distance)
cls.addAttribute(cls.target_position)

#变形器节点具有默认的outputGeom属性,因此我们没必要再创建一个输出的属性,我们可以直接利用这个默认的outputGemo属性
output_geom = ommpx.cvar.MPxGeometryFilter_outputGeom

cls.attributeAffects(cls.max_distance, output_geom)
cls.attributeAffects(cls.target_position,output_geom)

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = ommpx.MFnPlugin(plugin, vendor, version) # 定义插件

try:
plugin_fn.registerNode(AttractorDeformerNode.TYPE_NAME,
AttractorDeformerNode.TYPE_ID,
AttractorDeformerNode.creator,
AttractorDeformerNode.initialize,
ommpx.MPxNode.kDeformerNode)
except:
om.MGlobal.displayError("Failed to register node: {0}".format(AttractorDeformerNode.TYPE_NAME))

cmds.makePaintable(AttractorDeformerNode.TYPE_NAME, "weights", attrType="multiFloat", shapeMode = "deformer") # 使其能绘制权重

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
cmds.makePaintable(AttractorDeformerNode.TYPE_NAME, "weights",remove=True) # 移除使其能绘制权重
plugin_fn = ommpx.MFnPlugin(plugin)
try:
plugin_fn.deregisterNode(AttractorDeformerNode.TYPE_ID)
except:
om.MGlobal.displayError("Failed to deregister node: {0}".format(AttractorDeformerNode.TYPE_NAME))

if __name__ == '__main__':
cmds.file(new=True,f=True)
plugin_name = "attractor_deformer_node.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred(
'if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred(
'if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

cmds.evalDeferred('cmds.file("D:/ZhangRuiChen/zrctest/attractor_test.ma",o=True,f=True)')
cmds.evalDeferred('cmds.select("pSphere1"); cmds.deformer(typ="attractordeformernode")')
#cmds.evalDeferred('cmds.connectAttr("locator1.translate","attractordeformernode1.targetPosition",f=True)')

CallBack

CallBack介绍

image.png
image.png

image.png
image.png

CallBack vs ScriptJobs

image.png
image.png

MMessage

image.png
image.png
详情:https://help.autodesk.com/view/MAYAUL/2018/ENU/?guid=__py_ref_class_open_maya_1_1_m_message_html
点击才能展开
image.png
image.png
MEventMessage:在发生添加的全局事件时执行(例如场景被打开,选择发生变化,事件发生改变)时添加callback
MSceneMessage: 场景事件添加callback
MTimmerMessage: 在特定的事件间隔内调用一个函数

管理回调函数

image.png
image.png

获取MEventMessage的事件名字

import maya.api.OpenMaya as om
om.MEventMessage.getEventNames()
其中常用的事件:
deleteAll,undoSupressed(撤销后的返回),undo(撤销),timeChanged(时间轴变化)
其他事件的解释可以通过scriptjob命令的帮助文档找到:
https://help.autodesk.com/view/MAYAUL/2018/ENU/?guid=__CommandsPython_index_html

举例:

MEventMessage
MSceneMessage
MConditionMessage
MUiMessage
MTimerMessage

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
84
85
# coding: utf-8
import maya.api.OpenMaya as om
import maya.api.OpenMayaAnim as oma
import maya.api.OpenMayaUI as omui
import maya.cmds as cmds

def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass

callback_ids = [] # 定义全局变量存放callback_id

def on_new_scene(client_data):
""" 当新场景加载时调用,client_data为调用这个函数时传递的参数(可以为None) """
print("New Scene opened")

def on_time_changed(client_data):
""" 当时间轴发生改变时调用 """
print("Time changed: {0}".format(oma.MAnimControl.currentTime().asUnits(om.MTime.uiUnit()))) # 输出当前时间轴的帧数

def on_selection_changed(client_data):
""" 当选择发生改变时调用 """
print("Selection changed")

def before_import(client_data):
""" 导入前执行 """
print("Import pre-processing")

def after_import(client_data):
""" 导入后执行 """
print("Import post-processing")

def on_viewport_changed(model_panel,*args):
""" 在指定的视口面板处切换相机时调用 """
print("Camera changed in model panel {0}".format(model_panel))

def on_playing_back_state_changed(is_playing, client_data):
""" 当使用播放键预览拍屏时调用 """
print("Playing state changed: {0}".format(is_playing))

def on_timer_fired(elapsed_time, previous_execution_time, client_data):
"""
定时器调用此函数,每经过设定的秒数就调用一次这个函数
Args:
elapsed_time (float): 设定的秒数
previous_execution_time (_type_): 以前的运行的时间,为了防止此函数还没执行完就再次被调用
client_data (_type_): 用户自定义的参数,可以不传递
"""
print("Timer fired")

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
global callback_ids # 调用全局变量
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本
# 注册回调,参数为事件类型名,回调函数,还有第三个参数可选(传递给回调函数的参数),返回值为回调函数id
callback_ids.append(om.MEventMessage.addEventCallback("NewSceneOpened", on_new_scene))
callback_ids.append(om.MEventMessage.addEventCallback("timeChanged", on_time_changed))
callback_ids.append(om.MEventMessage.addEventCallback("SelectionChanged", on_selection_changed))

callback_ids.append(om.MSceneMessage.addCallback(om.MSceneMessage.kBeforeImport, before_import))
callback_ids.append(om.MSceneMessage.addCallback(om.MSceneMessage.kAfterImport, after_import))

callback_ids.append(om.MConditionMessage.addConditionCallback("playingBack", on_playing_back_state_changed))

callback_ids.append(omui.MUiMessage.addCameraChangedCallback("modelPanel4", on_viewport_changed))

callback_ids.append(om.MTimerMessage.addTimerCallback(2.5, on_timer_fired))

om.MFnPlugin(plugin, vendor, version) # 定义插件

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
global callback_ids
om.MEventMessage.removeCallbacks(callback_ids)
callback_ids = []

if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
plugin_name = "callback_example.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

遍历dag

image.png
image.png

什么是dag

image.png
image.png
image.png
image.png
image.png

DG vs DAG

image.png
DG:
image.png
DAG:
image.png

遍历dag的常用api class

image.png

MItDag

https://help.autodesk.com/view/MAYAUL/2019/CHS/?guid=Maya_SDK_MERGED_cpp_ref_class_m_it_dag_html
image.png
image.png

MFnDagNode

image.png
image.png

MDagPath 和 MDagPathArray

image.png
image.png

代码举例

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
# coding:utf-8
import maya.api.OpenMaya as om
selection_list = om.MGlobal.getActiveSelectionList() # 获取当前选择的对象并为其创建列表
traversal_type = om.MItDag.kBreadthFirst # 遍历方式为广度优先(默认是深度优先)
filter_type = om.MFn.kMesh # 过滤器设置,只遍历kMesh类型
dag_iter = om.MItDag(traversal_type,filter_type) # 创建迭代器,并指定遍历方式和遍历类型

if not selection_list.isEmpty():

dag_fn = om.MFnDagNode(selection_list.getDependNode(0))

#print("Child:")
for i in range(dag_fn.childCount()):
child_obj = dag_fn.child(i)
child_fn = om.MFnDagNode(child_obj)
#print(child_fn.fullPathName())

#print("Parent:")
for i in range(dag_fn.parentCount()):
parent_obj = dag_fn.parent(i)
parent_fn = om.MFnDagNode(parent_obj)
#print(parent_fn.fullPathName())

# 如果场景中有选择的物体,那么就以选择的物体为迭代起始点
dag_iter.reset(selection_list.getDependNode(0),traversal_type,filter_type)
while not dag_iter.isDone():
# print(dag_iter.partialPathName()) # 输出短名
# print(dag_iter.fullPathName()) # 输出长名
# dag_path = dag_iter.getPath() # 获取dagpath对象
# print(dag_path.fullPathName()) # 输出dagpath对象的长名

# 迭代器遍历类型为kmesh,然后通过类型为kmesh的shape节点得到父节点transform的名字
shape_obj = dag_iter.currentItem()
dag_fn = om.MFnDagNode(shape_obj)
dag_fn = om.MFnDagNode(shape_obj)
for i in range(dag_fn.parentCount()):
parent_fn = om.MFnDagNode(dag_fn.parent(i))
print(parent_fn.fullPathName())

dag_iter.next()

contexts(工具,以下所有的上下文的翻译统统理解为工具)

什么是contexts

image.png
image.png

contexts class

image.png
image.png

MPxContext

image.png
image.png

MPxContextCommand

image.png
image.png

MPxToolCommand

image.png
image.png

限制

image.png
image.png

image.png
image.png

举例

context工具模板

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# coding: utf-8
# 此插件只适用于maya2020及以上版本
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import maya.cmds as cmds
import os

def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass

class SimpleContext(omui.MPxContext):

TITLE = "Simple Context"

HELP_TEXT = "<insert help text here>"

def __init__(self):
super(SimpleContext, self).__init__()

self.setTitleString(SimpleContext.TITLE)
plugin_dir_path = os.path.dirname(cmds.pluginInfo("template_context.py",p=True,q=True))
self.setImage(plugin_dir_path + "/icons/icon_windows.png", omui.MPxContext.kImage1) # 设置工具的图标

def helpSlateHasChanged(self, event):
self.setHelpString(SimpleContext.HELP_TEXT)

def toolOnSetup(self, event):
""" 工具加载时执行 """
print("toolOnSetup")

def toolOffCleanup(self):
""" 取消工具加载时执行(在使用工具的同时创建模型maya会自动先取消加载工具再自动加载工具) """
print("toolOffCleanup")

def doPress(self, event, draw_manager, frame_context):
""" 按下键时执行 """
mouse_button = event.mouseButton() # 获取鼠标的按键

if mouse_button == omui.MEvent.kLeftMouse:
# 如果按下鼠标左键执行
print("Left mouse button pressed")
elif mouse_button == omui.MEvent.kMiddleMouse:
# 如果按下鼠标中键执行
print("Middle mouse button pressed")

def doRelease(self, event, draw_manager, frame_context):
""" 松开键时执行 """
print("Mouse button released")

def doDrag(self, event, draw_manager, frame_context):
""" 按住鼠标左键并进行移动时执行 """
print("Mouse drag")

def completeAction(self):
""" 按下enter键时执行 """
print("Complete action (enter/return key pressed)")

def deleteAction(self):
""" 按下delete键或者backspace键时执行 """
print("Delete action (backspace/delete key pressed)")

def abortAction(self):
""" 按下esc键时执行 """
print("Abort action (escape key pressed)")

class SimpleContextCmd(omui.MPxContextCommand):

COMMAND_NAME = "rcSimpleCtx"

def __init__(self):
super(SimpleContextCmd, self).__init__()

def makeObj(self):
return SimpleContext()

@classmethod
def creator(cls):
return SimpleContextCmd()

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件
try:
plugin_fn.registerContextCommand(SimpleContextCmd.COMMAND_NAME, SimpleContextCmd.creator)
except:
om.MGlobal.displayError("Failed to register context command: {0}".format(SimpleContextCmd.COMMAND_NAME))

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterContextCommand(SimpleContextCmd.COMMAND_NAME)
except:
om.MGlobal.displayError("Failed to deregister context command: {0}".format(SimpleContextCmd.COMMAND_NAME))

if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
cmds.file(new=True,force=True)

plugin_name = "template_context.py" # 插件的文件名

# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

cmds.evalDeferred('context = cmds.rcSimpleCtx(); cmds.setToolTo(context)')

custom select 工具

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# coding: utf-8
# 此插件只适用于maya2020及以上版本
# 插件介绍: 使用context时按ctrl键框选只选择mesh,按ctrl+shift键时只选择light,不按只框选就全选择
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import maya.cmds as cmds
import os

def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass

class CustomSelectContext(omui.MPxContext):

TITLE = "Custom Select Context"

HELP_TEXT = "Ctrl to select only meshes. Ctrl+Shift to select only lights."

def __init__(self):
super(CustomSelectContext, self).__init__()

self.setTitleString(CustomSelectContext.TITLE)
plugin_dir_path = os.path.dirname(cmds.pluginInfo("custom_select_context.py",p=True,q=True))
self.setImage(plugin_dir_path + "/icons/icon_windows.png", omui.MPxContext.kImage1) # 设置工具的图标
test_file_path = plugin_dir_path + "/test_scene/custom_select_context_test.ma"

def helpStateHasChanged(self, event):
""" 在左下角显示帮助 """
self.setHelpString(CustomSelectContext.HELP_TEXT)

def toolOnSetup(self, event):
""" 工具加载时执行 """
print("toolOnSetup")

def toolOffCleanup(self):
""" 取消工具加载时执行(在使用工具的同时创建模型maya会自动先取消加载工具再自动加载工具) """
print("toolOffCleanup")

def doPress(self, event, draw_manager, frame_context):
""" 按下键时执行 """
self.viewport_start_pos = event.position

self.light_only = False # 用来判断是否只选择灯光类型
self.meshes_only = False # 用来判断是否只选择mesh类型

if event.isModifierControl():
if event.isModifierShift():
self.light_only = True # 当ctrl和shift键同时按下时为True
else:
self.meshes_only = True # 当ctrl键按下时为True

def doRelease(self, event, draw_manager, frame_context):
""" 松开键时执行 """
self.viewport_end_pos = event.position

initial_selection = om.MGlobal.getActiveSelectionList() # 获取场景中已经选择的对象的列表

om.MGlobal.selectFromScreen(self.viewport_start_pos[0], self.viewport_start_pos[1],
self.viewport_end_pos[0], self.viewport_end_pos[1],
om.MGlobal.kReplaceList) # 根据矩形框选择矩形中的对象(这个命令所进行的选择不会进入undo堆栈,因此需要通过其他命令设置正常的堆栈)

selection_list = om.MGlobal.getActiveSelectionList() # 获取通过矩形框选中的对象的列表

if self.light_only or self.meshes_only:
for i in reversed(range(selection_list.length())):
obj = selection_list.getDependNode(i)
shape = om.MFnDagNode(obj).child(0)

if self.light_only and not shape.hasFn(om.MFn.kLight): # 如果shape节点是light类型
selection_list.remove(i)
elif self.meshes_only and not shape.hasFn(om.MFn.kMesh): # 如果shape节点是mesh类型
selection_list.remove(i)

om.MGlobal.setActiveSelectionList(initial_selection, om.MGlobal.kReplaceList) # 首先选择场景中之前已经选择的列表
om.MGlobal.selectCommand(selection_list, om.MGlobal.kReplaceList) # 通过调用内置的maya选择命令来选择,并且让maya负责维护堆栈

def doDrag(self, event, draw_manager, frame_context):
""" 按住鼠标左键并进行移动时执行 """
self.viewport_end_pos = event.position

self.draw_selection_rectangle(draw_manager,
self.viewport_start_pos[0],self.viewport_start_pos[1],
self.viewport_end_pos[0],self.viewport_start_pos[1],
self.viewport_end_pos[0],self.viewport_end_pos[1],
self.viewport_start_pos[0],self.viewport_end_pos[1])

def draw_selection_rectangle(self, draw_manager, x0, y0, x1, y1, x2, y2, x3, y3):
""" 根据鼠标拖拽的范围进行矩形绘制 """
draw_manager.beginDrawable() # 开始绘制
draw_manager.setLineWidth(1.0) # 设置绘制线的宽度
draw_manager.setColor(om.MColor((1.0, 0.0, 0.0))) # 设置绘制的颜色(颜色数值是一个集合)

draw_manager.line2d(om.MPoint(x0,y0),om.MPoint(x1,y1))
draw_manager.line2d(om.MPoint(x1,y1),om.MPoint(x2,y2))
draw_manager.line2d(om.MPoint(x2,y2),om.MPoint(x3,y3))
draw_manager.line2d(om.MPoint(x3,y3),om.MPoint(x0,y0))

draw_manager.endDrawable() # 结束绘制


class CustomSelectContextCmd(omui.MPxContextCommand):

COMMAND_NAME = "rcCustomSelectCtx"

def __init__(self):
super(CustomSelectContextCmd, self).__init__()

def makeObj(self):
return CustomSelectContext()

@classmethod
def creator(cls):
return CustomSelectContextCmd()

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件
try:
plugin_fn.registerContextCommand(CustomSelectContextCmd.COMMAND_NAME, CustomSelectContextCmd.creator)
except:
om.MGlobal.displayError("Failed to register context command: {0}".format(CustomSelectContextCmd.COMMAND_NAME))

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterContextCommand(CustomSelectContextCmd.COMMAND_NAME)
except:
om.MGlobal.displayError("Failed to deregister context command: {0}".format(CustomSelectContextCmd.COMMAND_NAME))

if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
cmds.file(new=True,force=True)

plugin_dir_path = os.path.dirname(cmds.pluginInfo("custom_select_context.py",p=True,q=True))
test_file_path = plugin_dir_path + "/test_scene/custom_select_context_test.ma"

plugin_name = "custom_select_context.py" # 插件的文件名

# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))
cmds.evalDeferred('cmds.file(test_file_path,o=True,f=True)')
cmds.evalDeferred('context = cmds.rcCustomSelectCtx(); cmds.setToolTo(context)')

创建骨骼工具

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# coding: utf-8
# 此插件只适用于maya2020及以上版本
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import maya.cmds as cmds
import os

def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass

class JointCreateContext(omui.MPxContext):

TITLE = "JointCreate Context"

HELP_TEXT = ["Select first joint location",
"Select second joint location",
"Select final joint location",
"Press Enter to complete"]

def __init__(self):
super(JointCreateContext, self).__init__()

self.setTitleString(JointCreateContext.TITLE)
plugin_dir_path = os.path.dirname(cmds.pluginInfo("joint_create_context.py",p=True,q=True))
self.setImage(plugin_dir_path + "/icons/icon_windows.png", omui.MPxContext.kImage1) # 设置工具的图标

self.state = 0 # 判断通过工具选择的对象的个数
self.context_selection = om.MSelectionList() # 通过工具选择的对象的列表

def helpStateHasChanged(self, event):
self.update_help_string()

def update_help_string(self):
self.setHelpString(JointCreateContext.HELP_TEXT[self.state])

def toolOnSetup(self, event):
""" 工具加载时执行 """
om.MGlobal.selectCommand(om.MSelectionList()) # 确保工具刚开始使用时是一个健康的选择状态
self.reset_state()

def toolOffCleanup(self):
""" 取消工具加载时执行(在使用工具的同时创建模型maya会自动先取消加载工具再自动加载工具) """
self.reset_state()

def doRelease(self, event, draw_manager, frame_context):
""" 松开键时执行 """
if self.state >= 0 and self.state < 3:
om.MGlobal.selectFromScreen(event.position[0], event.position[1],event.position[0],event.position[1],om.MGlobal.kReplaceList)

active_selection = om.MGlobal.getActiveSelectionList() # 获取当前选择的物体
if active_selection.length() == 1:
self.context_selection.merge(active_selection) # 使用merge方法防止重复的对象出现在context_selection中

om.MGlobal.setActiveSelectionList(self.context_selection) # 更改当前选择的物体

self.update_state

def completeAction(self):
""" 按下enter键时执行 """
selection_count = self.context_selection.length()
if selection_count == 3:
om.MGlobal.setActiveSelectionList(om.MSelectionList())

for i in range(selection_count):
transform_fn = om.MFnTransform(self.context_selection.getDependNode(i))

cmds.joint(position = transform_fn.translation(om.MSpace.kTransform))
cmds.delete(transform_fn.name())

cmds.select(clear=True)
self.reset_state()

else:
om.MGlobal.displayError("Three objects must be selected")

def deleteAction(self):
""" 按下delete键或者backspace键时执行 """
selection_count = self.context_selection.length()
if selection_count > 0:
self.context_selection.remove(selection_count - 1)

om.MGlobal.setActiveSelectionList(self.context_selection)

self.update_state()

def abortAction(self):
""" 按下esc键时执行 """
self.reset_state()

def update_state(self):
""" 更新状态 """
self.state = self.context_selection.length()

self.update_help_string()

def reset_state(self):
""" 重置状态 """
om.MGlobal.setActiveSelectionList(om.MSelectionList())

self.context_selection.clear()
self.update_state

class JointCreateContextCmd(omui.MPxContextCommand):

COMMAND_NAME = "rcJointCreateCtx"

def __init__(self):
super(JointCreateContextCmd, self).__init__()

def makeObj(self):
return JointCreateContext()

@classmethod
def creator(cls):
return JointCreateContextCmd()

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件
try:
plugin_fn.registerContextCommand(JointCreateContextCmd.COMMAND_NAME, JointCreateContextCmd.creator)
except:
om.MGlobal.displayError("Failed to register context command: {0}".format(JointCreateContextCmd.COMMAND_NAME))

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterContextCommand(JointCreateContextCmd.COMMAND_NAME)
except:
om.MGlobal.displayError("Failed to deregister context command: {0}".format(JointCreateContextCmd.COMMAND_NAME))

if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
cmds.file(new=True,force=True)
plugin_dir_path = os.path.dirname(cmds.pluginInfo("joint_create_context.py",p=True,q=True))
test_file_path = plugin_dir_path + "/test_scene/joint_create_context_test.ma"

plugin_name = "joint_create_context.py" # 插件的文件名

# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))
cmds.evalDeferred('cmds.file(test_file_path,o=True,f=True)')
cmds.evalDeferred('context = cmds.rcJointCreateCtx(); cmds.setToolTo(context)')

各种绘制举例

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# coding: utf-8
# 此插件只适用于maya2020及以上版本
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import maya.cmds as cmds
import os

def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass

class SimpleContext(omui.MPxContext):

TITLE = "Simple Context"

HELP_TEXT = "<insert help text here>"

def __init__(self):
super(SimpleContext, self).__init__()

self.setTitleString(SimpleContext.TITLE)
plugin_dir_path = os.path.dirname(cmds.pluginInfo("draw_persistence_example_context.py",p=True,q=True))
self.setImage(plugin_dir_path + "/icons/icon_windows.png", omui.MPxContext.kImage1) # 设置工具的图标

def helpStateHasChanged(self, event):
self.setHelpString(SimpleContext.HELP_TEXT)

def toolOnSetup(self, event):
""" 工具加载时执行 """
print("toolOnSetup")

def toolOffCleanup(self):
""" 取消工具加载时执行(在使用工具的同时创建模型maya会自动先取消加载工具再自动加载工具) """
print("toolOffCleanup")

def doPress(self, event, draw_manager, frame_context):
""" 按下键时执行 """
mouse_button = event.mouseButton() # 获取鼠标的按键

if mouse_button == omui.MEvent.kLeftMouse:
# 如果按下鼠标左键执行
print("Left mouse button pressed")
elif mouse_button == omui.MEvent.kMiddleMouse:
# 如果按下鼠标中键执行
print("Middle mouse button pressed")

def doRelease(self, event, draw_manager, frame_context):
""" 松开键时执行 """
print("Mouse button released")

def doDrag(self, event, draw_manager, frame_context):
""" 按住鼠标左键并进行移动时执行 """
print("Mouse drag")

def completeAction(self):
""" 按下enter键时执行 """
print("Complete action (enter/return key pressed)")

def deleteAction(self):
""" 按下delete键或者backspace键时执行 """
print("Delete action (backspace/delete key pressed)")

def abortAction(self):
""" 按下esc键时执行 """
print("Abort action (escape key pressed)")

def doPtrMoved(self, event, draw_manager, frame_context):
""" 当鼠标移动并且没有按下鼠标按钮时执行 """
self.position = om.MPoint(event.position)
# self.draw_circle(event, draw_manager)
self.draw_info(draw_manager, frame_context)

def drawFeedback(self, draw_manager, frame_context):
""" 无论执行什么操作都会始终执行绘制 """
# self.draw_sphere(draw_manager)
# self.draw_info(draw_manager, frame_context) # 在这个函数中执行绘制text有显示的bug,不推荐使用
pass

def draw_circle(self, event, draw_manager):
""" 在鼠标指针的位置处绘制圆形 """
center = om.MPoint(event.position)
radius = 20

draw_manager.beginDrawable()
draw_manager.circle2d(center, radius, True)
draw_manager.endDrawable()

def draw_sphere(self, draw_manager):
""" 在坐标原点绘制一个半径为5cm的圆球 """
draw_manager.beginDrawable()
draw_manager.sphere(om.MPoint(0,0), 5)
draw_manager.endDrawable()

def draw_info(self, draw_manager, frame_context):
""" 在视口左上角绘制文字信息 """
viewport_height = frame_context.getViewportDimensions()[3]

info_position = om.MPoint(20, viewport_height - 20)
text = "Mouse pos: {0}, {1}".format(self.position.x, self.position.y)

draw_manager.beginDrawable()
draw_manager.text2d(info_position, text)
draw_manager.endDrawable()

class SimpleContextCmd(omui.MPxContextCommand):

COMMAND_NAME = "rcSimpleCtx"

def __init__(self):
super(SimpleContextCmd, self).__init__()

def makeObj(self):
return SimpleContext()

@classmethod
def creator(cls):
return SimpleContextCmd()

def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件
try:
plugin_fn.registerContextCommand(SimpleContextCmd.COMMAND_NAME, SimpleContextCmd.creator)
except:
om.MGlobal.displayError("Failed to register context command: {0}".format(SimpleContextCmd.COMMAND_NAME))

def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
plugin_fn = om.MFnPlugin(plugin)
try:
plugin_fn.deregisterContextCommand(SimpleContextCmd.COMMAND_NAME)
except:
om.MGlobal.displayError("Failed to deregister context command: {0}".format(SimpleContextCmd.COMMAND_NAME))

if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
cmds.file(new=True,force=True)

plugin_name = "draw_persistence_example_context.py" # 插件的文件名

# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

cmds.evalDeferred('context = cmds.rcSimpleCtx(); cmds.setToolTo(context)')

第三卷

Locators(可参考HelloWorldNode)

MPxLocatorNode

image.png

MPxDrawOverride(VP2.0)

image.png
image.png

代码举例

创建带形状变形器,通过更改序号改变定位器的形状

image.pngimage.png

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# coding: utf-8
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import maya.api.OpenMayaRender as omr

import maya.cmds as cmds


def maya_useNewAPI():
""" 告知maya,使用的是maya api 2.0 """
pass


class SimpleLocatorNode(omui.MPxLocatorNode):
TYPE_NAME = "simplelocator"
TYPE_ID = om.MTypeId(0x0007F7FE)
DRAW_CLASSIFICATION = "drawdb/geometry/simplelocator"
DRAW_REGISTRANT_ID = "SimpleLocatorNode"

def __init__(self):
super(SimpleLocatorNode, self).__init__()

@classmethod
def creator(cls):
return SimpleLocatorNode()

@classmethod
def initialize(cls):
numeric_attr = om.MFnNumericAttribute()

cls.shape_index_obj = numeric_attr.create("shapeIndex", "si", om.MFnNumericData.kInt, 0)
numeric_attr.setMin(0)
numeric_attr.setMax(2)

cls.addAttribute(cls.shape_index_obj)

class SimpleLocatorUserData(om.MUserData):
""" 创建MUserData类使其对象能够将数据在prepareForDraw与addUIDrawables之间相互传递 """
def __init(self, deleteAfterUse = False): # 设置为使用数据后不删除数据
super(SimpleLocatorUserData, self).__init__(deleteAfterUse)

self.shape_index = 0
self.wireframe_color = om.MColor((1.0, 1.0, 1.0)) # 线框颜色

class SimpleLocatorDrawOverride(omr.MPxDrawOverride):
NAME = "SimpleLocatorDrawOverride"

def __init__(self, obj):
super(SimpleLocatorDrawOverride, self).__init__(obj, None, True) # 第一个参数是maya object,第二个参数是绘制是callback函数,第三个参数是默认为True,为True时将会将此标志位dirty状态,因此可以持续更新,建议为True,除非遇到了性能问题可以为False

def supportedDrawAPIs(self):
return omr.MRenderer.kAllDevices

def hasUIDrawables(self):
return True

def prepareForDraw(self, obj_path, camera_path, frame_context, old_data):
""" 在使用绘制图形的方法之前,使用这个方法来将数据检索与缓存 """
data = old_data
if not data:
data = SimpleLocatorUserData()

locator_obj = obj_path.node()
node_fn = om.MFnDependencyNode(locator_obj)

data.shape_index = node_fn.findPlug("shapeIndex", False).asInt()

display_status = omr.MGeometryUtilities.displayStatus(obj_path)
if display_status == omr.MGeometryUtilities.kDormant:
data.wireframe_color = om.MColor((0.0, 0.1, 0.0)) # 当节点处于未选中状态时设置颜色为深绿色
else:
data.wireframe_color = omr.MGeometryUtilities.wireframeColor(obj_path) # 其他状态按照maya的标准来设置线框颜色

return data

def addUIDrawables(self, obj_path, draw_manager, frame_context, data):
""" 绘制图形的方法

Args:
obj_path (_type_): 指向正在绘制的对象的路径,这里是指locator节点
draw_manager (_type_): 用于绘制简单的几何图形
frame_context (_type_): 包含当前渲染框架的一些全局信息
data (_type_): 用户创建的数据对象,存储缓存数据,类型为MUserDataObject
"""
draw_manager.beginDrawable()

draw_manager.setColor(data.wireframe_color)

if data.shape_index == 0: # 绘制圆形
draw_manager.circle(om.MPoint(0.0, 0.0, 0.0), om.MVector(0.0, 1.0, 0.0), 1, False)
elif data.shape_index == 1: # 绘制矩形
draw_manager.rect(om.MPoint(0.0, 0.0, 0.0), om.MVector(0.0, 0.0, 1.0), om.MVector(0.0, 1.0, 0.0), 1.0, 1.0, False)
elif data.shape_index == 2: # 绘制三角形
point_array = om.MPointArray([(-1.0, 0.0, -1.0), (0.0, 0.0, 1.0),(1.0, 0.0, -1.0),(-1.0, 0.0, -1.0)])
draw_manager.lineStrip(point_array, False)

draw_manager.endDrawable()

@classmethod
def creator(cls, obj):
return SimpleLocatorDrawOverride(obj)


def initializePlugin(plugin):
""" 插件加载时执行这个函数"""
vendor = "RuiChen" # 插件制作人的名字
version = "1.0.0" # 插件的版本

plugin_fn = om.MFnPlugin(plugin, vendor, version) # 定义插件
try:
plugin_fn.registerNode(SimpleLocatorNode.TYPE_NAME,
SimpleLocatorNode.TYPE_ID,
SimpleLocatorNode.creator,
SimpleLocatorNode.initialize,
om.MPxNode.kLocatorNode,
SimpleLocatorNode.DRAW_CLASSIFICATION)
except:
om.MGlobal.displayError("Failed to register node: {0}".format(SimpleLocatorNode.TYPE_NAME))

try:
omr.MDrawRegistry.registerDrawOverrideCreator(SimpleLocatorNode.DRAW_CLASSIFICATION,
SimpleLocatorNode.DRAW_REGISTRANT_ID,
SimpleLocatorDrawOverride.creator)
except:
om.MGlobal.displayError("Failed to register draw override: {0}".format(SimpleLocatorDrawOverride.NAME))


def uninitializePlugin(plugin):
""" 插件取消加载时执行这个函数"""
plugin_fn = om.MFnPlugin(plugin)
try:
omr.MDrawRegistry.deregisterDrawOverrideCreator(SimpleLocatorNode.DRAW_CLASSIFICATION,
SimpleLocatorNode.DRAW_REGISTRANT_ID)
except:
om.MGlobal.displayError("Failed to deregister draw override: {0}".format(SimpleLocatorDrawOverride.NAME))

try:
plugin_fn.deregisterNode(SimpleLocatorNode.TYPE_ID)
except:
om.MGlobal.displayError("Failed to deregister node: {0}".format(SimpleLocatorNode.TYPE_NAME))


if __name__ == '__main__':
""" 注册后,在maya脚本编辑器中的使用方法 """
cmds.file(new=True,f=True)

plugin_name = "simple_locator_node.py" # 插件的文件名
# 如果插件加载了就先取消加载插件
cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
# 如果插件没有加载就加载插件
cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))

cmds.evalDeferred('cmds.createNode("simplelocator")')


测量两个网格体的距离的定位器

image.png
image.png

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# coding: utf-8
# 不适用于2018及以下版本
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import maya.api.OpenMayaRender as omr


import maya.cmds as cmds



def maya_useNewAPI():
    """ 告知maya,使用的是maya api 2.0 """
    pass



class DistanceBetweenLocator(omui.MPxLocatorNode):
    TYPE_NAME = "distanceBetweenLocator"
    TYPE_ID = om.MTypeId(0x0007F7FD)
    DRAW_CLASSIFICATION = "drawdb/geometry/distancebetweenlocator"
    DRAW_REGISTRANT_ID = "DistanceBetweenLocator"


    point1_obj = None
    point2_obj = None
    distance_obj = None


    def __init__(self):
        super(DistanceBetweenLocator, self).__init__()
   
    def compute(self, plug, data):
        point1 = om.MPoint(data.inputValue(self.point1_obj).asFloatVector()) # 得到MPoint对象
        point2 = om.MPoint(data.inputValue(self.point2_obj).asFloatVector())


        distance = point1.distanceTo(point2) # 使用MPoint的计算距离的方法


        data.outputValue(self.distance_obj).setDouble(distance)


        data.setClean(plug) # 使plug变成干净的状态


    @classmethod
    def creator(cls):
        return DistanceBetweenLocator()


    @classmethod
    def initialize(cls):
        numeric_attr = om.MFnNumericAttribute()


        cls.point1_obj = numeric_attr.createPoint("point1", "p1")
        numeric_attr.readable = False
        numeric_attr.keyable = True


        cls.point2_obj = numeric_attr.createPoint("point2", "p2")
        numeric_attr.readable = False
        numeric_attr.keyable = True


        cls.distance_obj = numeric_attr.create("distance", "dist", om.MFnNumericData.kDouble, 0.0)
        numeric_attr.writable = False


        cls.addAttribute(cls.point1_obj)
        cls.addAttribute(cls.point2_obj)
        cls.addAttribute(cls.distance_obj)


        cls.attributeAffects(cls.point1_obj, cls.distance_obj)
        cls.attributeAffects(cls.point2_obj, cls.distance_obj)


class DistanceBetweenUserData(om.MUserData):


    def __init(self, deleteAfterUse = False):
        super(DistanceBetweenUserData, self).__init__(deleteAfterUse)


        self.distance = 0
        self.point1 = om.MPoint()
        self.point2 = om.MPoint()


class DistanceBetweenDrawOverride(omr.MPxDrawOverride):
    """ 负责在视口中绘制几何图形 """
    NAME = "DistanceBetweenDrawOverride"


    def __init__(self, obj):
        super(DistanceBetweenDrawOverride, self).__init__(obj, None, False) # 第一个参数是maya object,第二个参数是绘制是callback函数,第三个参数是默认为True,为True时将会将此标志位dirty状态,因此可以持续更新,建议为True,除非遇到了性能问题可以为False


    def refineSelectionPath(self, select_info, hit_item, path, components, obj_mask):
        """ 重新定义选择,使绘制的几何图形不能够被选中 """
        return False


    def supportedDrawAPIs(self):
        """ 让maya知道支持哪个图形api,kAllDevices指的是使用OpenGL and Direct X 11"""
        return omr.MRenderer.kAllDevices


    def hasUIDrawables(self):
        """ 表示使用addUIDrawables方法来绘制图形 """
        return True


    def prepareForDraw(self, obj_path, camera_path, frame_context, old_data):
        """ 在使用绘制图形的方法之前,使用这个方法来将数据检索与缓存 """
        data = old_data
        if not data:
            data = DistanceBetweenUserData()


        node_fn = om.MFnDependencyNode(obj_path.node())


        data.point1 = om.MPoint(node_fn.findPlug("point1X", False).asDouble(),
                                node_fn.findPlug("point1Y", False).asDouble(),
                                node_fn.findPlug("point1Z", False).asDouble())


        data.point2 = om.MPoint(node_fn.findPlug("point2X", False).asDouble(),
                                node_fn.findPlug("point2Y", False).asDouble(),
                                node_fn.findPlug("point2Z", False).asDouble())


        data.distance = node_fn.findPlug("distance", False).asDouble()
       
        return data


    def addUIDrawables(self, obj_path, draw_manager, frame_context, data):
        """ 绘制图形的方法


        Args:
            obj_path (_type_): 指向正在绘制的对象的路径,这里是指locator节点
            draw_manager (_type_): 用于绘制简单的几何图形
            frame_context (_type_): 包含当前渲染框架的一些全局信息
            data (_type_): 用户创建的数据对象,存储缓存数据
        """
        draw_manager.beginDrawable()
       
        draw_manager.setFontSize(14)
        draw_manager.setFontWeight(100)
        draw_manager.setColor(om.MColor((1.0, 0.5, 0.5)))


        pos = om.MPoint((om.MVector(data.point1) + om.MVector(data.point2))/2.0) # 得到两点之间的重点
        text = "{0:.3f}".format(data.distance)


        draw_manager.text(pos, text, omr.MUIDrawManager.kCenter)
        draw_manager.line(data.point1,data.point2)


        draw_manager.endDrawable()


    @classmethod
    def creator(cls, obj):
        return DistanceBetweenDrawOverride(obj)



def initializePlugin(plugin):
    """ 插件加载时执行这个函数"""
    vendor = "RuiChen"  # 插件制作人的名字
    version = "1.0.0"  # 插件的版本


    plugin_fn = om.MFnPlugin(plugin, vendor, version)  # 定义插件
    try:
        plugin_fn.registerNode(DistanceBetweenLocator.TYPE_NAME,
                               DistanceBetweenLocator.TYPE_ID,
                               DistanceBetweenLocator.creator,
                               DistanceBetweenLocator.initialize,
                               om.MPxNode.kLocatorNode,
                               DistanceBetweenLocator.DRAW_CLASSIFICATION)
    except:
        om.MGlobal.displayError("Failed to register node: {0}".format(DistanceBetweenLocator.TYPE_NAME))


    try:
        omr.MDrawRegistry.registerDrawOverrideCreator(DistanceBetweenLocator.DRAW_CLASSIFICATION,
                                                      DistanceBetweenLocator.DRAW_REGISTRANT_ID,
                                                      DistanceBetweenDrawOverride.creator)
    except:
        om.MGlobal.displayError("Failed to register draw override: {0}".format(DistanceBetweenDrawOverride.NAME))



def uninitializePlugin(plugin):
    """ 插件取消加载时执行这个函数"""
    plugin_fn = om.MFnPlugin(plugin)
    try:
        omr.MDrawRegistry.deregisterDrawOverrideCreator(DistanceBetweenLocator.DRAW_CLASSIFICATION,
                                                        DistanceBetweenLocator.DRAW_REGISTRANT_ID)
    except:
        om.MGlobal.displayError("Failed to deregister draw override: {0}".format(DistanceBetweenDrawOverride.NAME))


    try:
        plugin_fn.deregisterNode(DistanceBetweenLocator.TYPE_ID)
    except:
        om.MGlobal.displayError("Failed to deregister node: {0}".format(DistanceBetweenLocator.TYPE_NAME))


def rc_distance_between_test():
    cmds.setAttr("persp.translate", 3.5, 5.5, 10.0)
    cmds.setAttr("persp.rotate", -27.0, 19.0, 0.0)


    cube1 = cmds.polyCube()[0]
    cube2 = cmds.polyCube()[0]


    cmds.setAttr("{0}.translateX".format(cube1), -2.5)
    cmds.setAttr("{0}.translateX".format(cube2), 2.5)


    distance_locator = cmds.createNode("{0}".format(DistanceBetweenLocator.TYPE_NAME))


    cmds.connectAttr("{0}.translate".format(cube1),"{0}.point1".format(distance_locator))
    cmds.connectAttr("{0}.translate".format(cube2),"{0}.point2".format(distance_locator))


    cmds.select(distance_locator)





if __name__ == '__main__':


    cmds.file(new=True, force=True)


    """ 注册后,在maya脚本编辑器中的使用方法 """
    plugin_name = "distance_between_locator.py"  # 插件的文件名
    # 如果插件加载了就先取消加载插件
    cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name))
    # 如果插件没有加载就加载插件
    cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name))


    cmds.evalDeferred('rc_distance_between_test()')

设置自定义变形器节点在大纲处的图标以及节点编辑器中的图标

默认图标:image.png
设置自定义图标:

image.png

image.png

自定义节点属性编辑器的排列模板

名字格式: AE + 节点名字 + Template
image.png
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// distanceBetweenLocator节点在属性编辑器中的模板
// 在mel中不需要在意缩进,这里的缩进只是为了排版更好看而已
global proc AEdistanceBetweenLocatorTemplate(string $nodeName)
{
editorTemplate -beginScrollLayout;

editorTemplate -beginLayout "Distance Between Attributes" -collapse 0; //collapse 意思是展开,不折叠

editorTemplate -addControl "point1";
editorTemplate -addControl "point2";
editorTemplate -addControl "minDistance";
editorTemplate -addControl "maxDistance";

AEaddRampControl($nodeName + ".colorRamp");
AEaddRampControl($nodeName + ".curveRamp");

editorTemplate -endLayout;

editorTemplate -addExtraControls;
editorTemplate -endScrollLayout;
}
// 刷新属性编辑器,便于快速看到代码改变带来的影响
refreshEditorTemplates;

如何将单一功能的插件组合起来

之前的教程写插件都是写一个节点,命令,工具,但是一个插件功能往往包含这几个而不是单一的。因此这节课讲如何将它们组合起来。
image.png
image.png
image.png
image.png

如何将文件功能分成多个文件使其更方便管理

21_multifile_plugins_part1_哔哩哔哩_bilibili

maya_modules

maya的module是一种共享多文件插件的方法

简单的插件管理方式

image.png
image.png

maya moudules的介绍

image.png
image.png

image.png
image.png

image.png
image.png
image.png
image.png

使用创建moudules的步骤举例

一个工具的所需内容放到一个文件夹里,在这个文件夹下创建一个mod文件

image.png

mod文件夹下添加这个工具

最后的./是相对路径
image.png

在maya.env文件下添加mod:

image.png

image.png

检查工作

image.png
image.png