前言

  本来我是想先看Udemy的Python For Maya - Artist Friendly Programming,不过看到那套教程使用pycharm 之后,我就迫不及待地想了解一下pluralsight最新的Python教程使用哪一个IDE
  没想到pluralsight这套居然使用了VScode,果断转来看这套了。
  不过看了一下才发现,原来两套教程的作者是同一个人(:з」∠)
  后面还是觉得这套新教程讲得比较浅,先看完在说吧。

教程分析

  如果将这套教程和Delano出的Python三教程比较的话,这套教程更加偏向编程本身。

  delano的教程更多的是针对于Maya自身提供的Python API进行教学,不过年代受限,使用的方法也是比较老。
  这套教程不仅仅分析了Maya自身的代码编辑器,也从Python的多个角度进行拓展展望。

相关概念讲解

  • 开头讲解如何使用Maya脚本编辑器
  • 分析了Maya中可以使用的三种编程语言 MEL Python C++ 比较了三门语言在 Maya 的优劣
    • MEL上手容易,但是执行效率低,只能开发脚本,无法开发插件。
    • Python覆盖面很广,上手容易,能够胜任各种情况。
    • C++比较复杂,上手困难,执行效率高。
      教程截图
  • 演示如何讲MEL转成Python代码
  • Maya编辑器可以快速通过网络查询相关函数的文档
  • 下面就是Python的基础概念
    • 变量(variable)的概念
    • 节点(node)的概念
    • Python2 VS Python3
      教程截图

Python基础教学

  • Maya Python语言基础教学
    • for 循环
    • while 循环
    • if/else
    • getAttr & setAttr
    • function
    • selection
    • string
    • Scope 域
      教程截图
    • reload函数 方便外部修改

外部编辑器使用

  • 使用外部编辑器 - 指出为什么使用VScode
    教程截图
    教程截图
    教程截图
  • 搭建VScode autocomplete 链接
  • 如何让VScode和Maya联动使用

UI搭建

  • Maya新的界面组成 - Qt
    教程截图
  • 在Maya中创建UI 可以选择的方案
    教程截图

编程实战前 - 讲解分析

  搭建 Randomizer 和 Alinger 两个插件,演示Python插件的制作流程。
  本教程只使用了Maya的cmd创建窗口,非常适合新手入门。
  Maya cmd 提供的窗口布局
教程截图
  中间也穿插了Python CLass的概念(学过面向对象的都知道,过于复杂的部分也很少会用到)

  • class的概念
    class的概念
  • 实例的概念
    实例的概念
  • self的作用
    self的作用
  • 继承
    继承
  • 内置Magic方法
    内置Magic方法
  • 超类继承
    超类继承

编程实战

文件组成架构
文件组成

  classdemo.py 的内容只是代码演示 不再赘述
  我将英文注释去掉,加入自己的理解 (面向对象相关的知识建议看其他教程,这个教程的篇幅受限,讲得很浅)

randomizer Python 代码分析

baseWindow.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from maya import cmds

class Window(object):

# 构造函数 实例化的时候调用
def __init__(self, name):

# 如果窗口已经存在 则删除存在的窗口
if cmds.window(name, query=True, exists=True):
cmds.deleteUI(name)

# 创建窗口 并且窗口名为name变量
cmds.window(name)

# 执行BuildUI 构建窗口相关内容
self.buildUI()

# 显示当前窗口(如果buildUI没有执行构建,就是一个空的窗口)
cmds.showWindow()

def buildUI(self):
print "No UI is defined"
randomizer.py
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
from maya import cmds
# 导入random库 从而可以调用random函数
import random
# 导用上面写的 baseWindow(务必放在 文档->maya版本->script 文件夹)
import baseWindow

# RandomizerUI 继承 baseWindow的Window类
class RandomizerUI(baseWindow.Window):

# 创建构造函数
def __init__(self, name='Randomizer'):

# 执行Window的构造函数 父类有执行BuildUI函数 因此执行此函数可以执行buildUI函数
super(RandomizerUI, self).__init__(name)

# 重写buildUI函数
def buildUI(self):
# 标题分区
column = cmds.columnLayout()
cmds.frameLayout(label="Choose an object type")

# 复选框
cmds.columnLayout()
self.objType = cmds.radioCollection("objectCreationType")
cmds.radioButton(label="Sphere")
cmds.radioButton(label="Cube", select=True)
cmds.radioButton(label="Cone")

# 个数输入框
self.intField = cmds.intField("numObjects", value=3)

# 标题分区
cmds.setParent(column)
frame = cmds.frameLayout("Choose your max ranges")

# 网格布局
cmds.gridLayout(numberOfColumns=2, cellWidth=100)

# for循环生成 XYZ 输入
for axis in 'xyz':
cmds.text(label='%s axis' % axis)
cmds.floatField('%sAxisField' % axis, value=random.uniform(0, 10))

# 设置
cmds.setParent(frame)

# 横向布局 柱子分段为2 用于容纳两个复选框
cmds.rowLayout(numberOfColumns=2)

cmds.radioCollection("randomMode")
cmds.radioButton(label='Absolute', select=True)
cmds.radioButton(label='Relative')

cmds.setParent(column)

# 和上面一样 容纳两个按钮
cmds.rowLayout(numberOfColumns=2)
# 指定按钮的名称和调用的函数
cmds.button(label="Create", command=self.onCreateClick)
cmds.button(label="Randomize", command=self.onRandomClick)

# 点击 Create 调用的函数
# *args 是因为Maya cmd 无法让函数的参数为零
def onCreateClick(self, *args):
# 获取插件窗口的选项
radio = cmds.radioCollection(self.objType, query=True, select=True)
mode = cmds.radioButton(radio, query=True, label=True)

# 获取生成的个数
numObjects = cmds.intField(self.intField, query=True, value=True)

# 传入变量 执行生成物体函数
createObjects(mode, numObjects)
# 顺带执行随机位置函数
onRandomClick()

# 随机位置函数
def onRandomClick(self, *args):
# 获取插件窗口的选项
radio = cmds.radioCollection("randomMode", query=True, select=True)
mode = cmds.radioButton(radio, query=True, label=True)

# for循环处理 xyz 三个轴向
for axis in 'xyz':
# 获取插件窗口的数值
val = cmds.floatField("%sAxisField" % axis, query=True, value=True)
# 传入变量 执行随机化的函数
randomize(minValue=val*-1, maxValue=val, mode=mode, axes=axis)

def createObjects(mode, numObjects=5):
# 数组
objList = []

# 根据传入的参数 设置生成的物体
for n in range(numObjects):
if mode == 'Cube':
obj = cmds.polyCube()
elif mode == 'Sphere':
obj = cmds.polySphere()
elif mode == "Cylinder":
obj = cmds.polyCylinder()
elif mode == 'Cone':
obj = cmds.polyCone()
else: cmds.error("I don't know what to create")

# 将生成的物体赋予到数组中
objList.append(obj[0])

# 选择这个数组中的对象
cmds.select(objList)
return objList

def randomize(objList=None, minValue=0, maxValue=10, axes='xyz', mode='Absolute'):
# objList如果是空 也就是直接按Randomize按钮
if objList is None:
# 获取选中的物体
objList = cmds.ls(selection=True)

# 循环选中的物体
for obj in objList:
# 循环三个轴向(参数'xyz')
for axis in axes:

# current归零
current = 0

# 如果是相对的情况就获取当前物体的位置
if mode == 'Relative':
current = cmds.getAttr(obj+'.t%s' % axis)

# uniform可以生成相应区间的随机值
val = current + random.uniform(minValue, maxValue)
# 将随机的变量设置到相应的轴向中 .t 为.translate 简写
cmds.setAttr(obj+'.t%s' % axis, val)

插件窗口

Aligner 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
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
# 导入cmd库
from maya import cmds
# 导入partial库,可以预先加载函数参数
from functools import partial

def align(nodes=None, axis='x', mode='mid'):
# 如果没有传入nodes参数 获取当前选中的物体
if not nodes:
nodes = cmds.ls(sl=True)

# 如果当前没有任何选择 弹出错误
if not nodes:
cmds.error('Nothing selected or provided')

# 这里需要将选择的部分转换为点,并且需要将范围flatten
# 没有flatten的话 范围取值 polyCube1.vtx[1:5]
# 这种形式不利于编程调用,需要转换为 polyCube1.vtx[1], polyCube1.vtx[2], polyCube1.vtx[3]

# 创建一个临时的数组
_nodes = []
for node in nodes:
# 面的选择形式: polyCube1.f[2]
# 可以寻找'.f[' 来确定是否选中了面
if '.f[' in node:
# 如果有面选择起来就转换为点
node = cmds.polyListComponentConversion(node, fromFace=True, toVertex=True)
elif '.e[' in node:
# 同理 边的处理也是一样的
node = cmds.polyListComponentConversion(node, fromEdge=True, toVertex=True)

# flatten点范围 需要先将点选择起来
cmds.select(node)
# 使用fl命令进行 flatten
node = cmds.ls(sl=True, fl=True)

# 然后将当前的数组添加到_nodes中
_nodes.extend(node)

# 重新选择我们选中的物体
cmds.select(nodes)
# 将临时数组赋值给nodes
nodes = _nodes

# 这里检测当前选中的模式
# 三个变量将会获取到布尔值
# 布尔值为True的就是当前选择的模式
minMode = mode == 'min'
maxMode = mode == 'max'
midMode = mode == 'mid'

# 数组是从零开始的
# 后面轴向是数组延续的
# start变量就是xyz转成数组的形式
if axis == 'x':
start = 0
elif axis == 'y':
start = 1
elif axis == 'z':
start = 2
else:
# 如果全都不符合 就报错
cmds.error('Unknown Axis')

# 用来保存 碰撞盒 和 数值 的变量
bboxes = {}
values = []

# 获取数组中的元素
for node in nodes:
# 如果对象是点的话
if '.vtx[' in node:
# 点是没有碰撞盒的 所以直接获取点的世界坐标
ws = cmds.xform(node, q=True, t=True, ws=True)
# 点是没有体积的 所以三个值都是相等的
minValue = midValue = maxValue = ws[start]
else:
# 如果是个完整的物体 就获取它的碰撞盒
# 碰撞盒会返回一下的数组
# [x-min, y-min, z-min, x-max, y-max, z-max]
bbox = cmds.exactWorldBoundingBox(node)

# 通过start获取相应轴向的 最大最小值 及 中间值
minValue = bbox[start]
maxValue = bbox[start+3]
midValue = (maxValue+minValue)/2

# 将这些值存进上面声明的 bboxes 字典变量中
bboxes[node] = (minValue, midValue, maxValue)

# 根据选择的模式将相应的数值存入变量数组中
if minMode:
values.append(minValue)
elif maxMode:
values.append(maxValue)
else:
values.append(midValue)

# 更具选中的模式进行不同的计算
if minMode:
# 返回数组最小的值
target=min(values)
elif maxMode:
# 返回数组最大的值
target = max(values)
else:
# 获取中间的平均值
target = sum(values)/len(values)

# for循环计算每一个物体需要移动的距离
for node in nodes:
# 获取相应的bboxes字典的数据
bbox = bboxes[node]
# 分离出相应的变量
minValue, midValue, maxValue = bbox

# 获取选中物体的世界坐标
ws = cmds.xform(node, query=True,
translation=True,
ws=True)

# 计算出移动的距离
width = maxValue - minValue
if minMode:
distance = minValue - target
ws[start] = (minValue-distance) + width/2
elif maxMode:
distance = target-maxValue
ws[start] = (maxValue + distance) - width/2
else:
distance = target - midValue
ws[start] = midValue + distance

# 根据计算的值移动物体
cmds.xform(node, translation=ws, ws=True)

class Aligner(object):

def __init__(self):
# 创建窗口 并且让它只有一个存在。
name = "Aligner"
if cmds.window(name, query=True, exists=True):
cmds.deleteUI(name)

window = cmds.window(name)
self.buildUI()
cmds.showWindow()
cmds.window(window, e=True, resizeToFitChildren=True)

def buildUI(self):
column = cmds.columnLayout()
# 添加 radioButton 进行选择
cmds.frameLayout(label="Choose an axis")

cmds.gridLayout(numberOfColumns=3, cellWidth=50)

cmds.radioCollection()
self.xAxis = cmds.radioButton(label='x', select=True)
self.yAxis = cmds.radioButton(label='y')
self.zAxis = cmds.radioButton(label='z')

# 创建图片按钮
# partial实现点击图片也可以改变当前选项
createIconButton('XAxis.png', command=partial(self.onOptionClick, self.xAxis))
createIconButton('YAxis.png', command=partial(self.onOptionClick, self.yAxis))
createIconButton('ZAxis.png', command=partial(self.onOptionClick, self.zAxis))

# 给模式选择添加按钮
cmds.setParent(column)

cmds.frameLayout(label="Choose where to align")

cmds.gridLayout(numberOfColumns=3, cellWidth=50)

cmds.radioCollection()
self.minMode = cmds.radioButton(label='min')
self.midMode = cmds.radioButton(label='mid', select=True)
self.maxMode = cmds.radioButton(label='max')

createIconButton('MinAxis.png', command=partial(self.onOptionClick, self.minMode))
createIconButton('MidAxis.png', command=partial(self.onOptionClick, self.midMode))
createIconButton('MaxAxis.png', command=partial(self.onOptionClick, self.maxMode))


# 添加执行按钮
cmds.setParent(column)
# bgc是backgroundcolor的缩写
# 它对应的值是 rgb
cmds.button(label='Align', command=self.onApplyClick, bgc=(0.2, 0.5, 0.9))

def onOptionClick(self, opt):
# 获取传入的参数
# 改变当前按钮的选择
cmds.radioButton(opt, edit=True, select=True)

def onApplyClick(self, *args):
# 获取当前轴向
if cmds.radioButton(self.xAxis, q=True, select=True):
axis = 'x'
elif cmds.radioButton(self.yAxis, q=True, select=True):
axis = 'y'
else:
axis = 'z'

# 获取当前模式
if cmds.radioButton(self.minMode, q=True, select=True):
mode = 'min'
elif cmds.radioButton(self.midMode, q=True, select=True):
mode = 'mid'
else:
mode = 'max'

# 执行对齐功能函数
align(axis=axis, mode=mode)

def getIcon(icon):
import os
# 当前脚本路径应该在 script 文件夹中
# __file__ 是当前脚本的路径
# os.path.dirname 返回完整的路径名
scripts = os.path.dirname(__file__)

# 获取 icons 文件夹路径
icons = os.path.join(scripts, 'icons')

# 最后找到相应的 icon 并返回相应的路径
icon = os.path.join(icons, icon)
return icon

# 创建图标函数
def createIconButton(icon, command=None):

if command:
cmds.iconTextButton(image1=getIcon(icon), width=50, height=50, command=command)
else:
cmds.iconTextButton(image1=getIcon(icon), width=50, height=50)


插件窗口

总结

  Python For Maya - Artist Friendly Programming这套教程我也差不多看完了,这一部讲得比较浅显,非常适合入门,而最近看的这一部涉及的内容更多,还包含了pyqt和pymel,进阶的话可以看我最近看的这部教程(近期内会在B站更新)。
  掌握了JavaScript之后,感觉Python其实也没什么难度(可能只是我还没有接触到深奥的地方),编程很多地方都是相通的。
  如今要加快对 Maya 库的学习,除此之外,插件开发很重要的一点是要对Maya足够熟悉。