Maya-Python-编程秘籍-全-
Maya Python 编程秘籍(全)
原文:
zh.annas-archive.org/md5/20b40462713b482ecbd4c267c31718a8译者:飞龙
前言
本书是使用 Python 脚本语言在 Maya 中自动化任务和构建工具的指南。前两章提供了 Maya 中 Python 脚本和 UI 创建的基础概述。从那里,第三章到第七章各自覆盖了不同的问题领域,大致按照它们在现实世界项目中的遇到顺序。我们开始建模,经过纹理、绑定、动画和渲染。最后三章涵盖了将脚本扩展为完整工具管道所需的主题,包括文件输入和输出以及通过 Web 与 Maya 外部世界通信。在最后一章,我们将涵盖一些更高级的主题,例如脚本节点和脚本作业。
每一章都提供了如何在该领域实现特定任务的几个示例,包括完整的代码列表和解释。每个示例都是独立的,读者应自由地在书中移动,以满足需要。
本书涵盖内容
第一章, 开始使用 Maya,涵盖了使用 Maya 脚本编辑器的基础以及设置编写自己脚本的准备工作。它还涵盖了 Maya 嵌入式语言(MEL)和 Python 之间的区别。它还涵盖了 Maya 内置命令的基本部分,以及它们可以调用的不同模式(创建、查询和编辑)。
第二章, 创建用户界面,向读者介绍为脚本创建用户界面的方法。它涵盖了创建窗口、添加布局以及填充控件。嵌套布局和自定义菜单也进行了说明。
第三章, 处理几何体,涵盖了使用 Python 处理几何数据。它从如何检索模型信息开始,包括多边形和 NURBS 模型。它还涵盖了创建新的曲线和新的面,以及操纵现有数据以变形模型。
第四章, 给物体上色 – UV 和材质,讨论了为渲染准备模型,包括处理 UV 数据以及创建和应用着色网络。
第五章, 添加控件 – 绑定脚本,涵盖了与使用脚本进行绑定相关的话题,包括如何创建骨骼和编辑它们的属性。它还涵盖了如何创建驱动集的关键关系以及如何设置反向动力学(IK)。
第六章, 让事物动起来 – 动画脚本,处理查询和设置关键帧数据以创建和修改动画。它还涵盖了从一物体复制关键帧到另一物体以及使用代码创建自定义表达式。
第七章, 渲染脚本,涵盖了与实际生成帧相关的话题。示例展示了如何创建灯光和摄像机,以及如何渲染图像。它还涵盖了使用 Python 图像库(PIL)在渲染后合并图像。
第八章, 处理文件输入/输出,涵盖了通过导入和导出自定义数据来构建更大工具链所需的话题。涵盖了读取和写入基于文本和二进制格式的内容。
第九章, 与网络通信,涵盖了如何从网络检索信息以在 Maya 中使用。涵盖了 XML 和 JSON 数据的解析,以及向网站发送 POST 数据。
第十章, 高级主题,涵盖了包括脚本作业和脚本节点在内的更多高级话题。它还涵盖了如何创建一个自定义上下文来使脚本更像 Maya 的内置工具。
你需要这本书什么
要充分利用这本书,你需要一份 Maya 和程序员友好的文本编辑器的副本。市面上有很多文本编辑器,很多人对它们都有自己的偏好。至少,你希望有一个可以保存纯文本并提供显示行号选项的编辑器。你可能还希望有一个提供 Python 语法高亮的编辑器。
本书的所有代码都是使用 Sublime Text 编写的(www.sublimetext.com/),这是一个优秀且价格低廉的文本编辑器,非常适合 Python,以及许多其他任务。虽然你不必使用它;任何允许你编辑纯文本的文本编辑器都可以正常工作。
书中的几乎所有示例都仅依赖于 Python 和 Maya,但有一个示例使用了 PIL。要安装 PIL,你可能会想使用 PIP,这是一个使安装 Python 包变得简单的包管理器。你可以在pip.pypa.io/en/stable/找到它。
这本书面向谁
这本书是为任何想要使用 Python 从 Maya 中获得更多功能的人而写的。预期你对 Maya 的界面和工具集有相当的了解。了解 Python 或其他编程语言是有帮助的,但不是必需的。
部分
在这本书中,你会发现一些经常出现的标题(准备,如何做...,它是如何工作的...,更多内容...,以及参见)。
为了清楚地说明如何完成食谱,我们使用以下部分:
准备
本节告诉你在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。
如何做...
本节包含遵循食谱所需的步骤。
它是如何工作的...
本节通常包含对上一节发生情况的详细解释。
更多内容…
本节包含有关食谱的附加信息,以便使读者对食谱有更多的了解。
另请参阅
本节提供了对其他有用信息的链接,这些信息对食谱很有帮助。
习惯用法
在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“我们可以通过使用include指令来包含其他上下文。”
代码块设置如下:
import maya.cmds as cmds
print("Imported the script!")
def makeObject():
cmds.polyCube()
print("Made a cube!")
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“通过转到Windows | 通用编辑器 | 脚本编辑器来打开脚本编辑器。”
注意
警告或重要注意事项以如下框中的形式出现。
小贴士
小技巧和窍门如下所示。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果你在某个领域有专业知识,并且你对撰写或参与一本书感兴趣,请参阅我们的作者指南,链接为www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持标签上。
-
点击代码下载与勘误表。
-
在搜索框中输入书籍的名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买这本书的地方。
-
点击代码下载。
文件下载完成后,请确保使用最新版本的软件解压缩或提取文件夹。
-
Windows 上的 WinRAR / 7-Zip
-
Mac 上的 Zipeg / iZip / UnRarX
-
Linux 上的 7-Zip / PeaZip
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Maya-Programming-with-Python-Cookbook。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/MayaProgrammingwithPythonCookbook_ColorImages.pdf下载此文件。
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。
盗版
互联网上对版权材料的盗版是一个持续存在的问题,在所有媒体中都是如此。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
询问
如果您在这本书的任何方面遇到问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章. 开始使用 Maya
本章将涵盖您从本书其余部分获得最大收益所需的所有内容,并让您对 MEL 和 Python 之间的差异有所了解,如下所示:
-
使用脚本编辑器来调查功能
-
从脚本编辑器运行代码
-
导入 Maya 的内置 Python 功能
-
访问特定命令的文档
-
理解创建、查询和编辑标志
-
将自定义文件夹添加到您的脚本路径
-
编写和运行外部脚本
-
使用 Python 调用 MEL 脚本
简介
在本章中,我们将介绍使用 Maya 和 Python 进行脚本编写的基础知识。如果您已经为 Maya 编写了一段时间的脚本,那么本章中涵盖的内容可能很多都是您熟悉的。如果您是 Maya 脚本编写的新手,本章将帮助您了解完成本书其余部分所需的所有知识。
使用脚本编辑器来调查功能
脚本编辑器是您了解 Maya 基于脚本的功能的主要工具,同时也是测试完整脚本之外的小段代码的好地方。脚本编辑器最有用的一个方面是,它会显示您在 Maya 界面中执行的操作对应的命令。
这是了解您日常 Maya 任务中涉及的命令的最好方法之一。例如,让我们用它来找出如何使用Maya 嵌入式语言(MEL)制作多边形立方体:
如何操作...
-
通过转到窗口 | 通用编辑器 | 脚本编辑器来打开脚本编辑器。
-
您可能会注意到,即使您最近才打开 Maya,显示的文本也已经很多。为了使内容更容易查看,请从脚本编辑器窗口的菜单中选择编辑 | 清除历史记录。
-
现在尝试通过按住空格键打开热键框,然后转到创建 | 多边形原形 | 立方体来制作一个多边形立方体。
-
使用交互式创建工具来指定多边形立方体的尺寸。
-
观察脚本编辑器上半部分的结果。您应该看到如下内容:
setToolTo CreatePolyCubeCtx; polyCube -ch on -o on -w 5.502056 -h 3.41434 -d 7.451427 -sw 5 -sd 5 -cuv 4 ; // Result: pCube1 polyCube1 //
它是如何工作的...
Maya 提供的输出以 MEL 命令的形式呈现,这些命令对应于您刚刚执行的操作。这可以是一个很好的方法来找出您在自己的脚本中需要使用哪些命令。在这种情况下,是polyCube命令,它将创建一个多边形立方体。Maya 中的每个命令都有两种形式——MEL 版本和相应的 Python 命令。
脚本编辑器以 MEL 语法显示命令,通常呈现如下形式:
commandName -option1Name option1Value -option2Name option2Value;
MEL 语法借鉴了批处理脚本,其中它依赖于选项名称(通常称为“标志”)及其对应值的字符串。相应的 Python 命令通常具有以下语法:
commandName(option1Name=option1Value, option1Name=option1Value)
如您所见,MEL 和 Python 版本相当相似,但有一些关键差异:
-
在 MEL 版本中,标志名用破折号表示,其值直接跟在后面,而在 Python 中,选项使用“optionName=value”语法
-
Python 将所有标志括在括号中,而 MEL 则不这样做
-
MEL 需要在每一行的末尾使用分号(;),而 Python 则不需要
MEL 和 Python 之间另一个很大的区别是它们处理空白字符(空格、制表符和换行符)的方式。MEL,像大多数语言一样,不关心空白字符;语句以分号结束,代码块由匹配的括号对定义。
然而,Python 使用空白字符来控制程序流程。这对于初学者来说可能是 Python 最奇怪的地方之一,但对于编程新手来说则不然。在 Python 中,代码块是通过缩进来定义的。你可以使用制表符或空格,但关键是要保持一致。在 Python 中,每次你在行首增加制表符(或空格)的数量,就相当于添加了一个开括号,每次减少这个数量,就相当于添加了一个闭括号。这通常会很令人困惑,因为你的程序结构是由可能实际上看不见的字符定义的。如果你是 Python 新手并且难以跟踪你的空白字符,你可能想要更改你的编辑器设置以显示空白字符。大多数程序员友好的文本编辑器都包括这样的选项,这可以大有帮助。
每个命令的具体选项列表可以在内置的 Python 文档中找到,通过在 Maya 中转到帮助 | Python 命令参考可以访问。对于大多数命令,你都会找到一个长长的选项列表。
要使事情更加复杂,每个选项都有短名和长名。例如,polyCube 允许你指定 X 轴上的细分数量。你可以使用长名“subdivisionsX”或短名“sx”来设置它。
例如,以下所有操作都将导致创建一个 1x1x1 的多边形立方体,X 轴上有五个细分。
MEL 的版本有:
polyCube -sx 5;
polyCube -subdivisionsX 5;
Python 的版本有:
maya.cmds.polyCube(sx=5)
maya.cmds.polyCube(subdivsionsX=5)
随意使用短版本或长版本来指定你的参数。你也可以混合使用,为一些参数使用短名,为其他参数使用长名。
在实践中,通常最好为常见的参数(你可能会记住的参数)使用短名,为不太常见/较少使用的参数使用长名。记住,尽管你的代码现在看起来完全合理,但当你 6 个月(或 6 年!)后再次查看时,它可能看起来很混乱。通过在必要时使用长名(并包括注释)来简化你未来的工作。
还有更多...
你可能想知道为什么 Maya 提供了两种不同的脚本编写方法,MEL 和 Python。这是一个简单的向后兼容性问题。MEL 是第一个出现的,在 Python 支持添加到 Maya 之前就已经可用。当时,你必须使用 MEL 来完成日常任务,如果 MEL 不能满足你的需求,你必须深入研究 C++ API(这在非 Windows 系统上相当复杂且难以操作)。Python 统一了这两种方法,但 MEL 仍然得到支持,以便旧脚本可以继续使用。也有可能你使用 MEL 的性能会比 Python 更好,因为 Python 功能是 MEL 的包装器。尽管如此,Python 是一个更易于工作的语言,所以这通常是一个值得的权衡。
注意,脚本编辑器默认不会显示你做的所有操作。在正常情况下,Maya 会根据你最可能感兴趣的内容显示稍微过滤后的输出。这通常是好事,但有时你可能希望禁用它。要显示 所有 输出,转到脚本编辑器并选择 历史 | 回显所有命令。这将导致 Maya 将 所有 输出都发送到脚本编辑器。这通常意味着比你想看到的输出多得多,但有时可能很有帮助。在实践中,你通常希望在尝试在脚本中复制特定功能时才开启这个选项,并且默认输出没有给你任何关于 Maya 正在做什么的洞察。
参见
如果你已经将 Maya 设置为使用交互模式来创建原始形状,你一定在脚本编辑器中看到以下输出:
setToolTo CreatePolyCubeCtx;
上下文是获取用户输入的另一种方式,我们将在 第十章,高级主题 中有更多关于它们的讨论。
从脚本编辑器运行代码
脚本编辑器不仅是一个查看与你在 Maya UI 中采取的操作对应的命令的好方法,而且也是一个方便地编写小段代码的方式。虽然你当然会想使用文本编辑器来编写你的脚本,但仍然重要的是要熟悉使用脚本编辑器来运行小段代码,无论是为了在将其包含到更大的脚本之前测试它,还是为了获取有关当前场景的更多信息。
准备工作
确保脚本编辑器已打开,并且你已经切换到输入(底部)部分的 Python 选项卡。
如何做到这一点...
在输入部分输入以下内容:
import maya.cmds
maya.cmds.polyCube()
一旦完成,可以通过在脚本编辑器顶部点击执行按钮,或者直接按 Control + Enter 来执行它。
你的代码将从输入部分消失,一个新的多边形立方体将被创建,结果将被粘贴到脚本编辑器的输出(“历史”)部分。
为了防止你的代码自动消失,首先使用 Command-A(选择所有内容)将其突出显示,然后按 Command + Enter。这将导致 Maya 只运行所选代码而不清除输入部分。
它是如何工作的...
虽然polyCube命令实际执行工作,但我们必须首先导入 Maya 的 Python 库,然后才能使用它。为此,我们必须首先使用 import maya.cmds。
脚本编辑器是尝试小段代码的好方法,但成功的代码被删除的事实可能会相当令人沮丧。对于任何真正的脚本开发,你将想要使用一个程序员友好的文本编辑器。
还有更多...
脚本编辑器的一个方便之处在于,你可以将编辑器中的代码保存到架子上。为此,在输入部分输入一些代码,然后从 脚本编辑器 菜单中选择 文件 | 保存脚本到架子上...。Maya 将要求你为脚本提供名称,然后(经过一段时间后),在 "自定义" 架子上将出现一个新按钮。按下该按钮将执行相应的代码。
虽然你的大多数脚本工作将涉及编写单独的脚本,但有时从脚本编辑器的历史(顶部)部分复制粘贴命令到输入(底部)部分并将它们全部保存到架子上是有用的。这有点像在 Photoshop 中录制动作,可以是一种快速而简单的方法来为常用功能创建新的快捷方式。
导入 Maya 的内置 Python 功能
Python 是一种非常有用的语言,但它实际上并没有提供很多开箱即用的功能,除了一些用于操作简单数据的基本命令。为了真正做一些有趣的事情,你通常需要通过一个或多个库来扩展 Python 的内置功能,包括提供访问 Maya 功能的库。
如何做到这一点...
首先,让我们导入 Python 的主要 Maya 脚本库 maya.cmds:
import maya.cmds as cmds
一旦我们做了这件事,我们就可以使用 cmds 而不是 maya.cmds。例如,如果我们有这段代码:
maya.cmds.polyCube()
我们可以改用以下方法:
cmds.polyCube()
这可能看起来像是一个微小的变化,但在整个脚本的过程中,它可以为你节省大量的输入。输入越少,错误就越少,所以这样做是非常值得的。
现在我们已经做了这件事,让我们看看 cmds 提供了什么,通过列出其内容。Python 提供了一种方便的方法来通过 dir() 命令显示任何对象的内容。让我们使用它来获取 maya.cmds 中所有命令的列表:
commandList = dir(cmds)
for command in commandList:
print(command)
运行前面的代码,你会看到一个长长的列表,列出了 maya.cmds 库中定义的所有内容。这确实是一个很长的列表。你将看到的多数命令都在官方文档中有介绍,但了解如何使用 dir 来调查特定的库是很好的。
你也可以使用 dir 来调查特定的命令。例如,尝试以下代码:
props = dir(cmds.polyCube)
for prop in props:
print(prop)
运行前面的代码,你会看到 polyCube 命令本身的全部属性。然而,结果可能会看起来有点奇怪,因为它们都与生成多边形立方体无关。这是因为 maya.cmds.[commandName] 是一个内置函数。所以,如果你使用 dir() 进一步调查它,你只会看到与 Python 函数共有的功能。有关命令的详细信息,请参阅 Maya 命令的内置文档,可以通过访问 帮助 | Maya 脚本参考 | Python 命令参考 来获取。
它是如何工作的...
就像 Python 功能的任何其他特定子领域一样,暴露 Maya 工具集给 Python 的命令是库的一部分。为了使用它们,你必须首先导入库。几乎你写的每个脚本都将需要“maya.cmds”库,并且你可能会偶尔需要包含额外的库以获得额外的功能,例如与 web 服务器通信或读取特定的文件格式。
虽然你可以直接使用 import maya.cmds,但这会需要输入很多额外的字符。通过使用 import [library] as [shortName] 语法,你可以告诉 Python 使用一个自定义名称作为 maya.cmds 的别名。你可以使用几乎任何你想要的名字(例如 import maya.cmds as MyUncleFred 也可以正常工作),但在实践中,你希望使用既简短又具有描述性的名称。同时,你还需要确保不要覆盖 Python 的任何内置库。例如,你可以这样做:
import maya.cmds as math
这会将 maya.cmds 重命名为 math,如果你想要使用数学库中定义的任何函数,这将会造成麻烦。不要这样做。
为了这本书的连贯性和与 Maya 文档的一致性,我们将使用“cmds”作为“maya.cmds”的简称。
还有更多...
maya.cmds 库只是可以用来将 Maya 与 Python 交互的几个库之一。Python 在 Maya 中的支持有一个很好的特点,那就是将旧的方法(既有用于日常任务的 MEL,也有用于更大规模插件的 C++ API)统一在 Python 的旗帜下。maya.cmds 库处理 MEL 组件,但对于之前通过 C++ API 访问的函数,你将想要使用 maya.OpenMaya。
它(maya.cmds)是围绕许多 Maya 用户已经习惯的 MEL 命令的一个轻量级包装器,并且它有一个好处,那就是由 Autodesk 正式支持。然而,这并不是访问 MEL 命令的唯一方式。还有一个第三方库 PyMEL(通过导入 pymel.core 访问)。PyMEL 的好处是更加“Pythonic”并提供更简洁的语法,但它不是由 Autodesk 直接支持的。它还在内置功能之上引入了额外的抽象层,这可能会导致性能下降。
访问特定命令的文档
Maya 是一个复杂的工具,它提供了一系列丰富的功能,每个功能都有相应的命令,可以通过脚本调用。每个命令都有自己的参数集,从易于理解到相当晦涩。
在编写 Maya 脚本(以及任何其他类型的编程)时,能够找到适当的文档并理解如何理解它至关重要。
如何做到这一点...
假设我们想要有关特定命令的更多信息,比如创建多边形立方体的命令。
查看命令帮助的一种方法是通过 Maya 的基于 Web 的命令帮助,您可以通过在 Maya 中转到帮助 | Python 命令参考来访问它。从那里,您可以点击“多边形”子部分或使用搜索框。
获取命令文档的另一种方法。您也可以直接从脚本编辑器窗口访问命令的文档。首先,使用 Maya 的界面执行相应的操作,例如调用热键并选择创建 | 多边形原语 | 立方体。
这将在脚本编辑器的输出部分显示相应的 MEL 命令,在本例中为“polyCube”。在脚本编辑器中,突出显示相关行,然后转到帮助 | 所选命令的帮助。这将打开一个浏览器窗口,显示该命令的文档。请注意,它将默认显示命令的 MEL 版本;对于 Python 版本,请点击窗口右上角的“Python”链接。
最后,您可以通过 Python 直接使用帮助命令检索有关命令的信息。尝试运行以下命令:
print(cmds.help('polyCube'))
这将生成一个列表,列出给定命令可用的标志,以及 Maya 期望每个标志的值类型,例如:
-sx -subdivisionsX Int
这意味着有一个名为“sx”或“subdivisionsX”的标志,它期望一个整数值。
它是如何工作的...
在您开发脚本时,您可能只想在浏览器窗口中打开 Python 命令参考。好的参考文档对于编写好的软件至关重要,您应该习惯于随时将参考文档放在手边。
还有更多...
您还可以使用帮助命令直接调用给定命令的基于 Web 的文档,例如:
cmds.help('polyCube', doc=True, language='python')
这将打开包含 polyCube 命令 Python 版本文档的网页。这绝对是一种访问帮助的笨拙方式,但如果您想为脚本用户提供一种直接从脚本的用户界面引用相关文档的简单方法,这可能是有用的。
理解创建、查询和编辑标志
关于 Maya 脚本的一个有点奇怪的地方是,同一个命令可以用多达三种不同的方式使用——创建、查询和编辑模式,具体细节因每个命令标志而异。这是由于 Python 功能性是围绕较老的基于 MEL 的脚本系统的一个包装,当你刚开始时可能会觉得有点困惑。尽管如此,了解三种模式之间的区别以及如何使用它们是很重要的。
准备工作
通过访问 帮助 | Python 命令参考 打开 Python 命令参考,并导航到 polyCube 命令的文档。
此外,请确保在 Maya 中打开脚本编辑器并处于 Python 选项卡。或者,你可以通过 Maya 的命令行运行示例命令;只需确保你在 Python 模式下运行而不是 MEL(点击 MEL 切换到 Python,点击 Python 返回 MEL)。
如何做到这一点...
首先,看一下属性列。你会看到每个标志都列出了 "C"、"E"、"Q" 和 "M" 的某种组合。这些指的是命令可以运行的不同方式,并具有以下含义:
-
C: "创建"标志仅在首次运行命令时相关,例如在最初创建多边形原形时
-
Q: "查询"标志在命令执行后可以查询,可以用来检索场景中某物的信息
-
E: "编辑"标志可以在事后编辑
-
M: "多重"标志可以在命令的单个实例中使用多次(例如,在创建曲线时指定多个点)
对于许多标志,你会看到创建、查询和编辑的全套功能,但通常至少有几个标志在一种或多种模式下不可访问。
让我们看看创建、编辑和查询在 polyCube 命令中的表现。
首先,让我们创建一个新的立方体并将结果存储在一个变量中,这样我们以后就可以使用它:
myCube = cmds.polyCube()
现在,让我们通过使用编辑模式来改变创建后的立方体的某些属性:
cmds.polyCube(myCube, edit=True, subdivisionsX=5)
这将导致我们使用第一个命令创建的立方体从默认状态(在 x 轴上没有细分)变为有五个细分。
现在,让我们使用查询模式将新的细分数量存储到一个变量中:
numberDivisions = cmds.polyCube(myCube, query=True, subdivisionsX=True)
print(numberDivisions)
你应该看到输出为 "5.0"。请注意,尽管多边形立方体的细分数量必须是整数,但 Maya 显示的是 "5.0"。
它是如何工作的...
对于查询和编辑模式,需要注意的是,你像平常一样运行命令(例如 cmds.polyCube),但有以下三个关键区别:
-
将对象名称作为第一个参数包含在内。这可以是直接作为字符串的名称
("pCube1",例如),或者是一个变量。 -
将
edit=True或query=True作为参数包含在内。 -
额外的参数,具体取决于你是在查询模式还是编辑模式下运行命令。
对于编辑模式,你需要指定你想要更改的属性名称以及新的值。对于查询模式,你只需要包含属性名称和 "=True"。想象一下,你是在说你想知道属性的值是 True。请注意,你一次只能查询一个标志。如果你需要查询多个值,请多次运行命令,每次更改传递的标志。
更多...
尽管许多最常用的属性可以在所有三种模式下使用,但有许多例子表明它们不能,主要是因为这样做没有意义。例如,在创建新对象时设置或关闭构造历史记录是完全合理的,并且在事后查询它也是合理的,但使用编辑模式来启用没有构造历史记录的对象的构造历史记录意味着什么?这将需要重建对象的整个历史,该对象自创建以来可能已经被以各种方式操作过。因此,"constructionHistory"(或"ch")标志只提供创建和查询选项。
你可能会觉得这有点笨拙,如果我们只是想设置新创建的立方体的细分数量,你是对的。然而,了解不同的命令模式很重要,这不仅是为了事后获取信息,而且因为它是构建用户界面元素和从它们获取信息的重要部分。
参见
我们将在本书的其余部分广泛使用查询模式来从用户界面元素中检索信息,从第二章 创建用户界面 开始。
将自定义文件夹添加到你的脚本路径
为了编写可重用的脚本,你需要将你的脚本保存到外部文件中,而为了做到这一点,你需要确保 Maya 知道它们的位置。
如何做到...
Maya 维护一个搜索脚本的位置列表,用于你在使用导入(Python)或源(MEL)命令时。如果你想查看完整的列表,你可以使用以下代码:
import sys
pathList = sys.path
for path in pathList:
print(path)
这将为你提供一个路径列表,包括以下内容:
/Users/[username]/Library/Preferences/Autodesk/maya/[maya version]/prefs/scripts
/Users/[username]/Library/Preferences/Autodesk/maya/[maya version]/scripts
/Users/[username]/Library/Preferences/Autodesk/maya/scripts
将你的脚本保存到这些文件夹中的任何一个,Maya 都可以找到它们。如果你愿意将所有脚本保存到这些文件夹中的任何一个,那是完全可以的。
然而,你可能想要将额外的文件夹添加到列表中。例如,你可能想在 我的文档 目录中的一个文件夹中保存你的脚本,可能像这样:
/Users/adrian/Documents/MayaScripting/examples/
注意,这是一个典型的 Macintosh 示例。在 Windows 机器上,它看起来可能更像是:
\Documents and Settings\[username]\MyDocuments\MayaScripting\examples
无论哪种方式,我们都需要告诉 Maya 去那里查找要执行的脚本。我们可以有几种方法来做这件事,但本着本书的主题,我们将使用 Python 来完成。
在脚本编辑器中运行以下代码:
import sys
sys.path.append('/Users/adrian/Documents/MayaScripting/examples')
这通过将 /Users/adrian/Documents/MayaScripting/examples 替换为你想要使用的任何文件夹来完成。
完成此操作后,你将能够导入该目录中存储的脚本。然而,每次启动 Maya 都必须输入前面的代码会相当令人沮丧。幸运的是,Maya 提供了一种方法,让我们在启动时执行自定义 Python 代码。
要使代码在 Maya 打开时执行,将其保存为名为 userSetup.py 的文件,并将其保存到以下位置(对于 Mac):
~/Library/Preferences/Autodesk/maya/<version>/scripts
或者对于 Windows,你可以保存到以下位置:
<drive>:\Documents and Settings\<username>\My Documents\maya\<version>\scripts
它是如何工作的...
userSetup.py 中包含的所有代码将在 Maya 启动时运行。
虽然上述操作不会改变 Maya 搜索脚本的基路径列表,但它会在 Maya 启动时将你的自定义文件夹添加到列表中,这在实际操作中效果相同。
还有更多...
你还可以通过创建 Maya.env 文件并将其保存到以下位置(在 Mac 机器上)来添加到你的路径中:
/Users/<username>/Library/Preferences/Autodesk/maya/version
或者
/Users/<username>/Library/Preferences/Autodesk/maya
在 Windows 机器上,保存到以下位置:
drive:\Documents and Settings\username\My Documents\maya\version
或者
drive:\Documents and Settings\username\My Documents\maya
关于 Maya.env 文件语法的具体信息,请参阅 Maya 的文档。然而,如果不小心编辑 Maya.env,可能会导致崩溃和系统不稳定,所以我建议依靠 userSetup.py。
编写和运行外部脚本
在这个菜谱中,我们将编写和运行我们的第一个实际脚本,作为一个外部 Python 文件。
准备工作
脚本编辑器窗口有点名不副实。虽然它是测试短代码片段的好方法,但对于任何类型的真实脚本开发来说都有些笨拙。为此,你需要有一个面向程序员的文本编辑器设置。市面上有很多选择,如果你正在阅读这本书,你很可能已经有一个你喜欢的编辑器了。无论是什么,确保它是面向编写代码的,并且以纯文本格式保存文件。
如何操作...
首先,我们需要一个脚本。在你的编辑器中创建一个新文件,并添加以下代码:
import maya.cmds as cmds
print("Imported the script!")
def makeObject():
cmds.polyCube()
print("Made a cube!")
现在将脚本保存为 myScript.py。完成此操作后,切换回 Maya,并从脚本编辑器或命令行运行以下内容:
import myScript
这将导致 Maya 读取该文件,你将在脚本编辑器输出中看到以下内容:
Imported the script!
然而,你将看不到一个新的立方体。这是因为我们(简单)脚本的实际功能是在名为 "makeObject" 的函数中定义的。
Maya 将你导入的每个 Python 文件视为其自己的模块,文件名即为模块名。一旦我们导入了一个文件,我们就可以通过调用 [moduleName].[function name] 来调用其中的函数。对于前面提到的示例,这意味着:
myScript.makeObject()
运行它,你应该会看到一个新创建的立方体出现在脚本编辑器的输出中,以及 "Made a cube!"。
现在,让我们尝试修改我们的脚本。首先,删除我们刚刚创建的立方体,然后切换到你的文本编辑器,并将脚本更改为以下内容:
import maya.cmds as cmds
def makeObject():
cmds.polySphere()
print("Made a sphere!")
切换回 Maya,再次使用以下命令运行脚本:
myScript.makeObject()
你会发现,尽管我们想要一个漂亮的球体,但最终我们还是得到了一个立方体。那是因为当你要求 Maya 执行它已经执行过的脚本时,它将默认重新运行之前运行的相同代码。为了确保我们得到最新的脚本版本,我们首先需要运行以下代码:
reload(myScript)
这将强制 Maya 重新加载文件。请注意,重新加载的参数不是文件名本身(在这种情况下是 myScript.py),而是该文件定义的模块名("myScript")。
完成这些操作后,你再次尝试:
myScript.makeObject()
这次,你会看到一个正确的多边形球体,正如我们预期的。
它是如何工作的...
似乎导入脚本并调用其函数是一个不必要的额外步骤,你确实可以让脚本自动执行代码(如通过调用 print("Imported Script!") 所演示的)。然而,将所有功能封装在函数中是一种更好的实践,因为它使得处理大型脚本变得更加容易。
如果你希望在脚本每次运行时都执行某些功能,最好定义一个名为 "main" 的函数,并在脚本的最后一行调用它。看看以下示例:
import maya.cmds as cmds
def makeObject():
cmds.polyCube()
print("Made a cube!")
makeObject()
这将定义 makeObject() 函数,然后在脚本的最后一行执行它。
在编写脚本时,每次都需要重新加载、导入和运行脚本会变得非常繁琐。一个简单的方法是将以下内容输入到脚本编辑器中:
import myScript
reload(myScript)
myScript.myCommand()
完成这些操作后,使用 文件 | 保存脚本到架... 来为自己提供一个按钮,以便轻松重新运行脚本的最新版本。请注意,前面的代码中既包含了导入也包含了重新加载。这是为了确保代码在第一次运行以及后续运行时都能正常工作。"导入" 命令确保模块至少被加载过一次(对于新脚本或在重启 Maya 时是必要的),而 "重新加载" 命令确保我们运行的是最新版本(如果我们已经对脚本进行了修改且 Maya 仍在运行)。
还有更多...
如果你有一个定义了大量功能的脚本,并且你不想每次都输入模块名,你可以使用我们用于 maya.cmds 的相同技巧来简化一些。具体来说,你可以使用 "as" 语法来提供一个更短的名字。例如,我可能做过以下操作:
import myScript as ms
ms.makeObject()
这将产生完全相同的效果。
使用 Python 调用 MEL 脚本
Maya 提供了两种不同的语言,可以通过脚本创建自定义功能——即 Maya 内嵌语言(MEL)脚本和 Python。在实践中,你只会想使用其中一种,而在两种中,Python 提供了更好、更灵活的体验。
然而,拥有在 Maya 添加 Python 功能之前编写的遗留脚本并不罕见。虽然“正确”的解决方案是使用 Python 重写这些脚本,但并不总是有足够的时间这样做。在这些情况下,有时能够从 Python 脚本中调用遗留的、基于 MEL 的功能会很有帮助。
准备工作
虽然 Python 无疑是创建新功能更好的方式,但有时您可能有一些用 MEL 编写的旧脚本,您希望将其结合进来。
最佳选择是将脚本重写为 Python,但如果脚本复杂或您没有时间,可能更容易在 Python 脚本中调用 MEL 功能。这样,您可以在不重新发明轮子的同时结合遗留功能。
如何做到这一点...
对于这个配方,您需要 MEL 脚本。如果您没有现成的,打开一个新文件并输入以下内容:
global proc myMELScript()
{
polyCube;
print("Hello from MEL!");
}
将此保存为myMELScript.mel到您的maya/scripts目录中。虽然我们不会深入探讨 MEL 的细节,但请注意,文件名与我们要定义的函数同名。大多数 MEL 脚本都会遵循这个约定。还要注意,在每行末尾包含分号。虽然 Python 不需要它们,但 MEL(以及许多其他语言)需要。
一旦您有了这个,创建一个新文件并将其命名为runMEL.py。输入以下内容:
import maya.cmds as cmds
import maya.mel as mel
def runMEL():
print("Running MEL from Python")
mel.eval("source myMELScript;")
mel.eval("myMELScript;")
runMEL()
保存脚本并使用以下命令运行:
import runMEL
因为我们的脚本最后一行调用了 runMEL 命令,它将自动生效。您应该在脚本编辑器中看到一个新的立方体,以及以下输出:
Running MEL from Python
Hello from MEL!
它是如何工作的...
在这个例子中,我们导入了maya.cmds和maya.mel。maya.mel库提供了将 Python 与 MEL 接口的支持,其中最有用的命令之一是 eval 函数,它接受任意字符串并尝试将其作为 MEL 命令运行。在先前的例子中,我们这样做两次,第一个命令是:
source myMEL;
源命令与重新加载执行相同的功能,即确保 Maya 将重新读取整个源文件,而不是重新运行可能过时的版本。这通常不会有什么影响,因为只有在您正在修改 MEL 脚本时才需要这样做(而且希望您不会这样做,最好使用 Python!),但以防万一,包含它是好事。
一旦我们这样做,我们实际上会使用以下命令运行 MEL 脚本:
myMEL;
第二章:创建用户界面
在本章中,我们将带您参观 Maya 的用户界面元素集合,并学习如何使用它们来创建您和您的团队成员都乐于使用的界面。以下主题将涵盖:
-
创建基本窗口
-
简单控件 – 创建一个按钮
-
从控件中检索输入
-
使用类来组织 UI 逻辑
-
使用嵌套布局
-
使用标签和滚动
-
向您的 UIs 添加菜单
简介
虽然为您的脚本创建图形用户界面(GUI)并非必需,但在几乎所有情况下,您都可能想要一个 GUI。非常常见的是,您会发现自己正在创建旨在供您的团队成员使用的脚本,其中一些人可能不太熟悉命令行工具。
在本章中,我们将探讨如何创建窗口,用界面元素填充它们,并将这些元素链接到 Maya 中的其他功能。
创建基本窗口
所有优秀的用户界面都是从窗口开始的。在这个例子中,我们将创建一个简单的窗口,并使用文本标签控件添加一个简单的消息。
我们最终会得到以下类似的结果:

如何做到这一点...
首先,在您的脚本目录中创建一个新文件,并将其命名为基本的Window.py。
添加以下代码:
import maya.cmds as cmds
def showUI():
myWin = cmds.window(title="Simple Window", widthHeight=(300, 200))
cmds.columnLayout()
cmds.text(label="Hello, Maya!")
cmds.showWindow(myWin)
showUI()
如果您运行脚本,您应该会看到一个包含文本Hello, Maya!的小窗口。
它是如何工作的...
要创建一个窗口,您需要使用窗口命令。
myWin = cmds.window(title="Simple Window", widthHeight=(300, 200))
虽然所有参数都是可选的,但您通常会默认包含一些参数。在这里,我们将窗口标题设置为“简单窗口”,并将窗口大小设置为 300 像素宽、200 像素高。请注意,我们还保存了命令的结果到变量myWin。这是使用showWindow命令所必需的。稍后我们将详细介绍这一点。
还有一个额外的要求,那就是为了向窗口添加一个元素,你必须首先指定一个布局。布局负责在给定区域(无论是窗口还是另一个布局)内排列项目。如果你没有为 Maya 提供布局,它将无法正确地定位你添加的任何控件,并且你的脚本将出错。在这个例子中,我们使用的是columnLayout,它将把所有我们添加的控件排列在一个单独的垂直列中。我们使用以下方式将布局添加到窗口中:
cmds.columnLayout()
一旦我们创建了一个窗口并指定了布局,我们就可以开始添加控件。在这种情况下,我们使用的是仅向窗口添加一些文本的文本控件。虽然您通常不会单独使用文本控件(将它们与其他控件一起使用以提供标签或描述性文本的情况要常见得多),但它是一个典型(尽管简单)控件的良好示例。
cmds.text(label="Hello, Maya!")
到目前为止,我们已经完成了我们的界面,但在 Maya 中创建一个窗口实际上并不会显示任何内容。为了使其在 Maya 的界面中显示,我们还需要使用showWindow命令显式地显示它。这样做的原因是,你通常不希望在窗口拥有所有你想要的控件和其他 UI 元素之前就显示它。然而,为了创建一个控件,你必须首先有一个窗口来添加它们。Maya 通过让你这样做来解决这个问题:
-
创建窗口。
-
添加你的控件。
-
在添加所有控件后显示窗口。
这就是为什么,保存window()命令的结果到一个变量中非常重要,这样我们就可以告诉 Maya 它应该向用户显示哪个窗口。将这些放在一起,就给出了我们 showUI 函数的最后一行:
cmds.showWindow(myWin)
还有更多...
注意,一旦创建了一个布局,它就成为了活动上下文,以便添加控件。你当然可以在单个窗口中拥有多个布局(甚至可以嵌套它们),但总是只有一个当前布局,Maya 会将新创建的控件插入其中。
这个示例的一个问题是,多次运行脚本会导致多个窗口副本,这通常不是你想要的。对于大多数用途,你希望确保在任何时候只有一个 UI 实例打开。
要做到这一点,我们需要:
-
为我们的窗口选择一个独特的名称
-
在创建窗口之前,检查是否已经存在具有该名称的窗口
-
如果已经存在一个同名窗口,则删除它
-
使用窗口命令创建窗口,并传递名称
在选择名称时,确保它不太可能与用户可能使用的其他脚本冲突。像“我的窗口”或“主窗口”这样的通用名称很可能会引起冲突;拥有一个独特的名称,如"CharacterRigControl",会更好。为了使其更加完美,可以在名称的开头添加你的首字母,或者你公司的首字母(例如,"ahCharacterRig")。请注意,名称(不会显示给用户)与标题(会显示)是不同的,因此拥有一个长或难以操作的名字是完全可以的。只需确保它是唯一的。
一旦你有了名称,我们希望首先测试一下是否有一个同名窗口存在。我们可以使用窗口命令和exists标志来完成这个操作。如果我们确实发现存在一个同名窗口,我们希望使用deleteUI命令将其删除:
if (cmds.window("ahExampleWindow", exists=True)):
cmds.deleteUI("ahExampleWindow")
最后,当我们创建一个新的窗口时,我们将确保将我们想要的名称作为第一个参数传递,这将给窗口赋予所需的名称。
myWin = cmds.window("ahExampleWindow", title="Simple Window", widthHeight=(300, 200))
或者,我们也可以在已经存在具有给定名称的窗口时停止脚本,但前面提到的方法更为常见。如果用户调用你的脚本,他们很可能希望从一个全新的开始,因此替换旧窗口通常是最佳选择。
简单控件——创建一个按钮
创建窗口只是开始。为了创建一个合适的界面,我们需要添加控件并将它们与功能关联起来。在这个例子中,我们将重新访问我们的好朋友polyCube命令,并将其与按钮点击关联起来。
生成的 UI(及其输出)将类似于以下内容:

如何做到这一点...
创建一个新的脚本并将其命名为buttonExample.py。添加以下代码:
import maya.cmds as cmds
def buttonFunction(args):
cmds.polyCube()
def showUI():
myWin = cmds.window(title="Button Example", widthHeight=(200, 200))
cmds.columnLayout()
cmds.button(label="Make Cube", command=buttonFunction)
cmds.showWindow(myWin)
showUI()
运行脚本,你应该看到一个 200x200 像素的窗口,里面有一个按钮。按下按钮将使用默认参数创建一个多边形立方体。
它是如何工作的...
为了从我们的 UI 触发功能,我们首先需要创建一个函数来包含我们想要触发的功能。我们在buttonFunction函数中这样做:
def buttonFunction(*args):
cmds.polyCube()
在这种情况下,我们只是在创建一个多边形立方体。请注意,即使我们没有使用它们,函数也接受参数。这实际上是必要的,因为当 Maya 从控制中触发函数时,它会将信息传递给相应的函数。假设我们编写一个不带参数的函数,如下所示:
def buttonFunction():
cmds.polyCube()
当我们尝试运行脚本时,我们会得到以下错误:
# Error: buttonFunction() takes no arguments (1 given)
有时候我们会想要使用传递进来的信息,即使我们完全打算忽略它,我们也必须编写 UI 驱动的函数来接受参数。
注意
*args语法是一点有用的 Python,允许传递可变数量的参数。技术上,真正重要的是;;*myEpicCollectionOfArguments同样可以工作,但*args是通用约定。
一旦我们有了想要触发的函数,我们就可以按照常规方式设置一个窗口,创建它并添加一个columnLayout:
def showUI():
myWin = cmds.window(title="Button Example", widthHeight=(200, 200))
cmds.columnLayout()
接下来,我们使用以下方式添加按钮本身:
cmds.button(label="Make Cube", command=buttonFunction)
这相当直接——我们使用label参数设置按钮内显示的文本,并使用command(或“c”)参数设置按下时执行的命令。请注意,函数名称后面没有括号。这是因为我们实际上并没有调用函数;我们只是将函数本身作为命令标志的值传递。我们包括括号,如下所示:
cmds.button(label="Make Cube", command=buttonFunction()) # (usually) a bad idea
这会导致函数被调用,并且它的返回值(而不是函数本身)被用作标志的值。这几乎肯定不是你想要的。唯一的例外是如果你恰好有一个创建函数并返回它的函数,这在某些情况下可能很有用。
剩下的只是以正常方式显示我们的窗口:
cmds.showWindow(myWin)
还有更多...
虽然这是使用按钮最常见的方式,但在某些特定情况下还有一些其他选项可能很有用。
例如,enable标志可以是一个很好的方法来防止用户执行他们不应该能做的操作,并提供反馈。假设我们已经创建了一个按钮,但它在用户执行其他操作之前不应该处于活动状态。如果我们将 enable 标志设置为 False,按钮将显示为灰色,并且不会响应用户输入:
myButton = cmds.button(label="Not Yet", enable=False)
之后,你可以通过使用编辑模式将 enable 标志设置为 True 来激活按钮(或其他控件),如下所示:
cmds.button(myButton, edit=True, enable=True)
在适当的时候才激活控件,可以是一个很好的方法使你的脚本更加健壮且易于使用。
从控件中检索输入
虽然你经常需要添加单方向控件(如按钮)来在用户输入时触发函数,但你也会经常需要在采取行动之前从用户那里检索信息。在这个例子中,我们将探讨如何从字段控件中获取输入,无论是整数还是浮点数。
完成的脚本将创建指定数量的多边形球体,每个球体具有指定的半径。生成的 UI 将看起来如下:

使用之前提到的设置(4 个球体,每个球体的半径为 0.5 单位)按下制作球体按钮将导致沿 x 轴出现一串球体:

如何做到...
创建一个新的脚本,并将其命名为makeSpheres.py。添加以下代码:
import maya.cmds as cmds
global sphereCountField
global sphereRadiusField
def showUI():
global sphereCountField
global sphereRadiusField
myWin = cmds.window(title="Make Spheres", widthHeight=(300, 200))
cmds.columnLayout()
sphereCountField = cmds.intField(minValue=1)
sphereRadiusField = cmds.floatField(minValue=0.5)
cmds.button(label="Make Spheres", command=makeSpheres)
cmds.showWindow(myWin)
def makeSpheres(*args):
global sphereCountField
global sphereRadiusField
numSpheres = cmds.intField(sphereCountField, query=True, value=True)
myRadius = cmds.floatField(sphereRadiusField, query=True, value=True)
for i in range(numSpheres):
cmds.polySphere(radius=myRadius)
cmds.move((i * myRadius * 2.2), 0, 0)
showUI()
运行脚本,在两个字段中输入一些值,然后点击按钮。你应该看到一条整齐的、沿着 x 轴运行的球体线。
它是如何工作的...
这里有几个不同的事情在进行中,所有这些都需要从用户那里获取信息。首先,我们创建两个变量来保存对控件的全局引用。
import maya.cmds as cmds
sphereCountField
sphereRadiusField
我们需要为字段变量设置变量,因为我们需要调用相应的函数两次——一次是在创建模式(创建控件)中,再次是在查询模式中确定当前值。我们还想让这些变量具有全局作用域,这样我们就可以有单独的函数来创建 UI 并实际执行操作。
变量或函数的“作用域”指的是它被定义的上下文。如果一个变量在任何函数外部定义,它就是全局的,这意味着它始终可访问。然而,在函数内部定义的变量是局部的,这意味着它们只存在于定义它们的函数中。由于我们需要从两个不同的函数中引用我们的控件,我们需要确保它们具有全局作用域。
注意
并非严格必要在脚本顶部声明变量,就像我这里所做的那样。我们可以直接在各个函数中引用它们,只要我们小心地包含全局关键字,它仍然可以工作。
然而,我认为如果我们一开始就声明所有全局变量,会使事情更容易理解。
接下来,我们设置窗口的用户界面。请注意,我们重复了用于声明全局变量的行。这是必要的,以便告诉 Python 我们想要使用全局作用域变量:
def showUI():
global sphereCountField
global sphereRadiusField
如果我们省略了global sphereCountField,我们仍然会得到一个名为sphereCountField的变量,其值由intField命令的输出设置。然而,这个变量将是局部作用域的,并且只能在showUI()函数内部访问。
在这种情况下,global sphereCountField几乎可以被视为一个导入语句,因为它将全局变量引入了 showUI 函数的作用域。我们在makeSpheres函数中也做同样的事情,以确保我们使用相同的变量。
接下来,我们使用window()和columnLayout()命令设置窗口,就像我们过去做的那样:
myWin = cmds.window(title="Make Spheres", widthHeight=(300, 200))
cmds.columnLayout()
完成这些操作后,我们可以使用两个字段来收集用户的输入。由于球体的数量应该始终是整数,并且我们至少应该创建一个球体,我们使用intField并将最小值设置为 1:
sphereCountField = cmds.intField(minValue=1)
对于球体的半径,我们希望允许非整数值,但可能还想确保一个合理的最小尺寸。为此,我们创建一个最小值为 0.5 的floatField。以下是我们的代码:
sphereRadiusField = cmds.floatField(minValue=0.5)
最后,我们添加一个按钮来触发球体的创建并使用showWindow()命令显示窗口。
转到创建球体的函数中,我们首先(再次)告诉 Python 我们想要使用我们的两个全局变量,如下所示:
def makeSpheres(*args):
global sphereCountField
global sphereRadiusField
完成这些操作后,我们检索intField和floatField的当前值。在两种情况下,我们通过重新运行用于创建控件的相同命令来实现,但有一些差异:
-
我们将控制的名字(在我们创建它时保存的)作为第一个参数传递。
-
我们设置
query=True以指示 Maya 我们想要检索有关控件的信息。 -
我们设置
value=True以指示我们想要检索的特定属性是控件的价值。
将所有这些放在一起,我们得到以下内容:
numSpheres = cmds.intField(sphereCountField, query=True, value=True)
myRadius = cmds.floatField(sphereRadiusField, query=True, value=True)
看起来将这两行合并成以下内容可能更好:
global numSpheres = cmds.intField(sphereCountField, query=True, value=True)
然而,这实际上并不起作用,因为 Python 处理全局变量的方式。Python 要求全局变量的声明必须与设置变量值的任何命令保持分离。
一旦我们知道要创建多少个球体以及每个球体的大小,我们就使用 for 循环来创建和定位它们:
for i in range(numSpheres):
cmds.polySphere(radius=myRadius)
cmds.move((i * myRadius * 2.2), 0, 0)
注意
循环允许你多次重复相同的代码。Python 的实现方式与其他大多数语言略有不同,它们总是遍历某种类型的列表。这意味着如果我们想执行 X 次操作,我们必须有一个包含 X 个项目的列表。为此,我们将需要内置的 range() 函数。通过将 numSpheres 传递给 range(),我们要求 Python 创建一个从 0 开始到(numSpheres-1)的数字列表。然后我们可以使用这个列表与 for 关键字一起设置我们的索引变量(i),使其等于列表中的每个值,在这种情况下意味着从 0 步进到(numSpheres-1)。
注意,我们使用半径标志设置每个球体的半径。我们还使用移动函数将每个球体与其邻居稍微分开,比它们的直径(myRadius * 2.2)略大。默认情况下,移动命令将影响当前选定的对象(或对象)。由于 polySphere 命令将创建的球体作为唯一选定的对象留下,因此我们将移动这个对象。
默认情况下,移动命令将接受三个数字来指定移动所选对象(或对象)的量——每个轴一个。移动命令还有许多其他用法;请务必查阅文档以获取详细信息。
使用类来组织 UI 逻辑
使用全局变量是允许脚本的不同部分相互通信的一种方式,但还有更好的方法。与其使用全局变量,不如使用自定义类来组织你的脚本。
为你的脚本创建一个类不仅可以让你轻松地从各种函数中访问 UI 元素,而且还可以使你轻松地整齐地包含其他类型的数据,这在更高级的脚本中非常有用。
如何做到这一点...
创建一个新的脚本并将其命名为 SpheresClass.py。添加以下代码:
import maya.cmds as cmds
class SpheresClass:
def __init__(self):
self.win = cmds.window(title="Make Spheres", widthHeight=(300,200))
cmds.columnLayout()
self.numSpheres = cmds.intField(minValue=1)
cmds.button(label="Make Spheres", command=self.makeSpheres)
cmds.showWindow(self.win)
def makeSpheres(self, *args):
number = cmds.intField(self.numSpheres, query=True, value=True)
for i in range(0,number):
cmds.polySphere()
cmds.move(i*2.2, 0, 0)
SpheresClass()
运行脚本,你应该会得到一个窗口,允许你沿着 x 轴创建一串多边形球体。
它是如何工作的...
脚本的整体布局与我们之前所做的是相似的,即我们有一个用于设置界面的函数,另一个用于实际执行工作的函数。然而,在这种情况下,我们将所有内容都封装在一个类中,具体如下:
class SpheresClass:
注意,类的名称已经被大写,考虑到我们之前所有的函数都使用小写,这可能会显得有些奇怪。尽管这并不是必需的,但通常的惯例是将类名大写,因为这有助于区分类和函数。否则,调用函数可能会看起来非常类似于实例化一个类,从而导致混淆。以下是我们有以下代码:
myResult = myFunction() # run a function and store the result in myResult
myInstance = MyClass() # create a new instance of the MyClass class and name it
# myInstance
实例化一个类意味着你创建了这个类的一个全新的副本,这个新副本被称为这个类的"实例"。定义一个类和实例化它是两个不同的动作。以"类"关键字开始的整个代码块构成了类的定义,并定义了类的所有属性和能力。它可以被视为该类的蓝图。然而,为了实际使用一个类,我们必须实际创建一个。一旦你定义了一个类,你可以创建尽可能多的实例,每个实例都有自己的属性。类定义就像产品的 CAD 文件,而实例就像实际物理产品,它从装配线上滚下来。
一旦我们有一个类,我们可以通过添加函数来向它添加功能。我们至少需要创建一个名为__init__的函数,该函数将负责初始化每个类实例。这个函数将在每次调用类实例时自动调用。
注意,__init__函数接受一个参数,我们将其标记为"self"。当 Python 实例化一个类时,它总是将实例本身的引用传递给所有成员函数。我们可以称它为任何我们想要的名称,但"self"是惯例,我们将遵守这个惯例。
在__init__函数中,我们将完成所有我们需要做的设置 UI 的工作。在这种情况下,我们将创建一个字段和一个按钮。我们将字段的引用存储在实例变量中,作为 self 对象的属性(记住,self 只是类实例本身)。这样做将允许我们在脚本中稍后检索控件值:
self.numSpheres = cmds.intField(minValue=1)
类似地,当我们想要将我们的控件与实际功能关联起来时,我们需要在函数前加上"self."来引用我们的类方法。我们在下一行的按钮代码中这样做:
cmds.button(label="Make Spheres", command=self.makeSpheres)
将变量作为 self 的属性设置,将使它们在类内的其他函数中可访问。注意,我们存储了字段的引用,但没有存储按钮的引用;这是因为我们不太可能想要查询关于按钮的任何内容,或者在其创建后更改它。在这种情况下,使用局部变量或根本不存储结果都是可以的。
一旦我们有了字段和按钮,我们就显示窗口。现在我们准备添加makeSpheres函数:
def makeSpheres(self, *args):
注意,函数签名包括作为第一个参数的"self",以及作为第二个参数的"*args",它是任何传递值的通配符。这是 Python 如何将类实例传递给每次调用的所有成员函数的另一个例子。
makeSpheres函数的其余代码与我们在非类示例中写的非常相似。我们使用查询模式来检索intField中的数字,然后制作这么多球体,通过将每个球体移动到半径的相应倍数来使它们分布得很好。
number = cmds.intField(self.numSpheres, query=True, value=True)
for i in range(0,number):
cmds.polySphere()
cmds.move(i*2.2, 0, 0)
通过这样,我们就完成了类的定义。然而,我们还需要实际创建一个实例,以便看到任何变化。脚本的最后一条命令正是这样做的:
SpheresClass()
这创建了我们SpheresClass类的一个新实例,并且在这个过程中运行了__init__函数,该函数反过来设置我们的 UI 并将其显示给用户。
还有更多...
面向对象编程(OOP)是一个很大的主题,全面处理所有细节超出了本书的范围。如果你长时间使用 Python(或任何其他面向对象的语言),你可能会熟悉它。
如果这是你第一次看到这个,请务必阅读 Python 文档中的类部分。面向对象编程(OOP)的实践可能一开始看起来是很多不必要的开销,但它们最终会使解决复杂问题变得更加容易。
使用嵌套布局
非常常见,你想要创建的界面不能仅用一个布局实现。在这种情况下,你需要将布局嵌套在彼此内部。
在这个例子中,我们将在单个columnLayout内创建rowLayouts。每个rowLayout将允许我们在水平方向上并排放置两个控件(在这种情况下,一些文本和intField),而父columnLayout将垂直堆叠组合的文本/字段对。
最终的结果将类似于这样:

如何做到这一点...
创建一个新的脚本,命名为nestedLayouts.py。添加以下代码:
import maya.cmds as cmds
class NestedLayouts:
def __init__(self):
self.win = cmds.window(title="Nested Layouts", widthHeight=(300,200))
cmds.columnLayout()
cmds.rowLayout(numberOfColumns=2)
cmds.text(label="Input One:")
self.inputOne = cmds.intField()
cmds.setParent("..")
cmds.rowLayout(numberOfColumns=2)
cmds.text(label="Input Two:")
self.inputTwo = cmds.intField()
cmds.setParent("..")
cmds.showWindow(self.win)
NestedLayouts()
运行脚本,你应该看到两行,每行都有一些文本和一个intField。
它是如何工作的...
在这个例子中,我们首先创建一个columnLayout,就像我们在之前的例子中所做的那样。然后,我们立即创建另一个布局,这次是一个行布局:
cmds.columnLayout()
cmds.rowLayout(numberOfColumns=2)
当你创建一个布局时,它立即成为你创建的任何其他元素(无论是控件还是其他布局)的默认父级。因此,在这个例子中,我们有一个包含两个列布局的columnLayout。
完成这些后,我们可以向行布局添加元素,这可以通过以下几行代码实现:
cmds.text(label="Input One:")
self.inputOne = cmds.intField()
到目前为止,我们的第一行布局已经填满,因为我们创建了一个有两个列的布局,并且已经向其中添加了两个控件。如果我们尝试添加另一个控件,我们会得到一个类似于以下错误的错误:
# Error: RuntimeError: file /nestedLayouts.py line 13: Too many children in layout: rowLayout21
为了继续向我们的 UI 添加元素,我们需要回到columnLayout的上一级。在任何给定时刻,Maya 都会添加控件到一个默认的父级,且只有一个。每次你创建一个新的布局,它就会自动成为默认的父级。有时,你可能需要直接更改默认的父级,这可以通过setParent命令实现,如下所示:
cmds.setParent("..")
使用setParent并传递".."作为参数将在布局的层次结构中向上移动一个级别。在这种情况下,这意味着我们从行布局回到列布局。一旦我们这样做,我们就可以创建第二个行布局,再次包含两个列。然后我们可以自由地添加一个包含文本字段和整数字段的第二个组:
cmds.setParent("..") # move one level up the UI hierarchy
cmds.rowLayout(numberOfColumns=2) # add a second rowLayout
cmds.text(label="Input Two:") # add a text control to the row
self.inputTwo = cmds.intField() # add an intField to the row
还有更多...
在层次结构中跳转可能会有些繁琐。如果你要添加多个控件并为其添加标签文本,最好为你的脚本类创建一个辅助函数来添加新的控件。
下面是一个可能的样子示例:
def addLabeledIntField(self, labelText):
cmds.rowLayout(numberOfColumns=2)
cmds.text(label=labelText)
newField = cmds.intField()
cmds.setParent("..")
return newField
在这里,我们接收用于标签的文本,并返回对新创建的 intField 的引用。使用上述方法重写我们的示例将得到以下类似的结果:
def __init__(self):
self.win = cmds.window(title="Nested Layouts", widthHeight=(300,200))
cmds.columnLayout()
self.inputThree = self.addLabeledIntField("Input Three")
self.inputFour = self.addLabeledIntField("Input Four")
cmds.showWindow(self.win)
这确实整洁得多。
注意,我们的 addLabeledIntField 接受两个参数,但当我们调用它时,我们只传递一个参数。这是由于 Python 处理类的方式;每个类方法总是接收对类本身的引用。所以,我们想要使用的任何参数都从第二个开始。
使用标签和滚动
在这个例子中,我们将探讨如何创建包含标签的 UI 以及如何提供可滚动的容器。
我们的 UI 将包含两个水平排列的标签页,每个标签页包含一个可滚动的包含 20 个按钮的列。最终结果将类似于以下:

如何做到...
创建一个新的脚本,命名为tabExample.py,并添加以下代码:
import maya.cmds as cmds
class TabExample:
def __init__(self):
self.win = cmds.window(title="Tabbed Layout", widthHeight=(300, 300))
self.tabs = cmds.tabLayout()
# add first tab
firstTab = cmds.columnLayout()
cmds.tabLayout(self.tabs, edit=True, tabLabel=[firstTab, 'Simple Tab'])
cmds.button(label="Button")
cmds.setParent("..")
# add second tab, and setup scrolling
newLayout = cmds.scrollLayout()
cmds.tabLayout(self.tabs, edit=True, tabLabel=[newLayout, 'Scrolling Tab'])
cmds.columnLayout()
for i in range(20):
cmds.button(label="Button " + str(i+1))
cmds.setParent("..")
cmds.setParent("..")
cmds.showWindow(self.win)
TabExample()
它是如何工作的...
创建标签布局非常简单;所需做的只是调用tabLayout函数。
self.tabs = cmds.tabLayout()
注意,我们将tabLayout命令的输出保存到实例变量中,稍后我们需要用到它。所以现在我们有了标签布局,但我们还没有准备好添加任何控件。这是因为标签布局实际上不能直接包含控件;它只是用来包含其他布局的。
对于第一个标签页,我们将保持简单,只添加一个columnLayout:
firstTab = cmds.columnLayout()
注意,我们还存储了输出,在这个例子中是列布局的名称(例如“columnLayout17”或类似)。现在我们可以开始添加控件了,但在此之前我们还有一件事要做。
默认情况下,在标签布局的实际标签中显示的文本将是子布局的名称。这几乎永远不会是你想要的;你通常会想要给你的标签页提供一些好理解的标签,而不是让它们保留像“columnLayout23”和“scrollLayout19”这样的名称。
要做到这一点,我们需要编辑我们的标签布局并使用tabLabel参数。tabLabel参数期望一个包含两个字符串的数组,其中第一个字符串是标签布局的子项名称(在本例中,是我们的列布局),第二个是要显示的文本。将这些内容组合起来,我们得到以下结果:
cmds.tabLayout(self.tabs, edit=True, tabLabel=[firstTab, 'Simple Tab'])
我们在编辑模式下调用 tabLayout 命令,直接针对我们的标签页布局(我们将其存储在self.tabs变量中),并将输入设置为 tabLabel,这样我们给 columnLayout 分配的标签是“简单标签”。
接下来,我们添加一个单独的按钮,这样我们就有一些内容在标签页内:
cmds.button(label="Button")
到目前为止,我们已经完成了第一个标签页,并准备开始第二个标签页。但在我们这样做之前,我们需要在层次结构中向上跳一级,这样我们就可以向标签页布局添加新内容,而不是继续添加到我们在其中创建的列布局。我们使用setParent命令来完成这个操作:
cmds.setParent("..")
现在我们准备开始第二个标签页。这次,我们将添加一个滚动布局,如下所示:
newLayout = cmds.scrollLayout()
再次编辑原始标签页布局,以便第二个标签页有一个合适的名称。
cmds.tabLayout(self.tabs, edit=True, tabLabel=[newLayout, 'Scrolling Tab'])
为了完成整个设置,我们将在滚动布局内创建一个列布局,并添加一些按钮。
cmds.columnLayout()
for i in range(20):
cmds.button(label="Button " + str(i+1))
最后,我们将使用setParent两次(一次用于列布局,再次用于滚动布局)来回到标签页布局的层次结构:
cmds.setParent("..")
cmds.setParent("..")
如果我们想添加更多标签页,我们现在已经准备好了。
还有更多...
如果你需要知道当前哪个标签页被选中,你可以通过selectTabIndex或 sti 标志来查找。需要注意的一个问题是返回的数字是基于 1 索引的,而不是你可能期望的 0。如果你确实收到了 0,这意味着相关的标签页布局没有子项:
currTab = cmds.tabLayout(self.tabs, query=True, selectTabIndex=True)
你也可以使用selectTabIndex来设置当前活动的标签页。例如,如果我们想确保我们的示例从第二个标签页开始选中,我们可以在__init__函数中添加以下行:
cmds.tabLayout(self.tabs, edit=True, selectTabIndex=2)
在构建复杂的 UI 时,能够根据当前活动界面部分改变行为,或者以不同的部分显示来启动脚本,可以是一种使你的脚本更加响应和易于使用的好方法。
向你的 UIs 添加菜单
对于更复杂的脚本,在窗口顶部添加一个下拉菜单可能会有所帮助。例如,你可能希望你的脚本支持自定义配置文件,并允许用户将当前设置保存到磁盘,或者加载以前保存的设置。在这种情况下,实现文件菜单并带有保存和加载子选项可能是一个非常用户友好的选择。
在这个例子中,我们将创建一个带有自己菜单的窗口,以及查看如何通过选项框提供用户额外选项,就像 Maya 的内置菜单一样。

如何做到这一点...
创建一个新的脚本,并将其命名为customMenu.py。我们再次将创建一个自定义类来处理我们的 UI 创建和功能:
import maya.cmds as cmds
class CustomMenu:
def __init__(self):
self.win = cmds.window(title="Menu Example", menuBar=True, widthHeight=(300,200))
fileMenu = cmds.menu(label="File")
loadOption = cmds.menuItem(label="Load")
saveOption = cmds.menuItem(label="Save")
cmds.setParent("..")
objectsMenu = cmds.menu(label="Objects")
sphereOption = cmds.menuItem(label="Make Sphere")
cubeOption = cmds.menuItem(label="Make Cube")
cmds.setParent("..")
cmds.columnLayout()
cmds.text(label="Put the rest of your interface here")
cmds.showWindow(self.win)
CustomMenu()
如果你运行这段代码,你会得到一个带有两个项目菜单(文件和对象)的窗口,每个菜单都提供了两个选项。
要在用户选择选项时实际发生某些事情,我们需要为我们的 menuItem 控件中的一个或多个提供命令标志的值,如下所示(一些前面的代码已被删除以缩短示例):
def __init__(self):
# set up the window and add any additional menu items
# before the Objects menu
objectsMenu = cmds.menu(label="Objects")
sphereOption = cmds.menuItem(label="Make Sphere", command=self.makeSphere)
cubeOption = cmds.menuItem(label="Make Cube", command=self.makeCube)
cmds.setParent("..")
# continue with the rest of the interface, and end with
# cmds.showWindow()
def makeSphere(self, *args):
cmds.polySphere()
def makeCube(self, *args):
cmds.polyCube()
这将使制作球体和制作立方体两个菜单项都能创建相应的多边形几何形状。
它是如何工作的...
创建菜单相当简单,主要只需要你:
-
在创建初始窗口时包含
menuBare=True选项。 -
使用
menu()命令添加一个或多个菜单。 -
对于每个菜单,添加一个或多个 menuItem 控件,为每个提供命令。
还有更多...
许多 Maya 的命令提供了两种方式来触发它们——默认方式和通过命令的相应选项框,它为用户提供额外的命令选项。你可以通过添加第二个 menuItem 直接在你想要添加选项框的 menuItem 之后,并将第二个 menuItem 的optionBox标志设置为 true 来实现相同的事情。
假设我们想要提供一个创建多边形球体的命令。我们希望默认半径为 1 单位,但我们还希望提供一个选项框,当用户选择时,将允许用户指定自定义半径。为此,我们可能需要在我们的脚本中添加以下内容:
self.menu = cmds.menu(label="Objects")
sphereCommandMI = cmds.menuItem(label="Make a Sphere", command=self.myCommand)
sphereCommandMIOption = cmds.menuItem(optionBox=True, command=self.myCommandOptions)
尽管我们创建了两个 menuItem 控件,但它们会被用户作为“对象”菜单中的单个条目呈现,尽管其中一个带有选项框。当你将optionBox=True标志添加到 menuItem 控件时,Maya 会向最近创建的 menuItem 添加一个选项。在创建 menuItem 并设置optionBox=True之前不先创建一个正常的 menuItem,Maya 将没有东西可以添加选项框,并会导致错误。
这可能看起来有点奇怪,但考虑到默认命令和选项框是两个独立的可点击区域,所以将它们作为独立的控件实现并不是完全不合理。
一旦我们设置了两个控件,我们想要确保它们都做类似的事情,但其中一个(选项框)提供额外的输入。一个简单的方法是使用promptDialog命令,它提供了一个简单的方法从用户那里获取单个值。要要求用户输入球体半径的值,我们可以做以下操作:
promptInput = cmds.promptDialog(title="Sphere Radius", message='Specify Radius:', button=['OK', 'CANCEL'], defaultButton='OK', cancelButton='CANCEL', dismissString='CANCEL')
上述代码将导致一个新弹出窗口,包含一个字段和两个标签为确定和取消的按钮。你会注意到我们将 promptDialog 的结果存储在一个名为 promptInput 的变量中。
你可能会认为这个变量会保存用户输入的值,但这是不正确的。相反,它保存了用户按下的按钮来关闭对话框的值。这可能听起来很奇怪,但这是必要的,这样我们就可以确定用户是否实际上最终化了命令,或者他们决定取消。
要实际使用输入,我们首先需要检查用户是否确实按下了确定按钮。promptDialog 将返回两个值之一:
-
如果用户按下了其中一个按钮,返回值将是与该按钮关联的文本。
-
如果对话框以其他方式(如点击 X)关闭,则返回 dismissString 提供的内容。
在我们的例子中,如果用户按下了其中一个按钮,返回值将是确定或取消。请注意,我们也将 dismissString 设置为取消。所以,我们只需要检查是否返回了确定,如下所示:
if (promptInput == 'OK'):
注意
注意这里有两个等号,而不是一个。这是人们刚开始接触 Python 时常见的错误来源。要记住的关键点是,单个等号总是导致赋值,而你需要使用两个等号来执行相等性检查。这在许多语言中(不仅仅是 Python)都是正确的,并且源于将变量设置为给定值与检查两个值之间的差异是非常不同的操作。由于它们是两种不同的操作,Python(以及大多数其他语言)以不同的方式表示每个操作——一个等号用于赋值,两个用于比较。
如果这个检查通过,那么我们知道用户按下了确定,我们应该获取输入值。我们不得不以稍微不同的方式来做这件事。在其他例子中,我们保存了对创建的控件的引用,并使用该引用在查询模式下获取值时指定控件。
然而,在这种情况下,promptDialog 命令返回的是按下的按钮,而不是其字段的引用。那么我们如何引用正确的控件呢?
事实上,我们可以在第二次使用 promptDialog 命令时使用查询模式。即使我们没有指定要查询的特定 promptDialog,它仍然会工作,因为 Maya 会默认使用最近创建的那个。由于我们是在创建对话框后立即获取值,所以这会正常工作。将这些放在一起,我们得到以下内容:
if (promptInput == 'OK'):
radiusInput = cmds.promptDialog(query=True, text=True)
self.makeSphere(radiusInput)
注意,我们必须查询“文本”而不是“值”。另外,请注意,一旦我们有了输入,它就会被传递到另一个函数中,以实际执行工作。这是很重要的,这样我们就可以确保默认(非选项框)和选项框版本的菜单项触发的是完全相同的代码。
这在这里可能看起来有些多余,因为我们只是创建了一个球体,但仍然是个好主意。不要重复代码!
我们将得到三个功能——首先,实际执行工作(在这种情况下,创建一个球体),其次,使用默认值调用该函数(对于基本的 menuItem),最后,在从用户那里获取一些额外信息后调用该函数。将这些全部组合起来,我们得到类似以下内容(为了简洁,省略了__init__方法):
def myCommand(self, *args):
self.makeSphere(1)
def myCommandOptions(self, *args):
promptInput = cmds.promptDialog(title="Sphere Radius", message='Specify Radius:', button=['OK', 'CANCEL'], defaultButton='OK', cancelButton='CANCEL', dismissString='CANCEL')
if (promptInput == 'OK'):
radiusInput = cmds.promptDialog(query=True, text=True)
self.makeSphere(radiusInput)
def makeSphere(self, sphereRadius):
cmds.polySphere(radius=sphereRadius)
第三章. 使用几何体
在本章中,我们将探讨通过脚本创建和操作几何体的方法。以下主题将得到涵盖:
-
与选定的对象一起工作并检查节点类型
-
访问多边形模型中的几何数据
-
访问 NURBS 对象中的几何数据
-
创建曲线
-
创建新的多边形面
-
创建新的修改器(噪声)
-
创建新的原语(四面体)
简介
在本章中,我们将探讨如何通过脚本在 Maya 中操作几何体。首先,我们将看看如何确保我们选择了正确的类型的对象。从那里,我们将看看如何检索有关特定类型几何体的信息(包括多边形和 NURBS)。
我们还将探讨如何创建新的几何体(包括单个面和整个对象),以及如何对现有对象进行按顶点修改。
与选定的对象一起工作并检查节点类型
非常常见的情况是,你可能只想编写一个只对某些类型的对象以及用户在运行脚本之前已经存在的对象起作用的脚本。在这种情况下,你将希望能够不仅确定当前选中的对象是什么,还要验证选中的对象是否为适当的类型。在本例中,我们将创建一个脚本,用于验证当前选中的对象实际上是否是多边形几何体的实例,如果不是,则修改用户。
如何操作...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def currentSelectionPolygonal(obj):
shapeNode = cmds.listRelatives(obj, shapes=True)
nodeType = cmds.nodeType(shapeNode)
if nodeType == "mesh":
return True
return False
def checkSelection():
selectedObjs = cmds.ls(selection=True)
if (len(selectedObjs) < 1):
cmds.error('Please select an object')
lastSelected = selectedObjs[-1]
isPolygon = currentSelectionPolygonal(lastSelected)
if (isPolygon):
print('FOUND POLYGON')
else:
cmds.error('Please select a polygonal object')
checkSelection()
如果你运行前面的脚本而没有选择任何内容,你应该会得到一个错误提示,表明你应该选择一些内容。如果你选择了一个非多边形对象,你也会得到一个错误,但会提示你应该选择一个多边形对象。
然而,如果你选择了一个多边形对象,脚本将打印找到多边形。
它是如何工作的...
该脚本由两个函数组成——一个(currentSelectionPolygonal)用于测试给定的对象是否是多边形几何体,另一个(checkSelection)用于在当前选中的对象上调用它。因为checkSelection是脚本的入口点,所以我们将从这里开始。
我们必须做的第一件事是获取当前选中的对象或对象的列表。为此,我们将使用ls命令。ls命令是list的缩写,也是Maya 嵌入式语言(MEL)的 bash 脚本遗产的另一个例子,这种遗产延续到了 Python 命令列表中。ls命令可以执行各种操作,但最常见的方式可能是使用selection标志来返回当前选中的节点的列表,如下所示:
selectedObjs = cmds.ls(selection=True)
注意,尽管我们本质上是在向 Maya 提问,但使用query标志并不是必要的。实际上,使用ls命令的查询模式将生成错误。注意,我们将ls命令的结果存储在一个名为selectedObjects的变量中。这将给我们一个 Python 列表形式的对象集合,对象以它们被选择的顺序出现。首先,我们想要确保至少选择了一个对象,通过检查selectedObjs的长度:
if (len(selectedObjs) < 1):
cmds.error('Please select an object')
如果用户没有选择任何内容,我们使用error命令来通知用户并停止脚本的执行。添加有意义的错误信息是提供高质量反馈的绝佳方式。您还可以使用warning()命令向用户提供反馈而不停止脚本。在这两种情况下,错误(或警告)将以与内置错误(或警告)相同的方式显示给用户,出现在 Maya 界面的底部,并带有红色(或黄色)背景。
一旦我们知道我们至少选择了一个对象,我们想要确保给定的对象是一个多边形对象。使用-1作为列表中的索引允许我们从末尾开始计数。在这种情况下,这将给我们最近选择的对象。
lastSelected = selectedObjs[-1]
我们然后将该对象传递给我们的currentSelectionPolygonal函数,该函数将确定它实际上是否是一个多边形对象。这个函数将处理检查并返回True或False,这取决于所讨论的对象是否是多边形几何形状。
isPolygon = currentSelectionPolygonal(lastSelected)
通常将脚本分解成不同的部分是一个好主意,每个部分负责一个特定的任务。这使得编写和维护脚本变得更加容易。然而,这也要求不同的部分能够相互通信。return语句是完成这一点的最常见方式之一。它会导致当前函数停止并返回到上一个作用域。如果您给它一个值,该值将随它返回,从而允许信息从一个函数传递到另一个函数。
我们可以使用nodeType()命令来检查给定节点的类型,但这不仅仅是那样。如果我们检查所选对象本身的类型,我们几乎总是会得到变换。这是因为你在 Maya 中交互的大多数事物都是由两个节点组成的,而不是一个。通常有一个形状节点,它包含与给定对象相关的所有特定数据(面、顶点等),还有一个变换对象,它包含所有屏幕上出现并可移动的对象的共同位置、旋转和缩放(以及一些其他东西)。形状节点始终是其相应变换节点的子节点。
注意
当你在界面中点击某个对象时,例如多边形对象,你实际上是在点击形状节点,但 Maya 会自动跳到层次结构中的一步,到变换节点,这样你就可以移动它。这通常用于通过使曲线的形状节点成为其他变换的子节点来创建绑定控制,从而提供一种通过点击不可渲染的曲线(例如)来抓取模型内部骨骼的简单方法。
那么,我们实际上需要来测试几何类型的是与变换关联的形状节点。有几种方法可以做到这一点,但最好的方法是使用带有shapes=True的listRelatives()命令。这将给我们提供与输入节点关联的形状节点(如果有的话):
def currentSelectionPolygonal(obj):
shapeNode = cmds.listRelatives(obj, shapes=True)
一旦我们做了这件事,我们可以使用nodeType来测试其类型,看看我们有什么样的几何形状。如果我们有一个多边形对象,它将导致mesh。如果节点类型实际上是mesh,我们返回一个值为True。如果它不是mesh,我们则返回False:
if nodeType == "mesh":
return True
return False
注意,return False出现在else块之外。这主要是一种风格选择。如果你在条件(如我们在这里所做的那样)内部有一个return语句,那么有一个return语句是保证会被调用的,这样可以确保函数没有可能不提供返回值。
虽然有些人不喜欢在单个函数中有多个返回值,但如果你是其中之一,你也可以创建一个变量并返回它,如下所示:
isMesh = False
if (nodeType == "mesh"):
isMesh = True
return isMesh
或者,为了更紧凑(但可能稍微不那么易读)的方法,你只需返回比较本身的结果:
return (nodeType == "mesh")
所有这些都会产生相同的结果,即如果测试的对象具有类型为mesh的形状节点,则函数将返回True。在这个时候,我们完成了currentSelectionPolygonal函数,可以将注意力转回checkSelection。
剩下的就是检查返回值并通知用户结果:
if (isPolygon):
print('FOUND POLYGON')
else:
cmds.error('Please select a polygonal object')
还有更多...
我们可以使用与listRelatives获取形状节点并测试其类型相同的技巧来识别其他类型的对象。其中一些更有用的类型包括nurbsCurve用于 NURBS 曲线和nurbsSurface用于 NURBS 对象。
在多边形模型中访问几何数据
在这个例子中,我们将探讨如何获取多边形几何信息,这将成为更复杂脚本的基石。
准备工作
创建一个新的场景,并确保它包含一个或多个多边形对象。
如何做到这一点...
创建一个新的脚本,命名为polyStats.py,并添加以下代码:
import maya.cmds as cmds
# examine data for a currently-selected polygonal object
def getPolyData():
selectedObjects = cmds.ls(selection=True)
obj = selectedObjects[-1]
vertNum = cmds.polyEvaluate(obj, vertex=True)
print('Vertex Number: ',vertNum)
edgeNum = cmds.polyEvaluate(obj, edge=True)
print('Edge Number: ', edgeNum)
faceNum = cmds.polyEvaluate(obj, face=True)
print('Face Number: ',faceNum)
getPolyData()
运行前面的代码将在控制台打印出当前所选多边形对象的信息。
它是如何工作的...
polyEvaluate命令相当直观,可以用来确定多边形对象的各种信息。在这种情况下,我们只是获取对象包含的顶点、边和面的数量。
更多...
获取一个对象包含的组件数量本身并不是特别有用。为了执行有用的操作,您可能希望直接访问组件。
为了做到这一点,您需要理解每个对象都存储了一个 Python 列表中的组件集合,命名如下:
| 组件 | 列表名称 |
|---|---|
| 顶点 | vtx |
| 边 | e |
| Faces | f |
因此,为了选择给定对象(其名称存储在变量obj中)的第一个顶点,您可以这样做:
cmds.select(obj+'.vtx[0]', replace=True)
您可以用类似的方式获取第一个边:
cmds.select(obj+'.e[0]', replace=True)
或者第一个面:
cmds.select(obj+'.f[0]', replace=True)
由于组件列表只是普通的 Python 列表,您也可以使用冒号以及起始或结束索引(或两者)来引用组件集合。例如,如果我们想从5到12选择顶点,我们可以这样做:
cmds.select(obj+'.vtx[5:12]', replace=True)
这将有效,但如果您还想将起始和结束索引作为变量,这可能会变得有些尴尬,结果可能如下所示:
cmds.select(obj+'.vtx[' + str(startIndex) + ':' + str(endIndex) + ']', replace=True)
这将构建出传递给cmds.select的正确值(例如polySurface5.vtx[5:12]),但输入起来有些繁琐。一个更简单的方法是使用 Python 内置的字符串格式化功能,它可以用来将变量放入特定的字符串中。
要这样做,从一个您想要得到的字符串示例开始,如下所示:
myObject.vtx[5:12]
然后,确定字符串中将要改变的部分。在这种情况下,我们想要传递三个东西——对象名称、起始索引和结束索引。对于每一个,用花括号包裹的数字替换特定的值,如下所示:
{0}.vtx[{1}:{2}]
完成这些后,您可以在字符串上调用format(),传递值以替换花括号中的数字,如下所示:
"{0}.vtx[{1}:{2}]".format("myObject", 5, 12)
方括号内的数字作为索引,告诉 Python 传递给format的参数应该放在哪里。在这种情况下,我们说的是第一个参数(对象名称)应该放在开始处,接下来的两个应该放在方括号内。
这里是一个将所有这些放在一起示例:
objectName = "myObject"
startIndex = 5
endIndex = 12
cmds.select("{0}.vtx[{1}:{2}]".format(objectName, startIndex, endIndex), replace=True)
在 NURBS 对象中访问几何数据
在这个例子中,我们将探讨如何检索有关 NURBS 表面的信息,从它们包含的控制顶点(CVs)的数量开始。
然而,NURBS 对象中的 CV 数量并不像多边形对象中的顶点数量那样简单直接。尽管多边形对象相对简单,它们的形状直接由顶点的位置决定,但 NURBS 对象在任何给定点的曲率受多个点的影响。影响特定区域的确切点数取决于表面的度数。
为了了解这是如何工作的,我们将创建一个脚本,该脚本将确定 NURBS 表面每个方向(U和V)中的 CV 总数,并且我们将查看如何选择特定的 CV。
准备工作
确保你有包含至少一个 NURBS 表面的场景。
如何操作...
创建一个新文件,命名为getNURBSinfo.py(或类似),并添加以下代码:
import maya.cmds as cmds
def getNURBSInfo():
selectedObjects = cmds.ls(selection=True)
obj = selectedObjects[-1]
degU = cmds.getAttr(obj + '.degreeU')
spansU = cmds.getAttr(obj + '.spansU')
cvsU = degU + spansU
print('CVs (U): ', cvsU)
degV = cmds.getAttr(obj + '.degreeV')
spansV = cmds.getAttr(obj + '.spansV')
cvsV = degV + spansV
print('CVs (V): ', cvsV)
getNURBSInfo()
选择一个 NURBS 表面并运行脚本。您将看到每个参数化方向(U和V)中的 CV 数量输出到脚本编辑器。
它是如何工作的...
在这个例子中,我们使用getAttr命令来检索有关所选对象的信息。getAttr命令是“获取属性”的缩写,可以用来检索给定节点上任何属性的价值,这使得它在各种情况下都很有用。
在这个特定情况下,我们正在用它来获取沿着表面的每个方向上的两件事——跨度和度数,如下所示:
degU = cmds.getAttr(obj + '.degreeU')
spansU = cmds.getAttr(obj + '.spansU')
NURBS 表面(或曲线)的“度数”是影响几何中每个点的点的数量,范围从 1(线性)到 3。度数为 1 的曲线和表面是线性的,类似于多边形几何。度数大于 1 的曲线和表面通过插值多个点来生成曲率。曲线或表面中的 CV 总数始终等于跨度的数量加上度数。
一种理解这个概念简单的方法是考虑最简单的曲线——一条直线。这条曲线将有一个跨度(一个段),度数为 1(线性),并且仍然需要两个点(起点和终点)来定义。在这种情况下,我们将有:
(1 跨度) + (度数为 1) = 2 个点
对于更复杂的曲线,需要更多的点,但原理相同——最小数量总是(曲线度数)加一(因为不可能有一个没有跨度的曲线或表面)。
因此,要获取 CV 的总数,我们使用getAttr两次,一次获取跨度,再次获取度数,然后将总数相加,如下所示:
degU = cmds.getAttr(obj + '.degreeU')
spansU = cmds.getAttr(obj + '.spansU')
cvsU = degU + spansU
print('CVs (U): ', cvsU)
最后,我们将通过选择第一个和最后一个 CV 来完成脚本。在 NURBS 表面上选择 CV 与选择多边形的顶点非常相似,但有以下两个关键区别:
-
我们使用
.cv而不是.vtx -
我们需要指定两个索引(一个用于
U,一个用于V)而不是一个
选择第一个 CV 相当简单;我们只需为两个索引都使用零:
cmds.select(obj+'.cv[0][0]')
选择最后一个 CV 稍微复杂一些,需要我们将几个不同的部分组合在一起,以确保我们最终得到类似myObj.cv[8][8]的东西,如果表面在每个方向上有九个 CV。我们需要从 CV 总数中减去一个,并将其放在str()中,这样 Python 就会允许我们将它与文本组合在一起。将这些组合在一起,我们得到:
cmds.select(obj+'.cv[' + str(cvsU-1) + '][' + str(cvsV-1) + ']', add=True)
或者,我们可以使用字符串格式化来构建输入,如下所示:
cmds.select("{0}.cv[{1}][{2}]".format(obj, (cvsU-1), (cvsV-1), add=True)
还有更多...
之前提到的讨论是基于使用 NURBS 曲面。如果我们使用的是曲线而不是曲面,情况将非常相似,但我们将使用单个索引来指定 CV,而不是两个,如下所示:
degree = cmds.getAttr(obj + '.degree')
spans = cmds.getAttr(obj + '.spans')
cvs = degree + spans
print('CVs: ', cvs)
cmds.select(obj+'.cv[0]')
cmds.select(obj+'.cv[' + str(cvs-1) + ']', add=True)
还要注意,当我们检索度数和跨度值时,我们不指定U或V,因为曲线只有一个维度而不是两个。
创建曲线
在这个例子中,我们将探讨如何使用代码创建曲线。这可以用于多种不同的目的,例如作为进一步建模操作的基础或为复杂的装置创建自定义控件。
在这个例子中,我们将制作两条曲线——一条直接创建的简单曲线和一条逐点创建的更复杂的曲线。
这就是我们最终的输出,将两个曲线都从原点移开。

如何做到这一点...
创建一个新文件,并将其命名为makeCurves.py或类似名称。添加以下代码:
import maya.cmds as cmds
import math
def makeCurve():
theCurve = cmds.curve(degree=1, p=[(-0.5,-0.5,0),(0.5,- 0.5,0),(0.5,0.5,0), (-0.5,0.5,0), (-0.5, -0.5, 0)])
def curveFunction(i):
x = math.sin(i)
y = math.cos(i)
x = math.pow(x, 3)
y = math.pow(y, 3)
return (x,y)
def complexCurve():
theCurve = cmds.curve(degree=3, p=[(0,0,0)])
for i in range(0, 32):
val = (math.pi * 2)/32 * i
newPoint = curveFunction(val)
cmds.curve(theCurve, append=True, p=[(newPoint[0], newPoint[1], 0)])
makeCurve()
complexCurve()
如果你运行前面的代码,你将得到两条曲线——一个是正方形曲线,另一个是心形曲线。
它是如何工作的...
要创建一条新曲线,我们首先需要了解我们想要创建什么。在正方形的情况下,这很简单。我们只需要有四个点——每个点距离原点的一半宽度,在每个正负组合中((-,-), (-,+), (+,+), 和 (+,-)))。
要实际创建曲线,我们将使用curve命令并指定一系列点。我们还将设置曲线的度数为1,即使其线性,这对于正方形来说是有意义的。将这些放在一起,我们得到以下结果:
theCurve = cmds.curve(degree=1, p=[(-0.5,-0.5,0),(0.5,- 0.5,0),(0.5,0.5,0), (-0.5,0.5,0), (-0.5, -0.5, 0)])
注意,我们指定了五个点而不是四个。如果我们只留下四个点,我们最终会得到三个跨度而不是四个,导致正方形的一边缺失。解决这个问题的方法之一是简单地重复点列表末尾的第一个点来闭合曲线。
对于一个更复杂的例子,我们将使用一些有趣的数学方法创建一个心形曲线。为了使这个过程更容易,我们将创建一个函数,该函数将接受参数化输入并输出一个包含曲线该输入的 X 和 Y 坐标的两个元素的元组。我们还将逐点向曲线中添加点,因为对于更复杂的曲线,这有时是一种更容易的方法。
曲线的参数方程可以写成如下:
在这里,theta 的范围是从 0 到 2π。用 Python 表示这个值,我们得到以下结果:
def curveFunction(i):
x = math.sin(i)
y = math.cos(i)
x = math.pow(x, 3)
y = math.pow(y, 3)
return (x,y)
现在我们有一个函数可以给出我们想要的曲线,我们将创建一条新曲线并逐点添加到它。首先,我们创建曲线并将其设置为具有三次(立方)度数,这样它就会很平滑,如下所示:
theCurve = cmds.curve(degree=3, p=[(0,0,0)])
现在,我们将遍历从零到(2 * π)的范围,并在现有曲线上添加一个新点:
for i in range(0, 32):
val = (math.pi * 2)/32 * i
newPoint = curveFunction(val)
cmds.curve(theCurve, append=True, p=[(newPoint[0], newPoint[1], 0)])
我们首先计算输入为(2 * π)的 1/32 乘以我们的索引,并将其传递给曲线函数。然后我们再次使用curve命令,但进行了一些更改,即:
-
我们通过将其作为第一个参数传递来指定我们正在工作的曲线
-
我们使用
append=True标志让 Maya 知道应该将点添加到现有曲线而不是创建一个新的曲线。 -
我们使用
curveFunction的输出指定一个点,用于 X 和 Y 坐标,以及 Z 坐标的 0
还有更多...
虽然你可能在自己的项目中不需要心形线,但有很多情况下你可能想要逐步创建曲线。例如,你可能想根据动画序列创建一个曲线,通过在每一帧添加给定对象的位位置点。当我们查看动画脚本时,我们将看到如何逐帧获取位置。
参见
关于心形线(astroid curve)的更多信息,请查看 Wolfram MathWorld 网站的条目,mathworld.wolfram.com/Astroid.html。这只是该网站解释的有趣曲线之一,以及你可能觉得有用的各种其他数学资源。
创建新的多边形面
在这个例子中,我们将探讨如何使用代码创建新的多边形面,包括一个简单的四边形和一个更复杂的例子,该例子包含一个内部洞。
如何做到这一点...
创建一个新文件,命名为 polyCreate.py(或类似),并添加以下代码:
import maya.cmds as cmds
import math
def makeFace():
newFace = cmds.polyCreateFacet(p=[(-1,-1,0),(1,- 1,0),(1,1,0),(-1,1,0)])
def makeFaceWithHole():
points = []
# create the inital square
points.append((-5, -5, 0))
points.append(( 5, -5, 0))
points.append(( 5, 5, 0))
points.append((-5, 5, 0))
# add empty point to start a hole
points.append(())
for i in range(32):
theta = (math.pi * 2) / 32 * i
x = math.cos(theta) * 2
y = math.sin(theta) * 2
points.append((x, y, 0))
newFace = cmds.polyCreateFacet(p=points)
makeFace()
makeFaceWithHole()
如果你运行前面的脚本,你会看到创建了两个新对象,都在 XY 平面上——一个是简单的正方形,另一个是在中心有洞的正方形。
它是如何工作的...
polyCreateFacet 命令相当直接,并期望接收一个点位置数组。每个点应存储在一个包含三个值的元组中,每个值分别对应顶点的 X、Y 和 Z 位置。
在第一个例子中,我们只是直接调用 polyCreateFacet 命令,并提供组成以原点为中心、在 XY 平面上对齐的 2 单位正方形四个顶点的四个点。以下是我们的代码:
newFace = cmds.polyCreateFacet(p=[(-1,-1,0),(1,-1,0),(1,1,0),(- 1,1,0)])
你也可以创建带有内部洞的多边形,但为了做到这一点,你需要向 Maya 信号你正在开始一个洞。为此,你需要向 polyCreateFacet 命令提供一个空点作为空元组。
当创建更复杂的面时,创建一个数组来存储各种点并逐个将其推入它可能更容易,而不是尝试将单个长参数传递给 polyCreateFacet 命令。
我们再次从四个点开始,在 XY 平面上定义一个正方形,如下所示:
points = []
# create the inital square
points.append((-5, -5, 0))
points.append(( 5, -5, 0))
points.append(( 5, 5, 0))
points.append((-5, 5, 0))
为了让 Maya 开始在我们制作的面上创建一个洞,我们接下来添加一个空元组:
points.append(())
现在我们可以开始添加孔的点了。在这种情况下,我们将添加 32 个点来制作一个圆形孔。这可以通过一点三角学轻松完成。因为我们用 32 个段来制作孔,所以我们把一个完整的旋转(以弧度为单位,所以math.pi * 2)除以32,然后乘以我们的索引,得到我们提供给三角函数的值。
将所有这些放在一起,我们得到以下结果:
for i in range(32):
theta = (math.pi * 2) / 32 * i
x = math.cos(theta) * 2
y = math.sin(theta) * 2
points.append((x, y, 0))
然后,我们将有一个包含 37 个元组的数组,代表 36 个点加上一个空白条目,以指示切割区域的开始。将此传递给polyCreateFacet命令将给出最终结果。我们使用以下代码:
newFace = cmds.polyCreateFacet(p=points)
还有更多...
在创建多边形面时,指定顶点的顺序非常重要。很明显,如果顶点顺序错误,会导致生成的面以意想不到的方式弯曲,但顺序也会影响面或面的法线指向的方向。务必确保按照创建的面外围的顺时针方向指定你的点,这样法线就会指向屏幕外。
如果你想让法线指向另一个方向,要么以相反的顺序指定它们,要么使用以下polyNormal命令显式地反转创建的面的法线:
# with a polygonal object selected
cmds.polyNormal(normalMode=4)
4变量可能看起来很神秘,但polyNormal命令可以执行几个不同的特定功能(包括一些已弃用的选项),而normalMode标志是告诉 Maya 你想要哪个的一个方法。有关详细信息,请务必查阅 Python 命令文档。
如果你发现自己正在创建复杂的面,例如我们带有孔的第二例,你可能想确保你留下的面不超过四边。你当然可以通过一次创建一个面并将它们连接起来(我们将在稍后提到的自定义原语示例中这样做)来实现,或者你可以创建一个形状作为一个单一的面,然后将其三角化。
要三角化生成的面,在创建它之后运行polyTriangulate命令,如下所示:
cmds.polyCreateFacet(p=myPoints)
cmds.polyTriangulate()
你还可以通过运行以下polyQuad命令让 Maya 尝试将生成的三角形合并成四边形:
# attempt to form quads from a recently-triangulated poly mesh
cmds.polyQuad()
四角化并不总是有效,但它通常也不会造成伤害。另一方面,在网格中留下非四边形可能会导致各种问题,最好避免。
创建新的修改器(噪声)
许多 3D 建模和动画软件包提供了一种方法,可以在对象顶点上添加一些随机噪声,但 Maya 没有。这看起来可能是一个疏忽,但它也为我们提供了一个很好的项目示例。
在这个例子中,我们将编写一个脚本来遍历多边形对象的全部顶点,并将它们稍微移动一下。以下是一个简单的多边形球体在应用我们将开发的脚本之前和之后的示例:

如何做到这一点...
创建一个新的脚本,命名为addNoise.py,并添加以下代码:
import maya.cmds as cmds
import random
def addNoise(amt):
selectedObjs = cmds.ls(selection=True)
obj = selectedObjs[-1]
shapeNode = cmds.listRelatives(obj, shapes=True)
if (cmds.nodeType(shapeNode) != 'mesh'):
cmds.error('Select a mesh')
return
numVerts = cmds.polyEvaluate(obj, vertex=True)
randAmt = [0, 0, 0]
for i in range(0, numVerts):
for j in range(0, 3):
randAmt[j] = random.random() * (amt*2) - amt
vertexStr = "{0}.vtx[{1}]".format(obj, i)
cmds.select(vertexStr, replace=True)
cmds.move(randAmt[0], randAmt[1], randAmt[2], relative=True)
cmds.select(obj, replace=True)
addNoise(0.2)
如果您选择多边形对象并运行此代码,您会看到每个顶点都通过一个小随机量(0.2单位)移动。
它是如何工作的...
首先,我们希望确保我们已选择多边形对象:
-
获取当前选定的对象
-
确定最近选择的对象(如果有的话)附加的形状节点
-
测试形状节点以确保它是一个多边形对象
看看以下代码:
selectedObjs = cmds.ls(selection=True)
obj = selectedObjs[-1]
shapeNode = cmds.listRelatives(obj, shapes=True)
一旦我们这样做,我们希望遍历对象的每个顶点,但首先我们需要知道它包含多少个顶点。因此,我们使用polyEvaluate命令如下:
numVerts = cmds.polyEvaluate(obj, vertex=True)
现在我们已经准备好遍历顶点并移动每个顶点。因为我们希望每个轴都是独立的,所以我们首先创建一个变量来保存每个的偏移量:
randAmt = [0, 0, 0]
现在我们已经准备好遍历这个对象。对于每次遍历,我们希望将randAmt数组设置为随机变量,然后将这些变量应用到顶点的位置:
for j in range(0, 3):
randAmt[j] = random.random() * (amt*2) - amt
注意
关于我们如何设置随机数量的说明——我们希望确保产生的值在输入值(作为最大值)和它的负等效值(作为最小值)之间。
random.random()函数将产生一个介于 0 和 1 之间的随机数。将其乘以输入值的两倍将给我们一个介于 0 和(amt * 2)之间的值,减去输入值将给我们正确的范围。
现在,我们将首先通过选择单个顶点并使用move命令移动它来移动顶点:
vertexStr = "{0}.vtx[{1}]".format(obj, i)
cmds.select(vertexStr, replace=True)
cmds.move(randAmt[0], randAmt[1], randAmt[2], relative=True)
注意,Maya 还提供了一个polyMoveVertex命令,这可能看起来是调整每个顶点位置的一个更好的方法。虽然这绝对有效,但由于为每个移动的顶点创建另一个数据库可用性组(DAG)节点所带来的额外开销,它将运行得慢得多。如果您想亲自看看,尝试注释掉选择和移动顶点的行,并添加以下内容:
cmds.polyMoveVertex(vertexStr, t=randAmt)
尝试运行这个,看看它需要多长时间,然后注释掉这一行,重新启用选择和移动行并重新运行脚本。您可能会看到polyMoveVertex版本需要更长的时间。
一旦我们遍历了所有的顶点并将每个顶点稍微移动了一下,我们希望确保通过选择原始对象来完成,从而使用户能够对对象执行进一步的操作。看看以下代码:
cmds.select(obj, replace=True)
更多内容...
这个例子只会对多边形对象有效,但很容易扩展到与 NURBS 曲面甚至曲线一起工作。为此,我们需要做以下两件事:
-
测试几何类型(
nurbsSurface或nurbsCurve) -
修改点选择代码以引用适当的点类型
另一个更复杂的因素是,NURBS 曲面的 CVs 必须以二维数组的形式访问,而不是多边形表面的vtx列表的平面数组。
创建新的原语(四面体)
在这个例子中,我们将创建一个全新的(对 Maya 而言)几何原语——四面体。四面体在原理上很简单,但使用 Maya 的界面创建它们需要许多步骤。因此,它们非常适合脚本编写。
我们将创建一个脚本,该脚本将创建一个具有给定边宽的四面体作为多边形网格。
准备工作
在我们开始编写代码之前,我们想要确保我们对四面体的数学原理有很好的理解。四面体是最简单的正多面体,由四个面组成,每个面都是一个等边三角形。
每个四面体仅由四个点组成。为了方便,我们将底部的三个点命名为A、B和C,而顶部的点命名为D,如下面的插图所示:

为了使数学更容易,我们将点A设置为原点([0,0,0])。因为底面的每一边长度都相同,我们可以通过沿着x轴移动所需的边长来找到点B,给B的坐标为[长度, 0, 0]。
点C的处理稍微复杂一些。首先,我们注意到每个等边三角形都可以分成两个相似的直角三角形,如下所示:
找到点C的X坐标很容易;我们只需要将边长除以二。而Z坐标则等于前面提到的插图中所提到的每个半三角形的长度,但我们还不知道。然而,我们知道其他两边的长度,即较短的一边是边长的一半,而斜边则是完整的边长本身。
因此,根据勾股定理,我们知道:

或者,稍作改写,我们有以下公式:

最后,我们需要四面体顶点的坐标。我们将以类似于我们找到C坐标的方式获得这些坐标,即我们将使用另一个直角三角形,但这个三角形将略有不同;它将是点A、点D和底面中心点(我们将称之为点E)形成的三角形。

首先,让我们找到点E。因为它位于底部的中心,我们可以简单地平均A、B和C的X和Z坐标,从而得到E的位置。然后,我们可以构造一个三角形,这将帮助我们确定点D的垂直位置。

点D将具有与E相同的X和Z坐标,但需要在y轴上提升适当的量以创建一个合适的四面体。为了找到这个距离,我们将使用由A、E和D形成的三角形。斜边,再次强调,是四面体的一个完整边,所以这很简单。较短的底边(A-E)是A到底部中心的距离。为了找到这个距离,我们可以使用距离公式,通过选择点A作为原点来简化计算。

因为点A的X和Z坐标都是零,所以我们得到以下结果:

一旦我们完成了这个操作,我们就知道三角形的两边长度,我们可以通过再次使用勾股定理来计算第三边,如下所示:

现在我们已经很好地掌握了如何创建四面体,我们准备实际编写脚本。
如何做...
创建一个新的脚本,并将其命名为makeTetrahedron.py。添加以下代码:
import maya.cmds as cmds
import math
def makeTetra(size):
pointA = [0, 0, 0]
pointB = [size, 0, 0]
pointC = [size/2.0, 0, 0]
# set the Z position for C
pointC[2] = math.sqrt((size*size) - (size/2.0 * size/2.0))
pointE = [0,0,0]
# average the A, B, and C to get E
# first add all the values
for i in range(0,3):
pointE[i] += pointA[i]
pointE[i] += pointB[i]
pointE[i] += pointC[i]
# now divide by 3
for i in range(0,3):
pointE[i] = pointE[i] / 3.0
# start point D with the X and Z coordinates of point E
pointD = [0,0,0]
pointD[0] = pointE[0]
pointD[2] = pointE[2]
distanceAE = math.sqrt((pointE[0] * pointE[0]) + (pointE[2] * pointE[2]))
# set the Y coordinate of point D
pointD[1] = math.sqrt((size * size) - (distanceAE * distanceAE))
faces = []
faces.append(cmds.polyCreateFacet(p=[pointA, pointB, pointC], texture=1))
faces.append(cmds.polyCreateFacet(p=[pointA, pointD, pointB], texture=1))
faces.append(cmds.polyCreateFacet(p=[pointB, pointD, pointC], texture=1))
faces.append(cmds.polyCreateFacet(p=[pointC, pointD, pointA], texture=1))
cmds.select(faces[0], replace=True)
for i in range(1, len(faces)):
cmds.select(faces[i], add=True)
obj = cmds.polyUnite()
cmds.select(obj[0] + ".vtx[:]")
cmds.polyMergeVertex(distance=0.0001)
cmds.select(obj[0])
cmds.move(-pointE[0], 0, -pointE[2])
cmds.xform(pivots=(pointE[0], 0, pointE[2]))
cmds.makeIdentity(apply=True)
cmds.delete(ch=True)
makeTetra(5)
运行此代码,你应该得到一个边长为 5 个单位的四面体,底部中心位于原点。

它是如何工作的...
首先,我们计算所有需要的点,如前述“准备中”部分所述。每个点是一个包含X、Y和Z坐标的三元素数组。
前两个点很容易:
pointA = [0, 0, 0]
pointB = [size, 0, 0]
pointC稍微复杂一些,需要我们使用勾股定理:
pointC = [size/2.0, 0, 0]
# set the Z position for C
pointC[2] = math.sqrt((size*size) - (size/2.0 * size/2.0))
为了计算pointD的位置,我们首先确定底部的中心,我们将称之为pointE。使用以下代码:
pointE = [0,0,0]
# average the A, B, and C to get E
# first add all the values
for i in range(0,3):
pointE[i] += pointA[i]
pointE[i] += pointB[i]
pointE[i] += pointC[i]
# now divide by 3
for i in range(0,3):
pointE[i] = pointE[i] / 3.0
最后,我们可以通过将X和Z坐标设置为pointE的坐标,并使用勾股定理来确定Y坐标来确定pointD,如下所示:
# start point D with the X and Z coordinates of point E
pointD = [0,0,0]
pointD[0] = pointE[0]
pointD[2] = pointE[2]
distanceAE = math.sqrt((pointE[0] * pointE[0]) + (pointE[2] * pointE[2]))
一旦我们完成了这个操作,我们就可以使用polyCreateFacet命令创建单个面。我们将使用polyCreateFacet命令四次,每次用于四面体的一个面。我们还将结果存储到一个数组中,这样我们可以在稍后进行最终处理时选择所有面。我们有以下代码:
faces = []
faces.append(cmds.polyCreateFacet(p=[pointA, pointB, pointC], texture=1))
faces.append(cmds.polyCreateFacet(p=[pointA, pointD, pointB], texture=1))
faces.append(cmds.polyCreateFacet(p=[pointB, pointD, pointC], texture=1))
faces.append(cmds.polyCreateFacet(p=[pointC, pointD, pointA], texture=1))
在这一点上,我们已经创建了所有的几何形状,但我们还想做一些事情来完成对象,即:
-
将所有面组合成一个单一的对象。
-
将对象移动,使底部中心位于原点。
-
设置对象的轴心点,使其也位于底部中心。
-
冻结变换。
首先,让我们通过选择面将面组合成一个单一的对象。我们首先通过将当前选择替换为第一个面开始,然后通过在cmds.select()调用中使用add=True将额外的三个面添加到选择中。我们有以下代码:
cmds.select(faces[0], replace=True)
for i in range(1, len(faces)):
cmds.select(faces[i], add=True)
一旦我们选择了所有的面,我们就可以使用polyUnite将它们组合起来:
obj = cmds.polyUnite()
它将导致所有面合并成一个单一的多边形对象,但这只是开始。如果我们就这样结束,我们最终会得到不相连的面和在每个四面体的四个点上的多个顶点。为了完成它,我们想要确保重叠的顶点被合并。
要做到这一点,我们首先选择我们模型中的所有顶点:
cmds.select(obj[0] + ".vtx[:]")
注意,我们使用 vtx 列表来选择顶点,但我们省略了起始和结束索引,只使用一个冒号。这是一个简单的简写方式来引用列表的全部内容,并将导致选择我们模型中的所有顶点。一旦我们完成,我们告诉 Maya 使用 polyMergeVertex 命令合并附近的顶点,传递一个小阈值距离。
cmds.polyMergeVertex(distance=0.0001)
这告诉 Maya,任何小于 0.0001 单位距离的顶点都应该合并成一个单一的顶点。到目前为止,我们有一个有四个面和四个顶点的正确四面体。因为我们要执行的其余操作都是针对整个对象(而不是其顶点),所以我们通过重新选择对象来切换回对象模式。
cmds.select(obj[0])
现在我们有一个单一的对象,但我们想要将其居中。幸运的是,我们仍然有 pointE,它包含相对于原点的基座的 X 和 Z 坐标。因此,我们将首先以相同的量在 X 和 Z 方向上移动组合对象:
cmds.move(-pointE[0], 0, -pointE[2])
现在我们已经将对象放置到我们想要的位置,但它的重心点仍然位于原来的原点(pointA)。为了修复它,我们将使用 xform 命令来移动重心点,如下所示:
cmds.xform(pivots=(pointE[0], 0, pointE[2]))
pivots 标志将对象的重心移动到指定的位置。在这种情况下,我们以与移动对象相同的量(但方向相反)移动它,结果重心仍然保持在原点,尽管对象本身已经被移动。
最后,我们将通过冻结变换来结束,这样我们的对象在位置上以 0,0,0 开始,并且我们将删除构造历史。
cmds.makeIdentity(apply=True)
cmds.delete(ch=True)
然后,我们就剩下了一个完全形成的四面体,以原点为中心,具有干净的构造历史,并准备好进一步使用。
还有更多...
四面体是一个相当简单的对象,但我们用来创建它的所有原理都可以很容易地扩展到具有更多输入的更复杂形状。
第四章. 给事物涂上油漆 – UV 和材质
在本章中,我们将探讨与 UV 布局和着色器创建相关的主题:
-
查询 UV 数据
-
使用 Python 布局 UV
-
使用代码创建着色网络
-
将着色器应用到对象上
-
使用着色节点进行非渲染任务
简介
在上一章中,我们探讨了如何使用脚本操作几何体。然而,对于大多数项目来说,创建模型只是第一步。除非你希望一切看起来都像无聊的灰色塑料,否则你需要布局 UV,然后创建并应用着色网络。
在本章中,我们将探讨如何做到这一点。
查询 UV 数据
在这个例子中,我们将探讨如何获取多边形对象上的 UV 信息。我们将探讨检查对象包含多少 UV 集,获取对象特定部分的 UV,以及获取给定 UV 点的位置。
我们还将探讨如何将一种选择转换为另一种选择,并使用它来确定给定的边是否可以分割。
准备工作
确保你有包含至少一个具有 UV 的多边形对象的场景——无论是你解包的对象还是任何默认具有 UV 的内置基本形状。
如何做到这一点...
创建一个新文件,命名为uvInfo.py(或类似),并添加以下代码:
import maya.cmds as cmds
def uvInfo():
sel = cmds.ls(selection=True)
obj = sel[0]
uvs = cmds.polyEvaluate(obj, uvComponent=True)
uvPos = cmds.polyEditUV(obj + '.map[0]', query=True)
isFirstEdgeSplit = isSplitEdge(obj, 0)
print('Num UVs: ' + str(uvs))
print("Position of first UV: ", uvPos)
print("First edge is split: ", isFirstEdgeSplit))
cmds.select(obj, replace=True)
def isSplitEdge(obj, index):
result = cmds.polyListComponentConversion(obj + '.e[' + str(index) + ']', fromEdge=True, toUV=True)
cmds.select(result, replace=True)
vertNum = cmds.polyEvaluate(vertexComponent=True)
result = cmds.polyListComponentConversion(obj + '.e[' + str(index) + ']', fromEdge=True, toVertex=True)
cmds.select(result, replace=True)
uvNum = cmds.polyEvaluate(uvComponent=True)
if (uvNum == vertNum):
return False
return True
uvInfo()
如果你运行前面的脚本并选择一个多边形对象,你将获得有关对象 UV 的一些信息,特别是:
-
对象有多少 UV。
-
第一个 UV 点的位置(在 UV 空间中)
-
是否第一个边位于两个独立的 UV 壳之间的边界上。
它是如何工作的...
我们首先获取当前选定的对象并将其存储到我们的obj变量中。一旦我们这样做,我们就使用polyEvaluate命令来确定对象拥有的 UV 总数。这与我们在上一章中找到几何组件数量的方法类似,但这次我们使用uvComponent/uvc标志。
uvs = cmds.polyEvaluate(obj, uvComponent=True)
接下来,我们将找到第一个 UV 点的具体位置。UV 可以通过与其它多边形组件相同的方式访问,但使用“map”列表而不是“f”(面)、“e”(边)或“vtx”(顶点)。因此,如果我们想引用名为myObject的对象的第一个 UV,我们将使用以下方式:
myObject.map[0]
在这里,0 表示列表中的第一个条目,因此是对象的第一 UV。
要找到给定 UV 的特定 U 和 V 坐标,我们可以使用查询模式下的polyEditUV命令,如下所示:
uvPos = cmds.polyEditUV(obj + '.map[0]', query=True)
接下来是确定给定的边是否在 UV 壳内部,或者它是否位于两个不同壳之间的边界上。为此,我们创建一个函数,该函数接受一个对象名称和要检查的边的索引:
isFirstEdgeSplit = isSplitEdge(obj, 0)
我们正在做的关键事情是查看有多少顶点和 UV 与给定的边相对应。如果顶点的数量不等于 UV 的数量,那么这条边必须跨越两个不同的 UV 壳的边界。
为了确定有多少顶点/UV 与给定的边相对应,我们将使用polyListComponentConversion命令将边转换为所需的组件类型。为了正确执行,我们需要指定我们正在转换的内容(在这种情况下,是边),以及我们正在转换到什么(顶点或 UV)。我们这样做的方式有点奇怪;而不是指定每种类型的类型,我们必须将两个布尔标志设置为 true,一个用于源类型,一个用于目标类型。
例如,如果我们要将名为myObject的对象的第一个边转换为顶点,我们需要做以下操作:
cmds.polyListComponentConversion('myObject.e[0]', fromEdge=True, toVertex=True)
添加适当的变量来设置对象的名称和边的索引,我们得到:
result = cmds.polyListComponentConversion(obj + '.e[' + str(index) + ']', fromEdge=True, toVertex=True)
注意,我们将命令的输出存储到名为"result"的变量中。这很重要,因为要获得准确的数量,我们需要首先选择我们想要计数的组件。这可以很容易地按照以下方式完成:
cmds.select(result, replace=True)
一旦我们这样做,我们就可以使用带有适当标志的polyEvaluate命令来给出当前所选组件的数量。对于顶点和 UV,我们希望使用vertexComponent和uvComponent,分别。在两种情况下,我们将结果存储到另一个变量中,如下所示:
vertNum = cmds.polyEvaluate(vertexComponent=True)
在这一点上,我们有了与给定边相对应的顶点数量。然后我们执行相同的操作(但带有略微不同的标志)以确定 UV 的数量:
result = cmds.polyListComponentConversion(obj + '.e[' + str(index) + ']', fromEdge=True, toUV=True)
cmds.select(result, replace=True)
uvNum = cmds.polyEvaluate(uvComponent=True)
最后,我们将 UV 的数量与顶点的数量进行比较。如果它们不相同,那么所讨论的边必须存在于多个 UV 壳上,因此它代表了一个边界:
if (uvNum == vertNum):
return False
return True
在我们的主函数中,我们使用几个打印语句输出我们各种查询的结果:
print('Num UVs: ' + str(uvs))
print("Position of first UV: ", uvPos)
print("First edge is split: ", isFirstEdgeSplit)
最后,我们将确保再次选择原始对象,因为我们选择了isSplitEdge函数期间的子组件:
cmds.select(obj, replace=True)
使用 Python 布局 UV
在这个例子中,我们将查看如何使用 Python 实际布局 UV。我们将应用平面、圆柱和球面投影,每个投影到所选对象的不同面集。
准备工作
确保你有一个包含多边形对象的场景。我们将对对象的不同部分应用三种不同的映射(通过将总面数除以三来选择),因此最好有一个至少有几十个面的对象。如果你没有现成的模型,制作一个至少有 10 个或更多分度的多边形球体。
如何做...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def layoutUVs():
selected = cmds.ls(selection=True)
obj = selected[0]
totalFaces = cmds.polyEvaluate(obj, face=True)
oneThird = totalFaces/3
startFace = 0
endFace = oneThird - 1
cmds.polyProjection(obj + '.f[' + str(startFace) + ':' + str(endFace) + ']', type="planar")
startFace = oneThird
endFace = (oneThird * 2) - 1
cmds.polyProjection(obj + '.f[' + str(startFace) + ':' + str(endFace) + ']', type="cylindrical")
startFace = (oneThird * 2)
endFace = totalFaces - 1
cmds.polyProjection(obj + '.f[' + str(startFace) + ':' + str(endFace) + ']', type="spherical")
layoutUVs()
选择多边形对象后运行此脚本,然后切换到 UV 纹理编辑器面板以查看结果。
它是如何工作的...
我们在这里主要做的事情是为对象的面子集应用一个新的 UV 布局。这是一个有些人为的例子,因为我们只是通过将总数分成三份来选择面。
首先,我们获取当前选定的对象,并使用 polyEvaluate 确定它有多少个面:
selected = cmds.ls(selection=True)
obj = selected[0]
totalFaces = cmds.polyEvaluate(obj, face=True)
然后,我们确定那个数的三分之一是多少。请注意,Python 默认进行整数除法,因为 totalFaces 和 3 都是整数。这恰好是我们在这个应用中需要的,但如果你不期望这种情况,很容易出错:
oneThird = totalFaces/3
如果你想要确保得到一个正确的十进制值作为结果,只需确保除以一个浮点值即可,如下所示:
oneThirdAsDecimal = totalFaces/3.0
我们还创建了几个辅助变量来保存每三组面开始和结束的索引:
startFace = 0
endFace = oneThird - 1
我们在这里做的事情并没有什么特别困难的,尽管需要一些注意以确保包括整个面的范围。我们使用的值如下:
| 开始索引 | 结束索引 | 示例索引(基于一个 100 面的对象) | |
|---|---|---|---|
| 1st(平面)映射 | 0 | oneThird - 1 | 0-32 |
| 2nd(圆柱)映射 | oneThird | (oneThird * 2) - 1 | 33-65 |
| 3rd(球面)映射 | oneThird * 2 | totalFaces - 1 | 66-99 |
现在我们已经准备好脚本的主体——实际上,应用映射。所有三种映射类型(平面、圆柱和球面)都使用相同的命令 polyProjection。
小贴士
关于 UV 映射的简要说明——三种映射类型是平面、圆柱和球面,这可能会显得有些奇怪;为什么是这些特定的形状而不是其他形状呢?原因在于,如果你把模型的表面想象成一个二维的皮肤,那么模型的任何一部分都可以被归类为属于以下三个组之一:
-
这个区域没有任何显著的曲率。
-
这个区域在单一方向(水平或垂直)上有显著的曲率。
-
这个区域在两个方向上都有显著的曲率。
这完美地映射到平面(无曲线)、圆柱(单方向曲线)和球面(两个方向的曲率)三种选项。虽然你试图映射的部分可能与完美的平面、圆柱或球面非常不同,但首先问问自己它向多少个方向弯曲,然后相应地选择你的映射。
我们需要向 polyProjection 命令提供两样东西才能使其工作——应该接收映射的具体面以及要应用的映射类型。为了指定面的范围,我们将想要索引到对象的 faces 或 "f" 数组。我们可以使用两个索引并用冒号分隔来指定多个面。例如,如果我们的对象名为 mySphere,并且我们想要引用前六个面,我们可以这样做:
mySphere.f[0:5]
在这种情况下,我们希望使用所选对象的名称以及startFace和endFace变量作为索引。这样做将给出以下结果:
obj + '.f[' + str(startFace) + ':' + str(endFace) + ']'
既然我们已经有了指定面范围的方法,我们可以应用映射,使用type标志来指定要应用哪种映射:
cmds.polyProjection(obj + '.f[' + str(startFace) + ':' + str(endFace) + ']', type="planar")
从这里开始,我们只需要用不同的startFace和endFace值以及不同的类型标志选项重复这个过程。
还有更多...
如果你想要将映射应用于整个对象,你可能认为你只需要对象的名称并省略面索引。这不起作用,但有一个简单的方法可以告诉 Maya 你想要引用所有面。为此,只需省略两个索引,但保留冒号,如下所示:
myObject.f[:]
如果缺少起始索引,Maya 将用 0 替换,如果缺少结束索引,Maya 将用最大索引替换。如果两个都省略,则映射将应用于整个对象。
到目前为止,我们只看了选择连续的面序列,但有很多情况下你可能想要选择非连续索引的面。你可以通过将多个选择用逗号分隔作为函数的第一个参数(或多个参数)来实现这一点。
例如,假设我们想要选择myObject的前 5 个面以及 32 到 76 的面。我们可以使用以下命令:
cmds.select('myObject.f[0:4]', 'myObject.f[32:76]', replace=True)
将此应用于 UV 映射将给出以下类似的结果:
cmds.polyProjection('myObject.f[0:4]', 'myObject.f[32:76]', type="planar")
当处理面范围时,在运行时确定特定索引是非常常见的,无论是通过某种计算还是基于用户输入。这很容易做到,但可能导致过于复杂的字符串字面量和变量的序列,如下所示:
obj + '.f[' + str(startFace) + ':' + str(endFace) + ']'
很容易忘记使用str()命令将数值转换为字符串,这可能导致错误。幸运的是,Python 提供了一个以format命令的形式处理从变量构建格式化字符串的替代方法。
要使用格式命令,你创建一个包含你想要用变量替换的部分的字符串。每个可替换的部分都用包含数字的括号表示,例如{0}。然后你可以调用该字符串上的格式命令,并传入将替换{}子句的变量。数字用于指定哪些变量应该放在哪里(例如,{0}表示“用第一个变量替换”)。
因此,作为一个非常简单的例子,我们可以用以下方式祝人生日快乐:
personAge = 21
personName = "Alice"
"Congratulations on turning {0}, {1}!".format(personAge, personName)
# results in "Congratulations on turning 21, Alice!"
回到 Maya,假设我们想要有一个选择面范围的一般方法。我们希望传入对象的名称、起始索引和结束索引作为变量。我们可以这样做:
cmds.select(myObject + '.f[' + str(startFace) + ':' + str(endFace) + ']', replace=True)
这将正常工作,但有点难以阅读,并且是引入错误的一种简单方法。如果我们使用格式命令重写它,我们会得到如下所示的内容:
cmds.select("{0}.f[{1}:{2}]".format(myObj, startFace, endFace), replace=True)
这通常更容易思考,因为它允许你将结构(字符串)与应该填充到其中的变量分开。你当然不必使用格式,但鉴于 Maya 脚本经常需要以这种方式从变量中构建字符串,使用它可能会节省你很多麻烦。它还使你的代码更容易阅读。
参见
格式命令的官方 Python 文档有点难以理解,并且以过于晦涩的方式呈现信息。相反,我强烈建议您查看pyformat.info/,那里有对格式命令复杂性的详细且易于阅读的解释。
使用代码创建着色网络
在这个例子中,我们将探讨如何使用代码创建着色网络。我们将创建一个简单的卡通着色器,内部为纯色,而物体的边缘为不同的颜色。有几种不同的方法可以做到这一点,包括创建渐变着色器,但我们将以相对老式的方式使用samplerInfo节点,因为它提供了一个相对简单但相对新颖的着色网络的绝佳示例。
首先,让我们看看我们的着色器将做什么以及它是如何做到的。卡通着色器的关键特性是物体边缘有一个轮廓,随着物体的移动而改变。因此,我们首先需要知道模型的一部分与相机之间的角度。幸运的是,Maya 提供了一个名为samplerInfo的实用节点,它正好可以做到这一点。SamplerInfo节点为我们提供了一个facingRatio属性,其范围从 0(当表面垂直于相机时)到 1(当表面直接面向相机时)。
一旦我们得到了面对比,就需要以某种方式将其与颜色变化联系起来。最简单且最灵活的方法是使用具有线性插值的渐变纹理,以在边框颜色和内部颜色之间提供清晰的截止。
将所有这些组合起来,我们得到了一个相对简单的、由三个节点组成的着色网络,类似于以下内容:

如何操作...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def createNodes():
shaderNode = cmds.shadingNode('blinn', asShader=True)
rampTexture = cmds.shadingNode('ramp', asTexture=True)
samplerNode = cmds.shadingNode('samplerInfo', asUtility=True)
cmds.setAttr(rampTexture + '.interpolation', 0)
cmds.setAttr(rampTexture + '.colorEntryList[0].position', 0)
cmds.setAttr(rampTexture + '.colorEntryList[1].position', 0.45)
cmds.setAttr(rampTexture + '.colorEntryList[0].color', 0, 0, 0, type="float3")
cmds.setAttr(rampTexture + '.colorEntryList[1].color', 1, 0, 0, type="float3")
cmds.connectAttr(samplerNode + '.facingRatio', rampTexture + '.vCoord')
cmds.connectAttr(rampTexture + '.outColor', shaderNode + '.color')
createNodes()
如果你运行脚本,你应该在 hypershade 中看到一个新着色器出现,内部为红色,外部边缘为黑色。
它是如何工作的...
脚本分为三个部分——创建节点、设置它们的属性以及将它们相互连接。
首先,我们使用shadingNode命令创建所需的三个节点:
shaderNode = cmds.shadingNode('blinn', asShader=True)
rampTexture = cmds.shadingNode('ramp', asTexture=True)
samplerNode = cmds.shadingNode('samplerInfo', asUtility=True)
首先要注意的是,尽管我们创建了三个不同类型的节点(一个着色器、一个纹理和一个工具),但仍然使用shadingNode命令。在所有情况下,通过指定你想要创建的特定类型的节点(例如'blinn')以及包括以下标志之一设置为 True:asShader、asTexture、asUtility、asPosProcess、asRendering、asLight,你都可以得到你想要的结果。
如果省略这些标志,将会导致错误。如果你包含了错误的标志(例如在创建 Blinn 着色器时asTexture=True),命令仍然会工作,但我不建议这样做。
创建节点相当直接——只需确保你也将输出保存到一个变量中(就像我们在这里所做的那样),这样你就可以稍后设置它们的属性并将它们连接起来。
一旦我们创建了所有节点,我们需要设置它们的属性。在这种情况下,我们需要对渐变纹理执行以下几项不同的操作:
-
确保颜色插值设置为线性,以便在颜色之间提供清晰的过渡,而不是平滑的渐变。
-
确保颜色样本在渐变的长度上正确定位。
-
将两种颜色都设置为边缘颜色和我们想要的内部颜色。
对于上述所有内容,我们将使用setAttr命令。setAttr命令期望第一个参数是要设置的属性的名称,后面跟着应该设置的值。对于单个数值,这相当直接。例如,以下将渐变的插值设置为无:
cmds.setAttr(rampTexture + '.interpolation', 0)
虽然插值的类型实际上不是一个数值,但在 Maya(和其他地方)中,使用整数来表示各种选项是一种常见的做法。当设置在界面中以下拉菜单表示的属性值时,你通常会想要使用一个整数,其具体值对应于列表中选项的位置(其中 0 是第一个)。
接下来,我们需要设置渐变纹理的颜色,使其具有正确的位置和颜色。首先需要理解的是,渐变纹理维护一个节点数组,这些节点包含在其colorEntryList属性中。该列表中的每个条目代表渐变纹理中的一个停止点,并具有位置和颜色。
我们需要确保第一个条目位于渐变的非常开始处,第二个条目位于中间略低的位置,因为它为我们提供了着色器的好默认边缘厚度。我们使用setAttr来设置colorEntryList数组中前两个条目的位置,如下所示:
cmds.setAttr(rampTexture + '.colorEntryList[0].position', 0)
cmds.setAttr(rampTexture + '.colorEntryList[1].position', 0.45)
接下来,我们将想要设置颜色。这有点不同,因为我们需要将三个单独的值输入到 setAttr 命令中(每个分别用于红色、绿色和蓝色)。为此,我们需要提供所有三个数字,并且我们还需要告诉 Maya 通过使用 type 标志来期望 setAttr 命令有多个输入。
setAttr 命令是 Maya 提供的最强大和最灵活的命令之一。它可以用来改变任何节点的任何值。所有这些功能都需要命令能够接受各种类型的输入,所有这些输入都通过类型标志指定。在这种情况下,我们需要一个支持十进制值的格式(因为颜色是以从 0 到 1 的数字表示的),并且支持三个单独的值。float3 或 double3 都可以工作。将这些全部组合起来,我们得到以下内容:
cmds.setAttr(rampTexture + '.colorEntryList[0].color', 0, 0, 0, type="float3")
cmds.setAttr(rampTexture + '.colorEntryList[1].color', 1, 0, 0, type="float3")
到目前为止,我们已经设置了着色器所需的全部属性值。剩下要做的就是将节点连接起来。我们使用 connectAttr 命令来完成这个操作。这相当直接,只需要我们首先指定源属性,然后指定目标。
在这种情况下,我们想要建立两个连接:
-
样本信息中的
facingRatio属性与漫射的 V 坐标 -
漫射纹理的
outColor与着色器的颜色
做这件事的结果看起来像:
cmds.connectAttr(samplerNode + '.facingRatio', rampTexture + '.vCoord')
cmds.connectAttr(rampTexture + '.outColor', shaderNode + '.color')
还有更多...
创建节点并将它们的属性连接起来是处理 Maya 中各种任务的好方法,但有时可能会很繁琐。例如,如果我们想要创建一个 place2dTexture 工具节点并将其连接到纹理节点,我们可能需要做出十多个连接,这至少是繁琐的。幸运的是,Maya 提供了一个简单的快捷方式来创建具有默认行为的节点,即 defaultNavigation 命令。
这看起来会是这样:
fileTex = cmds.shadingNode('file', asTexture=True)
placeTex = cmds.shadingNode('place2dTexture', asUtility=True)
cmds.defaultNavigation(connectToExisting=True, source=placeTex, destination=fileTex)
注意包含 connectToExisting=True 以指示要连接的节点已经在场景中存在。这确实比 18 个单独的 connectAttr 调用要优雅得多。
你也可以使用 Python 的 disconnectAttr 命令断开节点之间的连接。例如,如果我们想要之前提到的由 place2dTexture 和文件纹理组成的两个节点网络共享除偏移属性之外的所有内容,我们可以这样做:
cmds.disconnectAttr(placeTex + '.offset', fileTex + '.offset')
有时,使用默认连接(带有 defaultNavigation)连接两个节点可能会更快,而不是手动创建所有你想要的连接,同时断开一些你不想保留的特定连接。
参见
一定要参考 setAttr 命令的内置文档,以获取它可以接受的输入类型的完整列表。文档有点密集,但绝对值得一看。
将着色器应用到对象上
一旦创建了一个着色网络,你通常会想要将其应用到一个或多个对象上。在这个例子中,我们将探讨如何实现这一点。在这个过程中,我们将创建一个脚本,可以用来将着色器应用到场景中所有没有着色器的对象上。
准备工作
确保你有一个包含几个不同对象的场景。选择几个对象,并使用 hypershade 的界面以正常方式将着色器应用到它们上。删除着色器,至少留下一个没有任何类型着色器的对象。
如何实现...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def shadersFromObject(obj):
cmds.select(obj, replace=True)
cmds.hyperShade(obj, shaderNetworksSelectMaterialNodes=True)
shaders = cmds.ls(selection=True)
return shaders
def isGeometry(obj):
shapes = cmds.listRelatives(obj, shapes=True)
shapeType = cmds.nodeType(shapes[0])
geometryTypes = ['mesh', 'nurbsSurface', 'subdiv']
if shapeType in geometryTypes:
return True
return False
def findUnattachedObjects():
objects = cmds.ls(type="transform")
unShaded = []
for i in range(0, len(objects)):
if (isGeometry(objects[i])):
shaders = shadersFromObject(objects[i])
if (len(shaders) < 1):
unShaded.append(objects[i])
newShader = cmds.shadingNode('blinn', asShader=True)
cmds.setAttr(newShader + '.color', 0, 1, 1, type="double3")
cmds.select(unShaded, replace=True)
cmds.hyperShade(assign=newShader)
findUnattachedObjects()
运行脚本,你应该会看到任何之前没有着色器的对象都添加了一个全新的、青色的 blinn 着色器。
它是如何工作的...
脚本通过以下方式工作:
-
获取场景中所有对象的列表。
-
遍历列表并检查给定的节点是否为几何形状。
-
对于任何几何节点,找到应用在其上的着色器。
-
如果给定的对象没有着色器,将其添加到非着色器对象列表中。
-
创建一个新的着色器以应用。
-
将着色器应用到没有着色器的对象上。
此脚本利用hyperShade命令的几种不同方式——用于查找附加到对象上的着色器、附加到着色器上的对象,以及应用着色器。
首先,让我们看看如何获取给定对象的着色器。为了简化后续操作,我们将创建一个函数来完成这个任务。我们有以下代码:
def shadersFromObject(obj):
cmds.select(obj, replace=True)
cmds.hyperShade(shaderNetworksSelectMaterialNodes=True)
shaders = cmds.ls(selection=True)
return shaders
我们主要使用的是带有shaderNetworksSelectMaterialNodes(或简称smn)标志设置为 true 的 hyperShade 命令。这将选择当前选中对象的着色器(或着色器)。因为该命令在选区上工作,我们必须确保在我们运行它之前,我们想要了解的对象(或对象)已被选中。一旦我们运行了它,我们需要检查当前选中的节点以获取着色器的列表。
接下来,我们创建一个函数,以便轻松判断给定的变换节点是否对应实际几何形状。我们需要它,因为我们将要遍历场景中的所有变换,而且有许多事物(灯光、相机等等)具有变换但不是几何形状。我们将节点的名称作为输入,并找到相应的形状节点:
def isGeometry(obj):
shapes = cmds.listRelatives(obj, shapes=True)
然后,我们检查形状节点以确定它是什么类型的对象:
shapeType = cmds.nodeType(shapes[0])
注意,listRelatives命令返回一个数组,因此我们需要索引到那里并获取第一个元素。一个对象不太可能有多个形状节点,但listRelatives也可以用来查找对象的孩子,这些孩子通常会是多个节点。由于有时会导致多个节点,因此即使只有一个项目,该命令也总是返回一个数组。
Maya 中的三种几何类型(多边形、NURBS 和细分曲面)各自对应一个形状节点。为了方便和代码可读性,我们将创建一个包含这些类型的数组,并检查当前形状节点的类型是否与之匹配:
geometryTypes = ['mesh', 'nurbsSurface', 'subdiv']
if shapeType in geometryTypes:
return True
到目前为止,我们已经准备好跳入脚本的真正核心部分。我们首先使用ls命令获取场景中所有变换的列表。到目前为止,我们主要使用它来找到当前选定的内容,但它也可以用来获取特定类型(无论是否选中)的所有节点:
objects = cmds.ls(type="transform")
然后,我们创建一个空列表,我们将把任何我们发现缺少着色器的对象添加到这个列表中,并开始遍历这个变换列表。首先,我们检查以确保所讨论的节点是某种几何形状。如果是这样,我们使用我们的shadersFromObject函数来找到应用于该对象的着色器。一旦我们完成这个操作,我们就检查返回列表的长度——如果它是零,那么该对象没有着色器,我们就将其添加到我们的列表中:
unShaded = []
for i in range(0, len(objects)):
if (isGeometry(objects[i])):
shaders = shadersFromObject(objects[i])
if (len(shaders) < 1):
unShaded.append(objects[i])
到目前为止,unShaded列表包含了场景中所有缺少着色器的对象。我们创建一个新的着色器,一个简单的 Blinn,并将其颜色设置为青色:
newShader = cmds.shadingNode('blinn', asShader=True)
cmds.setAttr(newShader + '.color', 0, 1, 1, type="double3")
最后,我们选择unShaded列表的内容,并应用我们刚刚创建的着色器。为此,我们将再次使用hyperShade命令,但这次使用分配标志将指定的着色器应用到当前选定的对象上。我们有以下代码:
cmds.select(unShaded, replace=True)
cmds.hyperShade(assign=newShader)
还有更多...
hyperShade命令可以用来完成在 hypershade 面板界面中通常可以完成的许多任务。在上一个例子中,我们从对象中获取了着色器,但该命令也可以使用objects标志来找到与给定着色器关联的对象。将这个功能封装在一个漂亮的函数中,以返回给定着色器的对象,看起来可能如下所示:
def objectsFromShader(shader):
cmds.hyperShade(objects=shader)
objects = cmds.ls(selection=True)
return objects
再次,hyperShade改变了当前的选择,我们使用ls命令来检索选择作为一个数组。
使用着色节点进行非着色任务
Maya 提供的各种节点中真正出色的一点是,在使用它们时几乎没有限制。对 Maya 来说,所有节点只是具有某些输入和输出的功能集合,只要数据类型匹配,它实际上并不关心你如何连接它们。
这意味着,完全有可能(并且通常非常有用)使用 hypershade 节点来完成与创建着色网络无关的任务。在这个例子中,我们将使用加减平均实用节点来设置给定对象的位置为多个其他对象的平均位置。例如,这可以用来确保角色的骨盆始终保持在控制其脚的 IK 手柄之间。
使用工具节点可以用于你可能需要编写表达式的任务,但它们的好处是它们会不断更新,而不仅仅是当播放头移动时。
准备工作
确保你的场景中至少有三个对象。
如何操作...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def keepCentered():
objects = cmds.ls(selection=True)
if (len(objects) < 3):
cmds.error('Please select at least three objects')
avgNode = cmds.shadingNode('plusMinusAverage', asUtility=True)
cmds.setAttr(avgNode + '.operation', 3)
for i in range(0, len(objects) - 1):
cmds.connectAttr(objects[i] + '.translateX', avgNode + '.input3D[{0}].input3Dx'.format(i))
cmds.connectAttr(objects[i] + '.translateZ', avgNode + '.input3D[{0}].input3Dz'.format(i))
controlledObjIndex = len(objects) - 1
cmds.connectAttr(avgNode + '.output3D.output3Dx', objects[controlledObjIndex] + '.translateX')
cmds.connectAttr(avgNode + '.output3D.output3Dz', objects[controlledObjIndex] + '.translateZ')
keepCentered()
选择至少三个对象,确保你想要控制的对象是最后一个选中的,然后运行脚本。一旦你这样做,尝试移动对象,你会发现控制对象的 X 和 Z 位置总是所有其他对象的 X 和 Z 位置的平均值。
它是如何工作的...
首先,我们检查确保至少有三个对象被选中,如果没有则报错:
objects = cmds.ls(selection=True)
if (len(objects) < 3):
cmds.error('Please select at least three objects')
如果我们有至少三个对象,我们就继续创建一个新的工具节点,在这种情况下是一个加减平均节点。由于加减平均节点可以执行三个完全不同的操作,我们还需要将其“操作”属性设置为平均(这恰好是相应下拉菜单中的第四个选项,因此其值为 3),如下所示:
avgNode = cmds.shadingNode('plusMinusAverage', asUtility=True)
cmds.setAttr(avgNode + '.operation', 3)
一旦我们这样做,我们就遍历所选对象的列表,并将除了最后一个之外的所有对象连接到工具节点作为输入。加减平均节点可以有一维、二维或三维输入。在这种情况下,我们将使用三维输入。
小贴士
我们只使用两个输入(X 和 Z),所以我们当然可以用二维输入来应付。然而,由于我们处理的是位置数据,我认为使用完整的 3D 输入并留空 Y 输入更好。这样,以后修改脚本以允许用户选择他们想要的任何 X、Y 和 Z 的组合会更容易。
当然,加减平均节点的“X”、“Y”和“Z”没有任何内在含义;它们只是计算的三条独立路径,我们当然可以用它们来做与位置无关的事情。
加减平均节点为每种类型的输入(一维、二维和三维)保留一个数组。因此,要使用它,我们首先需要访问正确的数组。如果我们有一个名为avgNode的加减平均节点并想对第二个一维输入进行操作,我们将使用以下方法:
avgNode.input1D[1]
对于二维和三维输入,我们需要指定不仅正确的数组,还要指定正确的条目。对于二维输入,数组指定如下:
avgNode.input2D[0].input2Dy
对于三维输入,数组指定如下:
avgNode.input3D[0].input3Dx
我们不需要明确地将输入添加到工具节点中;我们可以直接使用 connectAttr 将输入连接到节点的输入 3D 数组的连续索引。
在这种情况下,我们想要遍历所有选定的对象(除了最后一个),并将它们的 X 和 Z 位置连接起来,这很容易做到:
for i in range(0, len(objects) - 1):
cmds.connectAttr(objects[i] + '.translateX', avgNode + '.input3D[{0}].input3Dx'.format(i))
cmds.connectAttr(objects[i] + '.translateZ', avgNode + '.input3D[{0}].input3Dz'.format(i))
到那时,我们基本上就完成了。剩下要做的就是将 plusMinusAverage 节点的输出连接到受控对象上。将受控对象的索引存储为变量不是必要的,但这会使代码更易于阅读:
controlledObjIndex = len(objects) - 1
cmds.connectAttr(avgNode + '.output3D.output3Dx', objects[controlledObjIndex] + '.translateX')
cmds.connectAttr(avgNode + '.output3D.output3Dz', objects[controlledObjIndex] + '.translateZ')
还有更多...
在讨论的例子中,我们创建了一个非常简单的网络,但当然可以创建更复杂的网络。需要记住的一个重要事情是,尽管所有节点都有它们被创建时的特定用途,但这绝不限制它们可以被用于何种类型的用途。对玛雅来说,数字只是数字,没有任何东西阻止你使用颜色通道来控制位置或使用旋转来控制透明度。
第五章。添加控件 – 骨架脚本化
本章将介绍如何使用 Python 通过以下方式构建骨架:
-
使用脚本创建骨骼
-
使用脚本设置驱动关键帧关系
-
添加自定义属性以及锁定和隐藏属性
-
使用脚本设置反向运动学(IK)
简介
一旦你创建了你的模型,布局了 UV,并设置了着色网络,如果你想让它移动,你仍然需要将其中的控件构建进去。在本章中,我们将探讨如何使用脚本来实现这一点。
我们将探讨如何使用 Python 来自动化与骨架相关的任务。骨架已经是 3D 动画中较为技术性的方面之一,因此非常适合基于脚本的解决方案。
使用脚本创建骨骼
在这个例子中,我们将探讨如何使用脚本创建骨骼。我们将创建两个示例,一个是简单的骨骼链,另一个是分支集合,类似于你可能想要用于生物的手。
如何做到这一点...
创建一个新文件并添加以下代码:
def createSimpleSkeleton(joints):
'''
Creates a simple skeleton as a single chain of bones
ARGS:
joints- the number of bones to create
'''
cmds.select(clear=True)
bones = []
pos = [0, 0, 0]
for i in range(0, joints):
pos[1] = i * 5
bones.append(cmds.joint(p=pos))
cmds.select(bones[0], replace=True)
def createHand(fingers, joints):
'''
Creates a set of 'fingers', each with a set number of joints
ARGS:
fingers- the number of joint chains to create
joints- the number of bones per finger
'''
cmds.select(clear=True)
baseJoint = cmds.joint(name='wrist', p=(0,0,0))
fingerSpacing = 2
palmLen = 4
jointLen = 2
for i in range(0, fingers):
cmds.select(baseJoint, replace=True)
pos = [0, palmLen, 0]
pos[0] = (i * fingerSpacing) - ((fingers-1) * fingerSpacing)/2
cmds.joint(name='finger{0}base'.format(i+1), p=pos)
for j in range(0, joints):
cmds.joint(name='finger{0}joint{1}'.format((i+1),(j+1)), relative=True, p=(0,jointLen, 0))
cmds.select(baseJoint, replace=True)
createSimpleSkeleton(5)
createHand(5, 3)
运行此代码,你将看到两个独立的骨骼网络,它们都位于原点中心——一个是五根骨骼的垂直链,另一个近似于手(五个手指,每个手指有三个关节)。
最终结果应该看起来像以下这样(在将两个骨骼分开后显示)。

它是如何工作的...
我们将从createSimpleSkeleton命令开始。注意,我们在函数开始处使用了一个三引号注释:
def createSimpleSkeleton(joints):
'''
Creates a simple skeleton as a single chain of bones
ARGS:
joints- the number of bones to create
'''
通过在函数定义中作为第一件事放置注释,Python 会将其识别为文档字符串。
文档字符串是向最终用户提供有关代码做什么以及如何使用的文档的绝佳方式。如果你已经为你的函数添加了文档字符串,用户将能够使用 help 命令查看它们。例如,假设我们有一个名为myFunctions.py的文件,其中包含一些函数,我们以下述方式启动第一个函数:
def functionOne():
"""Description of function one"""
用户可以使用以下命令来查看functionOne函数的描述:
help(myFunctions.functionOne)
注意使用点语法来指定首先模块(Python 将所有文件视为模块),然后是其中的特定函数。还要注意函数名称后面没有括号;这是因为我们并没有调用函数。相反,我们正在将函数传递给 help 命令,这将导致 Python 输出该函数的文档字符串(如果存在)。
文档字符串也可以用来为类和模块提供文档。在两种情况下,确保文档字符串是类、函数或文件中首先出现的内容,无论是直接在“def [functionName]”之后,就像我们在这里所做的那样,还是在“class [className]:”之后(对于类),或者是在文件顶部(对于模块)。
在添加文档字符串时,通常一个好的做法是描述函数的每个输入以及它们的意义。在这种情况下,我们的函数有一个单一输入,它将指定要创建的骨骼数量。
现在我们已经正确地记录了我们的代码,是时候实际创建一些骨骼了。大部分工作是通过使用关节工具来创建一个新的骨骼来完成的,使用位置/p 标志来指定它应该去哪里,如下所示:
cmds.joint(position=(1, 1, 1))
在我们的第一个例子中,我们通过创建一个数组来存储骨骼的位置,并将它传递给每个后续的关节命令调用,使事情变得稍微简单一些。这样,我们可以轻松地只修改我们创建的关节的 Y 位置,同时保持 X 和 Z 坐标不变,以产生一个垂直的骨骼链:
pos = [0, 0, 0]
我们还创建了一个数组来存储在创建每个骨骼之后联合命令的输出,以便我们可以在骨骼创建后对它们执行进一步的操作:
bones = []
一旦我们有了这两者,我们只需通过一个循环,改变我们位置数组中的第二个元素来改变 Y 值,并创建一个新的关节:
for i in range(0, joints):
pos[1] = i * 5
bones.append(cmds.joint(p=pos))
这中最值得注意的是我们没有做什么。请注意,这段代码仅仅创建了骨骼;它没有明确创建任何它们之间的层次结构。尽管如此,前面的代码将导致一个正确的骨骼链,每个都是前一个骨骼的子对象。
这是因为,在创建骨骼时,Maya 会自动将任何新创建的关节作为当前选中对象的子对象,如果那个对象恰好是一个关节。结合 Maya 的所有命令在创建新对象时都会选中那个新对象的事实,这意味着,当我们构建关节链时,Maya 会自动将它们连接到正确的层次结构中。这也解释了为什么函数的第一行是:
cmds.select(clear=True)
这确保了没有任何东西被选中。在创建新的关节网络时,在开始之前确保你的选择是清晰的总是好的;否则,你可能会得到你不想有的连接。
现在我们已经查看了一个简单的骨骼链的创建,我们将继续到createHand函数中一个稍微复杂一点的例子。我们再次在函数的开始处添加一个文档字符串,以正确记录函数的输入和每个效果:
def createHand(fingers, joints):
'''
Creates a set of 'fingers', each with a set number of joints
ARGS:
fingers- the number of joint chains to create
joints- the number of bones per finger
'''
我们首先创建一个单一的关节作为根骨骼,并将其保存在baseJoint变量中,这样我们就可以稍后轻松地引用它。
baseJoint = cmds.joint(name='wrist', p=(0,0,0))
我们还会确保给我们的新骨骼一个合理的名称。在这种情况下,我们将使用“wrist”,因为它将作为所有手指的父骨骼。你可能想知道为什么我们既要设置名称又要将结果存储到变量中。这是必要的,以避免如果场景中已经存在名为“wrist”的东西时出现问题。如果确实存在名为“wrist”的东西,Maya 会在新创建的骨骼名称上附加一个数字,结果可能像“wrist1”。如果我们后来试图对“wrist”做些什么,我们最终会影响到不同的对象。所以,我们必须做两件事;我们将关节命令的输出存储到变量中,以便以后可以引用,并且给它一个名称,这样更容易处理。
在你的绑定中所有骨骼都命名为“jointX”是使事情变得不必要混乱的好方法,所以总是确保给你的骨骼合适的名称;只是不要相信那些名称总是唯一的。
现在我们有了基础骨骼,我们创建了一些变量来控制“手”的布局——一个用于手掌的长度,一个用于每个手指关节的长度,一个用于每个手指之间的间隙。

现在我们准备创建每个手指。我们每次通过循环的第一步是首先选择 baseJoint 骨骼。这就是我们确保拥有正确层次结构所需做的全部工作,每个手指都有一个独立的链,每个链都是 base joint 的父链。
我们每个手指都从比基础关节高 palmLen 单位的关节开始。水平间距稍微复杂一些,需要一些解释。我们有以下代码:
pos[0] = (i * fingerSpacing) - ((fingers-1) * fingerSpacing)/2
上述代码分为两部分:
(i * fingerSpacing)
这将确保手指以正确的数量水平分布,但如果我们就这样留下,所有手指都会在手腕的右侧。为了解决这个问题,我们需要将所有位置向左移动半个总宽度。总宽度等于我们的 fingerSpacing 变量乘以手指之间的间隙数。由于间隙的数量等于手指的数量减一,所以我们有:
((fingers-1) * fingerSpacing)/2
从第一部分减去第二部分将保持手指间的间距不变,但会将所有东西移动,使得手指都集中在基础关节上方。
现在我们已经确定了“手指”基础的位置,我们按照以下方式创建第一个关节:
cmds.joint(name='finger{0}base'.format(i+1), p=pos)
注意,我们使用字符串格式命令从一些字面量和当前手指的编号(加一,以便第一个手指是更易读的“1”而不是“0”)构建关节的名称。这将给出类似“finger1base”、“finger2base”等名称的关节。我们会对后续的关节做类似处理,用手指的名称和关节的名称来命名它们(例如,“finger1joint1”)。
一旦开始绘制手指,我们就会运行另一个循环来创建每个手指关节:
for j in range(0, joints):
cmds.joint(name='finger{0}joint{1}'.format((i+1),(j+1)), relative=True, p=(0,jointLen, 0))
注意,这里有一个小的不同,因为我们把看起来相同的位置传递给关节命令。这仍然有效,因为我们还使用了relative命令,这会导致 Maya 将新骨骼相对于其直接父级定位。在这种情况下,这意味着每个新骨骼都将创建在之前的骨骼上方jointLen个单位。
还有更多...
为了创建分支骨骼,在创建子骨骼之前必须更改当前选中的骨骼。在前面的例子中,我们直接这样做,通过在开始每个新分支之前再次显式选择我们的基础关节。
这不是唯一的方法,你还可以使用pickWalk命令。pickWalk命令作用于当前选择,并允许你在其层次结构中移动。要使用该命令,你必须指定一个方向——向上、向下、向左或向右。最有用的选项是向上,这将更改选择以成为当前选中节点的父节点,以及向下,这将更改选择为当前选中节点的子节点(假设它有子节点)。因此,创建关节分支网络的另一个选项是
导入 maya.cmds 作为 cmds,如下所示:
cmds.joint(p=(0,0,0))
cmds.joint(p=(-5, 5, 0))
cmds.pickWalk(direction="Up")
cmds.joint(p=(5, 5, 0))
前两行创建一个基础骨骼,并在上方和左侧各一个单位的位置添加一个子骨骼。然后,使用pickWalk命令将选择移动回基础关节,在创建第三个骨骼之前。
创建三个骨骼的连续结果。左图表示在创建第二个骨骼后使用 pickWalk 向上移动层次结构会发生什么,右图表示省略 pickWalk 会发生什么。

使用脚本设置驱动键关系
大量的绑定工作不过是设置属性之间的连接。有时,这些连接可能非常直接,例如确保两个关节在世界空间中始终处于完全相同的位置,但在其他时候,可能需要除了直接的一对一映射之外的其他映射。
有几种不同的方式可以将属性以非线性方式连接起来,包括使用 Maya 的驱动键功能将一个属性的任意范围映射到另一个属性的另一个任意范围。在本例中,我们将探讨如何通过脚本设置它。
我们的例子将设置使用驱动键的“Hello World”等效,一个手指的所有关节同时平滑弯曲,允许动画师为每个手指键帧一个属性,而不是三个(或甚至更多)。
准备工作
对于这个例子,你希望有一个至少由三个骨骼组成的简单链。脚本的输出将导致当父骨骼(指关节)旋转时,所选骨骼下游的所有骨骼都会旋转。你可以创建一个简单的骨骼链,或者使用本章示例中创建骨骼的输出。
如何操作...
创建一个新的脚本并输入以下代码:
import maya.cmds as cmds
def setDrivenKeys():
objs = cmds.ls(selection=True)
baseJoint = objs[0]
driver = baseJoint + ".rotateZ"
children = cmds. listRelatives(children=True, allDescendents=True)
for bone in children:
driven = bone + ".rotateZ"
cmds.setAttr(driver, 0)
cmds.setDrivenKeyframe(driven, cd=driver, value=0, driverValue=0)
cmds.setAttr(driver, 30)
cmds.setDrivenKeyframe(driven, cd=driver, value=30, driverValue=30)
cmds.setAttr(driver, 0)
setDrivenKeys()
一旦脚本准备就绪,选择“指关节”骨骼并运行它。然后,尝试围绕 z 轴旋转指关节骨骼。你应该看到从指关节下游的所有骨骼也会旋转:
[fig setDrivenKey_1]
它是如何工作的...
脚本有两个主要方面——实际的驱动键设置和一些遍历骨骼链的代码。
首先,我们开始获取当前选定的对象,就像我们过去做的那样。
objs = cmds.ls(selection=True)
baseJoint = objs[0]
我们将选定的对象存储到一个变量(baseJoint)中,这样我们就可以轻松地稍后引用它。我们还将需要一个容易引用的驱动属性,在这种情况下,是基础骨骼的 Z-旋转,因此我们也将它存储到一个变量中。
driver = baseJoint + ".rotateZ"
现在我们准备开始逐步遍历我们的骨骼链。为此,我们首先需要获取从所选关节的所有下游骨骼的列表。我们可以使用带有子节点标志的 listRelatives 命令来完成此操作。通常,这只会使用给定节点的直接子节点,但如果我们也将 allDescendents 标志设置为 True,我们将得到完整的子节点、孙节点以及整个层次结构中的所有节点列表:
children = cmds.listRelatives(children=True, allDescendents=True)
现在我们已经有一个所选节点(在这种情况下,我们的基础关节)的所有子节点的列表,我们准备遍历列表并在每个上设置一个驱动键关系。为此,我们将使用 setDrivenKeyframe 命令。
在循环的每次迭代中,我们将:
-
将我们的
driven变量设置为正确的属性(骨骼 + ".rotateZ")。 -
使用
setAttr将驱动属性设置为最小值。 -
使用
setDrivenKeyframe命令来链接两个属性。 -
重复步骤 2 和 3 来设置最大值。
setDrivenKeyframe 命令相当直接,需要我们传递驱动属性、驱动属性和每个属性的值。在两种情况下,所涉及的属性都需要是全名(节点名称、"." 和属性名称)。因此,为了设置当驱动属性在 -10 时,驱动属性在 0 时,我们可以使用以下命令:
cmds.setDrivenKeyframe(driven, cd=driver, value=-10, driverValue=0)
这应该足以得到我们想要的结果,但除非驱动值事先明确设置,否则该命令通常会失败。这就是为什么我们在调用 setDrivenKeyframe 之前使用 setAttr。
setAttr 命令是一个真正的多面手,你很可能会在许多不同的场景中使用它。幸运的是,它也非常容易使用;只需调用它,然后传入你想要设置的属性,然后是你想要设置的值,就像这样:
cmds.setAttr(driver, 30)
一旦我们在每个骨骼上设置了至少两个键,我们就会有一个合适的关键驱动键关系。将这些放在一起,我们得到以下循环:
for bone in children:
driven = bone + ".rotateZ"
cmds.setAttr(driver, 0)
cmds.setDrivenKeyframe(driven, cd=driver, value=0, driverValue=0)
cmds.setAttr(driver, 30)
cmds.setDrivenKeyframe(driven, cd=driver, value=30, driverValue=30)
最后,我们将通过一些清理工作来完成脚本,以确保我们留下的是我们找到的状态。在这种情况下,这意味着将驱动值设置回零。
cmds.setAttr(driver, 0)
还有更多...
在前面的示例中,我们只使用了两个关键帧,但如果你想要在属性和驱动变量之间建立更非线性的关系,你当然可以在图表上有超过两个点。例如,如果我们想要在范围的最后三分之一内,驱动变量以更快的速度变化,我们可以做如下操作:
cmds.setAttr(driver, 0)
cmds.setDrivenKeyframe(driven, cd=driver, v=0, dr=0)
cmds.setAttr(driver, 20)
cmds.setDrivenKeyframe(driven, cd=driver, v=20, dr=10)
cmds.setAttr(driver, 30)
cmds.setDrivenKeyframe(driven, cd=driver, v=30, dr=30)
在此代码中,驱动器的第一个二十个单位的改变(0-20)将导致驱动变量只有十个单位的改变(0-10),但驱动器的最后十个单位的改变(20-30)将驱动驱动属性二十个单位的改变。
你可能还想考虑你想创建什么类型的曲线。使用setDrivenKeyframe添加的每个关键帧都可以为其输入和输出分配自己的切线类型。要做到这一点,在调用函数时设置inTangentType或outTangentType。在任何情况下,你都会想要提供一个字符串,表示你想要的切线类型。
因此,如果我们想要为新的驱动关键帧的输入和输出都使用线性切线,我们可以这样做:
cmds.setDrivenKeyframe(driven, cd=driver, v=30, dr=30, inTangentType="linear", outTangentType="linear")
要获取允许选项的完整列表,请参阅setDrivenKeyframe命令的文档。
添加自定义属性以及锁定和隐藏属性
当你为模型建立绑定时,创建自定义属性通常很有帮助,这样你就可以将东西链接到forefingerRight.curl(例如),而不是forefingerRight.rotateZ。这不仅会使你的绑定更容易理解,而且还可以让你将绑定的动作与完全独立于任何内置效果(如旋转或平移)的值相关联。
正如有时你可能想要给某个节点添加属性一样,节点上往往有一些你知道永远不会想要动画化的属性。锁定这些属性并在通道框中隐藏它们是使你的绑定更容易工作的另一种方法。
在这个例子中,我们将探讨如何做这两件事——向节点添加新的自定义属性,并从视图中隐藏不希望或不重要的属性。更具体地说,我们将隐藏旋转和缩放属性,并添加一些你可能想要用于动画面部绑定的属性。
下面是示例脚本运行前后通道框的截图:

准备工作
在向节点添加或修改属性之前,确定确切需要什么非常重要。在这种情况下,我们将以类似于我们可能希望用于面部绑定控制的方式设置事物。这意味着对于节点本身,我们可能希望能够改变其位置,但不能改变其旋转或缩放。
我们还希望添加几个不同的属性来控制我们的绑定部分。这些属性自然会因绑定而异,但在所有情况下,考虑每个属性所需的数据类型都是必要的。为了举例,让我们假设我们想要以下控制:
-
一个"blink"属性,将导致上下眼睑同时闭合和睁开。
-
为每条腿添加一个"IK/FK"切换控制,可以在 IK 和 FK 控制之间切换
对于这些,我们需要考虑我们需要什么类型的数据。对于眨眼属性,我们想要一个可以从一个值(表示完全睁开)平滑变化到另一个值(表示完全闭合)的数字。为此,我们需要一个十进制数。
对于 IK/FK 切换,我们可以采取两种不同的方法。我们可以有一个表示 IK 是否开启的值,其中"关闭"表示当前正在使用 FK。为此,我们希望使用一个简单的开/关值。或者,我们可以将我们的 IK/FK 切换实现为一个选项下拉菜单。这可能是更好、更用户友好的方法。在这个例子中,我们将实现这两种方法以确保完整性。
如何操作...
创建一个新文件并添加以下代码:
def addCustomAttributes():
objs = cmds.ls(selection=True)
cmds.addAttr(objs[0], shortName="blink", longName="blink", defaultValue=0, minValue=-1, maxValue=1, keyable=True)
cmds.addAttr(objs[0], shortName="ikfkR", longName="ikfkRight", attributeType="bool", keyable=True)
cmds.addAttr(objs[0], shortName="ikfkL", longName="ikfkLeft", attributeType="enum", enumName="IK:FK", keyable=True)
cmds.setAttr(objs[0]+".rotateX", edit=True, lock=True, keyable=False, channelBox=False)
for att in ['rotateY','rotateZ','scaleX','scaleY','scaleZ']:
lockAndHide(objs[0], att)
def lockAndHide(obj, att):
fullAttributeName = obj + '.' + att
cmds.setAttr(fullAttributeName, edit=True, lock=True, keyable=False, channelBox=False)
setup()
addCustomAttributes()
选择一个对象并运行前面的脚本,确保在这样做时通道框是可见的。你应该会看到旋转和缩放属性消失,而新的属性出现。
它是如何工作的...
首先,我们获取当前选定的对象,就像我们过去做的那样。一旦我们这样做,我们就开始添加眨眼属性,如下所示:
cmds.addAttr(objs[0], shortName="blink", longName="blink", defaultValue=0, minValue=-1, maxValue=1, keyable=True)
这是一个相当复杂的命令,但基本思想是,对于我们要添加的每个属性,我们指定以下内容:
-
名称
-
属性的类型
-
特定类型的属性所需的所有附加信息
属性名称有两种形式——短名称和长名称。您必须指定其中之一才能使命令生效,但通常最好同时指定两者。在这种情况下,"blink"足够短,可以用于短名称和长名称版本。
如果我们没有直接指定创建的属性类型,Maya 将默认为数值类型,这恰好是我们想要的眨眼属性类型。由于"眨眼"属性有一个自然的上限和下限(因为眼睑只能打开有限的程度),因此为我们的属性指定最小值和最大值也很有意义,默认值位于两者之间。在这里,我们使用-1 和 1 作为最小值和最大值,这是相当标准的。
最后,为了确保我们的新属性出现在通道框中,我们需要确保将 keyable 标志设置为 true。
接下来是简单的开/关版本的 IK/FK 开关。为此,我们将使用布尔类型。对于非数值类型,我们需要使用 attributeType 标志并指定适当的值(在这种情况下,"bool")。我们仍然指定短名称和长名称,并使用 keyable 标志使其出现在通道框中:
cmds.addAttr(objs[0], shortName="ikfkR", longName="ikfkRight", attributeType="bool", keyable=True)
生成的属性将接受 0 或 1 的值,但在通道框中将以“关闭”或“开启”(分别)的形式显示。
对于我们的最后一个属性,我们将创建一个具有两种可能状态的属性,即“IK”或“FK”,以下拉列表的形式呈现给用户。为此,我们将创建一个类型为“enum”(即“枚举列表”)的属性。我们还需要使用 enumName 标志指定我们想要的特定选项。enumName 标志期望一个包含一个或多个选项的字符串,所有选项都由冒号分隔。
因此,为了有“IK”和“FK”选项,我们希望 enumName 标志的值为“IK:FK”。将这些全部放在一起,我们得到:
cmds.addAttr(objs[0], shortName="ikfkL", longName="ikfkLeft", attributeType="enum", enumName="IK:FK", keyable=True)
注意,为了实际上将我们的新属性连接到任何东西,了解每个选项的实际值非常重要。默认情况下,第一个选项的值将为 0,每个后续选项的值将增加 1。因此,在这种情况下,“IK”将对应于 0,“FK”将对应于 1。如果您想为特定选项指定特定的数值,这也是可能的。例如,如果我们想让“IK”对应于 5,“FK”对应于 23,我们可以使用以下方法:
cmds.addAttr(objs[0], longName="ikCustomVals", attributeType="enum", enumName="IK=5:FK=23", keyable=True)
到目前为止,我们已经完成了属性的添加,可以继续隐藏我们不需要的属性——旋转和缩放属性。为了正确地隐藏每个属性,我们需要做三件事,具体如下:
-
锁定属性,使其值不能更改。
-
将属性设置为不可键入。
-
将属性设置为不在通道框中显示。
所有这些都可以使用 setAttr 命令在编辑模式下完成,如下所示:
cmds.setAttr(objs[0]+".rotateX", edit=True, lock=True, keyable=False, channelBox=False)
注意,我们传递给 setAttr 的第一个参数是属性的完整名称(对象名称和属性名称,由一个点“.”连接)。虽然每次这样做可能有点繁琐,但我们可以创建一个函数,该函数接受对象和属性名称,并将其锁定并隐藏。
def lockAndHide(obj, att):
fullAttributeName = obj + '.' + att
cmds.setAttr(fullAttributeName, edit=True, lock=True, keyable=False, channelBox=False)
然后,我们可以使用 Python 内置的一些功能,通过遍历属性名称列表并将它们传递给我们的 lockAndHide 函数,使锁定一系列属性变得更加容易,如下所示:
for att in ['rotateY','rotateZ','scaleX','scaleY','scaleZ']:
lockAndHide(objs[0], att)
在这种情况下,Python 对 for 循环(遍历列表)的方法确实使事情变得非常简单明了。
还有更多...
如果你查看addAttr命令的文档,你会看到一个广泛的属性类型列表。不要让列表的长度吓到你;你很可能想要添加的大多数属性都可以作为默认(双精度)类型实现,并具有适当的最低和最高值。“双精度”在这里是“双精度”的缩写,意味着一个十进制值使用的字节数是典型浮点数的两倍。
虽然几种不同的整数和浮点数值类型在你的脚本中可能不会造成太大的差异,但一些更神秘的类型可能会派上用场。
你可能会发现的一项有用功能是能够向节点添加颜色属性。添加颜色需要添加一个复合属性,这比我们之前看到的要复杂一些。首先,你需要添加一个属性作为父属性,然后你需要添加连续的子属性,类型与父属性类型相同,数量与父属性类型正确。
对于颜色,我们需要使用具有三个值的父属性类型,例如“float3”。我们还想将usedAsColor标志设置为 true,以便 Maya 能够正确识别它为颜色。
cmds.addAttr(objs[0], longName='colorTest', attributeType='float3', usedAsColor=True)
一旦我们完成了这个步骤,我们就可以为父属性(在这种情况下,红色、绿色和蓝色组件的值)的每个组件添加属性。注意使用父标志正确地将新属性绑定到我们的“colorTest”组:
cmds.addAttr(objs[0], longName='colorR', attributeType='float', parent='colorTest' )
cmds.addAttr(objs[0], longName='colorG', attributeType='float', parent='colorTest' )
cmds.addAttr(objs[0], longName='colorB', attributeType='float', parent='colorTest' )
注意,某些类型的属性不会在通道框中显示。要查看此类属性,请选择已添加属性的节点,打开属性编辑器,并展开“额外属性”选项卡。

使用脚本设置反向运动学(IK)
虽然任何给定模型很可能都需要至少一些自定义骨架工作,但自动化设置在许多不同骨架中频繁出现的常见子组件通常很有帮助。在这个例子中,我们将这样做,并使用代码设置一个简单的反向运动学(IK)系统。
尽管我们的示例将很简单,但它仍然将演示一个常见问题——需要准确定位关节以匹配模型的特定比例。因此,脚本将有两个明显的部分:
-
一个初步步骤,其中我们创建表示各种关节创建位置的定位器。
-
一个次要步骤,其中我们构建骨架并根据定位器的位置设置其属性。
通过将脚本分成两部分,我们允许用户在创建定位器之后、实际骨架设置之前更改定位器的位置。这通常是一种更有效的方法来匹配骨架和角色,也可以是征求用户对其他类型任务意见的绝佳方式。
准备工作
当你运行示例脚本时不需要有合适的模型,但如果你有一个双足模型来适应我们将要创建的关节,你可能会觉得更有趣。
如何做到这一点...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def showUI():
myWin = cmds.window(title="IK Rig", widthHeight=(200, 200))
cmds.columnLayout()
cmds.button(label="Make Locators", command=makeLocators, width=200)
cmds.button(label="Setup IK", command=setupIK, width=200)
cmds.showWindow(myWin)
def makeLocators(args):
global hipLoc
global kneeLoc
global ankleLoc
global footLoc
hipLoc = cmds.spaceLocator(name="HipLoc")
kneeLoc = cmds.spaceLocator(name="KneeLoc")
ankleLoc = cmds.spaceLocator(name="AnkleLoc")
footLoc = cmds.spaceLocator(name="FootLoc")
cmds.xform(kneeLoc, absolute=True, translation=(0, 5, 0))
cmds.xform(hipLoc, absolute=True, translation=(0, 10, 0))
cmds.xform(footLoc, absolute=True, translation=(2, 0, 0))
def setupIK(args):
global hipLoc
global kneeLoc
global ankleLoc
global footLoc
cmds.select(clear=True)
pos = cmds.xform(hipLoc, query=True, translation=True, worldSpace=True)
hipJoint = cmds.joint(position=pos)
pos = cmds.xform(kneeLoc, query=True, translation=True, worldSpace=True)
kneeJoint = cmds.joint(position=pos)
pos = cmds.xform(ankleLoc, query=True, translation=True, worldSpace=True)
ankleJoint = cmds.joint(position=pos)
pos = cmds.xform(footLoc, query=True, translation=True, worldSpace=True)
footJoint = cmds.joint(position=pos)
cmds.ikHandle(startJoint=hipJoint, endEffector=ankleJoint)
showUI()
它是如何工作的...
由于此脚本需要两个独立的部分,我们需要实现一个简单的用户界面,以便用户可以运行脚本的第一部分,改变定位器的位置,并调用第二部分。用户界面并不复杂,只有两个按钮,如果你已经完成了第二章中的任何示例,应该会感到熟悉。我们简单地创建一个窗口,添加一个布局,并为脚本中的每个步骤添加一个按钮。我们有以下代码:
def showUI():
myWin = cmds.window(title="IK Rig", widthHeight=(200, 200))
cmds.columnLayout()
cmds.button(label="Make Locators", command=makeLocators, width=200)
cmds.button(label="Setup IK", command=setupIK, width=200)
cmds.showWindow(myWin)
当进入makeLocators函数时,事情开始变得有趣,该函数将在默认布局中创建四个定位器对象。在创建定位器之前,我们将创建四个全局变量,以便我们可以存储它们的引用以供后续使用。global关键字告诉 Python 这些变量应该被视为具有全局作用域,这意味着它们将在立即的局部作用域之外(在这种情况下,makeLocators函数)可用。稍后,我们将从我们的第二个函数(setupIK函数)内部再次调用全局变量,以便引用我们即将创建的定位器:
global hipLoc
global kneeLoc
global ankleLoc
global footLoc
现在我们已经准备好创建定位器了。定位器在绑定中特别有用,因为它们提供了一个非渲染的裸骨变换节点,但在 Maya 的界面中很容易选择。
要创建一个定位器,我们可以使用spaceLocator命令。我们将使用名称标志来设置创建的定位器的名称,但这主要是为了使最终用户的使用更加方便。
小贴士
虽然有时给创建的节点命名是个不错的选择,但你绝不应该依赖这些名称作为后续的参考,因为没有保证你指定的名称就是对象最终拥有的名称。如果你将某个东西命名为myObject,但你的场景中已经存在同名节点,Maya 会强制将新创建的对象命名为myObject1,而你编写的任何引用myObject的代码都会指向错误的对象。永远不要在 Maya 中信任名称;相反,将创建节点的命令输出存储到变量中,并使用这些变量来引用创建的对象。
我们将总共创建四个定位器,每个定位器对应于简单腿部的一个部分:臀部、膝盖、脚踝和脚趾。我们调用spaceLocator命令的每个输出都保存到我们的一个全局变量中:
hipLoc = cmds.spaceLocator(name="HipLoc")
kneeLoc = cmds.spaceLocator(name="KneeLoc")
ankleLoc = cmds.spaceLocator(name="AnkleLoc")
footLoc = cmds.spaceLocator(name="FootLoc")
如果你查看spaceLocator命令的文档,你会看到有一个position标志可以用来设置创建的定位器的位置。然而,请注意,我们在之前的代码中没有使用该标志。这是因为虽然定位器看起来会出现在指定的位置,但定位器的旋转中心点将保持在原点。由于我们创建定位器是为了在全局空间中获取位置,这给我们带来了困难。
尽管如此,有一个简单的解决方案,我们只需不指定位置,这将导致定位器和其旋转中心点都位于原点,然后使用xform(简称为“变换”)命令将每个定位器的位置设置到一个合理的起始位置。这最终看起来如下所示:
cmds.xform(kneeLoc, absolute=True, translation=(0, 5, 0))
cmds.xform(hipLoc, absolute=True, translation=(0, 10, 0))
cmds.xform(footLoc, absolute=True, translation=(2, 0, 0))
xform命令可以以几种不同的方式使用,所有这些都与查询或更改节点的变换(位置、旋转和缩放)值有关。在这种情况下,我们使用它来设置定位器的平移值。我们还设置了绝对标志为 true,以指示这些值代表定位器应在绝对坐标中移动到的位置(而不是从当前位置的相对位移)。
我们将髋关节向上移动一点,膝关节向上移动一半的距离,并在x轴上将脚(脚趾)关节向前移动一点。踝关节保持在原点。

一旦设置了必要的定位器,用户就可以调整它们的位置,以更好地匹配将要应用关节的模型的具体细节。完成这一步骤后,我们可以继续创建关节并设置 IK 系统,这我们在setupIK函数中处理。
首先,我们需要调用全局变量,以便我们可以获取定位器的位置并在每个位置创建骨骼。我们也清除选择,以确保安全。我们即将创建骨骼,不希望新创建的关节成为用户运行此脚本部分时可能选择的任何关节的子级。再次使用全局关键字来指定我们指的是全局范围内的变量,而不是局部变量:
global hipLoc
global kneeLoc
global ankleLoc
global footLoc
cmds.select(clear=True)
完成所有这些后,我们就可以开始创建骨骼了。对于每根骨骼,我们首先需要确定每个定位器的世界空间位置,这可以通过xform命令来完成。通过以查询模式调用 xform,我们将检索而不是设置定位器的位置。我们还将确保将worldSpace标志设置为 true,以获取定位器的真实(世界空间)位置,而不是它们的位置。
我们将从髋关节定位器开始,逐个处理我们的定位器列表,获取每个定位器的位置并将其输入到joint命令中以创建骨骼:
pos = cmds.xform(hipLoc, query=True, translation=True, worldSpace=True)
hipJoint = cmds.joint(name="hipBone", position=pos)
pos = cmds.xform(kneeLoc, query=True, translation=True, worldSpace=True)
kneeJoint = cmds.joint(name="kneeBone",position=pos)
pos = cmds.xform(ankleLoc, query=True, translation=True, worldSpace=True)
ankleJoint = cmds.joint(name="akleBone", position=pos)
pos = cmds.xform(footLoc, query=True, translation=True, worldSpace=True)
footJoint = cmds.joint(name="footBone", position=pos)
再次强调,我们依赖于 Maya 的默认行为,即自动连接关节来构建骨骼。一旦所有关节都创建完成,我们最终可以创建逆运动学系统。
设置逆运动学实际上非常简单;我们只需要调用ikHandle命令,并使用startJoint和endEffector标志指定适当的起始和结束关节。在我们的例子中,我们希望逆运动学系统从臀部运行到脚踝。将其转换为代码看起来如下:
cmds.ikHandle(startJoint=hipJoint, endEffector=ankleJoint)
一旦完成这个步骤,我们将拥有一个全新的逆运动学(IK)系统。
还有更多...
虽然这个例子涵盖了从定位器创建关节链并添加逆运动学手柄的基本方法,但还需要做许多其他事情才能完成。为了设置一个真正的逆运动学系统,你可能会想要约束链中每个关节的行为(例如,膝盖关节应该只围绕一个轴旋转)。
正确约束逆运动学系统中的关节通常至少涉及两件事——锁定那些根本不应该旋转的属性,以及设置应该旋转的轴的限制,以便一个应该是膝盖的关节不会向错误的方向弯曲。
要防止关节在给定的轴上旋转,我们可以将相关的jointType属性设置为 0,以完全禁用该轴的旋转。例如,如果我们想确保我们的膝盖关节不会绕x或y轴旋转,我们可以做以下操作:
cmds.setAttr(kneeJoint + ".jointTypeX", 0)
cmds.setAttr(kneeJoint + ".jointTypeY", 0)
这将完全防止任何绕x和y轴的旋转。对于剩余的轴(在本例中为z轴),我们可能希望限制旋转到特定的范围。为此,我们可以使用transformLimits命令,它将允许我们设置旋转的最小值和最大值。
要使用transformLimits命令,我们不仅需要指定特定的最小值和最大值,还需要启用限制。这类似于在属性编辑器中设置关节限制时所见到的,也就是说,除非你也点击了复选框来启用限制,否则最小值和最大值实际上并不适用。
假设我们希望膝盖只能从-90 度旋转到 0 度。我们可以通过以下代码行来设置:
cmds.transformLimits(kneeJoint, rotationZ=(-90, 0), enableRotationZ=(1,1))
在前面的代码中,rotationZ标志用于设置给定节点的最小值和最大值。enableRotationZ的命名有些令人困惑,因为它实际上控制的是旋转限制的设置。因此,将(1,1)传递给enableRotationZ意味着我们正在启用最小值和最大值的限制。如果我们只想有一个最小值(但没有最大值),我们可以做以下操作:
cmds.transformLimits(kneeJoint, rotationZ=(-90, 0), enableRotationZ=(1,0))
在前面的代码中,传递给enableRotationZ的(1,0)将同时启用最小限制并禁用最大限制。
第六章.让事物移动 - 动画脚本编写
本章将涵盖与脚本动画化对象相关的各种配方:
-
查询动画数据
-
与动画层一起工作
-
将动画从一个对象复制到另一个对象
-
设置关键帧
-
通过脚本创建表达式
简介
在本章中,我们将探讨如何使用脚本创建动画和设置关键帧。我们还将了解如何与动画层一起工作并从代码中创建表达式。
查询动画数据
在本例中,我们将探讨如何检索有关动画对象的信息,包括哪些属性是动画化的以及关键帧的位置和值。尽管这个脚本本身可能没有太大用处,但了解关键帧的数量、时间和值有时是进行更复杂动画任务的先决条件。
准备工作
要充分利用这个脚本,你需要有一个定义了动画曲线的对象。要么加载一个带有动画的场景,要么跳转到设置关键帧的配方。
如何做...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
def getAnimationData():
objs = cmds.ls(selection=True)
obj = objs[0]
animAttributes = cmds.listAnimatable(obj);
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True, keyframeCount=True)
if (numKeyframes > 0):
print("---------------------------")
print("Found ", numKeyframes, " keyframes on ", attribute)
times = cmds.keyframe(attribute, query=True, index=(0,numKeyframes), timeChange=True)
values = cmds.keyframe(attribute, query=True, index=(0,numKeyframes), valueChange=True)
print('frame#, time, value')
for i in range(0, numKeyframes):
print(i, times[i], values[i])
print("---------------------------")
getAnimationData()
如果你选择了一个具有动画曲线的对象并运行脚本,你应该会看到每个动画属性上每个关键帧的时间和值。例如,如果我们有一个简单的弹跳球动画,如下所示:

我们在脚本编辑器中可能会看到以下类似输出:
---------------------------
('Found ', 2, ' keyframes on ', u'|bouncingBall.translateX')
frame#, time, value
(0, 0.0, 0.0)
(1, 190.0, 38.0)
---------------------------
---------------------------
('Found ', 20, ' keyframes on ', u'|bouncingBall.translateY')
frame#, time, value
(0, 0.0, 10.0)
(1, 10.0, 0.0)
(2, 20.0, 8.0)
(3, 30.0, 0.0)
(4, 40.0, 6.4000000000000004)
(5, 50.0, 0.0)
(6, 60.0, 5.120000000000001)
(7, 70.0, 0.0)
(8, 80.0, 4.096000000000001)
(9, 90.0, 0.0)
(10, 100.0, 3.276800000000001)
(11, 110.0, 0.0)
(12, 120.0, 2.6214400000000011)
(13, 130.0, 0.0)
(14, 140.0, 2.0971520000000008)
(15, 150.0, 0.0)
(16, 160.0, 1.6777216000000008)
(17, 170.0, 0.0)
(18, 180.0, 1.3421772800000007)
(19, 190.0, 0.0)
---------------------------
工作原理...
我们首先像往常一样获取选定的对象。一旦我们完成了这个步骤,我们将遍历所有keyframeable属性,确定它们是否有任何关键帧,如果有,我们将遍历时间和值。要获取keyframeable属性的列表,我们使用listAnimateable命令:
objs = cmds.ls(selection=True)
obj = objs[0]
animAttributes = cmds.listAnimatable(obj)
这将给我们一个列表,列出了所选对象上所有可以动画化的属性,包括添加到其中的任何自定义属性。
如果你打印出animAttributes数组的内容,你可能会看到以下类似的内容:
|bouncingBall.rotateX
|bouncingBall.rotateY
|bouncingBall.rotateZ
虽然bouncingBall.rotateX部分可能是有意义的,但你可能想知道|符号的含义。这个符号由 Maya 用来指示节点之间的层次关系,以便提供完全限定的节点和属性名称。如果bouncingBall对象是名为ballGroup的组的子对象,我们会看到以下内容:
|ballGroup|bouncingBall.rotateX
每个这样的完全限定名称都将至少包含一个管道(|)符号,正如我们在第一个非分组示例中看到的那样,但可以有更多——每个额外的层次结构层都有一个。虽然这可能会导致属性名称变得很长,但它允许 Maya 利用可能具有相同名称但位于更大层次结构不同部分的对象(例如,为每个角色的每只手命名handControl控制对象)。
现在我们已经列出了该对象所有可能被动画化的属性,接下来我们想要确定是否有任何关键帧被设置在其上。为此,我们可以在查询模式中使用keyframe命令。
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True, keyframeCount=True)
到目前为止,我们有一个变量(numKeyframes),对于至少有一个关键帧的任何属性,它都将大于零。获取一个属性上的关键帧总数只是keyframe命令能做的事情之一;我们还将用它来获取每个关键帧的时间和值。
要做到这一点,我们将调用它两次,都是在查询模式下——一次获取时间,一次获取值:
times = cmds.keyframe(attribute, query=True, index=(0,numKeyframes), timeChange=True)
values = cmds.keyframe(attribute, query=True, index=(0,numKeyframes), valueChange=True)
这两行在所有方面都相同,只是我们请求的信息类型不同。这里需要注意的是索引标志,它用于告诉 Maya 我们感兴趣的是哪些关键帧。该命令需要一个表示要检查的关键帧的第一个(包含)和最后一个(不包含)索引的两个元素参数。所以,如果我们有总共 20 个关键帧,我们将传递(0,20),这将检查索引从 0 到 19 的键。
我们用来获取值的标志可能看起来有点奇怪——valueChange和timeChange可能让你认为我们会得到相对值,而不是绝对值。然而,当以之前提到的方式使用时,该命令将给出我们想要的结果——每个关键帧的实际时间和值,正如它们在图表编辑器中显示的那样。
如果你想要查询单个关键帧的信息,你仍然需要输入一对值——只需将你感兴趣的索引重复两次即可。例如,要获取第四帧,使用(3,3)。
到目前为止,我们有两个数组——times数组,它包含每个关键帧的时间值,以及values数组,它包含实际的属性值。剩下要做的就是打印出我们所找到的信息:
print('frame#, time, value')
for i in range(0, numKeyframes):
print(i, times[i], values[i])
还有更多...
使用索引来获取关键帧的数据是一种轻松地遍历曲线所有数据的方法,但这不是指定范围的唯一方式。keyframe命令也可以接受时间值。例如,如果我们想知道在帧 1 和帧 100 之间给定属性上有多少关键帧,我们可以这样做:
numKeyframes = cmds.keyframe(attributeName, query=True, time=(1,100) keyframeCount=True)
此外,如果你发现自己处理的是高度嵌套的对象,并且需要提取仅包含对象和属性名称,你可能发现 Python 内置的 split 函数很有帮助。你可以在一个字符串上调用 split,让 Python 将其拆分成一个部分列表。默认情况下,Python 会通过空格拆分输入字符串,但你也可以指定一个特定的字符串或字符来拆分。假设你有一个如下所示的字符串:
|group4|group3|group2|group1|ball.rotateZ
然后,你可以使用split根据|符号将其拆分。这将给你一个列表,使用-1作为索引将只得到ball.rotateZ。将其放入一个可以用于从完整字符串中提取对象/属性名称的函数中会很简单,它看起来可能如下所示:
def getObjectAttributeFromFull(fullString):
parts = fullString.split("|")
return parts[-1]
使用它看起来可能像这样:
inputString = "|group4|group3|group2|group1|ball.rotateZ"
result = getObjectAttributeFromFull(inputString)
print(result) # outputs "ball.rotateZ"
处理动画层
Maya 提供了在场景中创建多个动画层的功能,这可以是一个构建复杂动画的好方法。然后,这些层可以独立启用或禁用,或者混合在一起,使用户能够对最终结果有更多的控制。
在这个例子中,我们将查看场景中存在的层,并构建一个脚本以确保我们有一个具有特定名称的层。例如,我们可能想要创建一个脚本,该脚本可以为所选对象的旋转添加额外的随机运动,而不会覆盖它们现有的运动。为此,我们需要确保我们有一个名为randomMotion的动画层,然后我们可以向其中添加关键帧。
如何做到这一点...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def makeAnimLayer(layerName):
baseAnimationLayer = cmds.animLayer(query=True, root=True)
foundLayer = False
if (baseAnimationLayer != None):
childLayers = cmds.animLayer(baseAnimationLayer, query=True, children=True)
if (childLayers != None) and (len(childLayers) > 0):
if layerName in childLayers:
foundLayer = True
if not foundLayer:
cmds.animLayer(layerName)
else:
print('Layer ' + layerName + ' already exists')
makeAnimLayer("myLayer")
运行脚本,你应该会在通道框的Anim选项卡中看到名为myLayer的动画层。
它是如何工作的...
我们首先想做的事情是找出场景中是否已经存在具有给定名称的动画层。为此,我们首先获取根动画层的名称:
baseAnimationLayer = cmds.animLayer(query=True, root=True)
在几乎所有情况下,这应该返回两个可能值之一——要么是BaseAnimation,要么(如果还没有动画层)Python 内置的None值。
我们希望在以下两种可能的情况下创建一个新层:
-
目前还没有动画层
-
有动画层,但没有目标名称的
为了使上述测试更容易一些,我们首先创建一个变量来保存是否找到了动画层,并将其设置为False:
foundLayer = False
现在,我们需要检查是否确实存在两个动画层,并且其中一个具有给定的名称。首先,我们检查确实存在基础动画层:
if (baseAnimationLayer != None):
如果是这样的话,我们想要获取基础动画层的所有子代,并检查其中是否有任何一个具有我们正在寻找的名称。为了获取子动画层,我们将再次使用animLayer命令,再次在查询模式下:
childLayers = cmds.animLayer(baseAnimationLayer, query=True, children=True)
一旦我们做到了这一点,我们就会想看看是否有任何子层与我们要找的层匹配。我们还需要考虑到没有子层的情况(这可能发生在动画层被创建然后后来被删除,只留下基础层的情况下):
if (childLayers != None) and (len(childLayers) > 0):
if layerName in childLayers:
foundLayer = True
如果存在子层并且我们正在寻找的名称被找到,我们将我们的foundLayer变量设置为True。
如果找不到层,我们创建它。这可以通过再次使用 animLayer 命令并指定我们想要创建的层的名称来完成:
if not foundLayer:
cmds.animLayer(layerName)
最后,如果找到了层,我们通过打印一条消息来结束,让用户知道。
还有更多...
拥有动画层是很好的,因为我们可以在创建或修改关键帧时使用它们。然而,实际上我们无法在不首先将相关对象添加到动画层的情况下向层添加动画。
假设我们有一个名为 bouncingBall 的对象,并且我们想在 bounceLayer 动画层上为其 translateY 属性设置一些关键帧。设置关键帧的实际命令可能看起来像这样:
cmds.setKeyframe("bouncingBall.translateY", value=yVal, time=frame, animLayer="bounceLayer")
然而,只有在我们首先将 bouncingBall 对象添加到 bounceLayer 动画层的情况下,这才会按预期工作。为了做到这一点,我们可以在编辑模式下使用 animLayer 命令,并带有 addSelectedObjects 标志。请注意,因为标志作用于当前选中的对象,我们首先需要选择我们想要添加的对象:
cmds.select("bouncingBall", replace=True)
cmds.animLayer("bounceLayer", edit=True, addSelectedObjects=True)
默认情况下,添加对象将添加其所有可动画属性。你也可以添加特定的属性,而不是整个对象。例如,如果我们只想将 translateY 属性添加到我们的动画层,我们可以这样做:
cmds.animLayer("bounceLayer", edit=True, attribute="bouncingBall.translateY")
从一个对象复制动画到另一个对象
在这个例子中,我们将创建一个脚本,该脚本将把一个对象的全部动画数据复制到另一个或多个附加对象上,这可以在多个对象上复制运动时很有用。
准备工作
为了脚本能够工作,你需要一个设置了关键帧的对象。你可以创建一些简单的动画,或者跳到本章后面的示例,关于如何使用脚本创建关键帧。
如何做到这一点...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def getAttName(fullname):
parts = fullname.split('.')
return parts[-1]
def copyKeyframes():
objs = cmds.ls(selection=True)
if (len(objs) < 2):
cmds.error("Please select at least two objects")
sourceObj = objs[0]
animAttributes = cmds.listAnimatable(sourceObj);
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True, keyframeCount=True)
if (numKeyframes > 0):
cmds.copyKey(attribute)
for obj in objs[1:]:
cmds.pasteKey(obj, attribute=getAttName(attribute), option="replace")
copyKeyframes()
选择动画对象,按住 shift 键选择至少另一个对象,然后运行脚本。你会看到所有对象都有相同的运动。
它是如何工作的...
我们脚本的第一个部分是一个辅助函数,我们将使用它来从完整的对象名/属性名字符串中剥离属性名。更多关于它的内容将在稍后提供。
现在让我们来看脚本的主要内容。首先,我们运行一个检查以确保用户至少选择了两个对象。如果没有,我们将显示一个友好的错误消息,让用户知道他们需要做什么:
objs = cmds.ls(selection=True)
if (len(objs) < 2):
cmds.error("Please select at least two objects")
error 命令也会停止脚本的运行,所以如果我们还在继续,我们知道我们至少选择了两个对象。我们将第一个选中的对象设置为源对象。我们同样可以使用第二个选中的对象,但那样就意味着将第一个选中的对象作为目标,限制我们只能有一个目标:
sourceObj = objs[0]
现在,我们准备开始复制动画,但首先,我们需要确定哪些属性目前正在动画化,这需要通过找到所有可以动画化的属性,并检查每个属性上是否有关键帧的组合来实现:
animAttributes = cmds.listAnimatable(sourceObj);
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True, keyframeCount=True)
如果给定属性至少有一个关键帧,我们将继续复制:
if (numKeyframes > 0):
cmds.copyKey(attribute)
copyKey命令将导致给定对象的关键帧临时保存在内存中。如果没有使用任何附加标志,它将获取指定属性的 所有关键帧,这正是我们在这个情况下想要的。如果我们只想获取关键帧的子集,我们可以使用时间标志来指定一个范围。
我们传递由listAnimatable函数返回的每个值。这些将是完整名称(对象名称和属性)。这对于copyKey命令来说是可以的,但对于粘贴操作需要做一点额外的工作。
由于我们将键复制到与原始复制源不同的对象上,我们需要将对象和属性名称分开。例如,我们的attribute值可能如下所示:
|group1|bouncingBall.rotateX
从这里,我们想要剪掉属性名称(rotateX),因为我们是从选择列表中获取对象名称。为此,我们创建了一个简单的辅助函数,它接受完整的对象/属性名称并返回仅属性名称。这很简单,只需在.上拆分名称/属性字符串并返回最后一个元素即可,在这种情况下是属性:
def getAttName(fullname):
parts = fullname.split('.')
return parts[-1]
Python 的split函数将字符串拆分为字符串数组,使用负索引将从末尾开始计数,其中−1给我们最后一个元素。
现在,我们实际上可以粘贴我们的键了。我们将遍历所有剩余的选定对象,从第二个开始,粘贴我们的复制关键帧:
for obj in objs[1:]:
cmds.pasteKey(obj, attribute=getAttName(attribute), option="replace")
注意,我们利用 Python 的 for 循环的特性使代码更易于阅读。与其他大多数语言不同,我们不需要使用索引,而是可以使用for x in y结构。在这种情况下,obj将是一个临时变量,作用域限于 for 循环,它将取列表中每个项目的值。此外,我们不是传递整个列表,而是使用objs[1:]来表示整个列表,从索引1(第二个元素)开始。冒号允许我们指定objs列表的子范围,而将右手边留空将导致 Python 包含列表末尾的所有项目。
我们传递对象名称(来自原始选择),属性(通过我们的辅助函数从完整名称/属性字符串中剥离),并使用option="replace"来确保我们粘贴的关键帧将替换掉任何已经存在的内容。

原始动画(顶部)。在这里,我们看到使用默认设置粘贴键的结果(左侧)和使用替换选项的结果(右侧)。请注意,默认结果仍然包含原始曲线,只是被推到了后面的帧。
如果我们没有包含option标志,Maya 将默认在时间轴上移动任何现有键的同时插入粘贴的关键帧。
还有更多...
对于选项标志有很多其他选项,每个选项都以不同的方式处理与您粘贴的键以及可能已经存在的键的潜在冲突。请务必查看pasteKeys命令的内置文档以获取更多信息。
另一个,也许更好的选项来控制粘贴的键如何与现有的键交互,是将新键粘贴到单独的动画层中。例如,如果我们想确保粘贴的键最终出现在名为extraAnimation的动画层中,我们可以按如下方式修改对pasteKeys的调用:
cmds.pasteKey(objs[i], attribute=getAttName(attribute), option="replace", animLayer="extraAnimation")
注意,如果没有名为extraAnimation的动画层,Maya 将无法复制键。请参阅有关如何查询现有层和创建新层的动画层部分以获取更多信息。
设置关键帧
虽然在 Maya 中确实有各种方法可以使物体移动,但绝大多数运动都是由关键帧驱动的。在这个例子中,我们将探讨如何通过编写代码来创建关键帧,即那个老式的动画备用方案——弹跳球。
准备工作
我们将要创建的脚本将动画化当前选定的对象,所以请确保你有一个对象——无论是传统的球体还是你想要使其弹跳的其他东西。
如何操作...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
def setKeyframes():
objs = cmds.ls(selection=True)
obj = objs[0]
yVal = 0
xVal = 0
frame = 0
maxVal = 10
for i in range(0, 20):
frame = i * 10
xVal = i * 2
if i % 2 == 1:
yVal = 0
else:
yVal = maxVal
maxVal *= 0.8
cmds.setKeyframe(obj + '.translateY', value=yVal, time=frame)
cmds.setKeyframe(obj + '.translateX', value=xVal, time=frame)
setKeyframes()
在选定了对象后运行前面的脚本并触发播放。你应该看到对象上下移动。
它是如何工作的...
为了让我们的对象弹跳,我们需要设置关键帧,使对象在零的Y值和不断减少的最大值之间交替,这样动画就能模仿一个下落物体在每次弹跳时失去速度的方式。我们还将使其在弹跳时沿 x 轴移动。
我们首先获取当前选定的对象,并设置一些变量以使我们在循环中更容易阅读。我们的yVal和xVal变量将保存我们想要设置对象位置的当前值。我们还有一个frame变量来保存当前帧,以及一个maxVal变量,它将用于保存对象当前高度的Y值。
注意
这个例子足够简单,我们实际上不需要为帧和属性值设置单独的变量,但以这种方式设置可以使交换更复杂的数学或逻辑更容易,以控制关键帧的设置位置及其值。
这给我们以下结果:
yVal = 0
xVal = 0
frame = 0
maxVal = 10
脚本的大部分是一个单独的循环,在这个循环中,我们在X和Y位置上设置关键帧。
对于xVal变量,我们只需将一个常数值(在这种情况下,2 个单位)相乘。对于我们的框架,我们也将做同样的事情。对于yVal变量,我们希望交替使用一个不断减少的值(对于连续的峰值)和零(当球击中地面时)。
要在零和非零之间交替,我们需要检查我们的循环变量是否可以被 2 整除。一种简单的方法是取值modulo(%)2。这将给出当值被 2 除时的余数,对于偶数将是零,对于奇数将是 1。
对于奇数,我们将yVal设置为零,对于偶数,我们将它设置为maxVal。为了确保球每次弹跳时都会少一点,我们每次使用maxVal时都将其设置为当前值的 80%。
将所有这些放在一起,我们得到以下循环:
for i in range(0, 20):
frame = i * 10
xVal = i * 2
if (i % 2) == 1:
yVal = 0
else:
yVal = maxVal
maxVal *= 0.8
现在我们终于准备好实际上在我们的对象上设置关键帧了。这可以通过setKeyframe命令轻松完成。我们需要指定以下三件事:
-
关键帧的属性(对象名称和属性)
-
设置关键帧的时间
-
设置属性的实际值
在这种情况下,这最终看起来如下所示:
cmds.setKeyframe(obj + '.translateY', value=yVal, time=frame)
cmds.setKeyframe(obj + '.translateX', value=xVal, time=frame)
就这样!一个通过纯代码动画化的正确弹跳球(或其他对象)。
还有更多...
默认情况下,setKeyframe命令将创建具有内切线和外切线都设置为样条的键帧。这对于很多事情来说都很好,但对于应该击中硬表面的东西来说,会导致过度平滑的动画。
我们可以通过保持关键帧在对象达到最大高度时的平滑切线,但在最小值时设置为线性,来改进我们的弹跳动画。这将每次球击中地面时给我们一个很好的尖锐变化。
要做到这一点,我们只需要将inTangentType和outTangentType标志都设置为linear,如下所示:
cmds.setKeyframe(obj + ".translateY", value=animVal, time=frame, inTangentType="linear", outTangentType="linear")
为了确保当球击中地面时我们只有线性切线,我们可以设置一个变量来保存切线类型,并以与设置yVal变量相同的方式将其设置为两个值之一。
这将最终看起来像这样:
tangentType = "auto"
for i in range(0, 20):
frame = i * 10
if i % 2 == 1:
yVal = 0
tangentType = "linear"
else:
yVal = maxVal
tangentType = "spline"
maxVal *= 0.8
cmds.setKeyframe(obj + '.translateY', value=yVal, time=frame, inTangentType=tangentType, outTangentType=tangentType)
通过脚本创建表达式
虽然大多数 Maya 动画都是手动创建的,但直接通过脚本驱动属性通常很有用,特别是对于机械对象或背景项目。一种方法是使用 Maya 的表达式编辑器。
除了通过表达式编辑器创建表达式外,还可以通过脚本创建表达式,这是一个代码驱动的代码的美丽示例。在这个例子中,我们将创建一个脚本,可以用来创建一个基于正弦波的表达式,以平滑地改变给定属性在两个值之间的变化。请注意,表达式实际上不能直接使用 Python 代码;它们需要代码以 MEL 语法编写。但这并不意味着我们不能使用 Python 来 创建 表达式,这正是我们将在这个例子中做的。
准备工作
在我们深入研究脚本之前,我们首先需要很好地掌握我们将要创建的表达式的类型。有很多人不同的方法可以处理表达式,但在这个例子中,我们将保持相对简单,并将属性与基于当前时间的正弦波相关联。
为什么是正弦波?正弦波很棒,因为它们在两个值之间平滑变化,并且能够很好地从最小值和最大值中平滑过渡。虽然最小值和最大值的范围从 -1 到 1,但足以改变输出,使其在任意两个我们想要的数字之间移动。我们还将通过设置表达式依赖于一个自定义的 speed 属性来使事情更加灵活,该属性可以用来控制属性动画的速度。
最终结果将是一个值,它在用户指定的(并且可关键帧化的)速率下在任意两个数字之间平滑变化。
如何做到这一点...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def createExpression(att, minVal, maxVal, speed):
objs = cmds.ls(selection=True)
obj = objs[0]
cmds.addAttr(obj, longName="speed", shortName="speed", min=0, keyable=True)
amplitude = (maxVal – minVal)/2.0
offset = minVal + amplitude
baseString = "{0}.{1} = ".format(obj, att)
sineClause = '(sin(time * ' + obj + '.speed)'
valueClause = ' * ' + str(amplitude) + ' + ' + str(offset) + ')'
expressionString = baseString + sineClause + valueClause
cmds.expression(string=expressionString)
createExpression('translateY', 5, 10, 1)
它是如何工作的...
我们首先为我们的对象添加一个 speed 属性,就像在 第五章 的自定义属性配方中一样,添加控件 - 布尔脚本。我们将确保它可关键帧化,以便稍后动画:
cmds.addAttr(obj, longName="speed", shortName="speed", min=0, keyable=True)
在创建表达式时,通常至少包含一个可关键帧化的属性是一个好主意。虽然数学驱动的动画确实是一种强大的技术,但你可能仍然希望能够改变具体的细节。给自己一个或多个可关键帧化的属性是做到这一点的简单方法。
现在我们已经准备好构建我们的表达式。但首先,我们需要确切地了解我们想要什么;在这种情况下,一个在两个极端之间平滑变化的值,并且能够控制其速度。我们可以很容易地使用正弦函数构建一个表达式来实现这一点,以当前时间为输入。以下是一般形式的样子:
animatedValue = (sin(time * S) * M) + O;
位置:
-
S是一个值,它将加快(如果大于 1)或减慢(如果小于 1)正弦函数输入的变化速率。 -
M是一个乘数,用于改变值变化的整体范围 -
O是一个偏移量,以确保最小值和最大值正确。
你也可以从视觉上考虑——S 将使我们的波形在水平(时间)轴上拉伸或收缩,M 将使其垂直扩展或收缩,而 O 将将整个曲线的形状向上或向下移动。
S 已经处理好了;它是我们新创建的“速度”属性。M和O需要根据正弦函数总是产生从−1到1的值来计算。
值的范围整体应该从我们的minVal到maxVal,所以你可能认为M应该等于(maxVal – minVal)。然而,由于它应用于−1和1,这将使我们得到两倍于期望的变化。因此,我们想要的最终值是(maxVal – minVal)/2。我们将其存储到我们的振幅变量中,如下所示:
amplitude = (maxVal – minVal)/2.0
接下来是偏移值O。我们希望移动我们的图表,使得最小值和最大值位于它们应该的位置。这似乎意味着只需添加minVal,但如果我们这样做,我们的输出将有一半的时间低于最小值(任何正弦函数产生负输出的时刻)。为了解决这个问题,我们将O设置为(minVal + M),或者在我们的脚本中:
offset = minVal + amplitude
这样,我们将波的0位置移动到minVal和maxVal之间,这正是我们想要的。
为了更清晰地展示,让我们看看我们附加到sin()的不同部分,以及它们如何影响表达式输出的最小值和最大值。我们假设我们想要的最终结果是0到4的范围。
| 表达式 | 额外组件 | 最小值 | 最大值 |
|---|---|---|---|
| sin(time) | None- 原始正弦函数 | −1 | 1 |
| sin(time * speed) | 将输入乘以“速度” | −1 (更快) | 1 (更快) |
| sin(time * speed) * 2 | 将输出乘以 2 | −2 | 2 |
| (sin(time * speed) * 2) + 2 | 向输出添加 2 | 0 | 4 |
注意,2 = (4-0)/2 和 2 = 0 + 2。
这是当绘制时前面进展的样子:

使用正弦函数从 0 到 4 构建一个表达式的四个步骤。
好的,现在我们已经确定了数学关系,我们准备将其转换为 Maya 的表达式语法。如果我们想让名为myBall的对象使用前面的值沿Y轴动画,我们希望最终得到:
myBall.translateY = (sin(time * myBall.speed) * 5) + 12;
如果将其输入 Maya 的表达式编辑器,这将按预期工作,但我们想确保我们有一个更通用的解决方案,可以用于任何对象和任何值。这很简单,只需要从各种字面量和变量中构建前面的字符串,这正是我们在接下来的几行中做的:
baseString = "{0}.{1} = ".format(obj, att)
sineClause = '(sin(time * ' + obj + '.speed)'
valueClause = ' * ' + str(amplitude) + ' + ' + str(offset) + ')'
expressionString = baseString + sineClause + valueClause
我将字符串创建拆分成几行,以便更清晰地展示,但这并不是必需的。这里的关键思想是我们在这几个方面来回切换:字面量(sin(time *, .speed 等)和变量(obj, att, amplitude, 和 offset),以构建整个字符串。注意,我们必须在str()函数中包裹数字,以防止 Python 在将它们与字符串组合时发出抱怨。
到目前为止,我们已经准备好了我们的表达式字符串。剩下的只是将其作为表达式实际添加到场景中,这可以通过expression命令轻松完成:
cmds.expression(string=expressionString)
就这样!我们现在将有一个在任意两个值之间平滑变化的属性。
还有更多...
有许多其他方法可以使用表达式来驱动动画,以及各种可以采用的简单数学技巧。
例如,您可以通过每帧运行此操作来轻松地将值平滑地移动到目标值,并获得对目标的良好渐入效果:
animatedAttribute = animatedAttribute + (targetValue – animatedAttribute) * 0.2;
这会将目标与当前值之间的当前差异的 20%添加到属性中,这将使其向目标移动。由于添加的量始终是当前差异的百分比,因此当值接近目标时,每帧效果会减少,从而提供一种渐入效果。
如果我们将此与一些随机选择新目标值的代码相结合,我们就会得到一种简单的方法,例如,动画背景角色的头部随机朝向不同的位置(可能是为了提供体育场人群)。
假设我们已经为我们的对象添加了targetX、targetY和targetZ的自定义属性,最终看起来可能如下所示:
if (frame % 20 == 0)
{
myCone.targetX = rand(time) * 360;
myCone.targetY = rand(time) * 360;
myCone.targetZ = rand(time) * 360;
}
myObject.rotateX += (myObject.targetX - myCone.rotateX) * 0.2;
myObject.rotateY += (myObject.targetY - myCone.rotateY) * 0.2;
myObject.rotateZ += (myObject.targetZ - myCone.rotateZ) * 0.2;
注意,我们使用模数(%)运算符仅在帧是 20 的偶数倍时执行某些操作(设置目标)。我们还使用当前时间作为rand()函数的种子值,以确保在动画过程中获得不同的结果。
之前提到的示例是如果我们直接将其输入到 Maya 的表达式编辑器中,代码会看起来像这样;注意 MEL 风格的语法(而不是 Python)。通过 Python 生成此代码会比我们的正弦波示例复杂一些,但会使用所有相同的原则——从字面量和变量构建字符串,然后将该字符串传递给expression命令。
第七章:渲染脚本
在本章中,我们将探讨以下主题:
-
创建和编辑灯光
-
创建用于控制所有灯光的 GUI
-
从代码中创建摄像机
-
渲染精灵图集
简介
到目前为止,我们已经探讨了脚本如何帮助建模、纹理、绑定和动画。一旦所有这些都完成了,剩下的就是实际渲染场景。在本章中,我们将探讨如何设置灯光和摄像机,以及如何渲染场景。
创建和编辑灯光
在这个例子中,我们将构建一个脚本,通过脚本快速轻松地设置一个简单的三点照明设置。
这将最终为我们提供一个使用脚本创建不同类型灯光的精彩概述,同时也为我们留下了一个实用的工具。

脚本运行的结果-主光、填充光和背光都指向原点
准备工作
为了获得最佳效果,在运行脚本之前,请确保你的场景中有一个具有相当细节级别的对象。
如何做到这一点...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
def createLightRig():
offsetAmount = 10
lightRotation = 30
newLight = cmds.spotLight(rgb=(1, 1, 1), name="KeyLight")
lightTransform = cmds.listRelatives(newLight, parent=True)
keyLight = lightTransform[0]
newLight = cmds.spotLight(rgb=(0.8, 0.8, 0.8), name="FillLight")
lightTransform = cmds.listRelatives(newLight, parent=True)
fillLight = lightTransform[0]
newLight = cmds.directionalLight(rgb=(0.2, 0.2, 0.2), name="BackLight")
lightTransform = cmds.listRelatives(newLight, parent=True)
backLight = lightTransform[0]
cmds.move(0, 0, offsetAmount, keyLight)
cmds.move(0, 0, 0, keyLight + ".rotatePivot")
cmds.rotate(-lightRotation, lightRotation, 0, keyLight)
cmds.move(0, 0, offsetAmount, fillLight)
cmds.move(0, 0, 0, fillLight + ".rotatePivot")
cmds.rotate(-lightRotation, -lightRotation, 0, fillLight)
cmds.move(0, 0, offsetAmount, backLight)
cmds.move(0, 0, 0, backLight + ".rotatePivot")
cmds.rotate(180 + lightRotation, 0, 0, backLight)
rigNode = cmds.group(empty=True, name="LightRig")
cmds.parent(keyLight, rigNode)
cmds.parent(fillLight, rigNode)
cmds.parent(backLight, rigNode)
cmds.select(rigNode, replace=True)
createLightRig()
运行前面的代码,你会看到你留下了三个创建的灯光——两个聚光灯和一个方向性灯光。
它是如何工作的...
我们将创建三个灯光——两个聚光灯和一个方向性灯光。为了简化定位,我们将创建一些辅助变量:
offsetAmount = 10
lightRotation = 30
offsetAmount变量将是每个灯光从原点移动的距离,而lightRotation将控制灯光旋转的程度。接下来,我们创建我们的第一个灯光——主光源:
newLight = cmds.spotLight(rgb=(1, 1, 1), name="KeyLight")
创建灯光非常简单;我们只需调用spotLight命令。在此过程中,我们将使用rgb标志来设置灯光的颜色(全强度白色),并将名称设置为便于以后识别。我们将结果存储到newLight变量中。
出现了一个小问题,即灯光创建命令返回的是形状节点的名称,而不是变换。因为设置位置和旋转需要更改变换,我们将使用listRelatives命令来获取相关的变换节点:
lightTransform = cmds.listRelatives(newLight, parent=True)
正如我们在前面的例子中所看到的,我们必须考虑到listRelatives命令总是返回一个节点列表,即使可能只有一个节点(如使用父节点标志的情况)。我们将第一个条目存储到一个变量中,我们将使用这个变量在脚本中识别我们的灯光:
keyLight = lightTransform[0]
到目前为止,我们已经创建了三个灯光中的第一个。我们将做完全相同的事情来创建一个用于填充光的第二个聚光灯,唯一的区别是我们从略低于全白色的颜色开始:
newLight = cmds.spotLight(rgb=(0.8, 0.8, 0.8), name="FillLight")
lightTransform = cmds.listRelatives(newLight, parent=True)
fillLight = lightTransform[0]
最后,我们设置了背光。我们从一个深灰色灯光开始,并创建一个方向性灯光而不是聚光灯:
newLight = cmds.directionalLight(rgb=(0.2, 0.2, 0.2), name="BackLight")
lightTransform = cmds.listRelatives(newLight, parent=True)
backLight = lightTransform[0]
现在我们已经创建了所有灯光,我们准备设置它们的位置和旋转,以获得一个漂亮的默认三灯光设置。为此,我们将为每个灯光执行以下步骤:
-
沿着 z 轴将灯光移动到离原点一定的距离。
-
将灯光的旋转支点移回到原点。
-
设置灯光的旋转以获得我们想要的位置。
我们也可以直接使用一些三角函数来计算灯光位置,但让灯光各自绕原点旋转不仅会使脚本更简单,而且创建后也更容易更改。
首先,我们沿着 z 轴移动灯光:
cmds.move(0, 0, offsetAmount, keyLight)
一旦完成,我们想将旋转支点移回原点。我们将使用移动命令来完成此操作:
cmds.move(0, 0, 0, keyLight + ".rotatePivot")
注意,我们在灯光名称后追加 .rotatePivot,这样我们只移动支点而不是灯光本身。另外,注意我们将它移动到 (0,0,0)。这将最终给我们想要的结果,因为移动命令默认使用绝对坐标。所以,通过 (0,0,0) 移动实际上是告诉 Maya 将相关对象移动到原点。
一旦完成,我们可以使用旋转命令将灯光绕原点旋转。对于主光,我们将绕 x 轴旋转以将其向上移动,并绕 y 轴旋转以将其向右移动。注意,我们对于 x 轴的值取反,这样灯光在 X-Z 平面相对于顺时针旋转而不是逆时针旋转。这将确保灯光向上移动,而不是向下移动:
cmds.rotate(-lightRotation, lightRotation, 0, keyLight)
我们重复前面的代码两次,以设置填充光和背光,并使用默认位置:
cmds.move(0, 0, offsetAmount, fillLight)
cmds.move(0, 0, 0, fillLight + ".rotatePivot")
cmds.rotate(-lightRotation, -lightRotation, 0, fillLight)
填充光绕 x 轴旋转的量与主光相同,但在 y 轴的相反方向:
cmds.move(0, 0, offsetAmount, backLight)
cmds.move(0, 0, 0, backLight + ".rotatePivot")
cmds.rotate(180 + lightRotation, 0, 0, backLight)
背光则绕 x 轴旋转 180 度(以放置在原点后面)加上我们的 lightRotation 值,使其移动到原点上方。
到目前为止,我们有了三个具有默认设置和位置的灯光,但我们会希望使整个装置更容易操作。为此,我们将创建一个新的变换节点并将所有三个灯光作为子节点添加。
要创建新的变换节点,我们将使用带有 empty 标志的 group 命令,以允许我们创建一个空组。我们还将确保使用名称标志设置基本名称:
rigNode = cmds.group(empty=True, name="LightRig")
一旦完成,我们使用 parent 命令将所有三个灯光作为 LightRig 节点的子节点:
cmds.parent(keyLight, rigNode)
cmds.parent(fillLight, rigNode)
cmds.parent(backLight, rigNode)
作为最后的润色,我们确保选择了父节点,使用 select 调用并带有 replace 选项:
cmds.select(rigNode, replace=True)
当创建新的节点或节点组时,始终是一个好习惯,在脚本结束时保留新创建的对象(或对象组)选中状态,以便最终用户可以轻松地进行必要的进一步更改(例如移动整个装置)。
还有更多...
在这个例子中,我们将灯光的颜色设置为不同的值,以使填充光和背光的影响更小。或者,我们也可以将所有灯光设置为单一颜色,并使用强度来提供变化。这可能会看起来像以下这样:
keyLightShape = cmds.spotLight(rgb=(1, 1, 1), intensity=1, name="KeyLight")
fillLightShape = cmds.spotLight(rgb=(1,1,1), intensity=0.8, name="FillLight")
backLightShape = cmds.directionalLight(rgb=(1,1,1), intensity=0.2, name="BackLight")
前面的代码会给我们三个都是白色的灯光,但强度不同。如果你想在创建灯光后设置其强度,可以使用setAttr命令。例如,如果我们想在事后更改keyLight的强度,我们可以这样做:
cmds.setAttr(keyLightShape + ".intensity", 0.5)
脚本的一个很好的可能补充是更好地考虑几何形状的不同比例。在当前版本中,我们可以使用父组的比例来增加或减少灯光的间距。一个稍微好一点的方法是为我们的函数传递一个偏移量值。我们还可以传递一个旋转量值以支持更广泛的使用案例。
这样做将导致我们采取以下代码:
def createLightRig():
offsetAmount = 10
lighRotation = 30
newLight = cmds.spotLight(rgb=(1, 1, 1), name="KeyLight")
# rest of script
它将被更改为:
def createLightRig(offsetAmount, lightRotation):
newLight = cmds.spotLight(rgb=(1, 1, 1), name="KeyLight")
# rest of script
创建用于控制所有灯光的 GUI
大多数场景最终都会包含多个灯光,控制它们可能会变得非常麻烦。在这个例子中,我们将创建一个 GUI,它将向用户提供一种简单的方式来控制场景中所有灯光的颜色。
在场景中有三个灯光的情况下运行脚本会产生以下类似的结果:

准备工作
确保你的场景中至少有几点灯光。或者,可以使用上面的三点照明示例快速设置灯光系统。
如何操作...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
from functools import partial
class LightBoard():
def __init__(self):
self.lights = []
self.lightControls = []
self.lightNum = 0
if (cmds.window("ahLightRig", exists=True)):
cmds.deleteUI("ahLightRig")
self.win = cmds.window("ahLightRig", title="Light Board")
cmds.columnLayout()
lights = cmds.ls(lights=True)
for light in lights:
self.createLightControl(light)
cmds.showWindow(self.win)
def updateColor(self, lightID, *args):
newColor = cmds.colorSliderGrp(self.lightControls[lightID], query=True, rgb=True)
cmds.setAttr(self.lights[lightID]+ '.color', newColor[0], newColor[1], newColor[2], type="double3")
def createLightControl(self, lightShape):
parents = cmds.listRelatives(lightShape, parent=True)
lightName = parents[0]
color = cmds.getAttr(lightShape + '.color')
changeCommandFunc = partial(self.updateColor, self.lightNum)
newSlider = cmds.colorSliderGrp(label=lightName, rgb=color[0], changeCommand=changeCommandFunc)
self.lights.append(lightShape)
self.lightControls.append(newSlider)
self.lightNum += 1
LightBoard()
运行前面的脚本,你将得到一个新窗口,其中包含场景中每个灯光的控制。控制将提供颜色样本和滑块,更改它将导致相关灯光的颜色更新。
如何工作...
首先要注意的是,我们添加了一个第二个import语句:
import maya.cmds as cmds
from functools import partial
functools库提供了创建函数和将它们作为变量使用的能力。这在我们将要连接我们的控件时将非常有用。
下一个要注意的是,我们再次为这个例子设置了一个合适的类。因为我们需要维护场景中所有灯光的列表(以及每个灯光的控制列表),所以将我们的脚本包裹在一个合适的类中是最好的方法。
话虽如此,让我们继续我们的__init__函数。我们首先初始化一些成员变量——一个用于存储灯光列表,一个用于存储控制列表,一个帮助我们将适当的灯光链接到适当的控制:
def __init__(self):
self.lights = []
self.lightControls = []
self.lightNum = 0
然后我们为新窗口执行一些标准的初始化工作——创建窗口,设置其标题,并添加列布局。在我们创建窗口之前,我们首先删除之前的窗口(如果存在):
if (cmds.window("ahLightRig", exists=True)):
cmds.deleteUI("ahLightRig")
一旦我们完成了这个操作,我们创建我们的窗口,确保传递与我们检查的相同名称:
self.win = cmds.window(("ahLightRig", title="Light Board")
cmds.columnLayout()
现在我们已经准备好设置我们的控制。我们需要为场景中的每一盏灯创建一个控制,这意味着我们首先必须获取所有灯的列表。幸运的是,对于我们来说,ls 命令使得这变得很容易。我们只需要将 lights 标志设置为 true:
lights = cmds.ls(lights=True)
注意,ls 命令可以与几个不同的标志(lights、cameras 和 geometry)一起使用,以获取特定类型的对象。如果你想要获取没有特定标志的类型节点,你也可以这样做;只需使用 type 或 exactType 标志并指定你正在寻找的节点类型。
一旦我们有了场景中所有对象的列表,我们就遍历列表并为每个对象创建一个控制,使用我们的 createLightControl 方法,我们将在下一部分介绍。
一旦所有控制都创建完成,我们向用户显示窗口:
for light in lights:
self.createLightControl(light)
cmds.showWindow(self.win)
在我们真正创建我们的控制之前,我们需要一个函数,该函数可以用来更新特定的灯到给定滑块中的颜色。为此,我们创建了以下函数:
def updateColor(self, lightID, *args):
newColor = cmds.colorSliderGrp(self.lightControls[lightID], query=True, rgb=True)
cmds.setAttr(self.lights[lightID]+ '.color', newColor[0], newColor[1], newColor[2], type="double3")
这个函数看起来像它接受三个参数,但实际上只有一个很重要。首先,由于我们正在将此脚本作为类构建,我们必须考虑到 Python 将将类实例传递给每个成员函数的事实,我们通过 self 参数来实现这一点。接下来是我们真正关心的东西,在这种情况下,一个表示我们正在与之工作的控制/灯的整数值。最后,我们在 *args 中有一个通配符。
使用 *args 将提供一种方法,将可变数量的参数抓取到一个单独的数组中。这是必要的,因为许多 UI 控件会向它们调用的函数传递额外的数据。在这种情况下,我们实际上并不想使用它,但如果我们省略了 *args,Maya 就会向一个只接受两个参数的函数传递三个参数,从而产生错误。
我们真正关心的参数,lightID,告诉我们应该使用哪个灯/控制。灯和控制都存储在类成员变量中——self.lights 和 self.lightControls。为了将特定的灯设置到特定的滑块上,我们必须首先通过以查询模式运行 colorSliderGrp 命令来获取滑块的当前值,如下所示:
newColor = cmds.colorSliderGrp(self.lightControls[lightID], query=True, rgb=True)
注意,我们传递 self.lightControls 数组中的一个条目来指定控制,我们以查询模式运行命令,并将 rgb 标志设置为 True 来告诉 Maya 我们正在查询特定的属性。
一旦我们完成了这个操作,我们使用 setAttr 来设置相应灯的颜色为给定的红色、绿色和蓝色值。我们将使用 setAttr 来做这件事,但我们需要确保我们指定了类型,因为我们将会使用多个值。
cmds.setAttr(self.lights[lightID]+ '.color', newColor[0], newColor[1], newColor[2], type="double3")
好的,所以到目前为止,我们有一个可以调用的函数来更新特定的灯光到特定滑块的当前颜色值。所以,如果我们调用这个函数,我们会将第一盏灯设置为第一个滑块的当前颜色:
self.updateColor(0)
这是我们需要的一部分,但我们还想确保每次滑块值改变时都会调用这个函数。
接下来是创建单个控件。我们创建一个函数,它接受一个特定的灯光并执行以下操作:
-
存储对灯光的引用,以便我们稍后可以更改其属性
-
为灯光创建一个新的
colorSliderGrp控件
该函数将传递我们想要创建控件的灯光,我们还需要考虑到 Python 传递的是类实例本身,这给我们以下函数签名:
def createLightControl(self, lightShape):
在函数内部,我们首先会获取与灯光关联的变换节点。这并不是严格必要的,因为我们想要更改的节点(用于设置颜色)实际上是形状节点。然而,将我们的控件命名为类似Key Light而不是keyLightShape会更好一些。获取变换的方式与我们在上一个示例中做的方式相同:
parents = cmds.listRelatives(lightShape, parent=True)
lightName = parents[0]
在我们创建控制之前,有一些事情我们需要先做。我们需要确保colorSliderGrp从与灯光相同的颜色值开始。为了做到这一点,我们需要使用getAttr(获取属性)命令来获取灯光的当前颜色:
color = cmds.getAttr(light + '.color')
getAttr命令是一个真正的多面手,并且(就像许多其他命令一样)它总是返回一个数组,因为它的某些用途可能会返回多个值。关于它在特定情况下的行为,有一点令人惊讶的是,我们最终会得到一个只有一个元素的数组,而这个元素本身是一个包含红色、绿色和蓝色值的三个元素的列表。因此,当我们使用颜色变量时,我们需要使用color[0],而不是(正如你可能猜测的那样)只是color。
接下来,我们想要做的是创建一个当滑块值改变时被调用的函数。在colorSliderGrp控件的情况下,这包括移动滑块或点击颜色样本来选择颜色。在任一情况下,我们都会通过运行一些代码来更新我们的灯光颜色值。
这里会变得有些棘手,因为我们将调用的函数需要知道从哪个特定的 UI 控件获取数据,以及应用到哪个特定的灯光上。
我们将使用colorSliderGrp命令创建滑块,该命令提供了一个标志changeCommand,可以用来指定每次滑块值改变时应运行的命令。
如果我们想让滑块在改变时运行一个不接受任何参数的函数,我们可以这样做:
newSlider = cmds.colorSliderGrp(label=lightName, rgb=color[0], changeCommand=self.someFunction)
然而,在这种情况下,我们想要调用我们的updateColor函数,同时传递一个整数来指定要更新的灯光/控件。你可能想做一些如下所示的事情:
newSlider = cmds.colorSliderGrp(label=lightName, rgb=color[0], changeCommand=self.updateColor(0))
不幸的是,事情并没有那么简单。前面的代码会导致 Python 在我们创建控件时实际运行updateColor函数。因此,changeCommand标志的实际值将是self.updateColor的返回值(在这种情况下,None)。
这就是functools库中包含的partial命令的作用所在。我们可以使用partial函数来创建一个带有特定参数的函数副本。这几乎就像我们为每个灯光和滑块组合编写了一个单独的函数。我们将使用partial命令来创建self.updateColor命令的副本,其中包含一个表示当前灯光的数字,如下所示:
changeCommandFunc = partial(self.updateColor, self.lightNum)
partial的第一个参数是一个函数。注意在self.updateColor之后没有括号,这表明我们正在使用函数本身,而不是运行它。在函数之后,我们可以传递一个或多个参数来绑定到函数。例如,如果我们有以下函数:
def printANum(number):
print(number)
我们以以下方式使用了partial:
newFunction = partial(printANum, 23)
newFunction的值本身将是一个具有与调用printANum(23)完全相同行为的新函数。
因此,在这个脚本的这个点上,我们的changeCommandFunc变量包含一个新函数,它将具有与调用我们的updateColor函数并带有特定输入时的相同行为。有了这个,我们就准备好创建我们的滑块:
newSlider = cmds.colorSliderGrp(label=lightName, rgb=color[0], changeCommand=changeCommandFunc)
我们使用lightName变量来标记滑块,并传入我们的颜色变量(注意[0],因为它是一个数组),以确保滑块从当前灯光的颜色开始。
我们几乎完成了,但还需要做一些账目管理。我们想要确保我们维护对滑块及其对应灯光的引用。为此,我们将灯光的形状节点(最初传递给createLightControl函数)插入到类成员变量lights中。我们还将新创建的滑块插入到lightControls列表中:
self.lights.append(light)
self.lightControls.append(newSlider)
最后,我们将lightNum变量增加一,以便下一次通过函数时,我们将传递正确的值到partial命令中:
self.lightNum += 1
就这样!我们已经完成了类的创建,并使用创建实例的命令来完成脚本:
LightBoard()
还有更多...
在这个例子中,我们创建了控件来改变场景中灯光的颜色。你很可能会也想控制灯光的强度。这可以通过在createLightControl函数中创建一个额外的控件(可能是floatField或floatSlider)轻松实现。无论如何,你都会想要:
-
创建一个单独的类成员变量来保存对强度控件的引用。
-
确保更改强度滑块的值也会调用
updateColor函数。 -
在
updateColor函数中,确保你获取控制器的当前值,并使用它通过setAttr命令设置光线的强度。
从代码中创建相机
在这个例子中,我们将探讨如何使用代码来创建相机。我们将创建一组四个正交相机,适合用于渲染对象的多个视图,用作等距游戏的资源。
注意
等距游戏有着悠久的历史,其特点是用 2D 资源创建一个三分之四的俯视图来展示游戏环境。在完全 3D 游戏成为常态之前,这种方法非常普遍,它仍然经常出现在网页和移动游戏中。为等距游戏创建资源通常意味着为对象的每一面渲染一个视图,并确保渲染中没有透视畸变,这正是我们在这个例子中将要做的。

脚本的结果是四个正交相机,它们都指向原点
准备工作
为了获得最佳效果,请确保你的场景中有些几何体,放置在原点。
如何操作...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
def makeCameraRig():
aimLoc = cmds.spaceLocator()
offset = 10
for i in range(0, 4):
newCam = cmds.camera(orthographic=True)
cmds.aimConstraint(aimLoc[0], newCam[0], aimVector=(0,0,- 1))
xpos = 0
ypos = 6
zpos = 0
if (i % 2 == 0):
xpos = -offset
else:
xpos = offset
if (i >= 2):
zpos = -offset
else:
zpos = offset
cmds.move(xpos, ypos, zpos, newCam[0])
makeCameraRig()
运行脚本,你应该有四个等距相机,它们都朝向原点。
如何工作...
制作相机相当简单;我们只需要使用camera命令。然而,没有直接的方法可以通过代码创建相机和目标设置。相反,我们必须为每个我们创建的相机手动创建一个目标约束。
我们从创建一个定位器开始,作为目标约束的目标。
aimLoc = cmds.spaceLocator()
定位器将默认位于原点,这对我们的目的来说很合适。现在我们有了目标对象,我们就可以创建我们的相机了。我们启动一个 for 循环来创建四个相机,每个对角方向一个。
实际上,创建相机很简单;我们只需调用camera命令。在这种情况下,我们希望有正交相机,所以我们将orthographic标志设置为true:
for i in range(0, 4):
newCam = cmds.camera(orthographic=True)
接下来,我们将设置目标约束。要创建一个目标约束,我们需要传递两个变换节点,第一个是目标节点,第二个是将会控制其旋转的节点。请注意,由于spaceLocator和camera命令都返回两个节点(一个变换节点,一个形状节点),我们需要指定我们用来保存结果的变量的第一个索引。

默认相机,指向负 z 轴
我们还希望确保我们的相机通过设置正确的目标向量向下看定位器。由于相机的默认位置将使其沿 z 轴的负方向看去,我们将使用(0, 0, -1)作为目标向量。
将所有这些放在一起,我们得到以下创建aim约束的行:
cmds.aimConstraint(aimLoc[0], newCam[0], aimVector=(0,0,-1))
现在我们只需要将相机移动到正确的位置。因为目标约束将处理旋转,我们只需要担心位置。在这种情况下,我们想要确保每个相机都位于从原点出发的 45 度倍数的直线上。为此,我们需要确保X和Z的位置具有相同的幅度,只是从相机到相机符号改变。
首先,我们将为每个 x、y 和 z 位置创建变量,其中ypos变量被设置为默认值:
xpos = 0
ypos = 6
zpos = 0
对于 X 和 Z,我们希望每个象限都有值 - (+,+), (+,-), (-,-), 和 (-,+)。为了做到这一点,当我们的索引是奇数时,我们将其中一个值设为负,当索引大于或等于2时,另一个值设为负:
if (i % 2 == 0):
xpos = -offset
else:
xpos = offset
if (i >= 2):
zpos = -offset
else:
zpos = offset
注意我们正在使用offset变量,它是在循环外部设置的。
完成这些后,我们使用move命令将相机定位到正确的位置。
cmds.move(xpos, ypos, zpos, newCam[0])
就这样,我们完成了!
还有更多...
在这个例子中,我们创建了一个aim约束来复制从 Maya 的用户界面创建相机和瞄准时得到的行为。这个功能的好处在于我们可以移动定位器来改变观察位置。
如果我们只想让相机看向一个特定的位置,我们可以使用viewPlace命令,该命令可以移动相机并旋转它以看向特定位置。例如,如果我们想将相机定位在(5,6,5)并看向原点稍高的点(比如说 0,2,0),我们可以这样做:
newCam = cmds.camera()
cmds.viewPlace(newCam[0], eye=(5,6,5), lookAt=(0, 2, 0))
我们也仅仅触及了创建相机时可以做到的一些事情;你通常会想要设置诸如近/远裁剪平面、景深等。你可以直接在创建时设置这些,或者使用setAttr在之后修改它们。更多详情,请务必查阅camera命令的文档。
渲染精灵图集
在这个例子中,我们将构建一个工具,将一个对象的多个视图渲染到单个图像中。这可以用来创建等距游戏中的精灵图集。
在这个例子过程中,我们将使用 Maya 的 Python 库(用于渲染帧)和Python 图像库(PIL)将它们合并成单个图像。

四个视图渲染并合并成单个图像
准备工作
确保你的场景中有一个对象并且它位于原点。还确保你设置了一些数量的相机。你可以手动完成,或者参考如何从脚本创建相机的上一个例子。
你还想要确保你的系统上安装了 PIL。最好的方法是获取 Pillow(PIL 的一个分支)。关于 Pillow 的更多信息可以在pillow.readthedocs.io/en/3.2.x/找到。
为了以(相对)无痛的方式安装 Pillow,你可能想要获取 PIP,它是 Python 的一个强大的包管理器。有关 PIP 的更多信息,请查看pypi.python.org/pypi/pip。
如何做到这一点...
创建一个新文件,并添加以下代码:
import maya.cmds as cmds
import os
from PIL import Image
FRAME_WIDTH = 400
FRAME_HEIGHT = 300
def renderSpriteSheet():
allCams = cmds.listCameras()
customCams = []
for cam in allCams:
if (cam not in ["front", "persp", "side", "top"]):
customCams.append(cam)
# make sure we're rendering TGAs
cmds.setAttr("defaultRenderGlobals.imageFormat", 19)
# create a new image
fullImage = Image.new("RGBA", (FRAME_WIDTH*len(customCams), FRAME_HEIGHT), "black")
# run through each camera, rendering the view and adding it to the mage
for i in range(0, len(customCams)):
result = cmds.render(customCams[i], x=FRAME_WIDTH, y=FRAME_HEIGHT)
tempImage = Image.open(result)
fullImage.paste(tempImage, (i*FRAME_WIDTH,0))
basePath = cmds.workspace(query=True, rootDirectory=True)
fullPath = os.path.join(basePath, "images", "frames.tga")
fullImage.save(fullPath)
renderSpriteSheet()
运行脚本,你应该能在你的项目文件夹的images目录下得到一个新图像,命名为frames.tga,它包含了每个(非标准)摄像机的视图。如果你还没有设置项目,图像将保存在默认的项目目录中。如果你想将它们保存在特定的位置,确保在运行脚本之前设置好你的项目。
它是如何工作的...
首先,我们导入了一些额外的库来使脚本工作。首先是os库,它让我们能够以安全、跨平台的方式组合路径和文件名。然后,我们还从 PIL 中导入了 Image 模块,我们将使用它来创建我们的组合图像:
import maya.cmds as cmds
import os
from PIL import Image
接下来,我们定义了一些变量,我们将使用这些变量来设置渲染图像的大小。我们将使用这些变量来设置渲染大小,以及计算组合图像的大小:
FRAME_WIDTH = 400
FRAME_HEIGHT = 300
注意
注意,变量都是大写的,这并不是必需的。大写变量通常用来表示在脚本执行过程中在多个地方使用且不发生变化的常量。渲染帧的尺寸就是一个很好的例子,所以我给它们都使用了全大写名称,但如果你愿意,也可以使用不同的风格。
现在我们已经准备好开始渲染图像了。为了做到这一点,我们首先需要获取场景中所有摄像机的列表,然后过滤掉默认视图。我们可以使用ls命令来做这件事,但使用listCameras命令会更简单:
allCams = cmds.listCameras()
要忽略默认的摄像机视图,我们首先创建一个新的(空)列表,然后遍历我们的allCams列表。不在默认列表中的每个摄像机都会被添加,这样我们就得到了场景中所有非默认摄像机的方便列表。
customCams = []
for cam in allCams:
if (cam not in ["front", "persp", "side", "top"]):
customCams.append(cam)
到目前为止,我们有一个所有想要渲染的摄像机的列表。在我们开始渲染任何内容之前,我们想要确保我们正在渲染正确的图像格式。在这种情况下,我们将渲染 Targa 文件,因为它们既未压缩又包含 alpha 通道:
cmds.setAttr("defaultRenderGlobals.imageFormat", 19)
要设置图像类型,我们使用setAttr命令,但值可能不如我们希望的那样清晰。恰好targa格式对应于 19。其他常见的格式包括 JPG(8)、PNG(32)和 PSD(31)。要检查任何给定格式的值,请打开渲染全局窗口,从下拉菜单中选择所需的格式,并在脚本编辑器中观察输出。
在我们开始渲染图像之前,我们想要使用 PIL 创建一个更大的图像来存储所有帧。我们将创建一个高度与渲染大小相同,宽度等于渲染宽度乘以相机数量的单个图像。我们还将设置图像默认为黑色:
fullImage = Image.new("RGBA", (FRAME_WIDTH*len(customCams), FRAME_HEIGHT), "black")
注意,我们传入RGBA以设置图像模式为全色加上透明度。在我们创建基础图像后,我们就可以运行相机并渲染每一帧。
对于每个相机,我们想要:
-
在我们指定的宽度和高度上渲染当前视图
-
将渲染的图像粘贴到组合图像中
要渲染一个特定的视图,我们使用render命令并传入三个参数——渲染的相机,接着是渲染图像的宽度和高度:
result = cmds.render(customCams[i], x=FRAME_WIDTH, y=FRAME_HEIGHT)
我们将render命令的结果存储到结果变量中,以供以后使用。需要注意的是,输出不是图像本身,而是图像的路径(例如/Documents/maya/projects/default/images/tmp/MyScene.tga)。
现在我们已经渲染出了图像,我们想要使用 PIL 从指定的路径创建第二个Image对象:
tempImg = Image.open(result)
我们使用Image.open而不是Image.create,因为我们想要从一个给定的文件创建图像,而不是创建一个新的空白图像。最后,我们使用paste命令将新图像复制到组合图像中:
fullImage.paste(tempImg, (i*FRAME_WIDTH,0))
PIL 的粘贴命令允许将一个图像粘贴到另一个图像的特定位置。在这种情况下,我们在fullImage图像上调用它,并传入我们刚刚渲染出的图像(tempImg),以及一个表示位置的元组。在所有情况下,Y位置都锁定为0,而X位置设置为FRAME_WIDTH乘以循环索引,这样我们的图像就可以整齐地水平排列。
在完成那个循环后,我们就可以保存组合图像了。我们可以将其放在任何地方,但可能最好将其放在我们的项目目录中。为此,我们需要首先使用查询模式中的workspace命令获取当前项目目录:
basePath = cmds.workspace(query=True, rootDirectory=True)
保存图像的位置由你决定,但在这个例子中,我决定将其保存为项目目录中images文件夹的frames.tga。我们可以通过添加字符串来构建路径,但使用 Python 的os库来连接路径可以确保我们的脚本具有更好的跨平台支持:
fullPath = os.path.join(basePath, "images", "frames.tga")
最后,我们在fullImage变量上调用Image.save并传入我们刚刚创建的路径:
fullImage.save(fullPath)
还有更多...
虽然 Maya 的渲染功能提供了广泛的选择,但某些事情可能通过事后处理更容易实现。PIL 非常强大,值得深入研究。如果你发现自己需要在渲染上执行 2D 操作,使用 PIL 可能是一个不错的选择。
这个脚本,或者类似的东西,可以很容易地用来构建一个用于等距游戏的健壮的资产管道。你可以轻松地添加将组合图像连同特定结构或对象的元数据发送到中央服务器的功能。我们将在第九章第九章。与 Web 通信中探讨如何在 Web 上发送数据。
参见
PIL 的功能远不止我们在本例中使用的那样。更多详情,你可以在effbot.org的文档中深入了解(http://effbot.org/imagingbook/pil-index.htm)。
第八章。处理文件输入/输出
在本章中,我们将探讨通过脚本将自定义数据导入和导出 Maya 的方法:
-
使用 fileDialog2 命令导航文件系统
-
读取文本文件
-
写入文本文件
-
写入二进制数据
-
读取二进制数据
-
读取多种类型的文件
简介
尽管 Maya 是一个非常强大的工具,但它几乎总是更大工具链中的一步。无论你是使用 Maya 为电影和视频创建预渲染动画,还是创建用于实时应用的资产,你通常都需要将 Maya 或其中创建的内容与其他应用程序接口。这通常表现为读取或写入特定格式的数据。
在本章中,我们将探讨如何处理自定义数据格式,包括基于文本和二进制的数据,以及读取和写入数据。
使用 fileDialog2 命令导航文件系统
加载和保存文件几乎总是需要提示用户文件位置。在本例中,我们将探讨如何做到这一点。我们还将了解如何处理目录,包括创建新的目录。
我们将创建一个脚本,允许用户浏览当前项目目录中的customData文件夹中的文件。如果该文件夹不存在,则在脚本第一次运行时创建它。
如何操作...
创建一个新文件并添加以下内容:
import os
import maya.cmds as cmds
def browseCustomData():
projDir = cmds.internalVar(userWorkspaceDir=True)
newDir = os.path.join(projDir, 'customData')
if (not os.path.exists(newDir)):
os.makedirs(newDir)
cmds.fileDialog2(startingDirectory=newDir)
browseCustomData()
你将看到一个文件浏览器对话框。虽然对话框目前实际上不会做任何事情,但如果你检查你的项目目录,你会发现它现在包含一个名为customData的文件夹。
它是如何工作的...
关于这个脚本的第一点是我们在脚本开始处添加了一个额外的导入语句:
import os
os 库(简称"操作系统")提供了与宿主机的操作系统相关的各种功能,包括处理目录的能力。我们将使用它来检查目录是否存在,如果不存在则创建它。关于这方面的更多内容将在后面进行解释。
对于这个脚本,我们首先需要找出当前的项目目录。为此,我们可以使用internalVar命令。internalVar命令可以用来访问与当前用户环境相关的各种目录。它不能用来设置这些目录,只能用来查询它们。然而,请注意,我们实际上并没有在查询模式下使用它(Maya 的命令并不总是最一致的)。相反,我们将我们想要获取值的标志的值设置为 true。
在这种情况下,我们请求userWorkspaceDir,这将为我们提供当前项目目录:
projDir = cmds.internalVar(userWorkspaceDir=True)
接下来,我们想要测试当前工作空间内是否存在customData文件夹。为此,我们将首先创建该目录的完整路径(如果它存在),通过将"customData"添加到internalVar返回的值中来实现。我们可以通过字符串操作来完成它,但这有点复杂,因为不同的平台可以使用不同的字符来表示目录之间的分隔符。基于 Linux 的平台(包括 Macintosh)使用"/",而 Windows 机器使用""。一个更安全(因此更好的)方法是使用 Python 的os.path.join方法,它保证是安全的,如下所示:
newDir = os.path.join(projDir, 'customData')
现在我们有了customData文件夹的完整路径,但它可能实际上并不存在。我们可以使用os.path模块中的另一个函数os.path.exists来检查它是否存在,如果不存在则创建它:
if (not os.path.exists(newDir)):
如果我们发现路径实际上不存在,我们使用os.makedirs来创建它:
os.makedirs(newDir)
到目前为止,我们终于可以调用fileDialog2命令来向用户展示一个文件浏览器对话框。为了确保它从customData目录开始,我们将startingDirectory标志设置为newDir变量:
cmds.fileDialog2(startingDirectory=newDir)
注意,我们使用的是fileDialog2,它可能看起来有点奇怪。还有一个fileDialog命令,但它已被弃用(连同fileBrowserDialog命令)。因此,我们不得不使用这个有些不雅的名称fileDialog2。
还有更多...
fileDialog2命令还有许多其他选项,我们将在后面的例子中看到。internalVar命令也有许多其他可以提供的位置。其中一个经常有用的选项是userScriptDir,它将提供用户的脚本目录。
如果你想要获取用户脚本目录中当前所有脚本的列表,例如,你可以使用以下代码片段:
def listScripts():
scriptDir = cmds.internalVar(userScriptDir=True)
print(os.listdir(scriptDir))
os.listdir命令将提供一个给定目录中所有文件的数组。在这种情况下,我们可能想要创建一个 GUI,为每个脚本提供一个按钮,为用户提供一个方便的方式来选择和运行脚本。
读取文本文件
在这个例子中,我们将读取一个文本文件,并使用其内容在我们的场景中创建一些几何形状。
准备工作
为了进行任何类型的文件输入/输出,你首先需要了解你想要读取(或创建)的文件格式。在这个例子以及涉及写入文本文件的例子中,我们将使用一个示例文件格式——“foo”文件。“Foo”文件是基于文本的文件,每一行代表一个给定类型的几何原语,位于给定位置。几何原语类型由一个三字母字符串表示,其中“spr”表示球体,“cub”表示立方体。类型字符串后面跟着三个数字,分别代表项目的 X、Y 和 Z 位置。因此,一个示例.foo文件可能看起来像以下这样:
spr 0 0 0
cub -2 0 -2
虽然这肯定不是一个特别有用的格式,但它与许多常见的基于文本的格式有相似之处。例如,OBJ 格式是一个常见的 3D 模型标准,它使用类似的方法——每一行都包含一个标识符,表示它所包含的信息类型,后面跟着该条目的详细信息。例如,表示有一个位于 2、3 和 4 的顶点的行看起来如下:
v 2 3 4
因此,我们的“foo”文件,虽然故意设计得非常简单,但将以与许多 真实 文件格式相同的方式进行读取和处理。
在运行此示例的脚本之前,请确保你已经创建了一个 .foo 文件。为此,创建一个新的文本文件并添加一些行,这些行:
-
以 "spr"(代表球体)或 "cub"(代表立方体)开始
-
后面跟着三个数字(代表 X、Y 和 Z 位置),每个数字之间用空格分隔
请确保将文件保存为 .foo 文件,而不是 .txt 文件。
如何操作...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
def processFooLine(line):
parts = line.split()
if (len(parts) < 4):
cmds.error("BAD DATA " + line)
x = float(parts[1])
y = float(parts[2])
z = float(parts[3])
if (parts[0] == "spr"):
cmds.sphere()
elif (parts[0] == "cub"):
cmds.polyCube()
cmds.move(x, y, z)
def readFooFile():
filePath = cmds.fileDialog2(fileMode=1, fileFilter="*.foo")
fileRef = open(filePath[0], "r")
line = fileRef.readline()
while (line):
processFooLine(line)
line = fileRef.readline()
fileRef.close()
readFooFile()
运行文件,你将看到一个文件对话框,允许你找到 .foo 文件。一旦指定了一个包含有效 FOO 文件数据的文件,你应该会看到一些球体和立方体被创建。

它是如何工作的...
在脚本中,我们首先调用 fileDialog2 命令,以便让用户指定一个文件。我们将 fileMode 标志设置为 1,表示我们想要读取(而不是写入)文件。我们还使用了 fileFilter 标志,以便将用户指向我们的自定义文件格式。这是完全可选的,但它可以是一种防止用户给你错误类型数据的好方法。为此,我们需要向 Maya 提供两样东西:
-
一个简短的文件类型描述,以显示给用户,以及
-
一个或多个文件扩展名,后面跟着一个通配符字符("*")
因此,在这种情况下,我们希望限制用户只能打开“FOO”文件,并将这些文件识别为以 .foo 或 .fo 结尾的任何内容。要传递的字符串的最终值如下:
"FOO files (*.foo *.fo)"
注意,我们也可以允许用户打开其他类型的文件,通过用双分号分隔每个字符串来实现。假设我们想要允许用户打开文本(.txt)文件。为此,我们的 fileDialog2 调用将如下所示:
cmds.fileDialog2(fileMode=1, fileFilter="FOO files (*.foo *.fo);;Text files (*.txt)")
如果你允许用户打开多种类型的文件,每种类型都会在文件对话框底部的下拉菜单中可用。通过从下拉菜单中选择一个选项,用户可以更改对话框将接受的文件类型。现在我们已经涵盖了指定文件类型,让我们回到我们的常规示例。
我们将 fileDialog 的输出存储到一个变量中。在继续之前,我们还会检查该变量是否不为空。这样,我们就能确保如果用户点击了“取消”按钮,我们不会继续执行脚本:
filePath = cmds.fileDialog2(fileMode=1, fileFilter="FOO files (*.foo *.fo);;Text files (*.txt)")
if (filePath == None):
return
现在我们准备实际打开文件。为了做到这一点,我们使用 Python 的open命令,第一个参数是我们想要打开的文件的完整路径,第二个参数指示以何种模式打开文件,其中"r"表示“读取”:
fileRef = open(filePath[0], "r")
注意filePath是一个数组,因此我们需要将第一个元素传递给 open 命令。open 的返回值,我们将其存储在fileRef变量中,是对文件的引用,我们可以用它来读取数据。
对于大多数基于文本的文件类型(FOO 文件也不例外),我们希望逐行读取文件。我们首先从文件引用中读取一行:
line = fileRef.readline()
一旦我们做了这些,我们想要:
-
处理我们刚刚读取的行中的信息
-
从文件中读取下一行
-
继续读取直到我们阅读完整个文件
这可以通过一个 while 循环轻松完成。处理将由一个单独的函数来处理,我们将在下一部分看到:
while (line):
processFooLine(line)
line = fileRef.readline()
一旦我们到达文件的末尾,我们的行变量将为空,while 循环将终止。我们最后要做的事情是做一些清理工作,即关闭对文件的引用:
fileRef.close()
现在,让我们更仔细地看看我们如何在processFooLine函数中处理数据。我们首先使用 Python 的split函数将行拆分成部分。这将把输入字符串拆分成一个字符串数组,默认情况下是根据空白字符分隔的:
parts = line.split()
if (len(parts) < 4):
cmds.error("BAD DATA " + line)
因为我们的 FOO 文件规范指出,每一行应该是一个简短的字符串,后面跟着三个数字,所以如果我们的部分数组少于四个条目,我们会抛出一个错误。如果它至少有四个条目,我们将第二个、第三个和第四个条目转换为浮点数并将它们存储到变量中,用于x、y和z位置:
x = float(parts[1])
y = float(parts[2])
z = float(parts[3])
现在我们根据部件数组中的第一个条目创建对象,要么是一个球体,要么是一个立方体:
if (parts[0] == "spr"):
cmds.sphere()
elif (parts[0] == "cub"):
cmds.polyCube()
最后,我们将我们刚刚创建的对象移动到我们的x、y和z变量所指示的位置:
cmds.move(x, y, z)
还有更多...
尽管 FOO 格式规范有意简化,但我们很容易扩展它来存储更多信息或可能的可选信息。例如,我们可能还有一个可选的第五项来指示要创建的对象的大小(例如,立方体的面宽和球体的半径)。如果您想看看一个表面上类似于 FOO 文件但更有用的格式,我鼓励您查看 OBJ 文件格式。它不仅在 3D 中广泛使用,而且相对简单易懂,因此是文件解析的绝佳入门。
编写文本文件
在前面的例子中,我们探讨了如何读取自定义数据文件格式并使用它来创建场景中的几何形状。在这个例子中,我们将做相反的事情,即检查我们的场景中的多边形立方体和 NURBS 球体,并将我们找到的每个对象的坐标写入一个新的 FOO 文件。在这个过程中,我们将看到如何将数据写入自定义文本格式。
准备工作
在运行此示例之前,请确保场景中包含一些(NURBS)球体和多边形立方体。请确保创建的立方体和球体启用了构建历史,否则我们的脚本将无法正确识别几何形状。
如何做到这一点...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
def checkHistory(obj):
history = cmds.listHistory(obj)
geoType = ""
for h in history:
if (h.startswith("makeNurbSphere")):
geoType = "spr"
if (h.startswith("polyCube")):
geoType = "cub"
return geoType
def writeFOO():
filePath = cmds.fileDialog2(fileMode=0, fileFilter="FOO files (*.foo)")
if (filePath == None):
return
fileRef = open(filePath[0], "w")
objects = cmds.ls(type="transform")
for obj in objects:
geoType = checkHistory(obj)
if (geoType != ""):
position = cmds.xform(obj, query=True, translation=True, worldSpace=True)
positionString = " ".join(format(x, ".3f") for x in position)
newLine = geoType + " " + positionString + "\n"
fileRef.write(newLine)
fileRef.close()
writeFOO()
它是如何工作的...
我们首先提示用户指定一个文件。与文件读取示例一样,我们设置 fileFilter 标志,以便对话框仅限于 .foo 文件。不过,这次我们将 fileMode 标志设置为 0,表示我们想要写入文件(而不是值为 1,表示读取):
filePath = cmds.fileDialog2(fileMode=0, fileFilter="FOO files (*.foo)")
如果 fileDialog2 命令的结果为空(表示用户取消了),我们停止。否则,我们继续执行脚本并打开指定的文件进行写入。再次注意,fileDialog2 命令返回了一个数组,这意味着我们需要将其第一个条目传递给 open 命令。我们还设置了第二个参数为 "w",表示我们想要写入文件:
if (filePath == None):
return
fileRef = open(filePath[0], "w")
接下来,我们需要找到场景中的所有立方体和球体。为此,我们首先获取场景中的所有变换节点。
objects = cmds.ls(type="transform")
对于每个对象,我们想知道它是否是球体或立方体。一种方法是通过检查对象的构建历史,看看是否有 makeNurbSphere 或 polyCube 节点。为了保持事情整洁有序,我们将它封装在一个单独的函数 checkHistory 中。
要获取给定对象的 历史,我们可以使用 listHistory 命令,它将以数组的形式返回构建历史:
def checkHistory(obj):
history = cmds.listHistory(obj)
一旦我们完成了这个,我们就准备好遍历历史记录,看看我们是否能找到我们正在寻找的几何形状。但首先,我们设置一个变量来保存几何类型,并将其初始化为空字符串:
geoType = ""
如果所讨论的对象确实是我们要寻找的类型之一,它在其历史记录中将有 makeNurbSphere 或 polyCube 节点。然而,在两种情况下,节点名称的末尾都将有一个数字。因此,我们需要使用 Python 的 startswith 命令来进行检查,而不是仅仅测试直接相等。
如果我们找到了我们正在寻找的几何创建节点之一,我们将我们的 geoType 字符串设置为适当的缩写(基于 FOO 文件格式规范):
for h in history:
if (h.startswith("makeNurbSphere")):
geoType = "spr"
if (h.startswith("polyCube")):
geoType = "cub"
最后,我们返回 geoType 变量:
return geoType
所有这些的结果是,我们将为想要导出的对象有 "spr" 或 "cub",而对于其他所有内容则为空字符串。有了这个,我们可以将注意力转回到我们的主函数。
带着我们的 checkHistory 函数,我们现在准备好遍历场景中的所有对象,测试每个对象以查看它是否是我们感兴趣的几何形状:
for obj in objects:
geoType = checkHistory(obj)
if (geoType != ""):
如果checkHistory返回的值不是空字符串,我们知道我们有一些东西想要写入我们的文件。我们已经知道几何类型,但仍然需要获取世界空间位置。为此,我们使用xform命令,以查询模式。
position = cmds.xform(obj, query=True, translation=True, worldSpace=True)
现在我们终于准备好将数据写入我们的文件了。我们需要构建一个具有以下格式的字符串:
[geometry type ID] [x position] [y position] [z position]
我们将从xform接收到的位置创建一个字符串。位置最初是一个浮点数数组,我们需要将其转换成一个单一的字符串。这意味着我们需要做两件事:
-
将数字转换为字符串。
-
将字符串连接成一个单一的字符串。
如果我们有一个字符串数组,我们可以使用 Python 的join命令将它们连接起来。该命令的语法有点奇怪,但使用起来足够简单;我们从一个字符串开始,该字符串包含我们想要用作分隔符的内容。在这种情况下(以及大多数情况下),我们想要使用一个空格。然后我们调用该字符串上的 join,传入我们想要连接的元素列表。所以,如果位置数组包含字符串,我们可以这样做:
positionString = " ".join(position)
然而,事情并不那么简单,因为位置数组包含浮点值。因此,在我们运行 join 之前,我们需要首先将数值转换为字符串。如果我们只想做这件事,我们可以这样做:
positionString = " ".join(map(str, position))
在前面的代码中,我们使用 Python 的map函数将第一个参数(str或字符串函数)应用于第二个参数(位置数组)的每个元素。这把位置数组转换成了一个字符串数组,然后我们可以将其传递给 join 函数。
然而,我们可能想要对浮点数的格式化有更多的控制,这让我们来到了我们实际使用的行,即:
positionString = " ".join(format(x, ".3f") for x in position)
这与基于 map 的例子有点相似,我们在将内容传递给 join 函数之前,先对位置数组应用一个函数。然而,在这种情况下,我们使用format函数,这让我们对浮点数格式化的具体细节有更多的控制。在这种情况下,我们限制值的精度为三位小数。
在这一点上,我们已经有了一个字符串,表示当前对象的全位置。为了完成它,我们需要添加几何类型标识符(如我们的 FOO 文件格式规范中指定)。我们还想在末尾添加一个换行符(\n),以确保每条几何数据都有单独的一行。
注意
注意,如果你在 Windows 机器上的记事本中打开创建的文件,你会看到所有数据都显示为单行。这是因为类 Unix 系统(包括 Mac)使用\n作为换行符,而 Windows 使用\r\n。\r是回车符,而\n是换行符。使用两者是打字机在旧时代移动到下一行时执行两个动作的副产品——将纸张完全移到右边(\r)并将其向上移动(\n)。如果你在 Windows 上工作,你可能想添加\r\n而不是仅仅\n。
这给我们以下结果:
newLine = geoType + " " + positionString + "\n"
现在我们终于准备好将数据写入文件了。这可以通过调用我们文件的write()方法轻松完成:
fileRef.write(newLine)
一旦我们完成了对所有对象的循环遍历并保存了所有数据,我们通过关闭文件引用来完成操作:
fileRef.close()
还有更多...
本节中提供的示例可能看起来有点人为,但导出位置数据是一个相当常见的需求。通常情况下,对于艺术团队来说,使用 Maya 定位后来以编程方式使用的对象(例如,在游戏的情况下,生成点或物品拾取位置)可能更容易。
在这个例子中,我们通过检查它们的构建历史来识别要导出的对象。这可以工作,但如果构建历史被删除,它很容易被破坏。由于删除历史记录是常见的事情,因此拥有识别导出节点的替代方法是个好主意。
一种非常可靠的方法是为应该导出的节点添加自定义属性,并在遍历对象时使用它。例如,我们可能使用多边形立方体来指示游戏关卡中某些类型物品拾取的位置。为了更好地准备导出数据,我们可以在每个立方体上添加一个pickupType属性。
我们可以轻松地将它包装在一个漂亮的函数中,以添加属性并设置其值,如下所示:
def markAsPickup(obj, pickupType):
customAtts = cmds.listAttr(obj, userDefined=True)
if ("pickupType" not in customAtts):
cmds.addAttr(obj, longName="pickupType", keyable=True)
cmds.setAttr(obj + ".pickupType", pickupType)
上述代码将为指定的对象添加一个可键控的pickupType属性并设置其值。请注意,我们在添加属性之前检查节点上是否存在pickupType属性,因为添加已存在的属性将生成错误。为了检查属性,我们首先获取所有用户定义属性的列表,然后测试pickupType是否存在于该数组中。
当我们准备好导出数据时,我们可以使用同样的技巧来识别我们想要导出数据的对象。如果我们想为具有pickupType属性的每个对象写入数据,我们可以这样做:
def listPickups():
pickups = []
objects = cmds.ls(type="transform")
for obj in objects:
customAtts = cmds.listAttr(obj, userDefined=True)
if (customAtts != None):
if ("pickupType" in customAtts):
print(obj)
pickups.append(obj)
return pickups
我们首先创建一个新的列表来保存我们的拾取,然后获取场景中所有的变换节点。对于每个变换,我们获取添加到它上的所有自定义属性,并检查是否有任何属性被命名为pickupType。如果是这样,我们将对象添加到我们的列表中。一旦我们完成循环,我们就返回列表,以便在其他地方使用(可能用于写入它们的位置)。
参见
对于 FOO 文件格式的快速概述,请务必查看有关读取基于文本数据的先前示例。
写入二进制数据
到目前为止,在本章中,我们已查看读取和写入基于文本的数据格式。这将允许您处理许多类型的数据(并轻松创建自己的格式),但这只是问题的一半。在本例中,我们将查看另一半——二进制格式。
准备工作
在这个示例中,我们将编写我们的 FOO 文件的二进制版本。我们将这样的文件称为 FOB(foo,二进制)。与 FOO 文件一样,FOB 文件是真实格式中常见类型的一个简化示例。FOB 文件将包含我们在 FOO 文件中看到的数据,即对象类型和位置的列表,但以二进制格式常见的方式存储。
大多数二进制文件由两个主要部分组成:
-
一个标题,它是一个固定大小的块,描述了文档其余部分的内容。
-
根据标题中指定的数据布局读取的条目。
在我们的 FOB 文件的情况下,我们的标题将包含以下内容:
-
一个整数(1 字节),指定每个条目用于几何类型指定的字符数(我们的“spr”或“cub”)。
-
一个整数(1 字节),指定每个对象的最大属性数(至少是 X、Y 和 Z 位置,可能还有更多数据)。
-
一个整数(1 字节),指定每个属性的字节数。
因此,一个特定的 FOB 文件可能声明我们使用三个字节来表示几何类型,最大数据值数量为四个(X、Y、Z 位置和大小),每个值使用四个字节。这将给我们一个如下所示的标题:
3 4 4
在标题之后,将有一些条目,每个条目由 19 字节组成(3 个用于几何类型,加上 4 * 4,即 16 个字节的用于数据)。
在运行示例之前,请确保您的场景中有一个或多个 NURBS 球体和/或多边形立方体,并且它们是在启用构造历史记录的情况下创建的(默认选项)。
如何做到这一点...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
import struct
def checkHistory(obj):
history = cmds.listHistory(obj)
geoType = ""
for h in history:
if (h.startswith("makeNurbSphere")):
geoType = "spr"
if (h.startswith("polyCube")):
geoType = "cub"
return geoType
def writeFOBHeader(f):
headerStr = 'iii'
f.write(struct.pack(headerStr, 3, 3, 4))
def writeObjData(obj, geoType, f):
position = cmds.xform(obj, query=True, translation=True, worldSpace=True)
f.write(geoType)
f.write(struct.pack('fff', position[0], position[1], position[2]))
def saveFOBFile():
filePath = cmds.fileDialog2(fileMode=0, fileFilter="FOO Binary files (*.fob)")
if (filePath == None):
return
fileRef = open(filePath[0], "wb")
writeFOBHeader(fileRef)
objects = cmds.ls(type="transform")
for obj in objects:
geoType = checkHistory(obj)
if (geoType != ""):
writeObjData(obj, geoType, fileRef)
# positionString = " ".join(format(x, ".3f") for x in position)
fileRef.close()
saveFOBFile()
它是如何工作的...
首先要注意的是,我们在脚本的开始处有一个额外的导入语句:
import struct
struct库提供了我们将用于正确格式化数据以写入二进制的函数。更多内容将在稍后介绍。接下来是脚本本身...
首先,我们要求用户指定一个文件,就像我们在前面的示例中所做的那样。唯一的区别是我们稍微改变了fileFilter参数,以指定具有.fob扩展名的“FOO 二进制”类型的文件:
filePath = cmds.fileDialog2(fileMode=0, fileFilter="FOO Binary files (*.fob)")
我们检查以确保filePath变量有一个实际值(用户没有取消),如果没有,则停止脚本。然后我们打开文件进行写入:
fileRef = open(filePath[0], "wb")
注意,我们使用"wb"而不是"w"作为 open 命令的参数;这告诉 Python 我们想要以二进制模式("b")打开文件进行写入("w")。
现在,我们准备开始写入我们的文件。在我们能够写入任何数据之前,我们需要写入标题。在 FOB 文件的情况下,所有这些都是三个整数——一个用于存储几何标识符的字符数,一个用于存储每个对象的数据点数,一个用于存储每个数据点的字节数。
要实际写入数据,我们将使用struct库的 pack 函数。pack 函数将创建一个包含指定格式的数据的字节序列,该格式由格式字符串指定。格式字符串是一系列字符,其中每个字符代表要写入的数据类型。字符可以是以下任何一种以及许多其他类型:
| i | 整数 |
|---|---|
| f | 浮点数 |
| c | 字符 |
对于完整列表,请参阅 Python 的文档。
在这种情况下,我们希望存储三个整数,因此我们的格式字符串需要由三个 I 组成,如下所示:
headerStr = 'iii'
我们将格式字符串传递给struct.pack函数,后面跟着我们想要编码的值(在这种情况下,三个整数)。在这种情况下,我们希望为我们的几何标识符长度保留三个字符(以容纳"spr"和"cub"),三个数据点(X、Y 和 Z 位置),以及每个数据点四个字节。将这些全部放在一起,我们得到以下内容:
struct.pack(headerStr, 3, 3, 4)
一旦我们将数据打包,我们就使用write将其写入我们的文件。我们将所有这些封装在一个漂亮的功能中,如下所示:
def writeFOBHeader(f):
headerStr = 'iii'
f.write(struct.pack(headerStr, 3, 3, 4))
现在我们已经将标题写入文件,我们准备写入我们的对象数据。我们遍历场景,以与示例中保存文本数据相同的方式找到所有球体和立方体。对于我们找到的每个对象,我们将数据写入我们的文件。
for obj in objects:
geoType = checkHistory(obj)
if (geoType != ""):
writeObjData(obj, geoType, fileRef)
我们的writeObjData函数接受对象本身、对象类型字符串(由我们的checkHistory函数从文本输出示例中确定)以及我们正在写入的文件的引用。
在writeObjData函数中,我们首先使用xform命令获取对象在世界空间中的位置:
position = cmds.xform(obj, query=True, translation=True, worldSpace=True)
我们然后将几何类型标识符("spr"或"cub")写入文件。向二进制文件写入文本很容易——我们只需直接写入值。这将导致写入文件中的每个字符都是一个字节。
f.write(geoType)
最后,我们再次使用struct.pack函数将位置数据写入文件。然而,这次我们想要写入浮点值,因此我们使用三个 fs 作为格式字符串:
f.write(struct.pack('fff', position[0], position[1], position[2]))
最后,回到我们的 main 函数中,我们关闭我们的文件,该文件现在包含标题和所有我们的数据。
fileRef.close()
还有更多...
我们可以轻松地写出每个对象除了位置数据之外的数据。如果我们想要为每个球体写出半径值,我们需要做一些事情,即:
-
将我们的标题更改为指定每个对象四个值,而不是仅仅三个。
-
将传递给 pack 的格式字符串中的三个 fs 改为四个。
注意,尽管在立方体的情况下半径值没有意义,我们仍然需要在那个位置写上某些东西,以确保每个条目占用相同数量的字节。由于二进制文件通常通过一次读取一定数量的字节来处理,因此从条目到条目字节宽度发生变化将会干扰这个过程。
如果你认为这是一个限制,你是对的。二进制格式通常比基于文本的格式要严格得多,而且只有在你需要创建非常紧凑的文件时才值得。一般来说,如果你在考虑创建自定义格式,文本几乎总是更好的选择。将二进制输出保留在需要将数据输出到现有格式且该格式恰好是二进制的情况。
读取二进制数据
在这个示例中,我们将查看如何读取二进制数据。我们将使用相同的示例格式,即"FOO 二进制"格式,该格式包含一个包含三个整数的头部,后面跟着一个或多个条目,每个条目都有一个标识对象类型的字符串和三个或更多表示其位置的数字(以及可能的其他数据)。
准备工作
为了运行这个示例,你需要准备好一个.fob文件。手动创建二进制文件有点麻烦,所以我建议使用前面解释的示例为你生成一个。
如何做到这一点...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
import struct
def makeObject(objType, pos):
newObj = None
if (objType == "spr"):
newObj = cmds.sphere()
elif (objType == "cub"):
newObj = cmds.polyCube()
if (newObj != None):
cmds.move(pos[0], pos[1], pos[2])
def readFOBFile():
filePath = cmds.fileDialog2(fileMode=1, fileFilter="FOO binary files (*.fob)")
if (filePath == None):
return
f = open(filePath[0], "rb")
data = f.read()
headerLen = 12
res = struct.unpack('iii', data[0:headerLen])
geoTypeLen = res[0]
numData = res[1]
bytesPerData = res[2]
objectLen = geoTypeLen + (numData * bytesPerData)
numEntries = (len(data) - headerLen) / objectLen
dataStr = 'f'*numData
for i in range(0,numEntries):
start = (i * objectLen) + headerLen
end = start + geoTypeLen
geoType = data[start:end]
start = end
end = start + (numData * bytesPerData)
pos = struct.unpack(dataStr, data[start:end])
makeObject(geoType, pos)
f.close()
readFOBFile()
运行脚本,指向一个有效的.fob 文件,你应该在你的场景中看到一些球体和/或立方体。
它是如何工作的...
在这个示例中,我们还将使用 struct 库(来解包我们的数据),因此我们需要确保我们导入了它:
import struct
我们首先使用fileDialog2命令提示用户指定一个.fob 文件,如果没有提供任何内容,则退出脚本:
filePath = cmds.fileDialog2(fileMode=1, fileFilter="FOO binary files (*.fob)")
if (filePath == None):
return
如果我们有文件要打开,我们使用open命令打开它,传递"rb"作为模式("r"表示读取和"b"表示二进制):
f = open(filePath[0], "rb")
一旦文件打开,我们一次性读取所有数据,使用read函数:
data = f.read()
这将导致数据包含文件中所有字节的数组。一旦我们完成这个操作,我们就可以开始解析我们的内容了。对于读取的每份数据,我们将执行以下操作:
-
从我们的数据变量中读取一些字节。
-
将字节传递给
struct.unpack,同时传递一个格式字符串,指示它应该解释为哪种类型的数据。
我们首先需要读取文件的头部。在.fob 文件的情况下,这保证总是正好 12 字节——3 个整数,每个 4 字节。因此,我们首先读取数据数组中的前 12 字节,并将其传递给struct.unpack。我们使用的格式字符串将是"iii",表示字节应该被解释为三个整数:
headerLen = 12
res = struct.unpack('iii', data[0:headerLen])
unpack 函数的输出是一个包含数据的数组。在这种情况下,我们有几何标识符的每个字节数、每个条目的数据点数以及每个数据点的字节数。为了使事情更容易(并且代码更易于阅读),我们将每个元素存储在其自己的、命名的变量中:
geoTypeLen = res[0]
numData = res[1]
bytesPerData = res[2]
一旦我们完成了这个步骤,为了使接下来的内容更加清晰,我们还会做一件事——按照以下方式计算每个条目所需的字节数:
objectLen = geoTypeLen + (numData * bytesPerData)
一旦我们有了这个,我们可以通过将总字节数(减去头部长度消耗的字节数)除以每个条目的字节数来确定文件中的条目总数:
numEntries = (len(data) - headerLen) / objectLen
在读取数据之前,我们还需要处理一个细节;我们希望创建一个用于 struct.unpack 的格式字符串。在 .fob 文件的情况下,几何标识符字符串之后的所有内容都将是一个浮点数,但我们需要确保考虑到头中指定的条目数量。因此,如果我们每个对象有三个条目,我们将需要 "fff",但如果我们有四个,我们则需要 "ffff"。Python 通过乘法轻松地从给定数量的重复字符创建字符串,这给我们以下结果:
dataStr = 'f'*numData
这样,我们就完成了准备工作,可以继续实际读取我们的数据。我们从一个循环开始,循环次数为我们之前找到的条目数量:
for i in range(0,numEntries):
计算所需读取的索引的数学运算并不复杂,但可能会变得混乱,因此我们使用几个变量将它们拆分到单独的行上。
每个条目的起始字节是到目前为止读取的条目数量乘以每个条目的总长度,然后减去头部长度。结束索引是起始字节加上头部长度:
start = (i * objectLen) + headerLen
end = start + geoTypeLen
读取几何标识符很容易,因为它只是文本,每个字节对应一个字母:
geoType = data[start:end]
现在,我们将起始和结束变量设置为新的值来读取位置(以及可能的其他)数据。我们将起始设置为结束的前一个值。这是因为当从 Python 数组中读取一系列索引时,读取的值从第一个数字开始,读取到(但不包括)第二个数字。
数据的结束索引是起始字节加上数据的总字节数(numData * bytesPerData):
start = end
end = start + (numData * bytesPerData)
并且有了这个,我们最终可以读取我们对象的 数据。我们在数据数组中进行索引,并将结果传递给 struct.unpack,同时附带我们之前创建的格式字符串(dataStr):
pos = struct.unpack(dataStr, data[start:end])
一旦我们有了几何类型(geoType)和位置(pos),我们就将两者传递到一个函数中,以实际创建我们想要的几何形状:
makeObject(geoType, pos)
makeObject 函数相当直接——我们使用 geoType 参数创建两种可能的对象之一,如果成功,我们将创建的对象移动到 pos 数组中指定的位置:
def makeObject(objType, pos):
newObj = None
if (objType == "spr"):
newObj = cmds.sphere()
elif (objType == "cub"):
newObj = cmds.polyCube()
if (newObj != None):
cmds.move(pos[0], pos[1], pos[2])
还有更多...
到目前为止,我们只读取(或写入)了单一类型的二进制数据,例如整数(用于我们的标题)和浮点数(用于数据)。struct.pack和struct.unpack函数也可以与混合类型一起使用,只要你使用正确的格式字符串。例如,如果我们知道我们的标题包含三个浮点数和一个整数,我们可以使用以下代码来读取它:
struct.unpack('fffi', data[0:16])
注意,前面的代码使用 0 和 16 作为起始和结束索引,这可能会让人觉得我们在抓取 17 个字节。然而,Python 将范围解释为从开始到(但不包括)第二个。所以,我们真正说的是使用从 0 到(16-1),即 15 的索引。
读取多种类型的文件
有时候,你可能需要一个能够读取多种文件类型的单一脚本。例如,如果你正在构建一个复杂的系统来构建角色骨架,你可能希望有一个自定义格式来保存默认骨骼布局的信息,另一个类型来存储动画设置的信息,使用户能够混合和匹配任意两个文件。
在这些情况下,你可能希望你的脚本能够处理具有多个扩展名的文件——每个类型一个。在这个例子中,我们将通过创建一个可以用来读取 FOO(我们的示例基于文本的格式)或 FOB(我们的示例二进制格式)的脚本来查看如何做到这一点。
准备工作
确保你至少有一个每种类型的文件。对于 FOO 文件,你可以在文本编辑器中直接创建它们。对于 FOB 文件,最好使用写作二进制文件的示例中的脚本。
如何做到...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
import struct
import os
def readMultipleTypes():
fileRes = cmds.fileDialog2(fileMode=1, fileFilter="FOO files(*.foo);;FOO binary files (*.fob)")
if (fileRes == None):
return
filePath = fileRes[0]
pathParts = os.path.splitext(filePath)
extension = pathParts[1]
if (extension == ".foo"):
readFOOFile(filePath)
elif (extension == ".fob"):
readFOBFile(filePath)
else:
cmds.error("unrecognized file type")
readMultipleTypes()
注意,前面的代码使用了两个我们尚未定义的函数,即readFOOFile和readFOBFile。为了简洁,我省略了这些函数,但它们都使用了我们在之前示例中讨论的读取文本和二进制文件的相同代码。
如果你运行这个脚本,你将能够选择 FOO 文件,或者通过从文件类型下拉列表中选择“FOO 二进制文件”,选择 FOB 文件。无论哪种方式,你应该能看到相应的球体和立方体集合被添加到场景中。
它是如何工作的...
为了读取多个文件,我们首先必须将两种或更多类型添加到fileFilter参数中,用双分号分隔,如下所示:
FOO files(*.foo);;FOO binary files (*.fob)
除了这个之外,fileDialog2命令的使用方式与我们过去使用的方式相同。一旦我们得到命令的结果,我们将第一个条目(用户选择的路径)存储到filePath变量中。
一旦我们完成了这个,我们希望检查用户选择的文件扩展名。我们可以使用字符串函数来做这件事,但依靠 Python 的os.path.splitext函数会更安全一些,该函数专门设计用来从路径中分离扩展名,返回的是一个包含路径(包括文件名)和扩展名的数组:
filePath = fileRes[0]
pathParts = os.path.splitext(filePath)
extension = pathParts[1]
一旦我们有了扩展名,我们就将其与我们要处理的全部类型进行测试,为每种类型调用相应的函数:
if (extension == ".foo"):
readFOOFile(filePath)
elif (extension == ".fob"):
readFOBFile(filePath)
else:
cmds.error("unrecognized file type")
对于每种文件类型,我们调用一个函数来处理实际的文件处理,传入文件的路径。如果用户意外地选择了一个我们不处理的文件类型,我们通过抛出错误来结束操作。
还有更多...
你当然可以将这种方法扩展到单个脚本中处理多种文件类型,尽管如果你有很多种类型,从下拉菜单中选择合适的类型可能会让用户感到疲惫。
在这种情况下,你可能只想完全省略fileFilter,让脚本接受所有文件类型,并依赖扩展过滤逻辑来过滤掉你不想处理的任何类型。
然而,在实践中,如果你确实在处理大量不同的文件类型,你的脚本可能试图做太多事情。考虑将其拆分成更小的组件,每个组件都专注于为你构建的特定过程子集。
第九章。与 Web 通信
在本章中,我们将探讨以下方法,通过发送和接收 Web 请求,让你的脚本与外界进行通信:
-
从脚本中打开网页
-
从服务器获取数据
-
处理 XML 数据
-
处理 JSON 数据
-
从 Maya 向网络服务器发送 POST 数据
简介
在上一章中,我们探讨了如何读取和写入数据到磁盘,这对于构建团队的工具链和管道来说是一种很好的方法。然而,你几乎总是作为团队的一部分(或者作为 TD 支持团队)工作,这意味着你通常想要将数据读取和写入某个中央存储库。
为了做到这一点,你可能需要与某种类型的网络服务器进行通信。在本章中,我们将探讨如何做到这一点——如何从 Web 上拉取数据和推送到 Web。
从脚本中打开网页
如果你发现自己正在编写一个复杂的脚本,提供以网页形式存在的脚本文档通常很有帮助。一个很好的方法是通过提供一种简单的方式来向用户展示该页面。在本例中,我们将创建一个简单的脚本,该脚本将在用户的默认网络浏览器中打开指定的 URL。
如何做到这一点...
创建一个新的脚本并添加以下代码:
import maya.cmds as cmds
def showHelp():
cmds.showHelp("http://www.adrianherbez.net", absolute=True)
showHelp()
运行脚本,你将在默认浏览器中看到指定的 URL 出现。
它是如何工作的...
我们在这里真正做的是使用 showHelp 命令。这有点误导,因为 showHelp 命令也用于显示 Maya 对特定命令的文档。然而,只要将绝对标志设置为 true,你就可以传递一个你想要打开的 URL 的完整路径:
cmds.showHelp("http://www.adrianherbez.net", absolute=True)
注意,你可能遇到一些已弃用的命令,它们不再工作。在 Maya 的旧版本中,有一个 webBrowser 命令,它允许在基于脚本的 UI 中包含网页内容。不幸的是,该命令已被删除,需要使用 showHelp 命令在浏览器中打开内容。
还有更多...
如果你的脚本足够复杂,需要一页文档,那么它很可能还包括(可能是复杂的)UI。与其只提供一个显示帮助的按钮,不如轻松实现一个“帮助”菜单,这在其他程序中很常见。

这可以通过菜单和 menuItem 命令轻松完成。以下是生成前面结果的完整列表:
import maya.cmds as cmds
class helpWin():
def __init__(self):
self.win = cmds.window(menuBar=True, width=300, height=200)
cmds.menu(label="Help", helpMenu=True)
cmds.menuItem(label="View Help", command=self.showHelp)
cmds.columnLayout()
cmds.showWindow(self.win)
def showHelp(self, args):
cmds.showHelp("http://www.adrianherbez.net", absolute=True)
helpWin()
我们首先创建一个窗口,就像我们在之前的例子中所做的那样。然后,我们使用 menu 命令添加一个新的菜单。标签是将在菜单顶部显示的文本,指定 helpMenu=True 确保这个特定的菜单将被视为帮助菜单(显示在所有菜单选项的最右侧)。
一旦我们有了菜单,我们就可以向其中添加菜单项。这很像添加一个按钮,我们指定一个标签和一个当项目被选中时将执行的命令。
注意,新的menuItem将被添加到最新的菜单中。要添加不同菜单中的菜单项(例如,同时拥有“文件”和“帮助”类别),确保在添加其他项之前调用cmds.menu来启动一个新的菜单。
从服务器获取数据
在这个例子中,我们将查看从给定 URL 获取数据的最简单方法,使用 Python 的内置urllib2库。
准备工作
你需要确保你有一个要获取的 URL。你可以使用任何你喜欢的网站,但为了测试的目的,在你的本地机器上有一个最小的页面可能会有所帮助。如果你想那样做,首先创建一个简单的 html 文件,如下所示:
<html>
<head>
<title>
Maya scripting chapter 9
</title>
</head>
<body>
HELLO FROM THE WEB
</body>
</html>
一旦你做了这些,你将希望在自己的机器上提供这些内容作为页面。Python 提供了一种非常简单的方式来做到这一点。打开命令行(mac 上的终端)并导航到你保存 html 文件的地方。从那里,输入以下命令:
python -m SimpleHTTPServer
这将导致 Python 将当前目录的内容作为网站在本地主机上提供服务。-m标志告诉 Python 在运行解释器时包含一个给定的模块(在本例中为SimpleHTTPServer)。这相当于在 Python 脚本开头使用以下命令:
import SimpleHTTPServer
默认情况下,当前目录的内容将在端口 8000 上提供服务,这意味着你可以通过打开浏览器并访问以下链接来访问内容:
http://localhost:8000/
如何做到这一点...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
import urllib2
def getWebData():
url = 'http://localhost:8000'
print('grabbing web data from ', url)
try:
web = urllib2.urlopen(url)
except Exception as e:
print("ERROR: ", e)
return
print(web.getcode())
print(web.info())
print(web.read())
getWebData()
确保你已经在某个地方创建了一个index.htm(或 html)文件,并且已经从该目录运行了python -m SimpleHTTPServer。如果你已经那样做了,运行前面的脚本应该会输出以下内容,然后是整个index.htm文件的内容:
200
Server: SimpleHTTP/0.6 Python/2.7.10
Date: Tue, 26 Apr 2016 19:08:08 GMT
Content-type: text/html
Content-Length: 113
Last-Modified: Wed, 13 Apr 2016 06:31:18 GMT
它是如何工作的...
首先,我们必须确保导入urllib2库以及我们的标准maya.cmds:
import maya.cmds as cmds
import urllib2
这将使我们能够访问加载给定 URL 所需的所有命令。我们首先设置一个变量来保存我们正在加载的 URL,并打印一条消息,表明我们即将尝试加载它:
def getWebData():
url = 'http://localhost:8000'
print('grabbing web data from ', url)
现在我们准备实际尝试加载数据。当从 URL 加载数据时,你永远不要假设 URL 是可访问的。服务器端(服务器可能已关闭或未响应请求)或客户端(指定的 URL 可能被防火墙阻止,以太网电缆可能被拔掉等)可能发生任何错误,任何一种都会阻止 URL 的加载。
因此,我们将尝试获取 URL 的尝试包裹在 try/catch 块中。如果在加载 URL 的过程中出现任何错误,我们将打印出错误并返回:
try:
web = urllib2.urlopen(url)
except Exception as e:
print("ERROR: ", e)
return
如果我们成功检索到相关的 URL,我们将得到一个“文件-like 对象”。这意味着我们可以使用在打开文件时使用的所有函数,例如使用read()来获取内容。urllib2.urlopen返回的特定文件-like 对象还实现了几个额外的函数,我们在这里使用了它们。首先,我们获取 HTTP 代码:
print(web.getcode())
如果一切如预期进行,那么它应该会打印出"200",表示请求成功。接下来,我们检索一些关于 URL 的信息:
print(web.info())
这将显示头部信息(服务器类型、最后修改时间等):
print(web.info())
最后,我们使用read()函数从 Web 地址获取实际数据。调用read()而不指定要读取的字节数将获取文件的全部内容(或在这种情况下,网站的全部内容)。
更多...
在这个示例中,我们加载了整个网站。虽然你通常不会想为大多数网站这样做,但在请求来自 Web API 的数据时,这很有意义,因为结果通常是一小部分格式化的数据(XML 或 JSON)。
如果你只是想显示一个完整的网站(而不是通过 API 检索数据),请参考之前的示例,其中我们使用showHelp命令来显示指定的网站。
处理 XML 数据
当从 Web 服务器获取数据时,你很可能以某种结构化格式接收它,XML 和 JSON 是最常见的选项。在这个示例中,我们将看看如何利用作为 XML 提供的数据。
准备工作
要使用此示例,你需要在某台服务器上有一个可用的 XML 文件。最简单的方法是在你的机器上本地创建一个文件,然后运行以下命令:
python -m SimpleHTTPServer
从与文件相同的目录中,通过 localhost 提供对它的访问。以下是我将用作示例的文件:
<xml version="1.0">
<object type="cube">
<x>0</x>
<y>2</y>
<z>3</z>
<size>3</size>
</object>
<object type="sphere">
<size>2</size>
<x>0</x>
<y>0</y>
<z>0</z>
</object>
</xml>
文件相当简单,但它将允许我们查看遍历 XML 节点并解析属性和元素。
如何做到...
创建一个新文件并添加以下代码:
import maya.cmds as cmds
import urllib2
import xml.etree.ElementTree as ET
def makeObjectAt(type, position, size):
if (type == 1):
cmds.polyCube(height=size, width=size, depth=size)
elif (type == 2):
cmds.sphere(radius=size/2)
cmds.move(position[0], position[1], position[2])
def loadXML():
url = 'http://localhost:8000/data.xml'
try:
webData = urllib2.urlopen(url)
except Exception as e:
print("ERROR: ", e)
return
data = ET.parse(webData)
root = data.getroot()
for item in root:
objectType = 1
objectSize = 1
pos = [0,0,0]
if (item.attrib['type'] == "sphere"):
objectType = 2
for details in item:
tagName = details.tag
tagValue = float(details.text)
if (tagName == "size"):
objectSize = tagValue
elif (tagName == "x"):
pos[0] = tagValue
elif (tagName == "y"):
pos[1] = tagValue
elif (tagName == "z"):
pos[2] = tagValue
makeObjectAt(objectType, pos, objectSize)
loadXML()
确保将 URL 指向你的 XML 文件的正确位置,并运行脚本;你应该会看到一个立方体和一个球体出现。
它是如何工作的...
首先,我们添加另一个库xml.etree.ElementTree到我们的导入中,并给它一个更短的名字,以便更容易使用:
import maya.cmds as cmds
import urllib2
import xml.etree.ElementTree as ET
接下来,我们创建一个简单的函数来创建给定大小的球体或立方体,并将其移动到指定位置。这相当直接,并且在这个阶段可能看起来相当熟悉:
def makeObjectAt(type, position, size):
if (type == 1):
cmds.polyCube(height=size, width=size, depth=size)
elif (type == 2):
cmds.sphere(radius=size/2)
cmds.move(position[0], position[1], position[2])
接下来,我们像本章前面的示例一样从指定的 URL 获取数据:
def loadXML():
url = 'http://localhost:8000/data.xml'
try:
webData = urllib2.urlopen(url)
except Exception as e:
print("ERROR: ", e)
return
现在我们准备进入示例的核心——实际的 XML 解析。首先,我们使用xml.etree.ElementTree的解析命令将我们从 Web 接收到的数据解析成一个 XML 树。
data = ET.parse(webData)
解析命令可以接受字符串或文件对象。因为我们从urllib2.urlopen命令接收文件对象,我们可以直接传递结果。
一旦我们完成这些,我们就有了正确的 XML 节点树,我们就可以开始遍历树并解析我们的数据。要开始解析,我们首先需要获取根节点,我们使用getroot()命令来完成:
root = data.getroot()
实际的解析将根据你的 XML 模式性质有所不同。在这种情况下,我们有一些


浙公网安备 33010602011771号