前言

  这篇教程也算是千呼万唤始出来,之前的观看进度挺快的,没想到最后两个案例如此晦涩难懂,以至于我花了很多的时间。
  最可怕的是听不懂,所以就很困,进入不了状态。
  有时候真的觉得,编程教程好像没有什么意义,看源代码的逻辑就可以学到不少东西了,毕竟注释也很充足。(感觉像Three.js的学习历程)
  无论如何,现在是我学习 Python 最难受的时候了,很多东西都不懂,很多东西都需要查资料,心很累。
  国庆这几天的状态也不是很好,特别是回家干活了之后就有点感冒,难受。

  这部教程没有太多的Python基础教学,因此先看Pluralsight那套入门教程搭建好基础,再看这一套能构建一个比较完整的学习框架。
  不过这套教程自身也是由浅入深的过程,我觉得零基础也完全可以学,就是学习到后面会比较痛苦。

01 Introduction 解析

前期说明

  教程开始最重要的事情是交代相关代码的链接。
  作者已经将所有的代码以及注释全部上传在Github上。 链接
  学习编程更重要的是看代码,基本上依靠上面的代码注释,其实是不需要看这个教程的大部分案例视频的。
  视频案例的好处在于让你感受从零开始编程的流程和思路,比起一个完整结构的代码,逻辑性会更加清晰。
  需要注意的是作者使用的是Maya2017 IDE使用Pycharm

Maya 脚本编辑器 & 简单的Python代码

  交代 Maya 脚本编辑器的使用方法,并且讲解最简单的 Python hello World 命令。
  后面是交代如何执行Maya的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 导入 Maya python 库 
from maya import cmds

# 创建方块
cube = cmds.polyCube()
cubeShape = cube[0]

# 创建圆圈曲线
circle = cmds.circle()
circleShape = circle[0]

# 添加父子关系
cmds.parent(cubeShape,circleShape)

# 冻结对象
cmds.setAttr(cubeShape + ".translate" , lock = True)
cmds.setAttr(cubeShape + ".rotate" , lock = True)
cmds.setAttr(cubeShape + ".scale" , lock = True)

# 选择对象
cmds.select(circleShape)

  另外这里讲解了 Maya 中的一个很重要的概念。
  Maya中最简单的模型也是由 shape 节点 和 Transform 节点 组合的。
   shape 节点记录物体的形状布线
   Transform 节点记录物体的位置
  上面的代码可以创建一个方块和环形曲线,并且将方块冻结 并作为子对象添加到曲线上。

Maya开发可以使用的库

  下面这种图分析了Maya可以用来开发的库,以及它们之间的优劣。
Maya可以使用的库
优劣对比
  这个分析在上一个Pluralsight入门教程中也有,其实讲得差不多。

Maya 节点的概念

  讲解Maya的节点概念 可以实现可视化编程(Houdini的节点式会更加好用)
Maya的节点
节点操作
  Maya 的内核 和 Houdin 一样都是使用节点式追踪历史实现所有的模型效果。
  只是 Maya 的节点比 Houdin 的难用很多。

Python2 VS Python3

  Python3 的情况 (不能向前支持 Python2 的代码)
Python3 的情况
  Maya 使用 Python 的历史
Maya 使用 Python 的历史
  目前CG行业都是用 Python 2.7 版本
Python 2.7

保存脚本

  Load Script 将脚本加载到当前 脚本编辑器 当中
  Source Script 将立即执行脚本

02 Object Renamer

  第二章的内容基本和 Pluralsight 的那套入门教程相同,用一个相对简单的案例涵盖了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
#coding:utf-8
from maya import cmds

# 设置后缀的字典
SUFFIXES = {
"mesh": "geo",
"joint": "jnt",
"camera": None,
}

# 默认添加字符串
DEFAULT = "grp"

def rename(selection=False):
"""
Renames objects by adding suffixes based on the object type
Args:
selection (bool): Whether we should use the selection or not. Defaults to False

Raises:
RuntimeError: If nothing is selected

Returns:
list: A list of all the objects renamed
"""

# Our function has an input called selection.
# This is used to let it know if we should use the selection or not

# 获取当前选择
objects = cmds.ls(selection=selection, dag=True)

# 如果没有选择任何东西 就报错并停止代码
if selection and not objects:
raise RuntimeError("You don't have anything selected")

# 根据长度对选中的物体由长到短进行排序
objects.sort(key=len, reverse=True)

# 遍历所有的物体
for obj in objects:
# 根据 '|' 分割字符串 并且获取最后一个字符串(物体名称)
shortName = obj.split('|')[-1]

# 检查是否还有子对象
# 如果有的话获取 当前对象类型
children = cmds.listRelatives(obj, children=True) or []
if len(children) == 1:
child = children[0]
objType = cmds.objectType(child)
else:
objType = cmds.objectType(obj)

# 根据对象类型获取后缀名称 如果没有则获取默认名称
suffix = SUFFIXES.get(objType, DEFAULT)

# 如果 suffix 为空 跳过当前循环对象
if not suffix:
continue

# 如果当前对象已经有相同的后缀 跳过当前循环对象
if shortName.endswith('_'+suffix):
continue

# 重新命名对象
newName = '%s_%s' % (shortName, suffix)
cmds.rename(shortName, newName)

# 获取当前对象循环的序号
index = objects.index(obj)

# 将当前循环的对象的数组 替换为 新命名的名称
objects[index] = obj.replace(shortName, newName)

# 返回数组 从而可以从外部获取到重命名的对象
return objects

03 The Gear Creator

  这个脚本的原理并不复杂

  • 生成一个圆环
  • 获取圆环外侧的面 (Python 通过 Range 可以获取间隔的数列)
  • 对选择的面进行挤出
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
#coding:utf-8
import maya.cmds as cmds

class Gear(object):
def __init__(self):
# 构造函数
self.shape = None
self.transform = None
self.constructor = None
self.extrude = None

def create(self, teeth=10, length=0.3):
# 根据生成的齿数计算分段数
spans = teeth * 2

# 执行创建圆环命令
self.createPipe(spans)

# 执行挤压函数 创建齿轮
self.makeTeeth(teeth=teeth, length=length)

def createPipe(self, spans):
# 创建圆环 并且获取它的 Transform 节点和 shape 节点
self.transform, self.shape = cmds.polyPipe(subdivisionsAxis=spans)

# 找到生成 圆环的历史节点 (后面调整分段需要用到)
for node in cmds.listConnections('%s.inMesh' % self.transform):
if cmds.objectType(node) == 'polyPipe':
self.constructor = node
break

def makeTeeth(self, teeth=10, length=0.3):
# 清空选择
cmds.select(clear=True)
# 获取需要选择的面
faces = self.getTeethFaces(teeth)
# 选择这部分的面
for face in faces:
cmds.select('%s.%s' % (self.transform, face), add=True)

# 接入挤出节点
self.extrude = cmds.polyExtrudeFacet(localTranslateZ=length)[0]
cmds.select(clear=True)

def changeLength(self, length=0.3):
# 改变挤出节点的深度
cmds.polyExtrudeFacet(self.extrude, edit=True, ltz=length)

def changeTeeth(self, teeth=10, length=0.3):
# 改变圆环的分段数
cmds.polyPipe(self.constructor, edit=True, sa=teeth * 2)
# 重新调成挤出的序号
self.modifyExtrude(teeth=teeth, length=length)

def getTeethFaces(self, teeth):
# 获取需要生成的面的序号
spans = teeth * 2
sideFaces = range(spans * 2, spans * 3, 2)

# 将相关面的信息放到数组中
faces = []
for face in sideFaces:
faces.append('f[%d]' % face)
return faces

def modifyExtrude(self, teeth=10, length=0.3):
# 获取相关的面
faces = self.getTeethFaces(teeth)

# 修改挤出的面序号
cmds.setAttr('%s.inputComponents' % self.extrude, len(faces), *faces, type='componentList')

# 修改挤出的深度
self.changeLength(length)

04 The Animation Tweener

  在这个章节中介绍Qt(并没有使用) 并且提出了 UI 和功能分离的概念。
  主要还是介绍使用原生的 cmds 创建插件的过程。

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
#coding:utf-8
from maya import cmds

def tween(percentage, obj=None, attrs=None, selection=True):

# 如果没有参数 同时 没有选择对象的话 报错
if not obj and not selection:
raise ValueError("No object given to tween")

# 如果没有参数传入 获取当前选择对象
if not obj:
obj = cmds.ls(sl=1)[0]

# 如果没有属性列表 获取可以设置关键帧的属性
if not attrs:
attrs = cmds.listAttr(obj, keyable=True)

# 获取当前时间
currentTime = cmds.currentTime(query=True)

# 循环遍历参数列表
for attr in attrs:
# 获取参数的全名
attrFull = '%s.%s' % (obj, attr)

# 查询是否有关键帧
keyframes = cmds.keyframe(attrFull, query=True)

# 如果没有关键帧就忽略当前对象
if not keyframes:
continue

# 创建变量存储 当前时间以前的关键帧
previousKeyframes = []
# 循环所有的关键帧 获取当前时间以前所有的关键帧
for k in keyframes:
if k < currentTime:
previousKeyframes.append(k)

# 这是Python的简化写法 实现和上面一样的效果 获取当前时间后面的关键帧
laterKeyframes = [frame for frame in keyframes if frame currentTime]

# 如果前面 或者 后面没有关键帧 则跳过
if not previousKeyframes and not laterKeyframes:
continue

# 如果有前面的关键帧序列 寻找帧数最大的那个(最靠近当前时间的)
if previousKeyframes:
previousFrame = max(previousKeyframes)
else:
previousFrame = None

# 这个是上面的的简化版
nextFrame = min(laterKeyframes) if laterKeyframes else None

# 如果没有 前一帧就是下一帧
if previousFrame is None:
previousFrame = nextFrame

nextFrame = previousFrame if nextFrame is None else nextFrame

# 获取前后帧的关键帧信息
previousValue = cmds.getAttr(attrFull, time=previousFrame)
nextValue = cmds.getAttr(attrFull, time=nextFrame)

# 分析特殊情况 如果不是则获取两者之间的值进行过渡
if nextFrame is None:
currentValue = previousValue
elif previousFrame is None:
currentValue = nextValue
elif previousValue == nextValue:
currentValue = previousValue
else:
difference = nextValue - previousValue
biasedDifference = (difference * percentage) / 100.0
currentValue = previousValue + biasedDifference

# 将识别的值 设置到属性上
cmds.setAttr(attrFull, currentValue)
# 给属性设置关键帧
cmds.setKeyframe(attrFull, time=currentTime, value=currentValue)

class TweenerWindow(object):
# 窗口名字
windowName = "TweenerWindow"

def show(self):
# 检查窗口是否存在 如果存在先删除
if cmds.window(self.windowName, query=True, exists=True):
cmds.deleteUI(self.windowName)

# 创建窗口 命名为 TweenerWindow
cmds.window(self.windowName)

# 创建UI
self.buildUI()

# 显示窗口
cmds.showWindow()

def buildUI(self):
# 创建柱状布局
column = cmds.columnLayout()

# text注释说明
cmds.text(label="Use this slider to set the tween amount")

# 一行两个柱子 分别给滑竿和按钮
row = cmds.rowLayout(numberOfColumns=2)

# 创建滑竿 在变化时执行tween函数
self.slider = cmds.floatSlider(min=0, max=100, value=50, step=1, changeCommand=tween)

# 重置按钮
cmds.button(label="Reset", command=self.reset)

# 给 layout 设置父对象
cmds.setParent(column)

# 添加关闭按钮
cmds.button(label="Close", command=self.close)

# *args 允许传入任意参数 全部保存在args变量中
def reset(self, *args):
# 获取滑竿 设置为50%
cmds.floatSlider(self.slider, edit=True, value=50)

def close(self, *args):
# 点击关闭按钮 删除窗口
cmds.deleteUI(self.windowName)

05 The Controller Library

  这一个章节重点介绍了利用Qt实现的文件加载器

介绍Qt

  Qt英文读作cute
Qt发音
  PyQt 和 PySide 是Qt语言在Python中实现的库,正如QtQucik是用JavaScript实现的一样。
  要注意的是PyQt的license是禁止商用且必须开源的,PySide才是Qt的亲儿子。
  Maya不同版本使用的Qt不同
Qt版本

Qt VS cmds

   教程推荐尽可能使用Qt书写应用界面,以下是优缺点分析
Qt VS cmds

Duck Typing

  Duck Typing 不要求给函数的参数输入类型。
  只要传入的参数可以进行操作,那么就不会报错。
  如此一来就不用考虑传参的类型。

Qt.py

  可以到https://github.com/mottosso/Qt.py网页去下载Qt.py脚本
  这个脚本可以自动导入适合运行的Qt库。

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
from maya import cmds
import os
import json
import pprint

# 获取我的文档的Maya路径
USERAPPDIR = cmds.internalVar(userAppDir = True)
# 在该路径下添加controllerLibrary路径(python使用这种方法可以自动适配不同系统的路径斜杠)
DIRECTORY = os.path.join(USERAPPDIR,'controllerLibrary') # 默认路径

# 创建路径
def creatDirectory(directory=DIRECTORY):

# 如果路径中的文件夹不存在就创建一个新的文件夹
if not os.path.exists(directory):
os.mkdir(directory)



class ControllerLibrary(dict):
"""
# ControllerLibrary 字典保存格式

[
(u'test',
{
'name': u'test',
'path': u'C:/Users/Administrator/Documents/maya/controllerLibrary\\test.ma',
u'screenshot': u'C:/Users/Administrator/Documents/maya/controllerLibrary\\test'
}),
(u'sphere',
{
'name': u'sphere',
'path': u'C:/Users/Administrator/Documents/maya/controllerLibrary\\sphere.ma',
u'screenshot': u'C:/Users/Administrator/Documents/maya/controllerLibrary\\sphere'
})
]

"""

def save(self,name,directory=DIRECTORY,screenshot=True,**info):

# 调用路径创建函数
creatDirectory(directory)

# 默认路径下添加ma文件路径
path = os.path.join(directory,'%s.ma' % name)

# 默认路径下添加json文件路径
infoFile = os.path.join(directory,'%s.json' % name)

# 官方用法 重命名文件
cmds.file(rename = path)

# 判断如果当前场景有选中 就保存选中 无选中就保存整个场景
if cmds.ls(selection=True):
cmds.file( save=True , type='mayaAscii',exportSelection=True)
else:
cmds.file(save=True,type='mayaAscii',force=True)

# 如果开启截图 就执行截图函数
if screenshot:
info['screenshot'] = self.saveScreenshot(name,directory=directory)

# 将相关的信息存入json文件中,用于后面读取
# with as语法通常用于打开文件 它可以在执行scope代码前打开相关的文件 并在执行后关闭文件
# info存储相关的dictionary f是filestream indent是缩进字符
with open(infoFile,'w') as f:
json.dump(info,f,indent=4)

# 保存路径
self[name] = path
return path

def find(self,directory=DIRECTORY):

# 清空自己(字典)
self.clear()

# 如果路径不存在 退出函数
if not os.path.exists(directory):
return

# 获取路径中的所有文件名称 (不包含路径)
files = os.listdir(directory)
# 找到.ma结尾的文件 以f变量返回
mayaFiles = [f for f in files if f.endswith('.ma')]

# 在.ma的基础上找到json文件
for ma in mayaFiles:
# 分离文件后缀和文件名
name , ext = os.path.splitext(ma)
path = os.path.join(directory,ma)

# 找到相关的json文件
infoFile = '%s.json' % name

# 读取json文件
if infoFile in files:
# 获取json文件相应的路径
infoFile = os.path.join(directory,infoFile)

# 打开json文件进行读取
with open(infoFile,'r') as f:
info = json.load(f)

else:
# 如果不存在json文件则变量为空
info = {}

# 截图命名
screenshot = '%s.jpg' % name

# 保存截图路径
if screenshot in files:
info['screenshot'] = os.path.join(directory,name)

# 保存相关信息到info变量中
info['name'] = name
info['path'] = path

self[name] = info

# pprint.pprint(self)

# 加载文件
def load(self,name):

# self[name]等于对应info,在调用path获取文件加载路径
path = self[name]['path']

# i为import usingNamespaces是让导入的文件没有前缀
cmds.file(path,i=True,usingNamespaces=False)

# 保存截图
def saveScreenshot(self,name,directory=DIRECTORY):

# 图片保存路径
path = os.path.join(directory,'%s.jpg' % name)

# 聚焦到所有物体
cmds.viewFit()
# 设置图片保存的格式 8为jpg格式
cmds.setAttr('defaultRenderGlobals.imageFormat',8)

# 使用playblast的方式保存截图
cmds.playblast(completeFilename=path,forceOverwrite=True,format='image',width=200,height=200,showOrnaments=False,startTime=1,endTime=1,viewer=False)

return path
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
#coding:utf-8
import maya.cmds as cmds
import pprint
import controllerLibrary
reload(controllerLibrary)
from PySide2 import QtWidgets, QtCore, QtGui

class ControllerLibraryUI(QtWidgets.QDialog):

# 构建函数
def __init__(self):

# 调用QtWidgets.QDialog的init方法
super(ControllerLibraryUI, self).__init__()

# 设置Qt窗口名称
self.setWindowTitle('Controller Library UI')

# 调用功能函数
self.library = controllerLibrary.ControllerLibrary()
# 创建窗口UI
self.buildUI()
# 刷新调用
self.populate()

def buildUI(self):

# 创建master垂直的布局容器
layout = QtWidgets.QVBoxLayout(self)

"""
保存相关的容器
"""
# 保存相关的widget容器
saveWidget = QtWidgets.QWidget()
# 保存相关的水平布局容器 (add到saveWidget中)
saveLayout = QtWidgets.QHBoxLayout(saveWidget)
# 将相关的容器添加到主布局中
layout.addWidget(saveWidget)

# 输入框
self.saveNameField = QtWidgets.QLineEdit()
# 将输入框添加到saveLayout中
saveLayout.addWidget(self.saveNameField)

# save按钮
saveBtn = QtWidgets.QPushButton('save')
# 触发save按钮功能
saveBtn.clicked.connect(self.save)
saveLayout.addWidget(saveBtn)

# 列表控件
size = 64
buffer = 12
self.listWidget = QtWidgets.QListWidget()
self.listWidget.setViewMode(QtWidgets.QListWidget.IconMode) # 开启图标模式
self.listWidget.setIconSize(QtCore.QSize(size, size)) # 设置图标大小
self.listWidget.setResizeMode(
QtWidgets.QListWidget.Adjust) # 设置调整窗口的时候自动换行
self.listWidget.setGridSize(QtCore.QSize(
size+buffer, size+buffer)) # 设置图标之间的间距
layout.addWidget(self.listWidget)

# 横向按钮容器
btnWidget = QtWidgets.QWidget()
btnLayout = QtWidgets.QHBoxLayout(btnWidget)
layout.addWidget(btnWidget)

# 导入按钮
importBtn = QtWidgets.QPushButton('Import')
importBtn.clicked.connect(self.load)
btnLayout.addWidget(importBtn)

# 刷新按钮
refreshBtn = QtWidgets.QPushButton('Refresh')
refreshBtn.clicked.connect(self.populate)
btnLayout.addWidget(refreshBtn)

# 关闭按钮
closeBtn = QtWidgets.QPushButton('Close')
# 通过点击触发signal,connect链接close函数,close函数继承于QtWidgets.QDialog
closeBtn.clicked.connect(self.close)
btnLayout.addWidget(closeBtn)

def populate(self):

# 清理列表的内容 以免重复加载
self.listWidget.clear()
# 执行功能函数中的find功能
self.library.find()

# self.library是功能函数返回的字典
# items会遍历字典中的所有元素 for循环可以调用到字典相关的元素
for name, info in self.library.items():

# 添加item到list组件中 显示name名称
item = QtWidgets.QListWidgetItem(name)
self.listWidget.addItem(item)

# 获取截图路径
screenshot = info.get('screenshot')

# 如果截图存在
if screenshot:
# item设置图标
icon = QtGui.QIcon(screenshot)
item.setIcon(icon)

# 显示item的提示框内容
item.setToolTip(pprint.pformat(info))

# 加载按钮功能函数
def load(self):
# 获取当前选中的item
currentItem = self.listWidget.currentItem()

# 如果没有选中的item 终止
if not currentItem:
return

# 获取item的名称
name = currentItem.text()
# 执行加载函数
self.library.load(name)

# 保存按钮功能函数
def save(self):

# 获取输入框的文本内容
name = self.saveNameField.text()

# 如果文本为空,就警告并且不进行任何操作
if not name.strip():
cmds.warning("You must give a name!")
return

# 执行保存功能
self.library.save(name)
# 刷新
self.populate()
# 清空输入框
self.saveNameField.setText('')

def showUI():
ui = ControllerLibraryUI()
ui.show()
return ui

06 The Light Manager

   PyMel 介绍

  为什么一开始不用 PyMel

  • 很多工具都是使用 cmds 写的,更多公司流程更希望用 cmds 来写
  • PyMel 有时候运行得很慢 PyNode的数据非常多,循环遍历的效率会很低。
  • PyMel 是第三方插件 不是官方负责更新

  PyMel的优点可以参考官方文档的说明

  • 使用更方便 简洁
  • 代码指向更清晰 利于维护
  • Debug更方便
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# from Qt import QtWidgets,QtCore,QtGui
from PySide2 import QtWidgets, QtCore, QtGui
import pymel.core as pm
from functools import partial
import os
import json
import time
from maya import OpenMayaUI as omui

import logging
# 初始化logging系统
logging.basicConfig()
# 设置名称 可以针对当前工具进行记录
logger = logging.getLogger('LightingManager')
# 设置信息反馈的模式
logger.setLevel(logging.DEBUG)

import Qt
# 识别当前的使用的Qt库 从而导入正确的库
if Qt.__binding__.startswith('PyQt'):
logger.debug('Using sip')
from sip import wrapinstance as wrapInstance
from Qt.QtCore import pyqtSignal as Signal
elif Qt.__binding__ == 'PySide':
logger.debug('Using shiboken')
from shiboken import wrapInstance
from Qt.QtCore import Signal
else:
logger.debug('Using shiboken2')
from shiboken2 import wrapInstance
from Qt.QtCore import Signal

# 获取Maya的主窗口 用于 dock 窗口
def getMayaMainWindow():
# 通过OpenMayaUI API 获取Maya主窗口
win = omui.MQtUtil_mainWindow()
# 将窗口转换成Python可以识别的东西 这里是将它转换为QMainWindow
ptr = wrapInstance(long(win),QtWidgets.QMainWindow)
return ptr

def getDock(name='LightingManagerDock'):
# 首先删除重名的窗口
deleteDock(name)
# 生成可以dock的Maya窗口
# dockToMainWindow 将窗口dock进右侧的窗口栏中
# label 设置标签名称
ctrl = pm.workspaceControl(name,dockToMainWindow=('right',1),label="Lighting Manager")
# 通过OpenMayaUI API 获取窗口相关的 Qt 信息
qtCtrl = omui.MQtUtil_findControl(ctrl)
# 将 qtCtrl 转换为Python可以识别的形式
ptr = wrapInstance(long(qtCtrl),QtWidgets.QWidget)
return ptr

def deleteDock(name='LightingManagerDock'):
# 查询窗口是否存在
if pm.workspaceControl(name,query=True,exists=True) :
# 存在即删除
pm.deleteUI(name)

class LightManager(QtWidgets.QWidget):
# 用来显示下拉菜单
lightTypes = {
"Point Light": pm.pointLight,
"Spot Light": pm.spotLight,
"Direction Light": pm.directionalLight,
# partial 类似于 lambda 函数
# 可以将 partial 转换为函数的形式
# def createAreaLight(self):
# pm.shadingNode('areaLight', asLight=True)
# partial 和 lambda 的区别在于 lambda 的运行传入参数 partial是创建传入
"Area Light":partial(pm.shadingNode,'areaLight',asLight=True),
"Volume Light":partial(pm.shadingNode,'volumeLight',asLight=True),
}

def __init__(self,dock=True):
# parent = getMayaMainWindow()
# 如果设置 dock 窗口 执行 getdock 函数
if dock:
parent = getDock()
else:
# 删除dock窗口
deleteDock()

try:
# 删除窗口 如果窗口本身不存在 用try可以让代码不会停止运行并报错
pm.deleteUI('lightingManager')

except:
logger.debug('No previous UI exists')

# 获取Maya主窗口 并将窗口负载在Qt窗口上
parent = QtWidgets.QDialog(parent=getMayaMainWindow())
# 设置名称 可以在后面找到它
parent.setObjectName('lightingManager')
parent.setWindowTitle('Lighting Manager')
layout = QtWidgets.QVBoxLayout(parent)

# 执行父对象,并且设置parent
super(LightManager,self).__init__(parent=parent)
self.buildUI()
self.populate()

# 将自己添加到父对象中
self.parent().layout().addWidget(self)

# 如果没有dock窗口 则显示窗口
if not dock:
parent.show()


def populate(self):
# count() 获取 scrollLayout 的 item 个数
while self.scrollLayout.count():
# 获取 scrollLayout 第一个元素
widget = self.scrollLayout.takeAt(0).widget()
if widget:
# 隐藏元素
widget.setVisible(False)
# 删除元素
widget.deleteLater()

# 循环场景中所有的灯光元素
for light in pm.ls(type=["areaLight","spotLight","pointLight","directionalLight","volumeLight"]):
# 添加相关的灯光
self.addLight(light)

def buildUI(self):
# 创建 QGridLayout 可以快速将元素添加到网格位置中
layout = QtWidgets.QGridLayout(self)

# QComboBox 为下拉菜单
self.lightTypeCB = QtWidgets.QComboBox()
# 将 lightTypes 的元素添加到 QComboBox 中
for lightType in sorted(self.lightTypes):
self.lightTypeCB.addItem(lightType)

# 添加到(0,0)的位置 占用1行2列
layout.addWidget(self.lightTypeCB,0,0,1,2)

# 创建按钮
createBtn = QtWidgets.QPushButton('Create')
createBtn.clicked.connect(self.createLight)
layout.addWidget(createBtn,0,2)

# 滚动用的组件
scrollWidget = QtWidgets.QWidget()
# 设置滚动组件固定大小
scrollWidget.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
# 横向排布
self.scrollLayout = QtWidgets.QVBoxLayout(scrollWidget)

# 滚动区域
scrollArea = QtWidgets.QScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setWidget(scrollWidget)
layout.addWidget(scrollArea,1,0,1,3)

# 保存按钮
saveBtn = QtWidgets.QPushButton('Save')
saveBtn.clicked.connect(self.saveLights)
layout.addWidget(saveBtn,2,0)

# 导入按钮
importBtn = QtWidgets.QPushButton('Import')
importBtn.clicked.connect(self.importLights)
layout.addWidget(importBtn,2,1)

# 刷新按钮
refreshBtn = QtWidgets.QPushButton('Refresh')
refreshBtn.clicked.connect(self.populate)
layout.addWidget(refreshBtn,2,2)

def saveLights(self):
# 将数据保存为 json

properties = {}

# 寻找 LightWidget 类的对象
for lightWidget in self.findChildren(LightWidget):
# 获取灯光的Transform节点
light = lightWidget.light
transform = light.getTransform()

# 将相关的数据存入 properties 变量中
properties[str(transform)] = {
'translate' : list(transform.translate.get()),
'rotate' : list(transform.rotate.get()),
'lightType' : pm.objectType(light),
'intensity' : light.intensity.get(),
'color' : light.color.get()
}

# 获取数据的存储路径
directory = self.getDirectory()

# 设置存储文件的名称
lightFile = os.path.join(directory , 'lightFile_%s.json' % time.strftime('%m%d'))

# 写入数据
with open(lightFile,'w') as f:
json.dump(properties,f,indent=4)

logger.info('Saving file to %s' % lightFile)

def getDirectory(self):
# 获取文件保存路径
directory = os.path.join( pm.internalVar(userAppDir=True) , 'lightManager')
if not os.path.exists(directory):
os.mkdir(directory)
return directory

# json数据的保存格式
# {
# "pointLight1": {
# "color": [
# 1.0,
# 1.0,
# 1.0
# ],
# "intensity": 1.0,
# "translate": [
# 0.0,
# 7.269212547848552,
# 0.0
# ],
# "rotate": [
# 0.0,
# 0.0,
# 0.0
# ],
# "lightType": "pointLight"
# },
# "pointLight3": {
# "color": [
# 0.03610000014305115,
# 0.580299973487854,
# 0.0
# ],
# "intensity": 470.0,
# "translate": [
# 10.703503890939462,
# 17.997132841447666,
# 0.0
# ],
# "rotate": [
# 0.0,
# 0.0,
# 0.0
# ],
# "lightType": "pointLight"
# }
# }
def importLights(self):
# 读取 json 数据

# 获取存储路径
directory = self.getDirectory()
# 打开一个获取文件的 file browser 窗口 获取相关的json文件
fileName = QtWidgets.QFileDialog.getOpenFileName(self,"light Browser",directory)

# 读取 json 数据
with open(fileName[0],'r') as f:
properties = json.load(f)

# 根据 json 数据处理 生成相关的灯光和属性
for light,info in properties.items():
# 获取灯光类型
lightType = info.get('lightType')
# 循环遍历灯光类型
for lt in self.lightTypes:
# lightTypes 中的类型 需要提取出前半部分与Light结合 进行匹配
if ('%sLight' % lt.split()[0].lower()) == lightType:
break
else:
# for 循环 也有else语句 当循环没有被 break 时执行
logger.info('Cannot find a corresponding light type for %s (%s)' % (light,lightType))
continue

# 创建当前lt类型的灯光
light = self.createLight(lightType=lt)

# 设置 json 的数据到具体对象中
light.intensity.set(info.get('intensity'))

light.color.set(info.get('color'))

transform = light.getTransform()
transform.translate.set(info.get('translate'))
transform.rotate.set(info.get('rotate'))

# 刷新
self.populate()


def createLight(self,lightType=None,add=True):
# 创建灯光 如果没有类型参数传入 就属于点击创建按钮的情况 获取下拉菜单的类型
if not lightType:
lightType = self.lightTypeCB.currentText()

# 去到 lightTypes 的字典中 找到相关的函数进行调用
func = self.lightTypes[lightType]

# 返回灯光的 pymel 对象
light = func()

# 添加灯光到滚动区域中
if add:
self.addLight(light)

return light

def addLight(self,light):
# 添加滚动区域的组件
widget = LightWidget(light)
self.scrollLayout.addWidget(widget)
# 链接组件的 onSolo Signal 触发 onSolo 方法
widget.onSolo.connect(self.onSolo)

def onSolo(self,value):
# 找到 LightWidget 类的对象
lightWidgets = self.findChildren(LightWidget)

# 遍历所有的组件
for widget in lightWidgets:
# signal 的数据会通过 sender() 返回
# 如果返回是 True 则是不需要 disable 的对象
if widget != self.sender():
widget.disableLight(value)

class LightWidget(QtWidgets.QWidget):
# 灯光组件 放置在滚动区域中

# 注册 onSolo 信号
onSolo = QtCore.Signal(bool)

def __init__(self,light):
super(LightWidget,self).__init__()
# 如果灯光是字符串 可以将它转换为 pymel 的对象
if isinstance(light,basestring):
logger.debug('Converting node to a PyNode')
light = pm.PyNode(light)

# 如果获取的是 Transform 节点 就转而获取它的形状节点
if isinstance(light,pm.nodetypes.Transform):
light = light.getShape()

# 存储 shape 节点
self.light = light
self.buildUI()

def buildUI(self):
# 创建 grid 布局
layout = QtWidgets.QGridLayout(self)

# 创建 复选框 用来设置可视化属性
self.name = QtWidgets.QCheckBox(str(self.light.getTransform()))
self.name.setChecked(self.light.visibility.get())
self.name.toggled.connect(lambda val: self.light.getTransform().visibility.set(val))
layout.addWidget(self.name,0,0)

# 隔离显示按钮
soloBtn = QtWidgets.QPushButton('Solo')
soloBtn.setCheckable(True)
soloBtn.toggled.connect(lambda val:self.onSolo.emit(val))
layout.addWidget(soloBtn,0,1)

# 删除按钮
deleteBtn = QtWidgets.QPushButton('X')
deleteBtn.clicked.connect(self.deleteLight)
deleteBtn.setMaximumWidth(10)
layout.addWidget(deleteBtn,0,2)

# 强度滑竿
intensity = QtWidgets.QSlider(QtCore.Qt.Horizontal)
intensity.setMinimum(1)
intensity.setMaximum(1000)
intensity.setValue(self.light.intensity.get())
intensity.valueChanged.connect(lambda val:self.light.intensity.set(val))
layout.addWidget(intensity,1,0,1,2)

# 颜色按钮
self.colorBtn = QtWidgets.QPushButton()
self.colorBtn.setMaximumWidth(20)
self.colorBtn.setMaximumHeight(20)
self.setButtonColor()
self.colorBtn.clicked.connect(self.setColor)
layout.addWidget(self.colorBtn,1,2)

def setButtonColor(self,color=None):
# 设置按钮颜色

# 如果没有传入颜色参数 就获取灯光的颜色
if not color:
color = self.light.color.get()

# 类似于 lambda 函数 可转换为
# if not len(color) == 3:
# raise Exception("You must provide a list of 3 colors")
# 可以用来检测输入是否正确
assert len(color) ==3 , "You must provide a list of 3 colors"

# 获取相关的颜色数值到 r,g,b 变量中
r,g,b = [c*255 for c in color]

# 给按钮设置CSS样式
self.colorBtn.setStyleSheet('background-color:rgba(%s,%s,%s,1)'%(r,g,b))


def setColor(self):
# 点击颜色按钮设置颜色

# 获取灯光的颜色
lightColor = self.light.color.get()
# 打开 Maya 的颜色编辑器
color = pm.colorEditor(rgbValue=lightColor)

# Maya 返回了字符串
# 我们需要手动将其转换为可用的变量
r,g,b,a = [float(c) for c in color.split()]

# 保存新的颜色值
color = (r,g,b)

# 设置新的颜色值
self.light.color.set(color)
self.setButtonColor(color)

def disableLight(self,value):
# self.name 为复选框
# 设置复选框的状态
self.name.setChecked(not bool(value))

def deleteLight(self):
# 删除灯光组件
self.setParent(None)
self.setVisible(False)
self.deleteLater()

# 删除灯光
pm.delete(self.light.getTransform())

07 Finishing Up

  本次案例是脱离 Maya 写一个 Python 命令行执行的程序
  最后展望了可以用来写Python的IDE,这里就不赘述了

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
# 用来创建commandLine 的库
import argparse

# 导入用于正则表达式的库
import re

# 用来交互进行系统的交互
import os

# 用于复制文件调用的库
import shutil

def main():
"""
主函数默认情况下会被执行
"""

# 创建参数获取器 获取相关的参数
parser = argparse.ArgumentParser(description="This is a simple batch renaming tool to rename sequences of files",
usage="To replace all files with hello wtih goodbye: python renamer.py hello goodbye")

# 添加参数处理的帮助文档
parser.add_argument('inString', help="The word or regex pattern to replace")
parser.add_argument('outString',help="The word or regex pattern to replace it with")

parser.add_argument('-d', '--duplicate', help="Should we duplicate or write over the original files", action='store_true')
parser.add_argument('-r', '--regex', help="Whether the inputs will be using regex or not", action='store_true')

parser.add_argument('-o', '--out', help="The location to deposit these files. Defaults to this directory")

# 读取上面的参数
args = parser.parse_args()

# 通过获取的参数来执行重命名函数
rename(args.inString, args.outString, duplicate=args.duplicate,
outDir=args.out, regex=args.regex)

def rename(inString, outString, duplicate=True, inDir=None, outDir=None, regex=False):
"""
A simple function to rename all the given files in a given directory
Args:
inString: the input string to find and replace
outString: the output string to replace it with
duplicate: Whether we should duplicate the renamed files to prevent writing over the originals
inDir: what the directory we should operate in
outDir: the directory we should write to.
regex: Whether we should use regex instead of simple string replace
"""
# 如果没有提供输入路径 那么就获取当前脚本的路径
if not inDir:
inDir = os.getcwd()

# 如果没有提供输出路径 那么就获取当前脚本的路径
if not outDir:
outDir = inDir

# 将路径转换为绝对路径
outDir = os.path.abspath(outDir)

# 如果输入输出路径还是不存在 就报错
if not os.path.exists(outDir):
raise IOError("%s does not exist!" % outDir)
if not os.path.exists(inDir):
raise IOError("%s does not exist!" % inDir)

# 遍历路径文件夹的文件
for f in os.listdir(inDir):
# 以.开头的文件为隐藏的文件 不进行操作
if f.startswith('.'):
continue

# 如果设置了正则表达式 就用正则表达式的方法进行替换 否则就直接替换
if regex:
name = re.sub(inString, outString, f)
else:
name = f.replace(inString, outString)

# 如果名字相同就无需重命名了
if name == f:
continue

# 创建新文件的路径
src = os.path.join(inDir, f)
# 创建新文件的名称
dest = os.path.join(outDir, name)

# 如果是复制文件则执行复制 否则执行重命名的函数
if duplicate:
shutil.copy2(src, dest)
else:
os.rename(src, dest)

# 我们希望main函数理科执行 但是不希望 import 的时候执行 判断函数可以管理好这个问题
if __name__ == '__main__':
main()

总结

  这篇教程是我目前看过最棒的TD教程。
  所有 Maya TD 要用到的工具和库都娓娓道来,而且难度由浅入深,很适合初学者学习。
  最重要的是,他不是仅仅进行教学,也解释了背后为什么要这样做的逻辑,让我受益匪浅。

  最近患上了重感冒,这篇文章也拖了一小段时间才差不都完成。
  写这篇总结真的花了我好多无谓的时间,特别是截图和给所有的代码进行个人注释。
  我猜测以后估计不会再干这么无谓的事情了。
  注释代码、理顺教程思路固然好,但是编程的核心是自己动手写代码,最重要的是要跳出原作者的思维逻辑,自己去构思代码的编写。
  当然作者的思路也有值得借鉴的地方,但是如此细致入微地写总结就显得文章很琐碎,找不到重点。

  最近会继续重点学习 Python cmds 的库,不过本质上和MEL相差不大。
  另外PyQt也是近期的重头戏,不过我打算先消化一下近期的学习先。
  另外感冒了也想好好休息一下身体(:з」∠)