添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

前言

这篇教程也算是千呼万唤始出来,之前的观看进度挺快的,没想到最后两个案例如此晦涩难懂,以至于我花了很多的时间。
最可怕的是听不懂,所以就很困,进入不了状态。
有时候真的觉得,编程教程好像没有什么意义,看源代码的逻辑就可以学到不少东西了,毕竟注释也很充足。(感觉像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
    75
    #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
    131
    #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
    151
    #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
    441
    # 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,这里就不赘述了