【笔记】【Maya工具】Python For Maya(Cmds)(四)Controller Library & LightManager

一、Controller Library

1.1 Qt Environment

使用QT创建controller lib

和Maya cmdsUi相比,更推荐使用qt

CMDS是为MEL设计的,UI函数只有qt的一小部分

直接(mayapy -m )pip install Qt.py

1
2
3
import Qt
win = Qt.QtWidgets.QDialog()
win.show()

在maya上这样就直接运行了

但是在正常的python上,根据文档要这样

1
2
3
4
5
6
7
import sys
from Qt import QtWidgets

app = QtWidgets.QApplication(sys.argv)
button = QtWidgets.QPushButton("Hello World")
button.show()
app.exec_()

好像是现代maya版本就是Qt管理窗口,所以已经有QtApp了

1
2
3
from Qt import QtWidgets,QtCore,QtGui
from PySide2 import QtWidgets,QtCore,QtGui
from PyQt5 import QtWidgets,QtCore,QtGui

甚至我只mayapy安装了Qt.py,这三个命令都能用了

1.2 Controller Library

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

USERAPPDIR = cmds.internalVar(userAppDir = True)
print(USERAPPDIR)
DIRECTORY = os.path.join(USERAPPDIR,'controllerLibrary')

def createDirectory(directory = DIRECTORY):
"""
Creates the given directory
Args:
directory(str):The directory to create
"""
if not os.path.exists(directory):
os.mkdir(directory)

class ControllerLibrary(dict):# stm继承了个字典……
def save(self, name, directory = DIRECTORY,screenshot = True, **info):
createDirectory(directory)# 不存在路径则创建文件夹路径

path = os.path.join(directory,"{}.ma".format(name))
infoFile = os.path.join(directory,'{}.json'.format(name))# 保存数据

# ========保存文件========
cmds.file(rename = path)

if cmds.ls(selection = True):# 有选择则只保存选择
cmds.file(force = True, type = 'mayaAscii', exportSelected = True)
else:# 无选择则全部保存
cmds.file(save = True, type = 'mayaAscii', force = True)

# ========保存屏幕快照========

if screenshot:
info['screenshot'] = self.saveScreenShot(name,directory = directory)


# ========输出json========
info['name'] = name
info['path'] = path
with open(infoFile, 'w') as f:
json.dump(info, f, indent = 4)# 把输入的info储存为json
self[name] = info

def find(self, directory = DIRECTORY):

self.clear()
print('1',directory)
if not os.path.exists(directory):
return
files = os.listdir(directory)# 当前路径下所有文件
mayaFiles = [f for f in files if f.endswith('.ma')]
print(files)
for ma in mayaFiles:
name,ext = os.path.splitext(ma)# 分离文件名和扩展名
path = os.path.join(directory,ma)

infoFile = '{}'.format(name)
if infoFile in files:
infoFile = os.path.join(directory, infoFile)

with open(infoFile, 'r') as f:
info = json.load(f)
else:
print('No info file found')
info = {}

screenshot = '{}.jpg'.format(name)
if screenshot in files:
info['screenshot'] = os.path.join(directory,name)
info['name'] = name
info['path'] = path
self[name] = info
pprint.pprint(self)

def load(self, name):
path = self[name]['path']
cmds.file(path, i = True, usingNamespace = False)

def saveScreenShot(self, name, directory = DIRECTORY):
path = os.path.join(directory, '{}.jpg'.format(name))
cmds.viewFit()
cmds.setAttr('defaultRenderGlobals.imageFormat', 8)# jpeg
#cmds.setAttr("defaultArnoldDriver.aiTranslator",'png',type = "string") 阿诺德渲染器

cmds.playblast(completeFilename = path, forceOverwrite = True, format = 'image', width = 200, height = 200, showOrnaments = False, startTime = 1, endTime = 1, viewer = False)
return path

https://blog.csdn.net/tian0000hai/article/details/116952413阿诺德渲染器的渲染输出设置

libraryUI.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
from maya import cmds
from PySide2 import QtWidgets, QtCore, QtGui
import conLibrary.controllerLibrary as controllerLibrary
import pprint

import importlib
importlib.reload(controllerLibrary)

class ControllerLibraryUI(QtWidgets.QDialog):
"""
The ControllerLibraryUI is a dialog that lets us save and import controllers
"""

def __init__(self):
#super(ControllerLibraryUI, self).__init__()
super().__init__()
#super是一个,用于调用父类方法,用于多继承,而这里是为了不用考虑继承的父类是什么
#QtWidgets.QDialog.__init__(self)因为是单继承,和这个相等
self.setWindowTitle('Controller LibraryUI')
self.library = controllerLibrary.ControllerLibrary()

self.buildUI()
self.populate()

def buildUI(self):
"""
This method builds out the UI
"""
print('bulding UI')
# 整体行分布
layout = QtWidgets.QVBoxLayout(self)

# 行分布中的一行,提供纵向分布,编辑栏view
saveWidget = QtWidgets.QWidget()
saveLayout = QtWidgets.QHBoxLayout(saveWidget)
layout.addWidget(saveWidget)

# 行内组件
self.saveNameField = QtWidgets.QLineEdit()
saveLayout.addWidget(self.saveNameField)

# 行内组件
saveBtn = QtWidgets.QPushButton('Save')
saveBtn.clicked.connect(self.save)
saveLayout.addWidget(saveBtn)

# 新的一行,缩略图view
size = 64
buffer = 12
# create a grid list Widget to display our controller thumbnails
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)

# 新的一行button view
btnWidget = QtWidgets.QWidget()
btnLayout = QtWidgets.QHBoxLayout(btnWidget)
layout.addWidget(btnWidget)

# button view组件
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')
closeBtn.clicked.connect(self.close)# self.close继承于QDialog
btnLayout.addWidget(closeBtn)


def populate(self):
"""This clears the listWidget and then repopulates it with the content of our library"""
self.listWidget.clear()

print("populating")
self.library.find()

for name, info in self.library.items():
item = QtWidgets.QListWidgetItem(name)
self.listWidget.addItem(item)

screenshot = info.get('screenshot')
if screenshot:
icon = QtGui.QIcon(screenshot)
item.setIcon(icon)

item.setToolTip(pprint.pformat(info))

def load(self):
"""This loads the currently selected controller"""
currentItem = self.listWidget.currentItem()

if not currentItem:
return
name = currentItem.text()
self.library.load(name)
def save(self):
"""This saves the controller with the given file name"""
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():
"""
This shows and returns a handle to the UI
Returns:
Qdialog
"""
ui = ControllerLibraryUI()
ui.show()
return ui

二、Light Manager

创建一个场景的灯光管理窗口,并且可以和maya的窗口融合

2.1 PyMel

pymel包含了cmds、openmaya

1
2
3
4
import pymel.core as pm
cube = pm.polyCube()
print(cube)
#[nt.Transform('pCube1'), nt.PolyCube('polyCube1')]

哥们试了半天,maya2023只有pymel==1.3.0a1能用

可以看到pyme创建的polyCube是和openmaya一样附带Node的,并且有一个nt来封装

1
2
#cmds.rename(cube[0],'newName')
cube[0].rename('Foo')

image-20220703154445036

pymel返回的对象不像cmds里面直接是string,是有一个nt对象封装(==PyNode class==)的,因此使用rename方法也需要调用这个nt对象内部的方法

这就和OpenMaya是差不多的

  • mayatool使用cmd是很常见的
  • pymel一些情况会很慢,因为它封装了Node对象,不想cmd使用string
  • pymel也不被Audodesk直接支持,官方文档里也写了这一点

2.2 UI and Partial funcitons

lightingManager.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
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
from Qt import QtWidgets,QtCore,QtGui
import pymel.core as pm
from functools import partial
import Qt
import logging
from maya import OpenMayaUI as omui

logging.basicConfig()
logger = logging.getLogger('LightingManager')
logger.setLevel(logging.DEBUG)

if Qt.__binding__ == 'PySide':
logger.debug('Using Pyside with shiboken')
from shiboken import warpInstance
from Qt.QtCore import Signal
elif Qt.__binding__.startswith('PyQt'):
logger.debug('Using PyQt with sip')
from sip import warpInstance as warpInstance
from Qt.QtCore import pyqtSignal as Signal
else:
logger.debug('Using PySide2 with shiboken')
import shiboken2
from Qt.QtCore import Signal


def getMayaMainWindow():
win = omui.MQtUtil_mainWindow()
ptr = shiboken2.wrapInstance(int(win),QtWidgets.QMainWindow)# long是python2
# 并且 shiboken2有改动,不能直接import warpInstance
return ptr


def getDock(name = 'LightingManagerDock'):
deleteDock(name)
ctrl = pm.workspaceControl(name, dockToMainWindow = ('right',1), label = "LightingManager")
qtCtrl = omui.MQtUtil_findControl(ctrl)
ptr = shiboken2.wrapInstance(int(qtCtrl),QtWidgets.QWidget)
return ptr

def deleteDock(name = 'LightingManagerDock'):
if pm.workspaceControl(name, query = True, exists = True):
pm.deleteUI(name)

class LightManager(QtWidgets.QWidget):# QtWidgets.QDialog

lightTypes = {
"Point Light": pm.pointLight,
"Spot Light": pm.spotLight,
"Directional Light": pm.directionalLight,
"Area Light": partial(pm.shadingNode, 'areaLight', asLight = True),
"Volume Light": partial(pm.shadingNode, 'volumeLight', asLight = True)
}

def __init__(self, dock = True):
#
if dock:
parent = getDock()
else:
deleteDock()
try:
pm.deleteUI('LightingManager')
except:
logger.debug("No previous LightManagerUI exists")

#parent = getMayaMainWindow()
parent = QtWidgets.QDialog(parent = getMayaMainWindow())

parent.setObjectName('lightingManager')
parent.setWindowTitle('Lighting Manager')
layout = QtWidgets.QVBoxLayout(parent)

super().__init__(parent = parent)# 把这个窗口的父节点设为maya窗口
#self.setWindowTitle('Lighting Manager')
self.buildUI()
self.populate()

self.parent().layout().addWidget(self)

if not dock:
parent.show()
def populate(self):
while self.scrollLayout.count():
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):
layout = QtWidgets.QGridLayout(self)

self.lightTypeCB = QtWidgets.QComboBox()# 下拉菜单
for lightType in sorted(self.lightTypes):
self.lightTypeCB.addItem(lightType)
layout.addWidget(self.lightTypeCB, 0,0)


createBtn = QtWidgets.QPushButton('Create')
createBtn.clicked.connect(self.createLight)
layout.addWidget(createBtn, 0,1)

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,2)# 位于layout第二行第一列,占据1行2列


refreshBtn = QtWidgets.QPushButton('Refresh')
refreshBtn.clicked.connect(self.populate)
layout.addWidget(refreshBtn, 2,1)

def createLight(self):
lightType = self.lightTypeCB.currentText()
func = self.lightTypes[lightType]

light = func()
self.addLight(light)


def addLight(self, light):
widget = LightWidget(light)
self.scrollLayout.addWidget(widget)
widget.onSolo.connect(self.onSolo)

def onSolo(self, value):
lightWidgets = self.findChildren(LightWidget)
for widget in lightWidgets:
if widget != self.sender():
widget.disableLight(value)


class LightWidget(QtWidgets.QWidget):

onSolo = Signal(bool)#QtCore.pyqtSignal 使用pyqt;QtCore.Singal使用pyside2
# 在我们后面的定义中,对不同版本的Qt进行了判断,可以统一使用
def __init__(self, light):
super().__init__()

if isinstance(light, str):
light = pm.PyNode(light)
self.light = light
self.buildUI()


def buildUI(self):
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))
#def setLightVisibility(val):
# self.light.visibility.set(val)
# self.light 只表示shapeNode,因此需要在transofrm上设置可见项
layout.addWidget(self.name, 0,0)


# Solo按钮
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()
assert len(color) == 3, "You must provide a list of 3 Color"

r,g,b = [c*255 for c in color]
self.colorBtn.setStyleSheet('background-color: rgba({},{},{},1.0)'.format(r,g,b))


def setColor(self):
lightColor = self.light.color.get()
color = pm.colorEditor(rgbValue = lightColor)
#print(color) maya编辑器给返回的是个字符串
r,g,b,a = [float(c) for c in color.split()]
color = (r,g,b)
# 改变灯光颜色
self.light.color.set(color)
# 改变UI上的颜色
self.setButtonColor(color)

def disableLight(self,value):
self.name.setChecked(not value)

def deleteLight(self):
# 删除UI
self.setParent(None)
self.setVisible(False)
self.deleteLater()

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

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

1
2
3
4
5
import lightingManager
import importlib
importlib.reload(lightingManager)

ui = lightingManager.LightManager().show()

image-20220703181152950

将窗口设为maya窗口的子窗口后,窗口和maya工作区成为一个整体,在maya工作区工作不会让组件窗口消失了,在任务栏也不会看到额外的maya窗口

1
2
3
4
5
6
7
import lightingManager
import importlib
importlib.reload(lightingManager)

#ui = lightingManager.LightManager().show()
#lightingManager.LightManager().show()
lightingManager.getDock()

image-20220703181632027

2.3 Handle error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#raise "message"#raise会停止后面的语句

def do():
raise RuntimeError()
print("hello")
#用try except来处理,当出现错误又不希望程序停止时
try:
do()
except:
print("There was an Error")
print("Goodbye")

#Hello
#There was an Error
#Goodbye
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try:
raise RuntimeError()#IOError(),Exception()
except RuntimeError as e:
print("RuntimeError")
except:#else也可以用
print("other error")

try:
print("Running")
except RuntimeError as e:
print("RuntimeError")
else:
print("no error")
finally:
print("done")
#Running
#no error
#done
#执行try后,else一定会被执行(except 可以看作一种if)

2.4 Exporting and Importing Our Lights

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
saveBtn = QtWidgets.QPushButton('Save')
saveBtn.clicked.connect(self.saveLights)
layout.addWidget(saveBtn,2,0)


def saveLights(self):
properties = {}
for lightWidget in self.findChildren(LightWidget):
light = lightWidget.light
transform = light.getTransform()

properties[str(transform)] = {
"translate": list(transform.translate.get()),
"rotation": list(transform.rotate.get()),
"lightType": pm.objectType(light),
"intensity": light.intensity.get(),
"color": light.color.get()
}
directory = os.path.join(pm.internalVar(userAppDir = True),'lightManager')
if not os.path.exists(directory):
os.mkdir(directory)
lightFile = os.path.join(directory,'lightFile_{}.json'.format(time.strftime('%m%d')))
with open(lightFile, 'w') as f:
json.dump(properties, f, indent = 4)
logger.info("Saving file to {}".format(lightFile))


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
importBtn = QtWidgets.QPushButton('Import')
importBtn.clicked.connect(self.importLights)
layout.addWidget(importBtn,2,1)
def importLights(self):
directory = self.getDirectory()
fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Light Browser", directory)
#print(fileName)#('C:/Users/XZYW/文档/maya/lightManager/lightFile_0703.json', '所有文件 (*)')
with open(fileName[0], 'r') as f:
properties = json.load(f)

for light, info in properties.items():
lightType = info.get('lightType')
for lt in self.lightTypes:
if "{}Light".format(lt.split()[0].lower()) == lightType:
break
else:
logger.info("Cannot find a corresponding lightType for {} ({})".format(light, lightType))
continue
light = self.createLight(lightType = lt)
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('rotation'))

self.populate()

def createLight(self, lightType = None, add = True):
if not lightType:
lightType = self.lightTypeCB.currentText()
func = self.lightTypes[lightType]

light = func()
if add:
self.addLight(light)

return light

三、补充

3.1 命令行 文件重命名

cliRenamer.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
import argparse
import re
import os
import shutil

def main():
parser = argparse.ArgumentParser(description = "This is a batch renamer", usage = "To replace all files with hello with goodbye instead: python clirenamer.py hello goodbye")
parser.add_argument('inString', help = "The world to replace")
parser.add_argument('outString', help = "The world to replace it with")

parser.add_argument('-d', '--duplicate', help = "Whether to duplicate or replace in spot", action = 'store_true')#默认为False
parser.add_argument('r', '--regex',
help = "Whether the patterns are regex or not",
action = "store_true")
parser.add_argument('o', '--out',
help = "The output location. Defaults to here")
args = parser.parser_args()

print(args)
rename(args.inString,args.outString, duplicate = args.duplicate,
outDirectory = args.out, regex = args.regex)

def rename(inString, outString, duplicate = True, inDirectory = None,
outDirectory = None, regex = False):
if not inDirectory:
inDirectory = os.getcwd()
if not outDirectory:
outDIrectory = inDirectory
outDirectory = os.path.abspath(outDirectory)

if not os.path.exists(outDirectory):
raise IOError('{} does not exist'.format(outDirectory))
if not os.path.exists(inDirectory):
raise IOError('{} does not exist'.format(inDirectory))
for f in os.listdir(inDirectory):
if .startwith('.'):#linux,mac中.开头表示隐藏文件
continue

if regex:
name = re.sub(inString, outString, f)
else:
name = f.replace(inString, outString)

print(name)
if name == f:
continue

src = os.path.join(inDirectory, f)
dest = os.path.join(outDirectory, name)

if duplicate:
shutil.copy2(src, dest)
else:
os.rename(src, dest)

if __name__ == '__main__':z
main()#分离命名空间,只有当这个文件命名空间是main的时候才运行

3.2 sys模块

1
2
3
4
5
import sys
import subprocess#子进程
subprocess.call(['python', '../cliRenamer.py', '-h'])